@toolprint/hypertool-mcp
Version:
HyperTool MCP proxy server for routing requests between clients and multiple underlying MCP servers
256 lines • 8.82 kB
JavaScript
/**
* 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