import winston from 'winston';
import { loggingConfig, isDevelopment } from '../config';
import path from 'path';
import fs from 'fs';
// Ensure logs directory exists
const logsDir = path.dirname(loggingConfig.file.filename);
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir, { recursive: true });
}
// Custom log format
const logFormat = winston.format.combine(
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss'
}),
winston.format.errors({ stack: true }),
winston.format.json()
);
// Console format for development
const consoleFormat = winston.format.combine(
winston.format.colorize(),
winston.format.timestamp({
format: 'HH:mm:ss'
}),
winston.format.printf(({ timestamp, level, message, stack, ...meta }) => {
let log = `${timestamp} [${level}]: ${message}`;
if (stack) {
log += `\n${stack}`;
}
if (Object.keys(meta).length > 0) {
log += `\n${JSON.stringify(meta, null, 2)}`;
}
return log;
})
);
// Create winston logger
const logger = winston.createLogger({
level: loggingConfig.level,
format: logFormat,
defaultMeta: {
service: 'mcp-calendar-server',
version: process.env.npm_package_version || '1.0.0'
},
transports: []
});
// Add file transport if enabled
if (loggingConfig.file.enabled) {
logger.add(new winston.transports.File({
filename: loggingConfig.file.filename,
maxsize: parseSize(loggingConfig.file.maxSize),
maxFiles: loggingConfig.file.maxFiles,
tailable: true
}));
}
// Add console transport if enabled
if (loggingConfig.console.enabled) {
logger.add(new winston.transports.Console({
format: isDevelopment() ? consoleFormat : logFormat
}));
}
// Parse size string (e.g., "10m", "1g")
function parseSize(size: string): number {
const units: Record<string, number> = {
'b': 1,
'k': 1024,
'm': 1024 * 1024,
'g': 1024 * 1024 * 1024
};
const match = size.toLowerCase().match(/^(\d+)([bkmg]?)$/);
if (!match) return 10 * 1024 * 1024; // Default 10MB
const [, num, unit] = match;
return parseInt(num) * (units[unit] || 1);
}
// Structured logging methods
export class Logger {
// Basic logging methods
static error(message: string, error?: Error | unknown, meta?: Record<string, any>): void {
logger.error(message, {
error: error instanceof Error ? {
name: error.name,
message: error.message,
stack: error.stack
} : error,
...meta
});
}
static warn(message: string, meta?: Record<string, any>): void {
logger.warn(message, meta);
}
static info(message: string, meta?: Record<string, any>): void {
logger.info(message, meta);
}
static debug(message: string, meta?: Record<string, any>): void {
logger.debug(message, meta);
}
// Specific logging methods for different contexts
static auth(message: string, userId?: string, meta?: Record<string, any>): void {
logger.info(`[AUTH] ${message}`, {
userId,
category: 'authentication',
...meta
});
}
static calendar(message: string, userId?: string, operation?: string, meta?: Record<string, any>): void {
logger.info(`[CALENDAR] ${message}`, {
userId,
operation,
category: 'calendar',
...meta
});
}
static tool(toolName: string, userId?: string, duration?: number, success?: boolean, meta?: Record<string, any>): void {
logger.info(`[TOOL] ${toolName}`, {
userId,
toolName,
duration,
success,
category: 'tool_execution',
...meta
});
}
static performance(operation: string, duration: number, userId?: string, meta?: Record<string, any>): void {
logger.info(`[PERF] ${operation}`, {
userId,
operation,
duration,
category: 'performance',
...meta
});
}
static security(message: string, ip?: string, userId?: string, meta?: Record<string, any>): void {
logger.warn(`[SECURITY] ${message}`, {
userId,
ip,
category: 'security',
...meta
});
}
static database(message: string, operation?: string, duration?: number, meta?: Record<string, any>): void {
logger.debug(`[DB] ${message}`, {
operation,
duration,
category: 'database',
...meta
});
}
static cache(message: string, operation?: string, hit?: boolean, meta?: Record<string, any>): void {
logger.debug(`[CACHE] ${message}`, {
operation,
hit,
category: 'cache',
...meta
});
}
static rateLimit(message: string, userId?: string, ip?: string, meta?: Record<string, any>): void {
logger.warn(`[RATE_LIMIT] ${message}`, {
userId,
ip,
category: 'rate_limiting',
...meta
});
}
// Request logging
static request(method: string, url: string, statusCode: number, duration: number, userId?: string, ip?: string): void {
logger.info(`[REQUEST] ${method} ${url}`, {
method,
url,
statusCode,
duration,
userId,
ip,
category: 'http_request'
});
}
// MCP specific logging
static mcp(message: string, toolName?: string, userId?: string, meta?: Record<string, any>): void {
logger.info(`[MCP] ${message}`, {
toolName,
userId,
category: 'mcp',
...meta
});
}
// Application lifecycle logging
static startup(message: string, meta?: Record<string, any>): void {
logger.info(`[STARTUP] ${message}`, {
category: 'application_lifecycle',
...meta
});
}
static shutdown(message: string, meta?: Record<string, any>): void {
logger.info(`[SHUTDOWN] ${message}`, {
category: 'application_lifecycle',
...meta
});
}
// Health check logging
static health(component: string, status: 'healthy' | 'unhealthy', meta?: Record<string, any>): void {
if (status === 'healthy') {
logger.debug(`[HEALTH] ${component} is healthy`, {
component,
status,
category: 'health_check',
...meta
});
} else {
logger.error(`[HEALTH] ${component} is unhealthy`, {
component,
status,
category: 'health_check',
...meta
});
}
}
}
// Express middleware for request logging
export const requestLogger = (req: any, res: any, next: any): void => {
const startTime = Date.now();
// Log request start
Logger.debug(`Request started: ${req.method} ${req.url}`, {
method: req.method,
url: req.url,
userAgent: req.get('User-Agent'),
ip: req.ip,
userId: req.userId
});
// Override res.end to log completion
const originalEnd = res.end;
res.end = function(this: any, ...args: any[]) {
const duration = Date.now() - startTime;
Logger.request(
req.method,
req.url,
res.statusCode,
duration,
req.userId,
req.ip
);
// Call original end method
return originalEnd.apply(this, args);
};
next();
};
// Error logging helper
export const logError = (error: Error | unknown, context?: string, meta?: Record<string, any>): void => {
Logger.error(
`${context ? `[${context}] ` : ''}${error instanceof Error ? error.message : 'Unknown error'}`,
error,
meta
);
};
// Create a child logger for specific components
export const createChildLogger = (component: string) => {
return {
error: (message: string, error?: Error | unknown, meta?: Record<string, any>) =>
Logger.error(`[${component}] ${message}`, error, meta),
warn: (message: string, meta?: Record<string, any>) =>
Logger.warn(`[${component}] ${message}`, meta),
info: (message: string, meta?: Record<string, any>) =>
Logger.info(`[${component}] ${message}`, meta),
debug: (message: string, meta?: Record<string, any>) =>
Logger.debug(`[${component}] ${message}`, meta),
};
};
export default logger;