UNPKG

koatty_trace

Version:

Full link tracking and error interception for koatty.

1,568 lines (1,488 loc) 45.7 kB
/*! * @Author: richen * @Date: 2025-06-05 16:25:14 * @License: BSD (3-Clause) * @Copyright (c) - <richenlin(at)gmail.com> * @HomePage: https://koatty.org/ */ "use strict"; 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;