UNPKG

@toolprint/hypertool-mcp

Version:

HyperTool MCP proxy server for routing requests between clients and multiple underlying MCP servers

256 lines • 8.82 kB
/** * Enhanced logging framework with feature flag support * Supports both Pino (default) and mcp-logger implementations */ import { DEFAULT_PINO_LOGGING_CONFIG } from "./logger/pinoLogger.js"; import { getLoggerFactory, createLoggerWithFeatureFlag, createLoggerSyncWithFeatureFlag, } from "./logger/factory.js"; // Use Pino defaults as the main defaults export const DEFAULT_LOGGING_CONFIG = DEFAULT_PINO_LOGGING_CONFIG; /** * Create logging configuration based on runtime options and environment */ export function createLoggingConfig(runtimeOptions) { // Determine format from environment variable or default to pretty const format = (process.env.LOG_FORMAT === "json" ? "json" : "pretty"); return { ...DEFAULT_LOGGING_CONFIG, format, // Override level if provided in runtime options level: runtimeOptions?.logLevel || DEFAULT_LOGGING_CONFIG.level, }; } /** * Logger class that wraps the feature-flag-selected implementation * Maintains backward compatibility with existing API */ export class Logger { implementation; childLoggerCache = new Map(); constructor(config = {}, runtimeOptions) { // Use synchronous factory method for immediate availability this.implementation = createLoggerSyncWithFeatureFlag(config, runtimeOptions); } // Delegate all methods to the underlying implementation fatal(message, context) { this.implementation.fatal(message, context); } error(message, context) { this.implementation.error(message, context); } warn(message, context) { this.implementation.warn(message, context); } info(message, context) { this.implementation.info(message, context); } debug(message, context) { this.implementation.debug(message, context); } trace(message, context) { this.implementation.trace(message, context); } child(bindings) { // Create cache key from bindings const cacheKey = JSON.stringify(bindings); // Check cache first if (this.childLoggerCache.has(cacheKey)) { return this.childLoggerCache.get(cacheKey); } // Create new child logger const childImplementation = this.implementation.child(bindings); const childLogger = new Logger(); childLogger.implementation = childImplementation; // Cache the child logger this.childLoggerCache.set(cacheKey, childLogger); // Implement cache size limit to prevent unbounded growth if (this.childLoggerCache.size > 100) { const firstKey = this.childLoggerCache.keys().next().value; if (firstKey) { this.childLoggerCache.delete(firstKey); } } return childLogger; } updateConfig(config) { this.implementation.updateConfig(config); } // Legacy API compatibility get pino() { return this.implementation.pino; } get mcp() { return this.implementation.mcp; } /** * Get cache statistics for diagnostics */ getCacheStats() { return { childLoggerCount: this.childLoggerCache.size, cacheSize: this.childLoggerCache.size, cacheKeys: Array.from(this.childLoggerCache.keys()), }; } } // Global logger management let globalLogger = null; let selectedLoggingConfig = null; let globalRuntimeOptions = undefined; /** * Check if two logging configurations are equivalent */ function areConfigsEquivalent(config1, config2) { if (config1 === config2) return true; if (!config1 || !config2) return false; // Compare relevant config properties const finalConfig1 = { ...DEFAULT_LOGGING_CONFIG, ...config1 }; const finalConfig2 = { ...DEFAULT_LOGGING_CONFIG, ...config2 }; return (finalConfig1.level === finalConfig2.level && finalConfig1.enableConsole === finalConfig2.enableConsole && finalConfig1.enableFile === finalConfig2.enableFile && finalConfig1.serverName === finalConfig2.serverName && finalConfig1.format === finalConfig2.format); } /** * Check if runtime options are equivalent for logging purposes */ function areRuntimeOptionsEquivalent(opt1, opt2) { if (opt1 === opt2) return true; if (!opt1 || !opt2) return !opt1 && !opt2; return (opt1.transport === opt2.transport && opt1.logLevel === opt2.logLevel && opt1.debug === opt2.debug); } /** * Get or create the global logger instance */ export function getLogger(config, runtimeOptions) { const finalConfig = config || createLoggingConfig(runtimeOptions); // Check if we can reuse the existing global logger if (globalLogger && areConfigsEquivalent(selectedLoggingConfig, finalConfig) && areRuntimeOptionsEquivalent(globalRuntimeOptions, runtimeOptions)) { return globalLogger; } // Create new logger only if config has changed or no logger exists globalLogger = new Logger(finalConfig, runtimeOptions); selectedLoggingConfig = finalConfig; globalRuntimeOptions = runtimeOptions; return globalLogger; } /** * Async version of getLogger that properly initializes feature flags */ export async function getLoggerAsync(config, runtimeOptions) { const finalConfig = config || createLoggingConfig(runtimeOptions); const implementation = await createLoggerWithFeatureFlag(finalConfig, runtimeOptions); const logger = new Logger(); logger.implementation = implementation; if (config || runtimeOptions) { globalLogger = logger; selectedLoggingConfig = finalConfig; } return logger; } /** * Get the currently active logging configuration */ export function getActiveLoggingConfig() { return selectedLoggingConfig; } /** * Create a child logger with module context * This maintains backward compatibility with the existing API */ export function createChildLogger(bindings) { // If we don't have a global logger yet, create one with environment-aware config if (!globalLogger) { const config = createLoggingConfig(); // For early initialization, assume stdio transport (most common case) const runtimeOptions = { transport: "stdio", debug: false, insecure: false, }; globalLogger = new Logger(config, runtimeOptions); selectedLoggingConfig = config; } return globalLogger.child(bindings); } /** * Async version of createChildLogger that properly initializes feature flags */ export async function createChildLoggerAsync(bindings) { if (!globalLogger) { const config = createLoggingConfig(); const runtimeOptions = { transport: "stdio", debug: false, insecure: false, }; globalLogger = await getLoggerAsync(config, runtimeOptions); selectedLoggingConfig = config; } return globalLogger.child(bindings); } /** * Get diagnostic information about the logging system * Useful for debugging EventEmitter memory leaks */ export function getLoggerDiagnostics() { const result = { hasGlobalLogger: globalLogger !== null, implementationType: "unknown", }; if (globalLogger) { const impl = globalLogger.implementation; // Detect implementation type if (impl.constructor.name === "PinoLogger") { result.implementationType = "pino"; } else if (impl.constructor.name === "McpLoggerWrapper") { result.implementationType = "mcp-logger"; } // Get cache stats from the Logger wrapper itself if (typeof globalLogger.getCacheStats === "function") { result.cacheStats = globalLogger.getCacheStats(); } } return result; } /** * Reset global logger state (mainly for testing) */ export function resetGlobalLogger() { globalLogger = null; selectedLoggingConfig = null; globalRuntimeOptions = undefined; // Reset factory cache const factory = getLoggerFactory(); factory.resetCache(); // Clear Pino instance cache // Dynamic import to avoid circular dependency import("./logger/pinoLogger.js") .then(({ PinoLogger }) => { PinoLogger.clearInstanceCache(); }) .catch(() => { // Ignore errors during cache clearing }); } /** * Force feature flag for testing */ export function forceSetMcpLoggerEnabled(enabled) { const factory = getLoggerFactory(); factory.forceSetMcpLoggerEnabled(enabled); // Reset only the global logger state, not the factory cache globalLogger = null; selectedLoggingConfig = null; globalRuntimeOptions = undefined; } //# sourceMappingURL=logging.js.map