index.js•9.3 kB
/**
* Observability Module - Structured Logging and Metrics
*
* Provides:
* - Structured logging with severity levels
* - Performance metrics tracking
* - Tool execution monitoring
* - Cache statistics logging
* - Error tracking and reporting
*/
export var LogLevel;
(function (LogLevel) {
LogLevel["DEBUG"] = "debug";
LogLevel["INFO"] = "info";
LogLevel["WARN"] = "warn";
LogLevel["ERROR"] = "error";
})(LogLevel || (LogLevel = {}));
/**
* Logger with structured output
*/
export class Logger {
minLevel;
context;
constructor(minLevel = LogLevel.INFO, context = {}) {
this.minLevel = this.getLogLevelFromEnv() || minLevel;
this.context = context;
}
getLogLevelFromEnv() {
const level = process.env.LOG_LEVEL?.toLowerCase();
if (level && Object.values(LogLevel).includes(level)) {
return level;
}
return null;
}
shouldLog(level) {
const levels = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR];
return levels.indexOf(level) >= levels.indexOf(this.minLevel);
}
formatLog(level, message, context) {
const timestamp = new Date().toISOString();
const mergedContext = { ...this.context, ...context };
const logData = {
timestamp,
level,
message,
...mergedContext,
};
// JSON format for machine parsing
if (process.env.LOG_FORMAT === 'json') {
return JSON.stringify(logData);
}
// Human-readable format with context
const hasContext = Object.keys(mergedContext).length > 0;
const contextStr = hasContext ? ` ${JSON.stringify(mergedContext)}` : '';
return `[${timestamp}] ${level.toUpperCase()} ${message}${contextStr}`;
}
debug(message, context) {
if (this.shouldLog(LogLevel.DEBUG)) {
console.debug(this.formatLog(LogLevel.DEBUG, message, context));
}
}
info(message, context) {
if (this.shouldLog(LogLevel.INFO)) {
console.info(this.formatLog(LogLevel.INFO, message, context));
}
}
warn(message, context) {
if (this.shouldLog(LogLevel.WARN)) {
console.warn(this.formatLog(LogLevel.WARN, message, context));
}
}
error(message, context) {
if (this.shouldLog(LogLevel.ERROR)) {
console.error(this.formatLog(LogLevel.ERROR, message, context));
}
}
child(context) {
return new Logger(this.minLevel, { ...this.context, ...context });
}
}
/**
* Metrics collector
*/
export class MetricsCollector {
metrics = new Map();
toolMetrics = new Map();
enabled;
constructor(enabled = true) {
this.enabled = enabled;
}
recordMetric(name, value, unit = '', tags) {
if (!this.enabled)
return;
const metric = {
name,
value,
unit,
timestamp: Date.now(),
tags,
};
if (!this.metrics.has(name)) {
this.metrics.set(name, []);
}
this.metrics.get(name).push(metric);
// Keep only last 1000 metrics per name to prevent memory leaks
const metrics = this.metrics.get(name);
if (metrics.length > 1000) {
metrics.shift();
}
}
recordToolExecution(toolName, duration, success, cacheHit = false) {
if (!this.enabled)
return;
let metrics = this.toolMetrics.get(toolName);
if (!metrics) {
metrics = {
toolName,
executionCount: 0,
totalDuration: 0,
averageDuration: 0,
minDuration: Infinity,
maxDuration: 0,
errorCount: 0,
cacheHitRate: 0,
lastExecuted: 0,
};
this.toolMetrics.set(toolName, metrics);
}
metrics.executionCount++;
metrics.totalDuration += duration;
metrics.averageDuration = metrics.totalDuration / metrics.executionCount;
metrics.minDuration = Math.min(metrics.minDuration, duration);
metrics.maxDuration = Math.max(metrics.maxDuration, duration);
if (!success)
metrics.errorCount++;
metrics.lastExecuted = Date.now();
// Update cache hit rate
const cacheHits = cacheHit ? 1 : 0;
const totalRequests = metrics.executionCount;
metrics.cacheHitRate = (metrics.cacheHitRate * (totalRequests - 1) + cacheHits) / totalRequests;
// Record as time-series metric
this.recordMetric(`tool.${toolName}.duration`, duration, 'ms', {
success: success.toString(),
cacheHit: cacheHit.toString(),
});
}
getMetrics(name) {
return this.metrics.get(name) || [];
}
getToolMetrics(toolName) {
if (toolName) {
return this.toolMetrics.get(toolName) || this.createEmptyToolMetrics(toolName);
}
return Array.from(this.toolMetrics.values());
}
getAllMetrics() {
const result = {};
this.metrics.forEach((value, key) => {
result[key] = value;
});
return result;
}
getSummary() {
const tools = Array.from(this.toolMetrics.values());
const totalExecutions = tools.reduce((sum, t) => sum + t.executionCount, 0);
const totalErrors = tools.reduce((sum, t) => sum + t.errorCount, 0);
const avgCacheHitRate = tools.reduce((sum, t) => sum + t.cacheHitRate, 0) / (tools.length || 1);
return {
totalTools: tools.length,
totalExecutions,
totalErrors,
averageCacheHitRate: avgCacheHitRate,
toolSummaries: tools,
};
}
reset() {
this.metrics.clear();
this.toolMetrics.clear();
}
createEmptyToolMetrics(toolName) {
return {
toolName,
executionCount: 0,
totalDuration: 0,
averageDuration: 0,
minDuration: 0,
maxDuration: 0,
errorCount: 0,
cacheHitRate: 0,
lastExecuted: 0,
};
}
}
/**
* Global logger instance
*/
export const logger = new Logger(process.env.NODE_ENV === 'development' ? LogLevel.DEBUG : LogLevel.INFO, { service: 'cts-mcp-server', version: '3.0.0' });
/**
* Global metrics collector
*/
export const metrics = new MetricsCollector(true);
/**
* Performance monitoring decorator
*/
export function monitored(toolName) {
return function (target, propertyKey, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args) {
const startTime = performance.now();
const log = logger.child({ toolName, operation: propertyKey });
log.debug(`Starting ${propertyKey}`, { args: args.length });
try {
const result = await originalMethod.apply(this, args);
const duration = performance.now() - startTime;
metrics.recordToolExecution(toolName, duration, true);
log.info(`Completed ${propertyKey}`, { duration });
return result;
}
catch (error) {
const duration = performance.now() - startTime;
metrics.recordToolExecution(toolName, duration, false);
log.error(`Failed ${propertyKey}`, {
duration,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
};
return descriptor;
};
}
/**
* Export metrics in Prometheus format
*/
export function exportPrometheusMetrics() {
const summary = metrics.getSummary();
const lines = [];
// Help and type declarations
lines.push('# HELP cts_tool_executions_total Total number of tool executions');
lines.push('# TYPE cts_tool_executions_total counter');
lines.push('# HELP cts_tool_duration_seconds Tool execution duration in seconds');
lines.push('# TYPE cts_tool_duration_seconds gauge');
lines.push('# HELP cts_tool_errors_total Total number of tool errors');
lines.push('# TYPE cts_tool_errors_total counter');
lines.push('# HELP cts_tool_cache_hit_rate Cache hit rate for tool');
lines.push('# TYPE cts_tool_cache_hit_rate gauge');
// Metrics for each tool
summary.toolSummaries.forEach(tool => {
lines.push(`cts_tool_executions_total{tool="${tool.toolName}"} ${tool.executionCount}`);
lines.push(`cts_tool_duration_seconds{tool="${tool.toolName}",stat="avg"} ${(tool.averageDuration / 1000).toFixed(6)}`);
lines.push(`cts_tool_duration_seconds{tool="${tool.toolName}",stat="min"} ${(tool.minDuration / 1000).toFixed(6)}`);
lines.push(`cts_tool_duration_seconds{tool="${tool.toolName}",stat="max"} ${(tool.maxDuration / 1000).toFixed(6)}`);
lines.push(`cts_tool_errors_total{tool="${tool.toolName}"} ${tool.errorCount}`);
lines.push(`cts_tool_cache_hit_rate{tool="${tool.toolName}"} ${tool.cacheHitRate.toFixed(4)}`);
});
return lines.join('\n');
}
//# sourceMappingURL=index.js.map