UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

396 lines 13.4 kB
/** * Pino-based logging infrastructure for Optimizely MCP Server * @description Provides file-based logging that is safe for StdioServerTransport * * CRITICAL: This logging system is designed to prevent stdout contamination * which would break MCP StdioServerTransport communication. * * @author Optimizely MCP Server * @version 1.0.0 */ import pino from 'pino'; import path from 'path'; import fs from 'fs'; import os from 'os'; /** * Default logging configuration * @description Provides sensible defaults for out-of-the-box usage */ const DEFAULT_CONFIG = { logFile: './logs/optimizely-mcp.log', logLevel: 'info', consoleLogging: false, prettyPrint: false, maxFileSize: 10 * 1024 * 1024, // 10MB maxFiles: 5 }; /** * Optimizely MCP Server Logger * @description Thread-safe, async logging system designed for MCP server usage * * Key Features: * - File-based logging (safe for StdioServerTransport) * - Configurable via environment variables * - Automatic directory creation * - Fallback to temp directory if primary path fails * - Structured JSON logging for production * - Optional stderr logging for development * * Environment Variables: * - OPTIMIZELY_MCP_LOG_FILE: Log file path * - OPTIMIZELY_MCP_LOG_LEVEL: Log level (trace|debug|info|warn|error|fatal) * - OPTIMIZELY_MCP_CONSOLE_LOGGING: Enable stderr logging (true|false) * * @example * ```typescript * import { createLogger } from './logging/Logger.js'; * * const logger = createLogger(); * logger.info('Server started successfully'); * logger.error('Failed to connect to API', { error: 'Connection timeout' }); * ``` */ export class OptimizelyMCPLogger { logger; config; /** * Creates a new OptimizelyMCPLogger instance * @param config - Logger configuration options * @throws {Error} When logger initialization fails */ constructor(config = {}) { this.config = this.mergeConfig(config); this.logger = this.initializeLogger(); } /** * Merges user configuration with defaults and environment variables * @param userConfig - User-provided configuration * @returns Complete configuration with all required fields * @private */ mergeConfig(userConfig) { return { logFile: process.env.OPTIMIZELY_MCP_LOG_FILE || userConfig.logFile || DEFAULT_CONFIG.logFile, logLevel: process.env.OPTIMIZELY_MCP_LOG_LEVEL || userConfig.logLevel || DEFAULT_CONFIG.logLevel, consoleLogging: process.env.OPTIMIZELY_MCP_CONSOLE_LOGGING === 'true' || userConfig.consoleLogging || DEFAULT_CONFIG.consoleLogging, prettyPrint: userConfig.prettyPrint || DEFAULT_CONFIG.prettyPrint, maxFileSize: userConfig.maxFileSize || DEFAULT_CONFIG.maxFileSize, maxFiles: userConfig.maxFiles || DEFAULT_CONFIG.maxFiles }; } /** * Initializes the Pino logger with appropriate transports * @returns Configured Pino logger instance * @throws {Error} When logger setup fails * @private */ initializeLogger() { try { // Ensure log directory exists const logFile = this.ensureLogDirectory(this.config.logFile); // Archive existing large log file if needed this.archiveExistingLargeLog(logFile); // Create transport configuration const transports = []; // File transport with rotation support transports.push({ target: 'pino-roll', level: this.config.logLevel, options: { file: logFile, size: `${Math.floor(this.config.maxFileSize / (1024 * 1024))}m`, // Convert bytes to MB frequency: 'daily', // Also rotate daily limit: { count: this.config.maxFiles }, mkdir: true } }); // Optional stderr transport (safe for debugging) if (this.config.consoleLogging) { transports.push({ target: this.config.prettyPrint ? 'pino-pretty' : 'pino/file', level: this.config.logLevel, options: this.config.prettyPrint ? { destination: 2, // stderr colorize: true, translateTime: 'SYS:yyyy-mm-dd HH:MM:ss' } : { destination: 2 } // stderr }); } // Create logger with transport const logger = pino({ level: this.config.logLevel, timestamp: pino.stdTimeFunctions.isoTime, formatters: { level: (label) => ({ level: label }), bindings: (bindings) => ({ pid: bindings.pid, hostname: bindings.hostname, service: 'optimizely-mcp-server' }) } }, pino.transport({ targets: transports })); // Log successful initialization (to file only) logger.info({ logFile, logLevel: this.config.logLevel, consoleLogging: this.config.consoleLogging }, 'OptimizelyMCPLogger initialized successfully'); return logger; } catch (error) { // Fallback: Create basic logger that writes to temp directory const fallbackFile = path.join(os.tmpdir(), 'optimizely-mcp-fallback.log'); const fallbackLogger = pino({ level: 'error', timestamp: pino.stdTimeFunctions.isoTime }, pino.transport({ target: 'pino/file', options: { destination: fallbackFile } })); fallbackLogger.error({ error: error.message, fallbackFile }, 'Logger initialization failed, using fallback configuration'); return fallbackLogger; } } /** * Ensures the log directory exists and returns a valid log file path * @param logFilePath - Desired log file path * @returns Validated log file path * @throws {Error} When directory creation fails * @private */ ensureLogDirectory(logFilePath) { try { const logDir = path.dirname(logFilePath); // Create directory if it doesn't exist if (!fs.existsSync(logDir)) { fs.mkdirSync(logDir, { recursive: true }); } // Test write permissions const testFile = path.join(logDir, '.write-test'); fs.writeFileSync(testFile, 'test'); fs.unlinkSync(testFile); return logFilePath; } catch (error) { // Fallback to temp directory const fallbackDir = path.join(os.tmpdir(), 'optimizely-mcp-logs'); if (!fs.existsSync(fallbackDir)) { fs.mkdirSync(fallbackDir, { recursive: true }); } const fallbackFile = path.join(fallbackDir, 'optimizely-mcp.log'); // Use stderr for this critical error (safe for MCP) if (this.config.consoleLogging) { console.error(`[OptimizelyMCPLogger] Cannot write to ${logFilePath}, using fallback: ${fallbackFile}`); } return fallbackFile; } } /** * Archives existing log file if it exceeds the max size * @param logFile - Path to the log file * @private */ archiveExistingLargeLog(logFile) { try { if (fs.existsSync(logFile)) { const stats = fs.statSync(logFile); if (stats.size > this.config.maxFileSize) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5); // Remove milliseconds const archivePath = `${logFile}.${timestamp}.archive`; fs.renameSync(logFile, archivePath); // Log to stderr so it doesn't interfere with MCP if (this.config.consoleLogging) { console.error(`[OptimizelyMCPLogger] Archived large log file (${Math.round(stats.size / 1024 / 1024)}MB) to: ${path.basename(archivePath)}`); } } } } catch (error) { // Don't let archive failures break logging if (this.config.consoleLogging) { console.error(`[OptimizelyMCPLogger] Failed to archive large log: ${error.message}`); } } } /** * Logs a trace level message * @param obj - Object or message to log * @param msg - Optional message string */ trace(obj, msg) { this.logger.trace(obj, msg); } /** * Logs a debug level message * @param obj - Object or message to log * @param msg - Optional message string */ debug(obj, msg) { this.logger.debug(obj, msg); } /** * Logs an info level message * @param obj - Object or message to log * @param msg - Optional message string */ info(obj, msg) { this.logger.info(obj, msg); } /** * Logs a warning level message * @param obj - Object or message to log * @param msg - Optional message string */ warn(obj, msg) { this.logger.warn(obj, msg); } /** * Logs an error level message * @param obj - Object or message to log * @param msg - Optional message string */ error(obj, msg) { this.logger.error(obj, msg); } /** * Logs a fatal level message * @param obj - Object or message to log * @param msg - Optional message string */ fatal(obj, msg) { this.logger.fatal(obj, msg); } /** * Creates a child logger with additional context * @param bindings - Additional context to include in all log messages * @returns Child logger instance */ child(bindings) { const childLogger = new OptimizelyMCPLogger(this.config); childLogger.logger = this.logger.child(bindings); return childLogger; } /** * Flushes any pending log messages * @description Ensures all log messages are written before process exit * @returns Promise that resolves when flush is complete */ async flush() { return new Promise((resolve) => { this.logger.flush(() => { resolve(); }); }); } /** * Gets the current log level * @returns Current log level string */ getLevel() { return this.config.logLevel; } /** * Sets the log level dynamically * @param level - New log level to set */ setLevel(level) { this.config.logLevel = level; this.logger.level = level; } /** * Gets the current log file path * @returns Current log file path */ getLogFile() { return this.config.logFile; } } /** * Global logger instance for the Optimizely MCP Server * @description Singleton logger that can be imported throughout the application */ let globalLogger = null; /** * Creates and configures the global logger instance * @param config - Optional logger configuration * @returns Configured logger instance * * @example * ```typescript * import { createLogger } from './logging/Logger.js'; * * const logger = createLogger({ * logLevel: 'debug', * consoleLogging: true * }); * ``` */ export function createLogger(config) { if (!globalLogger) { globalLogger = new OptimizelyMCPLogger(config); } return globalLogger; } /** * Gets the global logger instance * @returns Global logger instance (creates one if none exists) * * @example * ```typescript * import { getLogger } from './logging/Logger.js'; * * const logger = getLogger(); * logger.info('Application started'); * ``` */ export function getLogger() { if (!globalLogger) { globalLogger = new OptimizelyMCPLogger(); } return globalLogger; } /** * Flushes all pending log messages and closes the logger * @description Should be called before process exit to ensure all logs are written * @returns Promise that resolves when shutdown is complete * * @example * ```typescript * import { shutdownLogger } from './logging/Logger.js'; * * process.on('SIGTERM', async () => { * await shutdownLogger(); * process.exit(0); * }); * ``` */ export async function shutdownLogger() { if (globalLogger) { await globalLogger.flush(); globalLogger = null; } } /** * Safe stderr logging function for emergency use * @description When all else fails, this provides a way to log to stderr * @param message - Message to log to stderr * * @example * ```typescript * import { emergencyLog } from './logging/Logger.js'; * * emergencyLog('Critical system failure - using emergency logging'); * ``` */ export function emergencyLog(message) { const timestamp = new Date().toISOString(); console.error(`[${timestamp}] [EMERGENCY] ${message}`); } //# sourceMappingURL=Logger.js.map