UNPKG

koatty_trace

Version:

Full link tracking and error interception for koatty.

1,685 lines (1,681 loc) 72.4 kB
import { IOCContainer } from 'koatty_container'; import { AppEvent } from 'koatty_core'; import { Helper } from 'koatty_lib'; import { context, trace, defaultTextMapSetter, SpanStatusCode, DiagLogLevel, diag } from '@opentelemetry/api'; import { W3CTraceContextPropagator, ExportResultCode } from '@opentelemetry/core'; import { DefaultLogger } from 'koatty_logger'; import { performance } from 'perf_hooks'; import { Exception, StatusCodeConvert, isException } from 'koatty_exception'; import { SemanticAttributes, ATTR_TELEMETRY_SDK_VERSION, ATTR_TELEMETRY_SDK_LANGUAGE, ATTR_TELEMETRY_SDK_NAME, SEMRESATTRS_DEPLOYMENT_ENVIRONMENT, ATTR_SERVICE_VERSION, ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions'; import { PrometheusExporter } from '@opentelemetry/exporter-prometheus'; import { MeterProvider } from '@opentelemetry/sdk-metrics'; import compress from 'koa-compress'; import { Stream, Readable } from 'stream'; import * as zlib2 from 'zlib'; import zlib2__default from 'zlib'; import { inspect } from 'util'; import { AsyncLocalStorage, AsyncResource } from 'async_hooks'; import { NodeSDK } from '@opentelemetry/sdk-node'; import { BatchSpanProcessor, BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'; import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; import { resourceFromAttributes } from '@opentelemetry/resources'; import { randomUUID } from 'crypto'; /*! * @Author: richen * @Date: 2026-04-24 08:20:32 * @License: BSD (3-Clause) * @Copyright (c) - <richenlin(at)gmail.com> * @HomePage: https://koatty.org/ */ var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); // src/opentelemetry/atomicCounter.ts var AtomicCounter = class { static { __name(this, "AtomicCounter"); } value = 0; /** * 递增计数器并返回新值 */ increment() { return ++this.value; } /** * 递减计数器并返回新值 */ decrement() { return --this.value; } /** * 获取当前值 */ get() { return this.value; } /** * 重置计数器为0 */ reset() { this.value = 0; } /** * 设置计数器为指定值 */ set(val) { this.value = val; } }; // src/opentelemetry/spanManager.ts var SpanManager = class { static { __name(this, "SpanManager"); } activeSpans = /* @__PURE__ */ new Map(); span; // ✅ 添加 WeakMap 用于存储 ctx -> span 映射 contextSpans = /* @__PURE__ */ new WeakMap(); propagator; options; cleanupInterval; startTime; isDestroyed = false; // ✅ 替换统计计数器为 AtomicCounter stats = { spansCreated: new AtomicCounter(), spansEnded: new AtomicCounter(), spansTimedOut: new AtomicCounter(), memoryEvictions: new AtomicCounter(), errors: new AtomicCounter() }; constructor(options) { this.propagator = new W3CTraceContextPropagator(); this.startTime = Date.now(); this.options = { spanTimeout: 3e4, samplingRate: 1, maxActiveSpans: 1e3, spanAttributes: void 0, ...options.opentelemetryConf }; this.cleanupInterval = setInterval(() => { this.performPeriodicCleanup(); }, Math.min(this.options.spanTimeout || 3e4, 6e4)); process.once("SIGTERM", () => this.destroy()); process.once("SIGINT", () => this.destroy()); } /** * Perform periodic cleanup of expired spans and memory monitoring */ performPeriodicCleanup() { if (this.isDestroyed) return; try { const now = Date.now(); const expiredSpans = []; for (const [traceId, entry] of this.activeSpans) { const age = now - entry.createdAt; if (age > (this.options.spanTimeout || 3e4)) { expiredSpans.push(traceId); } } for (const traceId of expiredSpans) { this.forceEndSpan(traceId, "timeout"); } this.checkMemoryPressure(); if (typeof DefaultLogger.debug === "function" && this.activeSpans.size > 0) { DefaultLogger.debug("SpanManager stats:", { activeSpans: this.activeSpans.size, spansCreated: this.stats.spansCreated.get(), spansEnded: this.stats.spansEnded.get(), spansTimedOut: this.stats.spansTimedOut.get(), memoryEvictions: this.stats.memoryEvictions.get(), errors: this.stats.errors.get(), memoryUsage: Math.round(process.memoryUsage().heapUsed / 1024 / 1024) + "MB" }); } } catch (error) { this.stats.errors.increment(); DefaultLogger.error("Error during periodic cleanup:", error); } } /** * Check memory pressure and perform eviction if necessary */ checkMemoryPressure() { const maxSpans = this.options.maxActiveSpans || 1e3; if (this.activeSpans.size >= maxSpans) { const evictCount = Math.ceil(maxSpans * 0.1); this.evictOldestSpans(evictCount); } const memUsage = process.memoryUsage(); if (memUsage.heapUsed > 500 * 1024 * 1024) { DefaultLogger.warn("High memory usage detected, performing aggressive span cleanup"); this.evictOldestSpans(Math.ceil(this.activeSpans.size * 0.2)); } } /** * Evict oldest spans to free memory */ evictOldestSpans(count) { const sortedEntries = Array.from(this.activeSpans.entries()).sort(([, a], [, b]) => a.createdAt - b.createdAt).slice(0, count); for (const [traceId] of sortedEntries) { this.forceEndSpan(traceId, "memory_pressure"); this.stats.memoryEvictions.increment(); } if (sortedEntries.length > 0) { DefaultLogger.warn(`Evicted ${sortedEntries.length} oldest spans due to memory pressure`); } } /** * Force end a span with proper cleanup */ forceEndSpan(traceId, reason) { const entry = this.activeSpans.get(traceId); if (!entry) return; try { clearTimeout(entry.timer); entry.span.addEvent("span_forced_end", { reason }); entry.span.end(); this.activeSpans.delete(traceId); if (reason === "timeout") { this.stats.spansTimedOut.increment(); } DefaultLogger.debug(`Span force-ended: ${traceId}, reason: ${reason}`); } catch (error) { this.stats.errors.increment(); DefaultLogger.error(`Error force-ending span ${traceId}:`, error); } } /** * Create a new span with enhanced safety and performance */ createSpan(tracer, ctx, serviceName) { if (this.isDestroyed) { DefaultLogger.warn("SpanManager is destroyed, cannot create span"); return void 0; } try { if (this.contextSpans.has(ctx)) { DefaultLogger.warn("Span already exists for this context"); return this.contextSpans.get(ctx); } const shouldSample = Math.random() < (this.options.samplingRate || 1); if (!shouldSample) { return void 0; } if (!tracer?.startSpan) { DefaultLogger.error("Invalid tracer provided to createSpan"); this.stats.errors.increment(); return void 0; } const span = tracer.startSpan(serviceName, { attributes: { "service.name": serviceName, "request.id": ctx.requestId || "unknown" } }); this.contextSpans.set(ctx, span); this.span = span; this.stats.spansCreated.increment(); this.setupSpanTimeout(ctx, span); this.injectContext(ctx, span); this.setBasicAttributes(ctx, span); return span; } catch (error) { this.stats.errors.increment(); DefaultLogger.error("Error creating span:", error); return void 0; } } /** * Get current span safely * @param ctx - KoattyContext to get span for (optional for backward compatibility) */ getSpan(ctx) { if (ctx) { return this.contextSpans.get(ctx); } return this.span; } /** * Setup span timeout with enhanced error handling * @param ctx - KoattyContext * @param span - Span instance */ setupSpanTimeout(ctx, span) { if (!this.options.spanTimeout || !span || this.isDestroyed) return; const traceId = span.spanContext().traceId; if (this.activeSpans.has(traceId)) { DefaultLogger.warn(`Span ${traceId} already exists in active spans`); return; } let timer = null; try { timer = setTimeout(() => { this.forceEndSpan(traceId, "timeout"); }, this.options.spanTimeout); this.activeSpans.set(traceId, { span, timer, createdAt: Date.now(), requestId: ctx.requestId }); this.checkMemoryPressure(); } catch (error) { if (timer) { clearTimeout(timer); } this.activeSpans.delete(traceId); this.stats.errors.increment(); DefaultLogger.error("Failed to setup span timeout:", error); throw error; } } /** * Inject context with error handling * @param ctx - KoattyContext * @param span - Span instance */ injectContext(ctx, span) { if (!span || this.isDestroyed) return; try { const carrier = {}; context.with(trace.setSpan(context.active(), span), () => { this.propagator.inject(context.active(), carrier, defaultTextMapSetter); Object.entries(carrier).forEach(([key, value]) => { if (ctx.set && typeof ctx.set === "function") { ctx.set(key, value); } }); }); } catch (error) { this.stats.errors.increment(); DefaultLogger.error("Error injecting context:", error); } } /** * Set basic attributes with validation * @param ctx - KoattyContext * @param span - Span instance */ setBasicAttributes(ctx, span) { if (!span || this.isDestroyed) return; try { const safeAttributes = {}; if (ctx.requestId) { safeAttributes["http.request_id"] = ctx.requestId; } if (ctx.method) { safeAttributes["http.method"] = ctx.method; } if (ctx.path) { safeAttributes["http.route"] = ctx.path; } span.setAttributes(safeAttributes); if (this.options.spanAttributes && typeof this.options.spanAttributes === "function") { try { const customAttrs = this.options.spanAttributes(ctx); if (customAttrs && typeof customAttrs === "object") { span.setAttributes(customAttrs); } } catch (error) { DefaultLogger.error("Error applying custom span attributes:", error); } } } catch (error) { this.stats.errors.increment(); DefaultLogger.error("Error setting basic attributes:", error); } } /** * Set span attributes safely * @param ctx - KoattyContext * @param attributes - SpanAttributes to set */ setSpanAttributes(ctx, attributes) { const span = this.contextSpans.get(ctx); if (!span || this.isDestroyed) return this; try { span.setAttributes(attributes); } catch (error) { this.stats.errors.increment(); DefaultLogger.error("Error setting span attributes:", error); } return this; } /** * Add span event safely * @param ctx - KoattyContext * @param name - Event name * @param attributes - Optional event attributes */ addSpanEvent(ctx, name, attributes) { const span = this.contextSpans.get(ctx); if (!span || this.isDestroyed) return; try { span.addEvent(name, attributes); } catch (error) { this.stats.errors.increment(); DefaultLogger.error("Error adding span event:", error); } } /** * End span with proper cleanup * @param ctx - KoattyContext */ endSpan(ctx) { const span = this.contextSpans.get(ctx); if (!span || this.isDestroyed) return; const traceId = span.spanContext().traceId; try { const entry = this.activeSpans.get(traceId); if (entry) { clearTimeout(entry.timer); this.activeSpans.delete(traceId); } span.end(); this.stats.spansEnded.increment(); this.contextSpans.delete(ctx); if (this.span === span) { this.span = void 0; } } catch (error) { this.stats.errors.increment(); DefaultLogger.error("SpanManager.endSpan error:", error); } } /** * Get manager statistics */ getStats() { return { spansCreated: this.stats.spansCreated.get(), spansEnded: this.stats.spansEnded.get(), spansTimedOut: this.stats.spansTimedOut.get(), memoryEvictions: this.stats.memoryEvictions.get(), errors: this.stats.errors.get(), activeSpansCount: this.activeSpans.size, uptime: Date.now() - this.startTime, isDestroyed: this.isDestroyed, memoryUsage: process.memoryUsage() }; } /** * Graceful shutdown with cleanup */ destroy() { if (this.isDestroyed) return; this.isDestroyed = true; try { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); } for (const [traceId] of this.activeSpans) { this.forceEndSpan(traceId, "manager_destroyed"); } if (this.span) { this.span.end(); this.span = void 0; } DefaultLogger.info("SpanManager destroyed successfully", this.getStats()); } catch (error) { DefaultLogger.error("Error during SpanManager destruction:", error); } } }; function catcher(ctx, err, ext) { const { message: sanitizedMessage, status } = getErrorInfo(ctx, err); const code = err.code || 1; const stack = err.stack; const span = ext.spanManager?.getSpan(); if (isException(err)) { return err.setCode(code).setStatus(status).setMessage(sanitizedMessage).setSpan(span).setStack(stack).handler(ctx); } const ins = IOCContainer.getInsByClass(ext.globalErrorHandler, [ sanitizedMessage, code, status, stack, span ]); if (Helper.isFunction(ins?.handler)) { return ins.handler(ctx); } return new Exception(sanitizedMessage, code, status, stack, span).handler(ctx); } __name(catcher, "catcher"); function getErrorInfo(ctx, err) { let status = 500; if ("status" in err && typeof err.status === "number") { status = err.status; } else if (err instanceof Error) { status = 500; } else if (ctx.status === 404 && !ctx.response._explicitStatus) { status = 404; } else { status = ctx.status || 500; } let message = ""; try { message = (err?.message ?? ctx?.message ?? "").toString(); message = message.replace(/"/g, '\\"'); } catch (e) { message = ""; } return { status, message }; } __name(getErrorInfo, "getErrorInfo"); var PathNormalizationCache = class PathNormalizationCache2 { static { __name(this, "PathNormalizationCache"); } cache = /* @__PURE__ */ new Map(); maxSize; totalHits = 0; totalAccesses = 0; accessCount = 0; REORG_THRESHOLD = 1e3; constructor(maxSize = 1e4) { this.maxSize = maxSize; } /** * 获取缓存值 * - 使用时间戳更新访问记录 * - 达到阈值时批量重组 */ get(path) { this.totalAccesses++; this.accessCount++; const entry = this.cache.get(path); if (entry) { this.totalHits++; entry.lastAccess = this.accessCount; if (this.accessCount >= this.REORG_THRESHOLD) { this.reorganize(); } return entry.value; } return void 0; } /** * 设置缓存值 */ set(path, normalized) { const existing = this.cache.get(path); if (existing) { existing.value = normalized; existing.lastAccess = this.accessCount; return; } if (this.cache.size >= this.maxSize) { this.evictLRU(); } this.cache.set(path, { value: normalized, lastAccess: this.accessCount }); } /** * 驱逐最少使用的条目 */ evictLRU() { let oldestKey = null; let oldestAccess = Infinity; for (const [key, entry] of this.cache) { if (entry.lastAccess < oldestAccess) { oldestAccess = entry.lastAccess; oldestKey = key; } } if (oldestKey) { this.cache.delete(oldestKey); } } /** * 定期重组:重置访问计数,避免溢出 */ reorganize() { this.accessCount = 0; const entries = Array.from(this.cache.entries()).sort((a, b) => a[1].lastAccess - b[1].lastAccess); this.cache.clear(); entries.forEach(([key, entry], index) => { entry.lastAccess = index; this.cache.set(key, entry); }); } clear() { this.cache.clear(); this.totalHits = 0; this.totalAccesses = 0; this.accessCount = 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, utilizationRate: this.cache.size / this.maxSize }; } }; var MetricsBatchProcessor = class MetricsBatchProcessor2 { static { __name(this, "MetricsBatchProcessor"); } collector; batchQueue = []; batchSize; flushInterval; flushTimer = null; isProcessing = false; constructor(collector, batchSize = 100, flushInterval = 1e3) { this.collector = collector; this.batchSize = batchSize; this.flushInterval = flushInterval; this.startFlushTimer(); } addMetric(type, labels, value) { this.batchQueue.push({ type, labels: { ...labels }, value, 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 = true; const batch = this.batchQueue.splice(0, this.batchSize); try { for (const metric of batch) { switch (metric.type) { case "request": this.collector.requestCounter.add(metric.value, metric.labels); break; case "error": this.collector.errorCounter.add(metric.value, metric.labels); break; case "response_time": this.collector.responseTimeHistogram.record(metric.value, metric.labels); break; case "connection": this.collector.connectionCounter.add(metric.value, metric.labels); break; } } } catch (error) { DefaultLogger.error("Failed to flush metrics batch:", error); } finally { this.isProcessing = false; } } destroy() { if (this.flushTimer) { clearInterval(this.flushTimer); this.flushTimer = null; } this.flush(); } }; var MetricsCollector = class _MetricsCollector { static { __name(this, "MetricsCollector"); } requestCounter; errorCounter; responseTimeHistogram; connectionCounter; serviceName; pathCache; batchProcessor; startTime; memoryMonitorTimer = null; // ✅ 合并为单个正则表达式,一次扫描完成所有替换 static ID_PATTERN = /\/(?:([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})|([a-f0-9]{24})|(\d+))/gi; constructor(meterProvider, serviceName) { this.serviceName = serviceName; this.startTime = Date.now(); this.pathCache = new PathNormalizationCache(); const meter = meterProvider.getMeter(serviceName); this.initializeMetrics(meter); this.batchProcessor = new MetricsBatchProcessor(this); this.setupMemoryMonitoring(); } initializeMetrics(meter) { this.requestCounter = meter.createCounter("requests_total", { description: "Total requests across all protocols", unit: "1" }); this.errorCounter = meter.createCounter("errors_total", { description: "Total errors across all protocols", unit: "1" }); this.responseTimeHistogram = meter.createHistogram("response_time_seconds", { description: "Response time in seconds across all protocols", unit: "s", advice: { explicitBucketBoundaries: [ 0.1, 0.5, 1, 2.5, 5, 10 ] } }); this.connectionCounter = meter.createCounter("websocket_connections_total", { description: "Total WebSocket connections", unit: "1" }); DefaultLogger.info(`Enhanced multi-protocol metrics initialized for service: ${this.serviceName}`); } /** * Collect request metrics with enhanced performance and safety */ collectRequestMetrics(ctx, duration) { try { const protocol = this.detectProtocol(ctx); const labels = this.createLabelsOptimized(ctx, protocol); this.batchProcessor.addMetric("request", labels, 1); this.batchProcessor.addMetric("response_time", labels, duration / 1e3); if (this.isErrorStatus(ctx.status, protocol)) { const errorLabels = { ...labels, error_type: this.getErrorType(ctx.status, protocol) }; this.batchProcessor.addMetric("error", errorLabels, 1); } this.collectProtocolSpecificMetricsOptimized(ctx, protocol); if (typeof DefaultLogger.debug === "function") { DefaultLogger.debug(`Metrics collected for ${protocol.toUpperCase()} ${ctx.method} ${ctx.path}: ${duration}ms, status: ${ctx.status}`); } } catch (error) { DefaultLogger.error("Failed to collect metrics (non-blocking):", error); } } /** * Optimized protocol detection with caching */ detectProtocol(ctx) { if (ctx._cachedProtocol) { return ctx._cachedProtocol; } let protocol; if (ctx.websocket || ctx.req?.headers?.upgrade === "websocket") { protocol = "websocket"; } else if (ctx.rpc || ctx.req?.headers?.["content-type"]?.includes("application/grpc")) { protocol = "grpc"; } else { protocol = "http"; } Object.defineProperty(ctx, "_cachedProtocol", { value: protocol, writable: false, enumerable: false }); return protocol; } /** * Optimized label creation with object pooling */ createLabelsOptimized(ctx, protocol) { const baseLabels = { method: ctx.method || "UNKNOWN", status: (ctx.status || 200).toString(), path: this.normalizePathOptimized(ctx.path || ctx.originalPath || "/"), protocol }; switch (protocol) { case "websocket": baseLabels["compression"] = this.getWebSocketCompression(ctx); break; case "grpc": baseLabels["grpc_service"] = this.getGrpcService(ctx); baseLabels["compression"] = this.getGrpcCompression(ctx); break; } return baseLabels; } /** * High-performance path normalization with caching */ normalizePathOptimized(path) { if (!path) return "/"; const cached = this.pathCache.get(path); if (cached !== void 0) { return cached; } const queryIndex = path.indexOf("?"); const cleanPath = queryIndex === -1 ? path : path.substring(0, queryIndex); const normalized = cleanPath.replace(_MetricsCollector.ID_PATTERN, (match, uuid, objectid, numeric) => { if (uuid) return "/:uuid"; if (objectid) return "/:objectid"; if (numeric) return "/:id"; return match; }); this.pathCache.set(path, normalized); return normalized; } /** * Optimized protocol-specific metrics collection */ collectProtocolSpecificMetricsOptimized(ctx, protocol) { switch (protocol) { case "websocket": if (ctx.websocket?.readyState === 1) { this.batchProcessor.addMetric("connection", { protocol, service: this.serviceName }, 1); } break; } } /** * Setup memory monitoring for proactive management */ setupMemoryMonitoring() { const monitorInterval = 6e4; this.memoryMonitorTimer = setInterval(() => { try { const memUsage = process.memoryUsage(); const cacheStats = this.pathCache.getStats(); if (typeof DefaultLogger.debug === "function") { DefaultLogger.debug("Metrics collector memory stats:", { heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024) + "MB", cacheSize: cacheStats.size, cacheHitRate: Math.round(cacheStats.hitRate * 100) + "%" }); } if (memUsage.heapUsed > 500 * 1024 * 1024) { this.pathCache.clear(); DefaultLogger.warn("High memory usage detected, cleared path normalization cache"); } } catch (error) { DefaultLogger.error("Memory monitoring error:", error); } }, monitorInterval); } /** * Check if status code represents an error for the given protocol */ isErrorStatus(status, protocol) { if (protocol === "grpc") { return status !== 0; } return status >= 400; } /** * Get WebSocket compression info */ getWebSocketCompression(ctx) { const extensions = ctx.req?.headers?.["sec-websocket-extensions"] || ""; return extensions.includes("permessage-deflate") ? "deflate" : "none"; } /** * Get gRPC service name */ getGrpcService(ctx) { const path = ctx.path || ctx.originalPath || ""; const match = path.match(/^\/([^\/]+)\/[^\/]+$/); return match ? match[1] : "unknown"; } /** * Get gRPC compression info */ getGrpcCompression(ctx) { const acceptEncoding = ctx.rpc?.call?.metadata?.get("accept-encoding")?.[0] || ""; if (acceptEncoding.includes("br")) return "brotli"; if (acceptEncoding.includes("gzip")) return "gzip"; return "none"; } /** * Get error type based on status code */ getErrorType(status, protocol) { if (protocol === "grpc") { if (status === 0) return "ok"; if (status >= 1 && status <= 16) return "grpc_error"; return "unknown_error"; } if (status >= 400 && status < 500) return "client_error"; if (status >= 500) return "server_error"; return "unknown_error"; } /** * Record custom business metrics with safety checks */ recordCustomMetric(name, value, labels = {}) { try { console.log(`Custom metric recorded: ${name} = ${value}`, labels); DefaultLogger.debug(`Custom metric recorded: ${name} = ${value}`, labels); } catch (error) { DefaultLogger.error(`Failed to record custom metric ${name}:`, error); } } /** * Get collector statistics for monitoring */ getStats() { return { serviceName: this.serviceName, uptime: Date.now() - this.startTime, pathCacheStats: this.pathCache.getStats(), memoryUsage: process.memoryUsage() }; } /** * Graceful shutdown */ destroy() { try { this.batchProcessor.destroy(); this.pathCache.clear(); if (this.memoryMonitorTimer) { clearInterval(this.memoryMonitorTimer); this.memoryMonitorTimer = null; } DefaultLogger.info(`Metrics collector for ${this.serviceName} destroyed`); } catch (error) { DefaultLogger.error("Error during metrics collector destruction:", error); } } }; var MetricsCollectorManager = class MetricsCollectorManager2 { static { __name(this, "MetricsCollectorManager"); } static instance = null; collector = null; lock = { locked: false }; constructor() { } static getInstance() { if (!MetricsCollectorManager2.instance) { MetricsCollectorManager2.instance = new MetricsCollectorManager2(); } return MetricsCollectorManager2.instance; } async setCollector(collector) { await this.acquireLock(); try { if (this.collector) { this.collector.destroy(); } this.collector = collector; } finally { this.releaseLock(); } } getCollector() { return this.collector; } async acquireLock() { while (this.lock.locked) { await new Promise((resolve) => setTimeout(resolve, 1)); } this.lock.locked = true; } releaseLock() { this.lock.locked = false; } }; function initPrometheusExporter(app, options) { const isProduction = options.metricsConf?.metricsEndpoint || process.env.NODE_ENV === "production"; if (!isProduction || !options.metricsConf?.metricsEndpoint) { DefaultLogger.info("Prometheus metrics disabled: not in production or no metricsEndpoint configured"); return null; } try { const exporter = new PrometheusExporter({ endpoint: options.metricsConf.metricsEndpoint, port: options.metricsConf.metricsPort || 9464 }); const meterProvider = new MeterProvider({ readers: [ exporter ] }); const manager = MetricsCollectorManager.getInstance(); const collector = new MetricsCollector(meterProvider, app.name || "koatty-app"); manager.setCollector(collector); const cleanup = /* @__PURE__ */ __name(() => { const currentCollector = manager.getCollector(); if (currentCollector) { currentCollector.destroy(); } }, "cleanup"); process.once("SIGTERM", cleanup); process.once("SIGINT", cleanup); DefaultLogger.info(`Enhanced Prometheus metrics initialized on port ${options.metricsConf.metricsPort || 9464}, endpoint: ${options.metricsConf.metricsEndpoint}`); return meterProvider; } catch (error) { DefaultLogger.error("Failed to initialize Prometheus exporter:", error); return null; } } __name(initPrometheusExporter, "initPrometheusExporter"); function getMetricsCollector() { return MetricsCollectorManager.getInstance().getCollector(); } __name(getMetricsCollector, "getMetricsCollector"); function collectRequestMetrics(ctx, duration) { const collector = getMetricsCollector(); if (collector) { collector.collectRequestMetrics(ctx, duration); } } __name(collectRequestMetrics, "collectRequestMetrics"); // src/utils/timeout.ts var TimeoutController = class { static { __name(this, "TimeoutController"); } timerId = null; isCleared = false; /** * 创建超时 Promise * @param ms 超时时间(毫秒) * @param signal 可选的 AbortSignal 用于外部取消 * @returns 永远拒绝的 Promise,超时时抛出错误 */ createTimeout(ms, signal) { if (this.isCleared) { return Promise.reject(new Error("TimeoutController already cleared")); } return new Promise((_, reject) => { this.timerId = setTimeout(() => { if (!this.isCleared) { reject(new Error("Deadline exceeded")); } }, ms); signal?.addEventListener("abort", () => { this.clear(); reject(new Error("Request aborted")); }, { once: true }); }); } /** * 清除定时器 */ clear() { if (this.timerId !== null) { clearTimeout(this.timerId); this.timerId = null; } this.isCleared = true; } /** * 检查是否已清除 */ get cleared() { return this.isCleared; } }; var BaseHandler = class { static { __name(this, "BaseHandler"); } commonPreHandle(ctx, ext) { ctx.encoding = ext?.encoding; this.setSecurityHeaders(ctx); this.startTraceSpan(ctx, ext); } commonPostHandle(ctx, ext, msg) { this.logRequest(ctx, ext, msg); this.endTraceSpan(ctx, ext, msg); this.collectMetrics(ctx, ext); } handleError(err, ctx, ext) { return catcher(ctx, err, ext); } /** * 通用超时处理包装器 * @param ctx - Koatty context * @param next - Next middleware * @param ext - Extension options * @param timeout - Timeout in milliseconds */ async handleWithTimeout(ctx, next, ext, timeout) { if (ext.terminated) { return; } const timeoutCtrl = new TimeoutController(); try { await Promise.race([ next(), timeoutCtrl.createTimeout(timeout) ]); } finally { timeoutCtrl.clear(); } } /** * 检查并设置响应状态 * - 将404转为200(如果有body) * - 状态>=400时抛出异常 */ checkAndSetStatus(ctx) { if (ctx.body !== void 0 && ctx.status === 404) { ctx.status = 200; } if (ctx.status >= 400) { throw new Exception(ctx.message, 1, ctx.status); } } setSecurityHeaders(ctx) { ctx.set("X-Content-Type-Options", "nosniff"); ctx.set("X-Frame-Options", "DENY"); ctx.set("X-XSS-Protection", "1; mode=block"); } startTraceSpan(ctx, ext) { if (ext.spanManager) { ext.spanManager.setSpanAttributes(ctx, { [SemanticAttributes.HTTP_URL]: ctx.originalUrl, [SemanticAttributes.HTTP_METHOD]: ctx.method }); } } endTraceSpan(ctx, ext, msg) { if (ext.spanManager) { ext.spanManager.setSpanAttributes(ctx, { [SemanticAttributes.HTTP_STATUS_CODE]: ctx.status, [SemanticAttributes.HTTP_METHOD]: ctx.method, [SemanticAttributes.HTTP_URL]: ctx.url }); ext.spanManager.addSpanEvent(ctx, "request", { "message": msg }); ext.spanManager.endSpan(ctx); } } /** * Collect metrics for the request * @param ctx - Koatty context object * @param ext - Extension options */ collectMetrics(ctx, ext) { if (ctx.startTime) { const duration = Date.now() - ctx.startTime; collectRequestMetrics(ctx, duration); } } logRequest(ctx, ext, msg) { DefaultLogger[ctx.status >= 400 ? "Error" : "Info"](msg); } }; var ProtocolType = /* @__PURE__ */ (function(ProtocolType2) { ProtocolType2["HTTP"] = "http"; ProtocolType2["GRPC"] = "grpc"; ProtocolType2["WS"] = "ws"; ProtocolType2["GRAPHQL"] = "graphql"; return ProtocolType2; })({}); var StatusEmpty = [ 204, 205, 304 ]; function safeJSONStringify(body, debug = false) { const seen = /* @__PURE__ */ new WeakSet(); try { const jsonString = JSON.stringify(body, (key, value) => { if (value === void 0) { return null; } if (typeof value === "bigint") { return value.toString() + "n"; } if (typeof value === "symbol") { return value.toString(); } if (typeof value === "function") { return `[Function: ${value.name || "anonymous"}]`; } if (value instanceof Error) { return { name: value.name, message: value.message, stack: debug ? value.stack : void 0, code: value.code }; } if (value instanceof Date) { return value.toISOString(); } if (value instanceof RegExp) { return value.toString(); } if (typeof value === "object" && value !== null) { if (seen.has(value)) { return "[Circular Reference]"; } seen.add(value); } return value; }); return { success: true, data: jsonString }; } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); DefaultLogger.Error("JSON serialization failed:", error); return { success: false, error: errorMsg }; } } __name(safeJSONStringify, "safeJSONStringify"); async function safeEnd(res, data) { return new Promise((resolve, reject) => { try { let callbackCalled = false; const endResult = res.end(data, (err) => { callbackCalled = true; if (err) { DefaultLogger.Error("res.end() failed:", err); reject(err); } else { resolve(); } }); setImmediate(() => { if (!callbackCalled) { resolve(); } }); } catch (err) { DefaultLogger.Error("res.end() threw exception:", err); reject(err); } }); } __name(safeEnd, "safeEnd"); async function handleStreamResponse(stream, res, ctx) { return new Promise((resolve, reject) => { let errorHandled = false; const handleError = /* @__PURE__ */ __name((err, source) => { if (errorHandled) return; errorHandled = true; DefaultLogger.Error(`Stream error from ${source}:`, err); if (!stream.destroyed) { stream.destroy(); } if (!res.headersSent) { try { res.statusCode = 500; res.setHeader("Content-Type", "text/plain"); res.end("Internal Server Error: Stream processing failed"); } catch (endErr) { DefaultLogger.Error("Failed to send error response:", endErr); } } else if (!res.writableEnded) { try { res.end(); } catch (endErr) { DefaultLogger.Error("Failed to end response:", endErr); } } reject(err); }, "handleError"); const handleFinish = /* @__PURE__ */ __name(() => { if (errorHandled) return; errorHandled = true; resolve(); }, "handleFinish"); stream.once("error", (err) => handleError(err, "stream")); res.once("error", (err) => handleError(err, "response")); res.once("close", () => { if (!stream.destroyed) { DefaultLogger.Warn("Client disconnected, destroying stream"); stream.destroy(); } }); try { stream.pipe(res); res.once("finish", handleFinish); } catch (err) { handleError(err, "pipe"); } }); } __name(handleStreamResponse, "handleStreamResponse"); function compressMiddleware(ctx) { const acceptEncoding = ctx.get("Accept-Encoding") || ""; const options = { threshold: 1024, filter(contentType) { return !/^image\//i.test(contentType); } }; if (acceptEncoding.includes("br")) { options.br = { params: { [zlib2__default.constants.BROTLI_PARAM_QUALITY]: 4 } }; return compress(options); } else if (acceptEncoding.includes("gzip")) { options.gzip = { flush: zlib2__default.constants.Z_SYNC_FLUSH }; options.br = false; return compress(options); } return (ctx2, next) => next(); } __name(compressMiddleware, "compressMiddleware"); async function respond(ctx, ext) { if (false === ctx.respond) return; if (!ctx.writable) return; const res = ctx.res; const body = ctx.body; const code = ctx.status; if (StatusEmpty.includes(code)) { ctx.body = null; return safeEnd(res); } if ("HEAD" === ctx.method) { if (!res.headersSent && !ctx.response.has("Content-Length")) { const { length } = ctx.response; if (Number.isInteger(length)) ctx.length = length; } return safeEnd(res); } if (null == body) { if (ctx.response._explicitNullBody) { ctx.response.remove("Content-Type"); ctx.response.remove("Transfer-Encoding"); return safeEnd(res); } const textBody = ctx.req.httpVersionMajor >= 2 ? String(code) : ctx.message || String(code); if (!res.headersSent) { ctx.type = "text"; ctx.length = Buffer.byteLength(textBody); } return safeEnd(res, textBody); } if (code === 404) { ctx.status = 200; } try { await compressMiddleware(ctx)(ctx, async () => { const currentBody = ctx.body; if (Buffer.isBuffer(currentBody)) { await safeEnd(res, currentBody); return; } if (typeof currentBody === "string") { await safeEnd(res, currentBody); return; } if (currentBody instanceof Readable) { await handleStreamResponse(currentBody, res, ctx); return; } const jsonResult = safeJSONStringify(currentBody, ext?.debug); if (!jsonResult.success) { const errMsg = jsonResult.error; DefaultLogger.Error(`JSON serialization failed: ${errMsg}`); if (!res.headersSent) { ctx.status = 500; ctx.type = "json"; } const errorResponse = JSON.stringify({ error: "Internal Server Error", message: "Failed to serialize response data", details: ext?.debug ? errMsg : void 0 }); await safeEnd(res, errorResponse); return; } const jsonData = jsonResult.data; if (!res.headersSent) { ctx.type = "json"; ctx.length = Buffer.byteLength(jsonData); } await safeEnd(res, jsonData); }); } catch (error) { DefaultLogger.Error("Response handling failed:", error); if (!res.headersSent) { try { res.statusCode = 500; res.setHeader("Content-Type", "text/plain"); res.end("Internal Server Error"); } catch (endErr) { DefaultLogger.Error("Failed to send error response:", endErr); } } } } __name(respond, "respond"); var HttpHandler = class _HttpHandler extends BaseHandler { static { __name(this, "HttpHandler"); } static instance; constructor() { super(); } static getInstance() { if (!_HttpHandler.instance) { _HttpHandler.instance = new _HttpHandler(); } return _HttpHandler.instance; } async handle(ctx, next, ext) { const timeout = ext.timeout || 1e4; let error = null; this.commonPreHandle(ctx, ext); try { await this.handleWithTimeout(ctx, next, ext, timeout); this.checkAndSetStatus(ctx); return respond(ctx, ext); } catch (err) { error = err; return this.handleError(err, ctx, ext); } finally { if (!error || ctx.status < 400) { const now = Date.now(); const msg = `{"action":"${ctx.method}","status":"${ctx.status}","startTime":"${ctx.startTime}","duration":"${now - ctx.startTime || 0}","requestId":"${ctx.requestId}","endTime":"${now}","path":"${ctx.originalPath || "/"}"}`; this.commonPostHandle(ctx, ext, msg); } else { this.endTraceSpanOnly(ctx, ext); this.collectMetricsOnly(ctx, ext); } } } /** * 只结束追踪span,不记录日志 */ endTraceSpanOnly(ctx, ext) { if (ext.spanManager) { const now = Date.now(); const msg = `{"action":"${ctx.method}","status":"${ctx.status}","startTime":"${ctx.startTime}","duration":"${now - ctx.startTime || 0}","requestId":"${ctx.requestId}","endTime":"${now}","path":"${ctx.originalPath || "/"}"}`; ext.spanManager.setSpanAttributes(ctx, { [SemanticAttributes.HTTP_STATUS_CODE]: ctx.status, [SemanticAttributes.HTTP_METHOD]: ctx.method, [SemanticAttributes.HTTP_URL]: ctx.url }); ext.spanManager.addSpanEvent(ctx, "request", { "message": msg }); ext.spanManager.endSpan(ctx); } } /** * 只收集指标,不记录日志 */ collectMetricsOnly(ctx, ext) { if (ctx.startTime) { const duration = Date.now() - ctx.startTime; collectRequestMetrics(ctx, duration); } } }; var GrpcHandler = class _GrpcHandler extends BaseHandler { static { __name(this, "GrpcHandler"); } static instance; constructor() { super(); } static getInstance() { if (!_GrpcHandler.instance) { _GrpcHandler.instance = new _GrpcHandler(); } return _GrpcHandler.instance; } async handle(ctx, next, ext) { const timeout = ext.timeout || 1e4; const acceptEncoding = ctx.rpc.call.metadata.get("accept-encoding")[0] || ""; const compression = acceptEncoding.includes("br") ? "brotli" : acceptEncoding.includes("gzip") ? "gzip" : "none"; let error = null; ctx?.rpc?.call?.sendMetadata(ctx.rpc.call.metadata); this.commonPreHandle(ctx, ext); (ctx?.rpc?.call).once("error", (err) => { error = err; this.handleError(err, ctx, ext); }); try { await this.handleWithTimeout(ctx, next, ext, timeout); this.checkAndSetStatus(ctx); if (compression !== "none" && ctx.body instanceof Stream) { try { const compressStream = compression === "gzip" ? zlib2.createGzip({ level: 6 }) : zlib2.createBrotliCompress({ params: { [zlib2.constants.BROTLI_PARAM_QUALITY]: 4 } }); compressStream.once("error", (compressErr) => { DefaultLogger.Error("gRPC compression stream error:", compressErr); ctx.body = ctx.body; }); ctx.body.once("error", (streamErr) => { DefaultLogger.Error("gRPC source stream error:", streamErr); compressStream.destroy(); }); ctx.body = ctx.body.pipe(compressStream); } catch (pipeErr) { DefaultLogger.Error("gRPC stream pipe error:", pipeErr); } } try { ctx.rpc.callback(null, ctx.body); } catch (callbackErr) { DefaultLogger.Error("gRPC callback error:", callbackErr); try { ctx.rpc.callback(callbackErr, null); } catch (fallbackErr) { DefaultLogger.Error("gRPC fallback callback error:", fallbackErr); } } return null; } catch (err) { error = err; return this.handleError(err, ctx, ext); } finally { if (!error || ctx.status < 400) { const now = Date.now(); const status = StatusCodeConvert(ctx.status); const msg = `{"action":"${ctx.method}","status":"${status}","startTime":"${ctx.startTime}","duration":"${now - ctx.startTime || 0}","requestId":"${ctx.requestId}","endTime":"${now}","path":"${ctx.originalPath}"}`; this.commonPostHandle(ctx, ext, msg); } else { this.endTraceSpanOnly(ctx, ext); this.collectMetricsOnly(ctx, ext); } ctx.res.emit("finish"); } } /** * 只结束追踪span,不记录日志 */ endTraceSpanOnly(ctx, ext) { if (ext.spanManager) { const now = Date.now(); const status = StatusCodeConvert(ctx.status); const msg = `{"action":"${ctx.method}","status":"${status}","startTime":"${ctx.startTime}","duration":"${now - ctx.startTime || 0}","requestId":"${ctx.requestId}","endTime":"${now}","path":"${ctx.originalPath}"}`; ext.spanManager.setSpanAttributes(ctx, { [SemanticAttributes.HTTP_STATUS_CODE]: ctx.status, [SemanticAttributes.HTTP_METHOD]: ctx.method, [SemanticAttributes.HTTP_URL]: ctx.url }); ext.spanManager.addSpanEvent(ctx, "request", { "message": msg }); ext.spanManager.endSpan(ctx); } } /** * 只收集指标,不记录日志 */ collectMetricsOnly(ctx, ext) { if (ctx.startTime) { const duration = Date.now() - ctx.startTime; collectRequestMetrics(ctx, duration); } } }; var WsHandler = class _WsHandler extends BaseHandler { static { __name(this, "WsHandler"); } static instance; constructor() { super(); } static getInstance() { if (!_WsHandler.instance) { _WsHandler.instance = new _WsHandler(); } return _WsHandler.instance; } async handle(ctx, next, ext) { const timeout = ext?.timeout || 1e4; const wsExtensions = ctx.req.headers["sec-websocket-extensions"] || ""; const useCompression = wsExtensions.includes("permessage-deflate"); this.commonPreHandle(ctx, ext); ctx?.res?.once("finish", () => { const now = Date.now(); const msg = `{"action":"${ctx.method}","status":"${ctx.status}","startTime":"${ctx.startTime}","duration":"${now - ctx.startTime || 0}","requestId":"${ctx.requestId}","endTime":"${now}","path":"${ctx.originalPath || "/"}"}`; this.commonPostHandle(ctx, ext, msg); }); try { await this.handleWithTimeout(ctx, next, ext, timeout); this.checkAndSetStatus(ctx); if (ctx?.websocket?.readyState === 1 && !Helper.isTrueEmpty(ctx.body)) { try { const sendOptions = useCompression ? { compress: true } : {}; const message = inspect(ctx.body, { depth: 10, breakLength: Infinity, compact: true }); ctx.websocket.send(message, sendOptions); if (!ctx.websocket.listenerCount || ctx.websocket.listenerCount("error") === 0) { ctx.websocket.once("error", (wsErr) => { DefaultLogger.Error("WebSocket error:", wsErr); }); } } catch (sendErr) { DefaultLogger.Error("WebSocket send error:", sendErr); } } return null; } catch (err) { return this.handleError(err, ctx, ext); } finally { ctx.res.emit("finish"); } } }; var HandlerFactory = class { static { __name(this, "HandlerFactory"); } static handlers = /* @__PURE__ */ new Map(); static { this.register(ProtocolType.HTTP, HttpHandler.getInstance()); this.register(ProtocolType.GRPC, GrpcHandler.getInstance()); this.register(ProtocolType.WS, WsHandler.getInstance()); } /** * register a handler for a protocol type. * @param type * @param handler */ static register(type, handler) { this.handlers.set(type, handler); } /** * get a handler for a protocol type. * @param type * @returns */ static getHandler(type) { const handler = this.handlers.get(type); if (!handler) { DefaultLogger.warn(`Handler for protocol ${type} not found, falling back to HTTP`); return this.handlers.get(ProtocolType.HTTP); } return handler; } }; var asyncLocalStorage = new AsyncLocalStorage(); var eventMethods = { add: [ "on", "addListener" ], remove: [ "off", "removeListener" ] }; var emitterResourceMap = /* @__PURE__ */ new WeakMap(); function createAsyncResource(key = (/* @__PURE__ */ Symbol("koatty-tracer")).toString()) { const resource = new AsyncResource(key); resource.emitDestroy = () => { AsyncResource.prototype.emitDestroy.call(resource); return resource; }; return resource; } __name(createAsyncResource, "createAsyncResource"); function wrapEmitter(emitter, asyncResource) { if (emitterResourceMap.has(emitter)) { return; } const wrappedHandlers = /* @__PURE__ */ new WeakMap(); emitterResourceMap.set(emitter, { resource: asyncResource, wrappedHandlers }); for (const method of eventMethods.add) { wrapEmitterMethod(emitter, method, (original) => function(name, handler) { let wrappedHandler = wrappedHandlers.get(handler); if (!wrappedHandler) { wrappedHandler = /* @__PURE__ */ __name((...args) => { asyncResource.runInAsyncScope(handler, emitter, ...args); }, "wrappedHandler"); wrappedHandlers.set(handler, wrappedHandler); } return original.call(this, name, wra