koatty_trace
Version:
Full link tracking and error interception for koatty.
1,661 lines (1,653 loc) • 74.8 kB
JavaScript
'use strict';
var koatty_container = require('koatty_container');
var koatty_core = require('koatty_core');
var koatty_lib = require('koatty_lib');
var api = require('@opentelemetry/api');
var core = require('@opentelemetry/core');
var koatty_logger = require('koatty_logger');
var perf_hooks = require('perf_hooks');
var koatty_exception = require('koatty_exception');
var semanticConventions = require('@opentelemetry/semantic-conventions');
var exporterPrometheus = require('@opentelemetry/exporter-prometheus');
var sdkMetrics = require('@opentelemetry/sdk-metrics');
var compress = require('koa-compress');
var stream = require('stream');
var zlib2 = require('zlib');
var util = require('util');
var async_hooks = require('async_hooks');
var sdkNode = require('@opentelemetry/sdk-node');
var sdkTraceBase = require('@opentelemetry/sdk-trace-base');
var autoInstrumentationsNode = require('@opentelemetry/auto-instrumentations-node');
var exporterTraceOtlpHttp = require('@opentelemetry/exporter-trace-otlp-http');
var resources = require('@opentelemetry/resources');
var crypto = require('crypto');
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
function _interopNamespace(e) {
if (e && e.__esModule) return e;
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var compress__default = /*#__PURE__*/_interopDefault(compress);
var zlib2__namespace = /*#__PURE__*/_interopNamespace(zlib2);
/*!
* @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 core.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 koatty_logger.DefaultLogger.debug === "function" && this.activeSpans.size > 0) {
koatty_logger.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();
koatty_logger.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) {
koatty_logger.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) {
koatty_logger.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();
}
koatty_logger.DefaultLogger.debug(`Span force-ended: ${traceId}, reason: ${reason}`);
} catch (error) {
this.stats.errors.increment();
koatty_logger.DefaultLogger.error(`Error force-ending span ${traceId}:`, error);
}
}
/**
* Create a new span with enhanced safety and performance
*/
createSpan(tracer, ctx, serviceName) {
if (this.isDestroyed) {
koatty_logger.DefaultLogger.warn("SpanManager is destroyed, cannot create span");
return void 0;
}
try {
if (this.contextSpans.has(ctx)) {
koatty_logger.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) {
koatty_logger.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();
koatty_logger.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)) {
koatty_logger.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();
koatty_logger.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 = {};
api.context.with(api.trace.setSpan(api.context.active(), span), () => {
this.propagator.inject(api.context.active(), carrier, api.defaultTextMapSetter);
Object.entries(carrier).forEach(([key, value]) => {
if (ctx.set && typeof ctx.set === "function") {
ctx.set(key, value);
}
});
});
} catch (error) {
this.stats.errors.increment();
koatty_logger.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) {
koatty_logger.DefaultLogger.error("Error applying custom span attributes:", error);
}
}
} catch (error) {
this.stats.errors.increment();
koatty_logger.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();
koatty_logger.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();
koatty_logger.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();
koatty_logger.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;
}
koatty_logger.DefaultLogger.info("SpanManager destroyed successfully", this.getStats());
} catch (error) {
koatty_logger.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 (koatty_exception.isException(err)) {
return err.setCode(code).setStatus(status).setMessage(sanitizedMessage).setSpan(span).setStack(stack).handler(ctx);
}
const ins = koatty_container.IOCContainer.getInsByClass(ext.globalErrorHandler, [
sanitizedMessage,
code,
status,
stack,
span
]);
if (koatty_lib.Helper.isFunction(ins?.handler)) {
return ins.handler(ctx);
}
return new koatty_exception.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) {
koatty_logger.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"
});
koatty_logger.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 koatty_logger.DefaultLogger.debug === "function") {
koatty_logger.DefaultLogger.debug(`Metrics collected for ${protocol.toUpperCase()} ${ctx.method} ${ctx.path}: ${duration}ms, status: ${ctx.status}`);
}
} catch (error) {
koatty_logger.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 koatty_logger.DefaultLogger.debug === "function") {
koatty_logger.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();
koatty_logger.DefaultLogger.warn("High memory usage detected, cleared path normalization cache");
}
} catch (error) {
koatty_logger.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);
koatty_logger.DefaultLogger.debug(`Custom metric recorded: ${name} = ${value}`, labels);
} catch (error) {
koatty_logger.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;
}
koatty_logger.DefaultLogger.info(`Metrics collector for ${this.serviceName} destroyed`);
} catch (error) {
koatty_logger.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) {
koatty_logger.DefaultLogger.info("Prometheus metrics disabled: not in production or no metricsEndpoint configured");
return null;
}
try {
const exporter = new exporterPrometheus.PrometheusExporter({
endpoint: options.metricsConf.metricsEndpoint,
port: options.metricsConf.metricsPort || 9464
});
const meterProvider = new sdkMetrics.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);
koatty_logger.DefaultLogger.info(`Enhanced Prometheus metrics initialized on port ${options.metricsConf.metricsPort || 9464}, endpoint: ${options.metricsConf.metricsEndpoint}`);
return meterProvider;
} catch (error) {
koatty_logger.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 koatty_exception.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, {
[semanticConventions.SemanticAttributes.HTTP_URL]: ctx.originalUrl,
[semanticConventions.SemanticAttributes.HTTP_METHOD]: ctx.method
});
}
}
endTraceSpan(ctx, ext, msg) {
if (ext.spanManager) {
ext.spanManager.setSpanAttributes(ctx, {
[semanticConventions.SemanticAttributes.HTTP_STATUS_CODE]: ctx.status,
[semanticConventions.SemanticAttributes.HTTP_METHOD]: ctx.method,
[semanticConventions.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) {
koatty_logger.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);
koatty_logger.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) {
koatty_logger.DefaultLogger.Error("res.end() failed:", err);
reject(err);
} else {
resolve();
}
});
setImmediate(() => {
if (!callbackCalled) {
resolve();
}
});
} catch (err) {
koatty_logger.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;
koatty_logger.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) {
koatty_logger.DefaultLogger.Error("Failed to send error response:", endErr);
}
} else if (!res.writableEnded) {
try {
res.end();
} catch (endErr) {
koatty_logger.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) {
koatty_logger.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__namespace.default.constants.BROTLI_PARAM_QUALITY]: 4
}
};
return compress__default.default(options);
} else if (acceptEncoding.includes("gzip")) {
options.gzip = {
flush: zlib2__namespace.default.constants.Z_SYNC_FLUSH
};
options.br = false;
return compress__default.default(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 stream.Readable) {
await handleStreamResponse(currentBody, res, ctx);
return;
}
const jsonResult = safeJSONStringify(currentBody, ext?.debug);
if (!jsonResult.success) {
const errMsg = jsonResult.error;
koatty_logger.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) {
koatty_logger.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) {
koatty_logger.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, {
[semanticConventions.SemanticAttributes.HTTP_STATUS_CODE]: ctx.status,
[semanticConventions.SemanticAttributes.HTTP_METHOD]: ctx.method,
[semanticConventions.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.Stream) {
try {
const compressStream = compression === "gzip" ? zlib2__namespace.createGzip({
level: 6
}) : zlib2__namespace.createBrotliCompress({
params: {
[zlib2__namespace.constants.BROTLI_PARAM_QUALITY]: 4
}
});
compressStream.once("error", (compressErr) => {
koatty_logger.DefaultLogger.Error("gRPC compression stream error:", compressErr);
ctx.body = ctx.body;
});
ctx.body.once("error", (streamErr) => {
koatty_logger.DefaultLogger.Error("gRPC source stream error:", streamErr);
compressStream.destroy();
});
ctx.body = ctx.body.pipe(compressStream);
} catch (pipeErr) {
koatty_logger.DefaultLogger.Error("gRPC stream pipe error:", pipeErr);
}
}
try {
ctx.rpc.callback(null, ctx.body);
} catch (callbackErr) {
koatty_logger.DefaultLogger.Error("gRPC callback error:", callbackErr);
try {
ctx.rpc.callback(callbackErr, null);
} catch (fallbackErr) {
koatty_logger.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 = koatty_exception.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 = koatty_exception.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, {
[semanticConventions.SemanticAttributes.HTTP_STATUS_CODE]: ctx.status,
[semanticConventions.SemanticAttributes.HTTP_METHOD]: ctx.method,
[semanticConventions.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 && !koatty_lib.Helper.isTrueEmpty(ctx.body)) {
try {
const sendOptions = useCompression ? {
compress: true
} : {};
const message = util.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) => {
koatty_logger.DefaultLogger.Error("WebSocket error:", wsErr);
});
}
} catch (sendErr) {
koatty_logger.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 getHa