koatty_trace
Version:
Full link tracking and error interception for koatty.
1,568 lines (1,488 loc) • 45.7 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/
*/
;
var e = require("koatty_container");
var t = require("koatty_lib");
var r = require("@opentelemetry/api");
var s = require("@opentelemetry/core");
var o = require("koatty_logger");
var i = require("node:perf_hooks");
var n = require("koatty_exception");
var a = require("@opentelemetry/semantic-conventions");
var c = require("@opentelemetry/exporter-prometheus");
var l = require("@opentelemetry/sdk-metrics");
var u = require("koa-compress");
var d = require("stream");
var h = require("node:zlib");
var p = require("zlib");
var m = require("util");
var f = require("async_hooks");
var g = require("@opentelemetry/sdk-node");
var v = require("@opentelemetry/sdk-trace-base");
var y = require("@opentelemetry/auto-instrumentations-node");
var T = require("@opentelemetry/exporter-trace-otlp-http");
var S = require("@opentelemetry/resources");
var b = require("node:crypto");
function E(e) {
var t = Object.create(null);
if (e) Object.keys(e).forEach((function(r) {
if (r !== "default") {
var s = Object.getOwnPropertyDescriptor(e, r);
Object.defineProperty(t, r, s.get ? s : {
enumerable: !0,
get: function() {
return e[r];
}
});
}
}));
t.default = e;
return Object.freeze(t);
}
var C = E(p);
/*
* @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 s.W3CTraceContextPropagator;
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 o.DefaultLogger.debug === "function" && this.activeSpans.size > 0) o.DefaultLogger.debug("SpanManager stats:", {
activeSpans: this.activeSpans.size,
...this.stats,
memoryUsage: Math.round(process.memoryUsage().heapUsed / 1024 / 1024) + "MB"
});
} catch (e) {
this.stats.errors++;
o.DefaultLogger.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) {
o.DefaultLogger.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) o.DefaultLogger.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++;
o.DefaultLogger.debug(`Span force-ended: ${e}, reason: ${t}`);
} catch (t) {
this.stats.errors++;
o.DefaultLogger.error(`Error force-ending span ${e}:`, t);
}
}
createSpan(e, t, r) {
if (this.isDestroyed) {
o.DefaultLogger.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)) {
o.DefaultLogger.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++;
o.DefaultLogger.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)) {
o.DefaultLogger.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++;
o.DefaultLogger.error("Failed to setup span timeout:", e);
throw e;
}
}
injectContext(e) {
if (!this.span || this.isDestroyed) return;
try {
const t = {};
r.context.with(r.trace.setSpan(r.context.active(), this.span), (() => {
this.propagator.inject(r.context.active(), t, r.defaultTextMapSetter);
Object.entries(t).forEach((([t, r]) => {
if (e.set && typeof e.set === "function") e.set(t, r);
}));
}));
} catch (e) {
this.stats.errors++;
o.DefaultLogger.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) {
o.DefaultLogger.error("Error applying custom span attributes:", e);
}
} catch (e) {
this.stats.errors++;
o.DefaultLogger.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++;
o.DefaultLogger.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++;
o.DefaultLogger.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++;
o.DefaultLogger.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;
}
o.DefaultLogger.info("SpanManager destroyed successfully", this.getStats());
} catch (e) {
o.DefaultLogger.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 M(r, s, o) {
var i;
const {message: a, status: c} = D(r, s);
const l = s.code || 1;
const u = s.stack;
const d = (i = o.spanManager) === null || i === void 0 ? void 0 : i.getSpan();
if (n.isException(s)) return s.setCode(l).setStatus(c).setMessage(a).setSpan(d).setStack(u).handler(r);
const h = e.IOCContainer.getInsByClass(o.globalErrorHandler, [ a, l, c, u, d ]);
if (t.Helper.isFunction(h === null || h === void 0 ? void 0 : h.handler)) return h.handler(r);
return new n.Exception(a, l, c, u, d).handler(r);
}
function D(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 w;
(function(e) {
e["HTTP"] = "http";
e["WEBSOCKET"] = "websocket";
e["GRPC"] = "grpc";
})(w || (w = {}));
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) {
o.DefaultLogger.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"
});
o.DefaultLogger.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 o.DefaultLogger.debug === "function") o.DefaultLogger.debug(`Metrics collected for ${r.toUpperCase()} ${e.method} ${e.path}: ${t}ms, status: ${e.status}`);
} catch (e) {
o.DefaultLogger.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 = w.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 = w.GRPC; else n = w.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 w.WEBSOCKET:
r["compression"] = this.getWebSocketCompression(e);
break;
case w.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 w.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 o.DefaultLogger.debug === "function") o.DefaultLogger.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();
o.DefaultLogger.warn("High memory usage detected, cleared path normalization cache");
}
} catch (e) {
o.DefaultLogger.error("Memory monitoring error:", e);
}
}), e);
}
isErrorStatus(e, t) {
if (t === w.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 === w.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);
o.DefaultLogger.debug(`Custom metric recorded: ${e} = ${t}`, r);
} catch (t) {
o.DefaultLogger.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;
}
o.DefaultLogger.info(`Metrics collector for ${this.serviceName} destroyed`);
} catch (e) {
o.DefaultLogger.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 P(e, t) {
var r, s;
const i = ((r = t.metricsConf) === null || r === void 0 ? void 0 : r.metricsEndpoint) || process.env.NODE_ENV === "production";
if (!i || !((s = t.metricsConf) === null || s === void 0 ? void 0 : s.metricsEndpoint)) {
o.DefaultLogger.info("Prometheus metrics disabled: not in production or no metricsEndpoint configured");
return null;
}
try {
const r = new c.PrometheusExporter({
endpoint: t.metricsConf.metricsEndpoint,
port: t.metricsConf.metricsPort || 9464
});
const s = new l.MeterProvider({
readers: [ r ]
});
const i = MetricsCollectorManager.getInstance();
const n = new MetricsCollector(s, e.name || "koatty-app");
i.setCollector(n);
const a = () => {
const e = i.getCollector();
if (e) e.destroy();
};
process.once("SIGTERM", a);
process.once("SIGINT", a);
o.DefaultLogger.info(`Enhanced Prometheus metrics initialized on port ${t.metricsConf.metricsPort || 9464}, endpoint: ${t.metricsConf.metricsEndpoint}`);
return s;
} catch (e) {
o.DefaultLogger.error("Failed to initialize Prometheus exporter:", e);
return null;
}
}
function L() {
return MetricsCollectorManager.getInstance().getCollector();
}
function I(e, t) {
const r = L();
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 M(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({
[a.SemanticAttributes.HTTP_URL]: e.originalUrl,
[a.SemanticAttributes.HTTP_METHOD]: e.method
});
}
endTraceSpan(e, t, r) {
if (t.spanManager) {
t.spanManager.setSpanAttributes({
[a.SemanticAttributes.HTTP_STATUS_CODE]: e.status,
[a.SemanticAttributes.HTTP_METHOD]: e.method,
[a.SemanticAttributes.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;
I(e, t);
}
}
logRequest(e, t, r) {
o.DefaultLogger[e.status >= 400 ? "Error" : "Info"](r);
}
}
var H;
(function(e) {
e["HTTP"] = "http";
e["GRPC"] = "grpc";
e["WS"] = "ws";
e["GRAPHQL"] = "graphql";
})(H || (H = {}));
const A = [ 204, 205, 304 ];
function q(e) {
const t = e.get("Accept-Encoding") || "";
const r = {
threshold: 1024,
filter: e => !/^image\//i.test(e)
};
if (t.includes("br")) {
r.br = {
params: {
[h.constants.BROTLI_PARAM_QUALITY]: 4
}
};
return u(r);
} else if (t.includes("gzip")) {
r.gzip = {
flush: h.constants.Z_SYNC_FLUSH
};
r.br = !1;
return u(r);
}
return (e, t) => t();
}
function _(e, t) {
if (!1 === e.respond) return;
if (!e.writable) return;
const r = e.res;
let s = e.body;
const i = e.status;
if (A.includes(i)) {
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(i); else s = e.message || String(i);
if (!r.headersSent) {
e.type = "text";
e.length = Buffer.byteLength(s);
}
return r.end(s);
}
if (i === 404) e.status = 200;
return q(e)(e, (async () => {
if (Buffer.isBuffer(s)) return r.end(s);
if ("string" === typeof s) return r.end(s);
if (s instanceof d.Readable) {
const e = s;
e.on("error", (t => {
o.DefaultLogger.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 n.Exception(e.message, 1, e.status);
return _(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 a = r.timeout || 1e4;
const c = e.rpc.call.metadata.get("accept-encoding")[0] || "";
const l = c.includes("br") ? "brotli" : c.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 = n.StatusCodeConvert(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 u = {};
try {
if (!r.terminated) {
u.timeout = new Promise(((e, t) => {
setTimeout((() => {
t(new Error("Deadline exceeded"));
}), a);
}));
await Promise.race([ t(), u.timeout ]).then((() => {
clearTimeout(u.timeout);
})).catch((e => {
clearTimeout(u.timeout);
throw e;
}));
}
if (e.body !== void 0 && e.status === 404) e.status = 200;
if (e.status >= 400) throw new n.Exception(e.message, 0, e.status);
if (l !== "none" && e.body instanceof d.Stream) {
const t = l === "gzip" ? C.createGzip({
level: 6
}) : C.createBrotliCompress({
params: {
[C.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 a = (s === null || s === void 0 ? void 0 : s.timeout) || 1e4;
const c = e.req.headers["sec-websocket-extensions"] || "";
const l = c.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 u = e.res;
try {
if (!s.terminated) {
u.timeout = new Promise(((e, t) => {
setTimeout((() => {
t(new Error("Deadline exceeded"));
}), a);
}));
await Promise.race([ r(), u.timeout ]).then((() => {
clearTimeout(u.timeout);
})).catch((e => {
clearTimeout(u.timeout);
throw e;
}));
}
if (e.body !== void 0 && e.status === 404) e.status = 200;
if (e.status >= 400) throw new n.Exception(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.Helper.isTrueEmpty(e.body)) {
const t = l ? {
compress: !0
} : {};
e.websocket.send(m.inspect(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) {
o.DefaultLogger.warn(`Handler for protocol ${e} not found, falling back to HTTP`);
return this.handlers.get(H.HTTP);
}
return t;
}
}
k = HandlerFactory;
HandlerFactory.handlers = new Map;
(() => {
k.register(H.HTTP, HttpHandler.getInstance());
k.register(H.GRPC, GrpcHandler.getInstance());
k.register(H.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 x = new f.AsyncLocalStorage;
const R = {
add: [ "on", "addListener" ],
remove: [ "off", "removeListener" ]
};
const O = new Map;
function N(e = Symbol("koatty-tracer").toString()) {
const t = new f.AsyncResource(e);
t.emitDestroy = () => {
O.clear();
f.AsyncResource.prototype.emitDestroy.call(t);
return t;
};
return t;
}
function z(e, t) {
for (const r of R.add) $(e, r, (r => function(s, o) {
const i = `${s}:${o.toString()}`;
if (!O.has(i)) {
const n = (...r) => {
t.runInAsyncScope(o, e, ...r);
};
O.set(i, n);
return r.call(this, s, n);
}
return r.call(this, s, o);
}));
for (const t of R.remove) $(e, t, (e => function(t, r) {
const s = `${t}:${r.toString()}`;
if (O.has(s)) O.delete(s);
return e.call(this, t, r);
}));
}
function $(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) {
o.DefaultLogger.error(e);
}
warn(...e) {
o.DefaultLogger.warn(e);
}
info(...e) {
o.DefaultLogger.info(e);
}
debug(...e) {
o.DefaultLogger.debug(e);
}
verbose(...e) {
o.DefaultLogger.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 T.OTLPTraceExporter {
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: s.ExportResultCode.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: s.ExportResultCode.FAILED,
error: r
});
}
tripCircuit() {
this.circuitState = "OPEN";
this.lastFailureTime = Date.now();
this.failureCount = 0;
o.DefaultLogger.warn("Circuit breaker tripped - stopping exports temporarily");
}
resetCircuit() {
this.circuitState = "CLOSED";
this.failureCount = 0;
o.DefaultLogger.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 U(e, t) {
const r = process.env.OTEL_SERVICE_NAME || e.name;
if (!r) throw new Error("Service name is required");
return S.resourceFromAttributes(Object.assign({
[a.ATTR_SERVICE_NAME]: r,
[a.ATTR_SERVICE_VERSION]: process.env.OTEL_SERVICE_VERSION || e.version || "1.0.0",
[a.SEMRESATTRS_DEPLOYMENT_ENVIRONMENT]: process.env.OTEL_ENV || e.env || "development",
[a.ATTR_TELEMETRY_SDK_NAME]: "opentelemetry",
[a.ATTR_TELEMETRY_SDK_LANGUAGE]: "nodejs",
[a.ATTR_TELEMETRY_SDK_VERSION]: 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 B = !1;
function F(e, t) {
var s, i, n, a, c, l, u, d;
const h = ((s = t.opentelemetryConf) === null || s === void 0 ? void 0 : s.endpoint) || process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
if (!h) throw new Error("OTLP endpoint is required");
const p = new RetryOTLPTraceExporter({
url: h,
headers: ((i = t.opentelemetryConf) === null || i === void 0 ? void 0 : i.headers) || {},
timeoutMillis: ((n = t.opentelemetryConf) === null || n === void 0 ? void 0 : n.timeout) || 1e4,
maxRetries: 3,
retryDelay: 1e3
});
const m = {
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: (l = t.opentelemetryConf) === null || l === void 0 ? void 0 : l.batchDelayMillis,
exportTimeoutMillis: (u = t.opentelemetryConf) === null || u === void 0 ? void 0 : u.batchExportTimeout
};
if (!B) {
const e = o.DefaultLogger.getLevel();
const t = Object.values(r.DiagLogLevel).find((t => t.toString() === e.toString())) || r.DiagLogLevel.INFO;
r.diag.setLogger(new Logger, t);
B = !0;
}
const f = P(e, t);
const T = {
resource: U(e, t),
traceExporter: p,
spanProcessors: [ new v.BatchSpanProcessor(p, m) ],
instrumentations: ((d = t.opentelemetryConf) === null || d === void 0 ? void 0 : d.instrumentations) || [ y.getNodeAutoInstrumentations({
"@opentelemetry/instrumentation-grpc": {
enabled: !0
},
"@opentelemetry/instrumentation-koa": {
enabled: !0
}
}) ]
};
if (f) T.readers = [ f ];
return new g.NodeSDK(T);
}
async function G(e, t, s) {
var i;
const n = async () => {
try {
await e.shutdown();
o.DefaultLogger.info("OpenTelemetry SDK shut down successfully");
} catch (e) {
o.DefaultLogger.error("Error shutting down OpenTelemetry SDK", e);
} finally {
t.off("appStop", n);
}
};
try {
await e.start();
o.DefaultLogger.info("OpenTelemetry SDK started successfully");
} catch (e) {
o.DefaultLogger.error(`OpenTelemetry SDK initialization failed: ${e.message}`, {
stack: e.stack,
code: e.code,
config: {
endpoint: (i = s.opentelemetryConf) === null || i === void 0 ? void 0 : i.endpoint,
serviceName: t.name
}
});
r.trace.setGlobalTracerProvider(new v.BasicTracerProvider);
return;
} finally {
t.on("appStop", n);
}
}
/**
*
* @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);
o.DefaultLogger.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 j(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.Helper.isArray(n) ? n.join(".") : n;
}
}
return i || W(r);
}
function W(e) {
return t.Helper.isFunction(e === null || e === void 0 ? void 0 : e.idFactory) ? e.idFactory() : b.randomUUID();
}
/**
*
* @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 K = {
timeout: 1e4,
requestIdHeaderName: "X-Request-Id",
requestIdName: "requestId",
idFactory: W,
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 Q(r, s) {
r = {
...K,
...r
};
const o = e.IOCContainer.getClass("ExceptionHandler", "COMPONENT");
let i;
let n;
if (r.enableTrace) {
i = s.getMetaData("spanManager")[0] || new SpanManager(r);
n = s.getMetaData("tracer")[0] || F(s, r);
s.once("appStart", (async () => {
await G(n, s, r);
}));
}
return async (e, a) => {
var c, l, u;
t.Helper.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 d = j(e, r);
t.Helper.define(e, "requestId", d);
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 ((u = (l = r.opentelemetryConf) === null || l === void 0 ? void 0 : l.enableTopology) !== null && u !== void 0 ? u : 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: d,
terminated: !1,
spanManager: i,
globalErrorHandler: o
};
if (r.asyncHooks && (e.req || e.res)) {
const t = N();
return x.run(d, (() => {
if (e.req) z(e.req, t);
if (e.res) z(e.res, t);
return V(e, a, r, h);
}));
}
return V(e, a, r, h);
};
}
async function V(e, t, r, s) {
const n = i.performance.now();
let a;
const c = r.retryConf || {
enabled: !1
};
try {
if (c.enabled) {
const o = c.count || 3;
const i = c.interval || 1e3;
for (let n = 0; n <= o; n++) try {
a = await Y(e, t, r, s);
break;
} catch (e) {
const t = c.conditions ? c.conditions(e) : !0;
if (!t || n === o) throw e;
if (i > 0) await new Promise((e => setTimeout(e, i)));
}
} else a = await Y(e, t, r, s);
} finally {
const t = i.performance.now() - n;
I(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) {
o.DefaultLogger.warn("Metrics reporter error:", e);
}
}
return a;
}
async function Y(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);
}
exports.Trace = Q;