koatty_trace
Version:
Full link tracking and error interception for koatty.
1,547 lines (1,470 loc) • 44.6 kB
JavaScript
/*!
* @Author: richen
* @Date: 2025-06-05 16:25:14
* @License: BSD (3-Clause)
* @Copyright (c) - <richenlin(at)gmail.com>
* @HomePage: https://koatty.org/
*/
import { IOCContainer as e } from "koatty_container";
import { Helper as t } from "koatty_lib";
import { context as r, trace as s, defaultTextMapSetter as o, DiagLogLevel as i, diag as n } from "@opentelemetry/api";
import { W3CTraceContextPropagator as a, ExportResultCode as c } from "@opentelemetry/core";
import { DefaultLogger as l } from "koatty_logger";
import { performance as d } from "node:perf_hooks";
import { isException as u, Exception as h, StatusCodeConvert as p } from "koatty_exception";
import { SemanticAttributes as m, ATTR_TELEMETRY_SDK_VERSION as f, ATTR_TELEMETRY_SDK_LANGUAGE as v, ATTR_TELEMETRY_SDK_NAME as g, SEMRESATTRS_DEPLOYMENT_ENVIRONMENT as y, ATTR_SERVICE_VERSION as T, ATTR_SERVICE_NAME as S } from "@opentelemetry/semantic-conventions";
import { PrometheusExporter as b } from "@opentelemetry/exporter-prometheus";
import { MeterProvider as M } from "@opentelemetry/sdk-metrics";
import C from "koa-compress";
import { Readable as w, Stream as E } from "stream";
import P from "node:zlib";
import * as I from "zlib";
import { inspect as H } from "util";
import { AsyncLocalStorage as k, AsyncResource as D } from "async_hooks";
import { NodeSDK as _ } from "@opentelemetry/sdk-node";
import { BatchSpanProcessor as A, BasicTracerProvider as q } from "@opentelemetry/sdk-trace-base";
import { getNodeAutoInstrumentations as x } from "@opentelemetry/auto-instrumentations-node";
import { OTLPTraceExporter as z } from "@opentelemetry/exporter-trace-otlp-http";
import { resourceFromAttributes as O } from "@opentelemetry/resources";
import { randomUUID as N } from "node:crypto";
/*
* @Description: Enhanced SpanManager with improved concurrency safety and performance
* @Usage:
* @Author: richen
* @Date: 2020-11-20 17:37:32
* @LastEditors: Please set LastEditors
* @LastEditTime: 2023-11-10 22:18:40
* @License: BSD (3-Clause)
* @Copyright (c) - <richenlin(at)gmail.com>
*/ class SpanManager {
constructor(e) {
this.activeSpans = new Map;
this.isDestroyed = !1;
this.stats = {
spansCreated: 0,
spansEnded: 0,
spansTimedOut: 0,
memoryEvictions: 0,
errors: 0
};
this.propagator = new a;
this.startTime = Date.now();
this.options = {
spanTimeout: 3e4,
samplingRate: 1,
maxActiveSpans: 1e3,
spanAttributes: void 0,
...e.opentelemetryConf
};
this.cleanupInterval = setInterval((() => {
this.performPeriodicCleanup();
}), Math.min(this.options.spanTimeout || 3e4, 6e4));
process.once("SIGTERM", (() => this.destroy()));
process.once("SIGINT", (() => this.destroy()));
}
performPeriodicCleanup() {
if (this.isDestroyed) return;
try {
const e = Date.now();
const t = [];
for (const [r, s] of this.activeSpans) {
const o = e - s.createdAt;
if (o > (this.options.spanTimeout || 3e4)) t.push(r);
}
for (const e of t) this.forceEndSpan(e, "timeout");
this.checkMemoryPressure();
if (typeof l.debug === "function" && this.activeSpans.size > 0) l.debug("SpanManager stats:", {
activeSpans: this.activeSpans.size,
...this.stats,
memoryUsage: Math.round(process.memoryUsage().heapUsed / 1024 / 1024) + "MB"
});
} catch (e) {
this.stats.errors++;
l.error("Error during periodic cleanup:", e);
}
}
checkMemoryPressure() {
const e = this.options.maxActiveSpans || 1e3;
if (this.activeSpans.size >= e) {
const t = Math.ceil(e * .1);
this.evictOldestSpans(t);
}
const t = process.memoryUsage();
if (t.heapUsed > 500 * 1024 * 1024) {
l.warn("High memory usage detected, performing aggressive span cleanup");
this.evictOldestSpans(Math.ceil(this.activeSpans.size * .2));
}
}
evictOldestSpans(e) {
const t = Array.from(this.activeSpans.entries()).sort((([, e], [, t]) => e.createdAt - t.createdAt)).slice(0, e);
for (const [e] of t) {
this.forceEndSpan(e, "memory_pressure");
this.stats.memoryEvictions++;
}
if (t.length > 0) l.warn(`Evicted ${t.length} oldest spans due to memory pressure`);
}
forceEndSpan(e, t) {
const r = this.activeSpans.get(e);
if (!r) return;
try {
clearTimeout(r.timer);
r.span.addEvent("span_forced_end", {
reason: t
});
r.span.end();
this.activeSpans.delete(e);
if (t === "timeout") this.stats.spansTimedOut++;
l.debug(`Span force-ended: ${e}, reason: ${t}`);
} catch (t) {
this.stats.errors++;
l.error(`Error force-ending span ${e}:`, t);
}
}
createSpan(e, t, r) {
if (this.isDestroyed) {
l.warn("SpanManager is destroyed, cannot create span");
return;
}
try {
const s = Math.random() < (this.options.samplingRate || 1);
if (!s) return;
if (!(e === null || e === void 0 ? void 0 : e.startSpan)) {
l.error("Invalid tracer provided to createSpan");
this.stats.errors++;
return;
}
this.span = e.startSpan(r, {
attributes: {
"service.name": r,
"request.id": t.requestId || "unknown"
}
});
this.stats.spansCreated++;
this.setupSpanTimeout(t);
this.injectContext(t);
this.setBasicAttributes(t);
return this.span;
} catch (e) {
this.stats.errors++;
l.error("Error creating span:", e);
return;
}
}
getSpan() {
return this.span;
}
setupSpanTimeout(e) {
if (!this.options.spanTimeout || !this.span || this.isDestroyed) return;
const t = this.span.spanContext().traceId;
if (this.activeSpans.has(t)) {
l.warn(`Span ${t} already exists in active spans`);
return;
}
let r = null;
try {
r = setTimeout((() => {
this.forceEndSpan(t, "timeout");
}), this.options.spanTimeout);
this.activeSpans.set(t, {
span: this.span,
timer: r,
createdAt: Date.now(),
requestId: e === null || e === void 0 ? void 0 : e.requestId
});
this.checkMemoryPressure();
} catch (e) {
if (r) clearTimeout(r);
this.activeSpans.delete(t);
this.stats.errors++;
l.error("Failed to setup span timeout:", e);
throw e;
}
}
injectContext(e) {
if (!this.span || this.isDestroyed) return;
try {
const t = {};
r.with(s.setSpan(r.active(), this.span), (() => {
this.propagator.inject(r.active(), t, o);
Object.entries(t).forEach((([t, r]) => {
if (e.set && typeof e.set === "function") e.set(t, r);
}));
}));
} catch (e) {
this.stats.errors++;
l.error("Error injecting context:", e);
}
}
setBasicAttributes(e) {
if (!this.span || this.isDestroyed) return;
try {
const t = {};
if (e.requestId) t["http.request_id"] = e.requestId;
if (e.method) t["http.method"] = e.method;
if (e.path) t["http.route"] = e.path;
this.span.setAttributes(t);
if (this.options.spanAttributes && typeof this.options.spanAttributes === "function") try {
const t = this.options.spanAttributes(e);
if (t && typeof t === "object") this.span.setAttributes(t);
} catch (e) {
l.error("Error applying custom span attributes:", e);
}
} catch (e) {
this.stats.errors++;
l.error("Error setting basic attributes:", e);
}
}
setSpanAttributes(e) {
if (!this.span || this.isDestroyed) return this;
try {
this.span.setAttributes(e);
} catch (e) {
this.stats.errors++;
l.error("Error setting span attributes:", e);
}
return this;
}
addSpanEvent(e, t) {
if (!this.span || this.isDestroyed) return;
try {
this.span.addEvent(e, t);
} catch (e) {
this.stats.errors++;
l.error("Error adding span event:", e);
}
}
endSpan() {
if (!this.span || this.isDestroyed) return;
const e = this.span.spanContext().traceId;
try {
const t = this.activeSpans.get(e);
if (t) {
clearTimeout(t.timer);
this.activeSpans.delete(e);
}
this.span.end();
this.stats.spansEnded++;
this.span = void 0;
} catch (e) {
this.stats.errors++;
l.error("SpanManager.endSpan error:", e);
}
}
getStats() {
return {
...this.stats,
activeSpansCount: this.activeSpans.size,
uptime: Date.now() - this.startTime,
isDestroyed: this.isDestroyed,
memoryUsage: process.memoryUsage()
};
}
destroy() {
if (this.isDestroyed) return;
this.isDestroyed = !0;
try {
if (this.cleanupInterval) clearInterval(this.cleanupInterval);
for (const [e] of this.activeSpans) this.forceEndSpan(e, "manager_destroyed");
if (this.span) {
this.span.end();
this.span = void 0;
}
l.info("SpanManager destroyed successfully", this.getStats());
} catch (e) {
l.error("Error during SpanManager destruction:", e);
}
}
}
/**
*
* @Description:
* @Author: richen
* @Date: 2024-11-11 11:36:07
* @LastEditTime: 2025-03-31 17:51:22
* @License: BSD (3-Clause)
* @Copyright (c): <richenlin(at)gmail.com>
*/ function R(r, s, o) {
var i;
const {message: n, status: a} = $(r, s);
const c = s.code || 1;
const l = s.stack;
const d = (i = o.spanManager) === null || i === void 0 ? void 0 : i.getSpan();
if (u(s)) return s.setCode(c).setStatus(a).setMessage(n).setSpan(d).setStack(l).handler(r);
const p = e.getInsByClass(o.globalErrorHandler, [ n, c, a, l, d ]);
if (t.isFunction(p === null || p === void 0 ? void 0 : p.handler)) return p.handler(r);
return new h(n, c, a, l, d).handler(r);
}
function $(e, t) {
var r, s;
let o = 500;
if ("status" in t && typeof t.status === "number") o = t.status; else if (t instanceof Error) o = 500; else if (e.status === 404 && !e.response._explicitStatus) o = 404; else o = e.status || 500;
let i = "";
try {
i = ((s = (r = t === null || t === void 0 ? void 0 : t.message) !== null && r !== void 0 ? r : e === null || e === void 0 ? void 0 : e.message) !== null && s !== void 0 ? s : "").toString();
i = i.replace(/"/g, '\\"');
} catch (e) {
i = "";
}
return {
status: o,
message: i
};
}
/**
* Prometheus metrics exporter
* @Description: Handle business metrics reporting for multiple protocols with enhanced concurrency safety and performance
* @Author: richen
* @Date: 2025-04-13
* @License: BSD (3-Clause)
*/ var L;
(function(e) {
e["HTTP"] = "http";
e["WEBSOCKET"] = "websocket";
e["GRPC"] = "grpc";
})(L || (L = {}));
class PathNormalizationCache {
constructor(e = 1e4) {
this.cache = new Map;
this.totalHits = 0;
this.totalAccesses = 0;
this.maxSize = e;
}
get(e) {
this.totalAccesses++;
const t = this.cache.get(e);
if (t) {
this.totalHits++;
this.cache.delete(e);
this.cache.set(e, t);
}
return t;
}
set(e, t) {
if (this.cache.has(e)) {
this.cache.delete(e);
this.cache.set(e, t);
return;
}
if (this.cache.size >= this.maxSize) {
const e = this.cache.keys().next().value;
if (e) this.cache.delete(e);
}
this.cache.set(e, t);
}
clear() {
this.cache.clear();
this.totalHits = 0;
this.totalAccesses = 0;
}
getStats() {
return {
size: this.cache.size,
maxSize: this.maxSize,
hitRate: this.totalAccesses > 0 ? this.totalHits / this.totalAccesses : 0,
totalHits: this.totalHits,
totalAccesses: this.totalAccesses
};
}
}
class MetricsBatchProcessor {
constructor(e, t = 100, r = 1e3) {
this.collector = e;
this.batchQueue = [];
this.flushTimer = null;
this.isProcessing = !1;
this.batchSize = t;
this.flushInterval = r;
this.startFlushTimer();
}
addMetric(e, t, r) {
this.batchQueue.push({
type: e,
labels: {
...t
},
value: r,
timestamp: Date.now()
});
if (this.batchQueue.length >= this.batchSize) this.flush();
}
startFlushTimer() {
if (this.flushTimer) clearInterval(this.flushTimer);
this.flushTimer = setInterval((() => {
if (this.batchQueue.length > 0) this.flush();
}), this.flushInterval);
}
async flush() {
if (this.isProcessing || this.batchQueue.length === 0) return;
this.isProcessing = !0;
const e = this.batchQueue.splice(0, this.batchSize);
try {
for (const t of e) switch (t.type) {
case "request":
this.collector.requestCounter.add(t.value, t.labels);
break;
case "error":
this.collector.errorCounter.add(t.value, t.labels);
break;
case "response_time":
this.collector.responseTimeHistogram.record(t.value, t.labels);
break;
case "connection":
this.collector.connectionCounter.add(t.value, t.labels);
break;
}
} catch (e) {
l.error("Failed to flush metrics batch:", e);
} finally {
this.isProcessing = !1;
}
}
destroy() {
if (this.flushTimer) {
clearInterval(this.flushTimer);
this.flushTimer = null;
}
this.flush();
}
}
class MetricsCollector {
constructor(e, t) {
this.memoryMonitorTimer = null;
this.serviceName = t;
this.startTime = Date.now();
this.pathCache = new PathNormalizationCache;
const r = e.getMeter(t);
this.initializeMetrics(r);
this.batchProcessor = new MetricsBatchProcessor(this);
this.setupMemoryMonitoring();
}
initializeMetrics(e) {
this.requestCounter = e.createCounter("requests_total", {
description: "Total requests across all protocols",
unit: "1"
});
this.errorCounter = e.createCounter("errors_total", {
description: "Total errors across all protocols",
unit: "1"
});
this.responseTimeHistogram = e.createHistogram("response_time_seconds", {
description: "Response time in seconds across all protocols",
unit: "s",
advice: {
explicitBucketBoundaries: [ .1, .5, 1, 2.5, 5, 10 ]
}
});
this.connectionCounter = e.createCounter("websocket_connections_total", {
description: "Total WebSocket connections",
unit: "1"
});
l.info(`Enhanced multi-protocol metrics initialized for service: ${this.serviceName}`);
}
collectRequestMetrics(e, t) {
try {
const r = this.detectProtocol(e);
const s = this.createLabelsOptimized(e, r);
this.batchProcessor.addMetric("request", s, 1);
this.batchProcessor.addMetric("response_time", s, t / 1e3);
if (this.isErrorStatus(e.status, r)) {
const t = {
...s,
error_type: this.getErrorType(e.status, r)
};
this.batchProcessor.addMetric("error", t, 1);
}
this.collectProtocolSpecificMetricsOptimized(e, r);
if (typeof l.debug === "function") l.debug(`Metrics collected for ${r.toUpperCase()} ${e.method} ${e.path}: ${t}ms, status: ${e.status}`);
} catch (e) {
l.error("Failed to collect metrics (non-blocking):", e);
}
}
detectProtocol(e) {
var t, r, s, o, i;
if (e._cachedProtocol) return e._cachedProtocol;
let n;
if (e.websocket || ((r = (t = e.req) === null || t === void 0 ? void 0 : t.headers) === null || r === void 0 ? void 0 : r.upgrade) === "websocket") n = L.WEBSOCKET; else if (e.rpc || ((i = (o = (s = e.req) === null || s === void 0 ? void 0 : s.headers) === null || o === void 0 ? void 0 : o["content-type"]) === null || i === void 0 ? void 0 : i.includes("application/grpc"))) n = L.GRPC; else n = L.HTTP;
Object.defineProperty(e, "_cachedProtocol", {
value: n,
writable: !1,
enumerable: !1
});
return n;
}
createLabelsOptimized(e, t) {
const r = {
method: e.method || "UNKNOWN",
status: (e.status || 200).toString(),
path: this.normalizePathOptimized(e.path || e.originalPath || "/"),
protocol: t
};
switch (t) {
case L.WEBSOCKET:
r["compression"] = this.getWebSocketCompression(e);
break;
case L.GRPC:
r["grpc_service"] = this.getGrpcService(e);
r["compression"] = this.getGrpcCompression(e);
break;
}
return r;
}
normalizePathOptimized(e) {
if (!e) return "/";
const t = this.pathCache.get(e);
if (t) return t;
const r = e.split("?")[0];
const s = r.replace(MetricsCollector.UUID_PATTERN, "/:uuid").replace(MetricsCollector.OBJECTID_PATTERN, "/:objectid").replace(MetricsCollector.NUMERIC_ID_PATTERN, "/:id");
this.pathCache.set(e, s);
return s;
}
collectProtocolSpecificMetricsOptimized(e, t) {
var r;
switch (t) {
case L.WEBSOCKET:
if (((r = e.websocket) === null || r === void 0 ? void 0 : r.readyState) === 1) this.batchProcessor.addMetric("connection", {
protocol: t,
service: this.serviceName
}, 1);
break;
}
}
setupMemoryMonitoring() {
const e = 6e4;
this.memoryMonitorTimer = setInterval((() => {
try {
const e = process.memoryUsage();
const t = this.pathCache.getStats();
if (typeof l.debug === "function") l.debug("Metrics collector memory stats:", {
heapUsed: Math.round(e.heapUsed / 1024 / 1024) + "MB",
cacheSize: t.size,
cacheHitRate: Math.round(t.hitRate * 100) + "%"
});
if (e.heapUsed > 500 * 1024 * 1024) {
this.pathCache.clear();
l.warn("High memory usage detected, cleared path normalization cache");
}
} catch (e) {
l.error("Memory monitoring error:", e);
}
}), e);
}
isErrorStatus(e, t) {
if (t === L.GRPC) return e !== 0;
return e >= 400;
}
getWebSocketCompression(e) {
var t, r;
const s = ((r = (t = e.req) === null || t === void 0 ? void 0 : t.headers) === null || r === void 0 ? void 0 : r["sec-websocket-extensions"]) || "";
return s.includes("permessage-deflate") ? "deflate" : "none";
}
getGrpcService(e) {
const t = e.path || e.originalPath || "";
const r = t.match(/^\/([^\/]+)\/[^\/]+$/);
return r ? r[1] : "unknown";
}
getGrpcCompression(e) {
var t, r, s, o;
const i = ((o = (s = (r = (t = e.rpc) === null || t === void 0 ? void 0 : t.call) === null || r === void 0 ? void 0 : r.metadata) === null || s === void 0 ? void 0 : s.get("accept-encoding")) === null || o === void 0 ? void 0 : o[0]) || "";
if (i.includes("br")) return "brotli";
if (i.includes("gzip")) return "gzip";
return "none";
}
getErrorType(e, t) {
if (t === L.GRPC) {
if (e === 0) return "ok";
if (e >= 1 && e <= 16) return "grpc_error";
return "unknown_error";
}
if (e >= 400 && e < 500) return "client_error";
if (e >= 500) return "server_error";
return "unknown_error";
}
recordCustomMetric(e, t, r = {}) {
try {
console.log(`Custom metric recorded: ${e} = ${t}`, r);
l.debug(`Custom metric recorded: ${e} = ${t}`, r);
} catch (t) {
l.error(`Failed to record custom metric ${e}:`, t);
}
}
getStats() {
return {
serviceName: this.serviceName,
uptime: Date.now() - this.startTime,
pathCacheStats: this.pathCache.getStats(),
memoryUsage: process.memoryUsage()
};
}
destroy() {
try {
this.batchProcessor.destroy();
this.pathCache.clear();
if (this.memoryMonitorTimer) {
clearInterval(this.memoryMonitorTimer);
this.memoryMonitorTimer = null;
}
l.info(`Metrics collector for ${this.serviceName} destroyed`);
} catch (e) {
l.error("Error during metrics collector destruction:", e);
}
}
}
MetricsCollector.UUID_PATTERN = /\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
MetricsCollector.OBJECTID_PATTERN = /\/[a-f0-9]{24}/g;
MetricsCollector.NUMERIC_ID_PATTERN = /\/\d+/g;
class MetricsCollectorManager {
constructor() {
this.collector = null;
this.lock = {
locked: !1
};
}
static getInstance() {
if (!MetricsCollectorManager.instance) MetricsCollectorManager.instance = new MetricsCollectorManager;
return MetricsCollectorManager.instance;
}
async setCollector(e) {
await this.acquireLock();
try {
if (this.collector) this.collector.destroy();
this.collector = e;
} finally {
this.releaseLock();
}
}
getCollector() {
return this.collector;
}
async acquireLock() {
while (this.lock.locked) await new Promise((e => setTimeout(e, 1)));
this.lock.locked = !0;
}
releaseLock() {
this.lock.locked = !1;
}
}
MetricsCollectorManager.instance = null;
function B(e, t) {
var r, s;
const o = ((r = t.metricsConf) === null || r === void 0 ? void 0 : r.metricsEndpoint) || process.env.NODE_ENV === "production";
if (!o || !((s = t.metricsConf) === null || s === void 0 ? void 0 : s.metricsEndpoint)) {
l.info("Prometheus metrics disabled: not in production or no metricsEndpoint configured");
return null;
}
try {
const r = new b({
endpoint: t.metricsConf.metricsEndpoint,
port: t.metricsConf.metricsPort || 9464
});
const s = new M({
readers: [ r ]
});
const o = MetricsCollectorManager.getInstance();
const i = new MetricsCollector(s, e.name || "koatty-app");
o.setCollector(i);
const n = () => {
const e = o.getCollector();
if (e) e.destroy();
};
process.once("SIGTERM", n);
process.once("SIGINT", n);
l.info(`Enhanced Prometheus metrics initialized on port ${t.metricsConf.metricsPort || 9464}, endpoint: ${t.metricsConf.metricsEndpoint}`);
return s;
} catch (e) {
l.error("Failed to initialize Prometheus exporter:", e);
return null;
}
}
function U() {
return MetricsCollectorManager.getInstance().getCollector();
}
function F(e, t) {
const r = U();
if (r) r.collectRequestMetrics(e, t);
}
class BaseHandler {
commonPreHandle(e, t) {
e.encoding = t === null || t === void 0 ? void 0 : t.encoding;
this.setSecurityHeaders(e);
this.startTraceSpan(e, t);
}
commonPostHandle(e, t, r) {
this.logRequest(e, t, r);
this.endTraceSpan(e, t, r);
this.collectMetrics(e, t);
}
handleError(e, t, r) {
return R(t, e, r);
}
setSecurityHeaders(e) {
e.set("X-Content-Type-Options", "nosniff");
e.set("X-Frame-Options", "DENY");
e.set("X-XSS-Protection", "1; mode=block");
}
startTraceSpan(e, t) {
if (t.spanManager) t.spanManager.setSpanAttributes({
[m.HTTP_URL]: e.originalUrl,
[m.HTTP_METHOD]: e.method
});
}
endTraceSpan(e, t, r) {
if (t.spanManager) {
t.spanManager.setSpanAttributes({
[m.HTTP_STATUS_CODE]: e.status,
[m.HTTP_METHOD]: e.method,
[m.HTTP_URL]: e.url
});
t.spanManager.addSpanEvent("request", {
message: r
});
t.spanManager.endSpan();
}
}
collectMetrics(e, t) {
if (e.startTime) {
const t = Date.now() - e.startTime;
F(e, t);
}
}
logRequest(e, t, r) {
l[e.status >= 400 ? "Error" : "Info"](r);
}
}
var G;
(function(e) {
e["HTTP"] = "http";
e["GRPC"] = "grpc";
e["WS"] = "ws";
e["GRAPHQL"] = "graphql";
})(G || (G = {}));
const W = [ 204, 205, 304 ];
function j(e) {
const t = e.get("Accept-Encoding") || "";
const r = {
threshold: 1024,
filter: e => !/^image\//i.test(e)
};
if (t.includes("br")) {
r.br = {
params: {
[P.constants.BROTLI_PARAM_QUALITY]: 4
}
};
return C(r);
} else if (t.includes("gzip")) {
r.gzip = {
flush: P.constants.Z_SYNC_FLUSH
};
r.br = !1;
return C(r);
}
return (e, t) => t();
}
function Q(e, t) {
if (!1 === e.respond) return;
if (!e.writable) return;
const r = e.res;
let s = e.body;
const o = e.status;
if (W.includes(o)) {
e.body = null;
return r.end();
}
if ("HEAD" === e.method) {
if (!r.headersSent && !e.response.has("Content-Length")) {
const {length: t} = e.response;
if (Number.isInteger(t)) e.length = t;
}
return r.end();
}
if (null == s) {
if (e.response._explicitNullBody) {
e.response.remove("Content-Type");
e.response.remove("Transfer-Encoding");
return r.end();
}
if (e.req.httpVersionMajor >= 2) s = String(o); else s = e.message || String(o);
if (!r.headersSent) {
e.type = "text";
e.length = Buffer.byteLength(s);
}
return r.end(s);
}
if (o === 404) e.status = 200;
return j(e)(e, (async () => {
if (Buffer.isBuffer(s)) return r.end(s);
if ("string" === typeof s) return r.end(s);
if (s instanceof w) {
const e = s;
e.on("error", (t => {
l.Error("Response stream error:", t);
e.destroy();
}));
return e.pipe(r);
}
s = JSON.stringify(s);
if (!r.headersSent) e.length = Buffer.byteLength(s);
return r.end(s);
}));
}
/**
*
* @Description:
* @Author: richen
* @Date: 2025-04-04 12:21:48
* @LastEditTime: 2025-04-04 20:00:41
* @License: BSD (3-Clause)
* @Copyright (c): <richenlin(at)gmail.com>
*/ class HttpHandler extends BaseHandler {
constructor() {
super();
}
static getInstance() {
if (!HttpHandler.instance) HttpHandler.instance = new HttpHandler;
return HttpHandler.instance;
}
async handle(e, t, r) {
var s;
const o = r.timeout || 1e4;
this.commonPreHandle(e, r);
(s = e === null || e === void 0 ? void 0 : e.res) === null || s === void 0 ? void 0 : s.once("finish", (() => {
const t = Date.now();
const s = `{"action":"${e.method}","status":"${e.status}","startTime":"${e.startTime}","duration":"${t - e.startTime || 0}","requestId":"${e.requestId}","endTime":"${t}","path":"${e.originalPath || "/"}"}`;
this.commonPostHandle(e, r, s);
}));
const i = e.res;
try {
if (!r.terminated) {
i.timeout = new Promise(((e, t) => {
setTimeout((() => {
t(new Error("Deadline exceeded"));
}), o);
}));
await Promise.race([ t(), i.timeout ]).then((() => {
clearTimeout(i.timeout);
})).catch((e => {
clearTimeout(i.timeout);
throw e;
}));
}
if (e.body !== void 0 && e.status === 404) e.status = 200;
if (e.status >= 400) throw new h(e.message, 1, e.status);
return Q(e, r);
} catch (t) {
return this.handleError(t, e, r);
} finally {
clearTimeout(i.timeout);
}
}
}
/**
*
* @Description:
* @Author: richen
* @Date: 2025-03-21 22:07:11
* @LastEditTime: 2025-03-23 11:41:02
* @License: BSD (3-Clause)
* @Copyright (c): <richenlin(at)gmail.com>
*/ class GrpcHandler extends BaseHandler {
constructor() {
super();
}
static getInstance() {
if (!GrpcHandler.instance) GrpcHandler.instance = new GrpcHandler;
return GrpcHandler.instance;
}
async handle(e, t, r) {
var s, o, i;
const n = r.timeout || 1e4;
const a = e.rpc.call.metadata.get("accept-encoding")[0] || "";
const c = a.includes("br") ? "brotli" : a.includes("gzip") ? "gzip" : "none";
(o = (s = e === null || e === void 0 ? void 0 : e.rpc) === null || s === void 0 ? void 0 : s.call) === null || o === void 0 ? void 0 : o.sendMetadata(e.rpc.call.metadata);
this.commonPreHandle(e, r);
e.res.once("finish", (() => {
const t = Date.now();
const s = p(e.status);
const o = `{"action":"${e.method}","status":"${s}","startTime":"${e.startTime}","duration":"${t - e.startTime || 0}","requestId":"${e.requestId}","endTime":"${t}","path":"${e.originalPath}"}`;
this.commonPostHandle(e, r, o);
}));
((i = e === null || e === void 0 ? void 0 : e.rpc) === null || i === void 0 ? void 0 : i.call).once("error", (t => {
this.handleError(t, e, r);
}));
const l = {};
try {
if (!r.terminated) {
l.timeout = new Promise(((e, t) => {
setTimeout((() => {
t(new Error("Deadline exceeded"));
}), n);
}));
await Promise.race([ t(), l.timeout ]).then((() => {
clearTimeout(l.timeout);
})).catch((e => {
clearTimeout(l.timeout);
throw e;
}));
}
if (e.body !== void 0 && e.status === 404) e.status = 200;
if (e.status >= 400) throw new h(e.message, 0, e.status);
if (c !== "none" && e.body instanceof E) {
const t = c === "gzip" ? I.createGzip({
level: 6
}) : I.createBrotliCompress({
params: {
[I.constants.BROTLI_PARAM_QUALITY]: 4
}
});
e.body = e.body.pipe(t);
}
e.rpc.callback(null, e.body);
return null;
} catch (t) {
return this.handleError(t, e, r);
} finally {
e.res.emit("finish");
}
}
}
class WsHandler extends BaseHandler {
constructor() {
super();
}
static getInstance() {
if (!WsHandler.instance) WsHandler.instance = new WsHandler;
return WsHandler.instance;
}
async handle(e, r, s) {
var o, i;
const n = (s === null || s === void 0 ? void 0 : s.timeout) || 1e4;
const a = e.req.headers["sec-websocket-extensions"] || "";
const c = a.includes("permessage-deflate");
this.commonPreHandle(e, s);
(o = e === null || e === void 0 ? void 0 : e.res) === null || o === void 0 ? void 0 : o.once("finish", (() => {
const t = Date.now();
const r = `{"action":"${e.method}","status":"${e.status}","startTime":"${e.startTime}","duration":"${t - e.startTime || 0}","requestId":"${e.requestId}","endTime":"${t}","path":"${e.originalPath || "/"}"}`;
this.commonPostHandle(e, s, r);
}));
const l = e.res;
try {
if (!s.terminated) {
l.timeout = new Promise(((e, t) => {
setTimeout((() => {
t(new Error("Deadline exceeded"));
}), n);
}));
await Promise.race([ r(), l.timeout ]).then((() => {
clearTimeout(l.timeout);
})).catch((e => {
clearTimeout(l.timeout);
throw e;
}));
}
if (e.body !== void 0 && e.status === 404) e.status = 200;
if (e.status >= 400) throw new h(e.message, 1, e.status);
if (((i = e === null || e === void 0 ? void 0 : e.websocket) === null || i === void 0 ? void 0 : i.readyState) === 1 && !t.isTrueEmpty(e.body)) {
const t = c ? {
compress: !0
} : {};
e.websocket.send(H(e.body), t);
}
return null;
} catch (t) {
return this.handleError(t, e, s);
} finally {
e.res.emit("finish");
}
}
}
var K;
class HandlerFactory {
static register(e, t) {
this.handlers.set(e, t);
}
static getHandler(e) {
const t = this.handlers.get(e);
if (!t) {
l.warn(`Handler for protocol ${e} not found, falling back to HTTP`);
return this.handlers.get(G.HTTP);
}
return t;
}
}
K = HandlerFactory;
HandlerFactory.handlers = new Map;
(() => {
K.register(G.HTTP, HttpHandler.getInstance());
K.register(G.GRPC, GrpcHandler.getInstance());
K.register(G.WS, WsHandler.getInstance());
})();
/**
*
* @Description:
* @Author: richen
* @Date: 2025-03-23 01:11:24
* @LastEditTime: 2025-03-23 10:47:44
* @License: BSD (3-Clause)
* @Copyright (c): <richenlin(at)gmail.com>
*/
/**
* Async context tracking module
*
* @Description: Provides async context tracking using async_hooks
* @Author: richen
* @Date: 2021-11-18 10:44:51
* @LastEditTime: 2025-03-23 00:56:47
* @License: BSD (3-Clause)
* @Copyright (c): <richenlin(at)gmail.com>
* @Usage:
*/ const V = new k;
const X = {
add: [ "on", "addListener" ],
remove: [ "off", "removeListener" ]
};
const Y = new Map;
function J(e = Symbol("koatty-tracer").toString()) {
const t = new D(e);
t.emitDestroy = () => {
Y.clear();
D.prototype.emitDestroy.call(t);
return t;
};
return t;
}
function Z(e, t) {
for (const r of X.add) ee(e, r, (r => function(s, o) {
const i = `${s}:${o.toString()}`;
if (!Y.has(i)) {
const n = (...r) => {
t.runInAsyncScope(o, e, ...r);
};
Y.set(i, n);
return r.call(this, s, n);
}
return r.call(this, s, o);
}));
for (const t of X.remove) ee(e, t, (e => function(t, r) {
const s = `${t}:${r.toString()}`;
if (Y.has(s)) Y.delete(s);
return e.call(this, t, r);
}));
}
function ee(e, t, r) {
if (!(t in e)) return;
const s = e[t];
if (typeof s !== "function") return;
const o = r(s, t);
e[t] = o;
return o;
}
/**
*
* @Description:
* @Author: richen
* @Date: 2025-03-20 12:08:57
* @LastEditTime: 2025-04-02 15:03:04
* @License: BSD (3-Clause)
* @Copyright (c): <richenlin(at)gmail.com>
*/ class Logger {
error(...e) {
l.error(e);
}
warn(...e) {
l.warn(e);
}
info(...e) {
l.info(e);
}
debug(...e) {
l.debug(e);
}
verbose(...e) {
l.debug(e);
}
}
/**
*
* @Description:
* @Author: richen
* @Date: 2025-04-04 12:21:48
* @LastEditTime: 2025-04-04 19:11:05
* @License: BSD (3-Clause)
* @Copyright (c): <richenlin(at)gmail.com>
*/ class RetryOTLPTraceExporter extends z {
constructor(e) {
super(e);
this.failureCount = 0;
this.lastFailureTime = 0;
this.circuitState = "CLOSED";
this.maxRetries = e.maxRetries || 3;
this.retryDelay = e.retryDelay || 1e3;
this.failureThreshold = e.failureThreshold || 5;
this.resetTimeout = e.resetTimeout || 3e4;
}
async export(e, t) {
if (this.circuitState === "OPEN") {
const e = Date.now();
if (e - this.lastFailureTime > this.resetTimeout) this.circuitState = "HALF_OPEN"; else {
t({
code: c.FAILED,
error: new Error("Circuit breaker is open - skipping export")
});
return;
}
}
let r;
for (let s = 1; s <= this.maxRetries; s++) try {
const r = await super.export(e, t);
if (this.circuitState === "HALF_OPEN") this.resetCircuit();
return r;
} catch (e) {
r = e;
this.failureCount++;
if (this.failureCount >= this.failureThreshold) this.tripCircuit();
if (s < this.maxRetries) await new Promise((e => setTimeout(e, this.retryDelay * s)));
}
t({
code: c.FAILED,
error: r
});
}
tripCircuit() {
this.circuitState = "OPEN";
this.lastFailureTime = Date.now();
this.failureCount = 0;
l.warn("Circuit breaker tripped - stopping exports temporarily");
}
resetCircuit() {
this.circuitState = "CLOSED";
this.failureCount = 0;
l.info("Circuit breaker reset - exports resumed");
}
}
/**
*
* @Description:
* @Author: richen
* @Date: 2025-04-04 12:21:48
* @LastEditTime: 2025-04-04 19:11:05
* @License: BSD (3-Clause)
* @Copyright (c): <richenlin(at)gmail.com>
*/ function te(e, t) {
const r = process.env.OTEL_SERVICE_NAME || e.name;
if (!r) throw new Error("Service name is required");
return O(Object.assign({
[S]: r,
[T]: process.env.OTEL_SERVICE_VERSION || e.version || "1.0.0",
[y]: process.env.OTEL_ENV || e.env || "development",
[g]: "opentelemetry",
[v]: "nodejs",
[f]: process.env.OTEL_SDK_VERSION || "1.0.0",
"process.pid": process.pid
}, t.otlpResourceAttributes || {}));
}
/**
*
* @Description:
* @Author: richen
* @Date: 2025-04-04 12:21:48
* @LastEditTime: 2025-04-04 19:11:05
* @License: BSD (3-Clause)
* @Copyright (c): <richenlin(at)gmail.com>
*/ let re = !1;
function se(e, t) {
var r, s, o, a, c, d, u, h;
const p = ((r = t.opentelemetryConf) === null || r === void 0 ? void 0 : r.endpoint) || process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
if (!p) throw new Error("OTLP endpoint is required");
const m = new RetryOTLPTraceExporter({
url: p,
headers: ((s = t.opentelemetryConf) === null || s === void 0 ? void 0 : s.headers) || {},
timeoutMillis: ((o = t.opentelemetryConf) === null || o === void 0 ? void 0 : o.timeout) || 1e4,
maxRetries: 3,
retryDelay: 1e3
});
const f = {
maxQueueSize: (a = t.opentelemetryConf) === null || a === void 0 ? void 0 : a.batchMaxQueueSize,
maxExportBatchSize: (c = t.opentelemetryConf) === null || c === void 0 ? void 0 : c.batchMaxExportSize,
scheduledDelayMillis: (d = t.opentelemetryConf) === null || d === void 0 ? void 0 : d.batchDelayMillis,
exportTimeoutMillis: (u = t.opentelemetryConf) === null || u === void 0 ? void 0 : u.batchExportTimeout
};
if (!re) {
const e = l.getLevel();
const t = Object.values(i).find((t => t.toString() === e.toString())) || i.INFO;
n.setLogger(new Logger, t);
re = !0;
}
const v = B(e, t);
const g = {
resource: te(e, t),
traceExporter: m,
spanProcessors: [ new A(m, f) ],
instrumentations: ((h = t.opentelemetryConf) === null || h === void 0 ? void 0 : h.instrumentations) || [ x({
"@opentelemetry/instrumentation-grpc": {
enabled: !0
},
"@opentelemetry/instrumentation-koa": {
enabled: !0
}
}) ]
};
if (v) g.readers = [ v ];
return new _(g);
}
async function oe(e, t, r) {
var o;
const i = async () => {
try {
await e.shutdown();
l.info("OpenTelemetry SDK shut down successfully");
} catch (e) {
l.error("Error shutting down OpenTelemetry SDK", e);
} finally {
t.off("appStop", i);
}
};
try {
await e.start();
l.info("OpenTelemetry SDK started successfully");
} catch (e) {
l.error(`OpenTelemetry SDK initialization failed: ${e.message}`, {
stack: e.stack,
code: e.code,
config: {
endpoint: (o = r.opentelemetryConf) === null || o === void 0 ? void 0 : o.endpoint,
serviceName: t.name
}
});
s.setGlobalTracerProvider(new q);
return;
} finally {
t.on("appStop", i);
}
}
/**
*
* @Description: 链路拓扑分析器
* @Author: richen
* @Date: 2025-04-06 12:40:00
* @LastEditTime: 2025-04-06 12:40:00
* @License: BSD (3-Clause)
* @Copyright (c): <richenlin(at)gmail.com>
*/ class TopologyAnalyzer {
constructor() {
this.serviceMap = new Map;
}
static getInstance() {
if (!TopologyAnalyzer.instance) TopologyAnalyzer.instance = new TopologyAnalyzer;
return TopologyAnalyzer.instance;
}
recordServiceDependency(e, t) {
if (!this.serviceMap.has(e)) this.serviceMap.set(e, {
name: e,
dependencies: new Set
});
if (t && !this.serviceMap.get(e).dependencies.has(t)) {
this.serviceMap.get(e).dependencies.add(t);
l.debug(`Recorded service dependency: ${e} -> ${t}`);
}
}
getServiceDependencies(e) {
const t = this.serviceMap.get(e);
return t ? Array.from(t.dependencies) : [];
}
getFullTopology() {
const e = {};
this.serviceMap.forEach(((t, r) => {
e[r] = Array.from(t.dependencies);
}));
return e;
}
visualizeTopology() {
let e = "digraph G {\n";
this.serviceMap.forEach((t => {
t.dependencies.forEach((r => {
e += ` "${t.name}" -> "${r}";\n`;
}));
}));
e += "}";
return e;
}
}
/*
* @Description:
* @Usage:
* @Author: richen
* @Date: 2020-11-20 17:37:32
* @LastEditors: Please set LastEditors
* @LastEditTime: 2023-11-10 22:18:40
* @License: BSD (3-Clause)
* @Copyright (c) - <richenlin(at)gmail.com>
*/ function ie(e, r) {
var s, o;
let i = "";
switch (e.protocol) {
case "grpc":
const n = (e === null || e === void 0 ? void 0 : e.getMetaData("_body")[0]) || {};
i = (e === null || e === void 0 ? void 0 : e.getMetaData(r.requestIdName)) || n[r.requestIdName] || "";
break;
default:
if (r.requestIdHeaderName) {
const n = ((s = e.headers) === null || s === void 0 ? void 0 : s[r.requestIdHeaderName.toLowerCase()]) || ((o = e.query) === null || o === void 0 ? void 0 : o[r.requestIdName]) || "";
i = t.isArray(n) ? n.join(".") : n;
}
}
return i || ne(r);
}
function ne(e) {
return t.isFunction(e === null || e === void 0 ? void 0 : e.idFactory) ? e.idFactory() : N();
}
/**
*
* @Description:
* @Author: richen
* @Date: 2025-04-04 12:21:48
* @LastEditTime: 2025-04-06 12:59:31
* @License: BSD (3-Clause)
* @Copyright (c): <richenlin(at)gmail.com>
*/ const ae = {
timeout: 1e4,
requestIdHeaderName: "X-Request-Id",
requestIdName: "requestId",
idFactory: ne,
encoding: "utf-8",
enableTrace: !1,
asyncHooks: !1,
metricsConf: {
defaultAttributes: {},
metricsEndpoint: "/metrics",
reportInterval: 5e3,
metricsPort: 9464
},
opentelemetryConf: {
endpoint: "http://localhost:4318/v1/traces",
enableTopology: !1,
headers: {},
resourceAttributes: {},
instrumentations: [],
timeout: 1e4,
spanTimeout: 3e4,
maxActiveSpans: 1e3,
samplingRate: 1,
batchMaxQueueSize: 2048,
batchMaxExportSize: 512,
batchDelayMillis: 5e3,
batchExportTimeout: 3e4
},
retryConf: {
enabled: !1,
count: 3,
interval: 1e3
}
};
function ce(r, s) {
r = {
...ae,
...r
};
const o = e.getClass("ExceptionHandler", "COMPONENT");
let i;
let n;
if (r.enableTrace) {
i = s.getMetaData("spanManager")[0] || new SpanManager(r);
n = s.getMetaData("tracer")[0] || se(s, r);
s.once("appStart", (async () => {
await oe(n, s, r);
}));
}
return async (e, a) => {
var c, l, d;
t.define(e, "startTime", Date.now());
if (((c = s === null || s === void 0 ? void 0 : s.server) === null || c === void 0 ? void 0 : c.status) === 503) {
e.status = 503;
e.set("Connection", "close");
e.body = "Server is in the process of shutting down";
return;
}
const u = ie(e, r);
t.define(e, "requestId", u);
if (r.enableTrace && n) {
const t = s.name || "unknownKoattyProject";
i.createSpan(n, e, t);
s.once("appStop", (() => {
i === null || i === void 0 ? void 0 : i.endSpan();
}));
}
if ((d = (l = r.opentelemetryConf) === null || l === void 0 ? void 0 : l.enableTopology) !== null && d !== void 0 ? d : r.enableTrace) {
const t = TopologyAnalyzer.getInstance();
const r = Array.isArray(e.headers["service"]) ? e.headers["service"][0] : e.headers["service"] || "unknown";
t.recordServiceDependency(s.name, r);
}
const h = {
debug: s.appDebug,
timeout: r.timeout,
encoding: r.encoding,
requestId: u,
terminated: !1,
spanManager: i,
globalErrorHandler: o
};
if (r.asyncHooks && (e.req || e.res)) {
const t = J();
return V.run(u, (() => {
if (e.req) Z(e.req, t);
if (e.res) Z(e.res, t);
return le(e, a, r, h);
}));
}
return le(e, a, r, h);
};
}
async function le(e, t, r, s) {
const o = d.now();
let i;
const n = r.retryConf || {
enabled: !1
};
try {
if (n.enabled) {
const o = n.count || 3;
const a = n.interval || 1e3;
for (let c = 0; c <= o; c++) try {
i = await de(e, t, r, s);
break;
} catch (e) {
const t = n.conditions ? n.conditions(e) : !0;
if (!t || c === o) throw e;
if (a > 0) await new Promise((e => setTimeout(e, a)));
}
} else i = await de(e, t, r, s);
} finally {
const t = d.now() - o;
F(e, t);
const s = r.metricsConf || {};
if (s.reporter) try {
s.reporter({
duration: t,
status: e.status || 200,
path: e.path,
attributes: {
...s.defaultAttributes || {},
requestId: e.requestId,
method: e.method,
protocol: e.protocol
}
});
} catch (e) {
l.warn("Metrics reporter error:", e);
}
}
return i;
}
async function de(e, t, r, s) {
var o, i;
if (r.requestIdName && e.setMetaData) e.setMetaData(r.requestIdName, e.requestId);
const n = ((e === null || e === void 0 ? void 0 : e.protocol) || "http").toLowerCase();
if (n === "grpc" || n === "ws" || n === "wss") e.respond = !1;
if (r.requestIdHeaderName) e.set(r.requestIdHeaderName, e.requestId);
if (((i = (o = e.rpc) === null || o === void 0 ? void 0 : o.call) === null || i === void 0 ? void 0 : i.metadata) && r.requestIdName) e.rpc.call.metadata.set(r.requestIdName, e.requestId);
const a = HandlerFactory.getHandler(n);
return a.handle(e, t, s);
}
export { ce as Trace };