UNPKG

koatty_trace

Version:

Full link tracking and error interception for koatty.

1,547 lines (1,470 loc) 44.6 kB
/*! * @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 };