@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
396 lines • 13.4 kB
JavaScript
/**
* 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