/**
* Structured logging with RFC 5424 levels
*
* Logs to stderr for MCP protocol compatibility (stdout is for protocol messages).
* Optionally logs to rotating daily files.
*/
import { appendFileSync, mkdirSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { getConfig } from '../config/index.js';
import type { LogLevel, LogEntry } from '../types/index.js';
const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
debug: 0,
info: 1,
notice: 2,
warning: 3,
error: 4,
critical: 5,
};
/**
* Logger class with structured output and file rotation
*/
export class Logger {
private readonly name: string;
private readonly minLevel: number;
private readonly logToFile: boolean;
private readonly logDir: string;
constructor(name: string) {
const config = getConfig();
this.name = name;
this.minLevel = LOG_LEVEL_PRIORITY[config.logLevel];
this.logToFile = config.logToFile;
this.logDir = config.logDir;
if (this.logToFile) {
this.ensureLogDir();
}
}
private ensureLogDir(): void {
if (!existsSync(this.logDir)) {
mkdirSync(this.logDir, { recursive: true });
}
}
private getLogFilePath(): string {
const date = new Date().toISOString().split('T')[0];
return join(this.logDir, `${getConfig().serverName}-${date}.log`);
}
private shouldLog(level: LogLevel): boolean {
return LOG_LEVEL_PRIORITY[level] >= this.minLevel;
}
private formatEntry(entry: LogEntry): string {
return JSON.stringify(entry);
}
private log(level: LogLevel, message: string, data?: Record<string, unknown>): void {
if (!this.shouldLog(level)) {
return;
}
const entry: LogEntry = {
level,
logger: this.name,
message,
data,
timestamp: new Date().toISOString(),
};
const formatted = this.formatEntry(entry);
// Always log to stderr for MCP compatibility
console.error(formatted);
// Optionally log to file
if (this.logToFile) {
try {
appendFileSync(this.getLogFilePath(), formatted + '\n');
} catch {
// Silently fail file logging to avoid disrupting MCP protocol
}
}
}
debug(message: string, data?: Record<string, unknown>): void {
this.log('debug', message, data);
}
info(message: string, data?: Record<string, unknown>): void {
this.log('info', message, data);
}
notice(message: string, data?: Record<string, unknown>): void {
this.log('notice', message, data);
}
warning(message: string, data?: Record<string, unknown>): void {
this.log('warning', message, data);
}
error(message: string, data?: Record<string, unknown>): void {
this.log('error', message, data);
}
critical(message: string, data?: Record<string, unknown>): void {
this.log('critical', message, data);
}
/**
* Create a child logger with a sub-namespace
*/
child(subName: string): Logger {
return new Logger(`${this.name}/${subName}`);
}
}
/**
* Create a logger instance for a module
*/
export function createLogger(name: string): Logger {
return new Logger(`${getConfig().serverName}/${name}`);
}