import fs from 'fs/promises';
import path from 'path';
/**
* Enhanced logging system with multiple levels and file output
*/
class Logger {
constructor(options = {}) {
this.level = options.level || (process.env.NODE_ENV === 'development' ? 'debug' : 'info');
this.enableConsole = options.enableConsole !== false;
this.enableFile = options.enableFile !== false;
this.logDir = options.logDir || path.join(process.cwd(), 'logs');
this.maxFileSize = options.maxFileSize || 10 * 1024 * 1024; // 10MB
this.maxFiles = options.maxFiles || 5;
this.levels = {
debug: 0,
info: 1,
warn: 2,
error: 3
};
this.colors = {
debug: '\x1b[36m', // Cyan
info: '\x1b[32m', // Green
warn: '\x1b[33m', // Yellow
error: '\x1b[31m', // Red
reset: '\x1b[0m'
};
this.emojis = {
debug: '🔍',
info: 'ℹ️',
warn: '⚠️',
error: '❌'
};
// Initialize logging
this.initialize();
}
async initialize() {
if (this.enableFile) {
try {
await fs.mkdir(this.logDir, { recursive: true });
} catch (error) {
console.error('Failed to create log directory:', error);
this.enableFile = false;
}
}
}
shouldLog(level) {
return this.levels[level] >= this.levels[this.level];
}
formatMessage(level, message, meta = {}) {
const timestamp = new Date().toISOString();
const emoji = this.emojis[level];
const upperLevel = level.toUpperCase();
let formatted = `[${timestamp}] ${emoji} ${upperLevel}: ${message}`;
if (Object.keys(meta).length > 0) {
formatted += ` | ${JSON.stringify(meta)}`;
}
return formatted;
}
formatConsoleMessage(level, message, meta = {}) {
const timestamp = new Date().toISOString();
const color = this.colors[level];
const reset = this.colors.reset;
const emoji = this.emojis[level];
let formatted = `${color}${emoji} ${message}${reset}`;
if (Object.keys(meta).length > 0) {
formatted += ` ${color}${JSON.stringify(meta, null, 2)}${reset}`;
}
return formatted;
}
async writeToFile(level, message, meta = {}) {
if (!this.enableFile) return;
try {
const logFile = path.join(this.logDir, `mcp-server.log`);
const formatted = this.formatMessage(level, message, meta) + '\n';
// Check file size and rotate if needed
try {
const stats = await fs.stat(logFile);
if (stats.size > this.maxFileSize) {
await this.rotateLogFile();
}
} catch (error) {
// File doesn't exist, no rotation needed
}
await fs.appendFile(logFile, formatted);
} catch (error) {
console.error('Failed to write to log file:', error);
}
}
async rotateLogFile() {
try {
const logFile = path.join(this.logDir, 'mcp-server.log');
// Rotate existing files
for (let i = this.maxFiles - 1; i > 0; i--) {
const oldFile = path.join(this.logDir, `mcp-server.log.${i}`);
const newFile = path.join(this.logDir, `mcp-server.log.${i + 1}`);
try {
await fs.rename(oldFile, newFile);
} catch (error) {
// File doesn't exist, continue
}
}
// Move current log to .1
const firstRotation = path.join(this.logDir, 'mcp-server.log.1');
try {
await fs.rename(logFile, firstRotation);
} catch (error) {
// File doesn't exist, continue
}
// Delete oldest file if it exceeds max files
const oldestFile = path.join(this.logDir, `mcp-server.log.${this.maxFiles}`);
try {
await fs.unlink(oldestFile);
} catch (error) {
// File doesn't exist, ignore
}
} catch (error) {
console.error('Failed to rotate log file:', error);
}
}
async log(level, message, meta = {}) {
if (!this.shouldLog(level)) return;
// Console output
if (this.enableConsole) {
const consoleMessage = this.formatConsoleMessage(level, message, meta);
if (level === 'error') {
console.error(consoleMessage);
} else if (level === 'warn') {
console.warn(consoleMessage);
} else {
console.log(consoleMessage);
}
}
// File output
await this.writeToFile(level, message, meta);
}
debug(message, meta = {}) {
return this.log('debug', message, meta);
}
info(message, meta = {}) {
return this.log('info', message, meta);
}
warn(message, meta = {}) {
return this.log('warn', message, meta);
}
error(message, meta = {}) {
return this.log('error', message, meta);
}
// Performance monitoring
async logPerformance(operation, duration, meta = {}) {
const performanceMeta = {
...meta,
operation,
duration: `${duration}ms`,
performance: true
};
if (duration > 1000) {
await this.warn(`Slow operation detected: ${operation}`, performanceMeta);
} else if (duration > 500) {
await this.info(`Performance: ${operation}`, performanceMeta);
} else {
await this.debug(`Performance: ${operation}`, performanceMeta);
}
}
// Tool usage logging
async logToolUsage(toolName, args, result, duration) {
const meta = {
tool: toolName,
args: Object.keys(args || {}),
success: !result?.error,
duration: `${duration}ms`
};
if (result?.error) {
await this.error(`Tool failed: ${toolName}`, { ...meta, error: result.error });
} else {
await this.info(`Tool executed: ${toolName}`, meta);
}
}
// Learning cycle logging
async logLearningCycle(stats) {
await this.info('Learning cycle completed', {
patterns: stats.patterns,
knowledge: stats.knowledge,
cycles: stats.cycles,
learningCycle: true
});
}
// Memory usage monitoring
async logMemoryUsage() {
const usage = process.memoryUsage();
const formatted = {
rss: `${Math.round(usage.rss / 1024 / 1024)}MB`,
heapTotal: `${Math.round(usage.heapTotal / 1024 / 1024)}MB`,
heapUsed: `${Math.round(usage.heapUsed / 1024 / 1024)}MB`,
external: `${Math.round(usage.external / 1024 / 1024)}MB`
};
await this.debug('Memory usage', formatted);
// Warn if memory usage is high
if (usage.heapUsed > 512 * 1024 * 1024) { // 512MB
await this.warn('High memory usage detected', formatted);
}
}
// Create child logger with additional context
child(context = {}) {
const childLogger = Object.create(this);
childLogger.context = { ...this.context, ...context };
// Override log method to include context
childLogger.log = async (level, message, meta = {}) => {
const combinedMeta = { ...childLogger.context, ...meta };
return this.log.call(this, level, message, combinedMeta);
};
return childLogger;
}
}
// Create default logger instance
const logger = new Logger({
level: process.env.LOG_LEVEL || (process.env.NODE_ENV === 'development' ? 'debug' : 'info'),
enableConsole: process.env.LOG_CONSOLE !== 'false',
enableFile: process.env.LOG_FILE !== 'false'
});
export default logger;
export { Logger };