/**
* Logger Utility for LearnMCP
* Follows Forest's winston logging patterns for consistency
*/
import winston from 'winston';
import path from 'path';
// Log levels matching Forest patterns
const LOG_LEVELS = {
error: 0,
warn: 1,
info: 2,
debug: 3,
};
// Colors for console output
const LOG_COLORS = {
error: 'red',
warn: 'yellow',
info: 'green',
debug: 'blue',
};
winston.addColors(LOG_COLORS);
/**
* Create a logger instance with Forest-compatible formatting
*/
export function createLogger(module = 'LearnMCP') {
// Get data directory for log files
const dataDir = process.env.FOREST_DATA_DIR || './.forest-data';
const logDir = path.join(dataDir, 'logs');
// File format for structured logging
const fileFormat = winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.printf(({ timestamp, level, message, stack, ...meta }) => {
// Build the log entry - ensure level is a string
const levelStr = typeof level === 'string' ? level : 'info';
let logEntry = `${timestamp} [${levelStr.toUpperCase()}]`;
// Add context if available
if (meta.module) {
logEntry += ` [${meta.module}]`;
}
if (meta.component) {
logEntry += ` [${meta.component}]`;
}
if (meta.projectId) {
logEntry += ` [Project:${meta.projectId}]`;
}
if (meta.sourceId) {
logEntry += ` [Source:${meta.sourceId}]`;
}
logEntry += `: ${message}`;
// Add stack trace for errors
if (stack) {
logEntry += `\n${stack}`;
}
// Add metadata if present
const metaKeys = Object.keys(meta).filter(
key =>
![
'module',
'component',
'projectId',
'sourceId',
'timestamp',
'level',
'message',
].includes(key)
);
if (metaKeys.length > 0) {
const metaData = {};
metaKeys.forEach(key => {
metaData[key] = meta[key];
});
logEntry += ` | Meta: ${JSON.stringify(metaData)}`;
}
return logEntry;
})
);
// Console format with colors
const consoleFormat = winston.format.combine(
winston.format.colorize(),
winston.format.timestamp({
format: 'HH:mm:ss.SSS',
}),
winston.format.printf(({ timestamp, level, message, ...meta }) => {
let logEntry = `${timestamp} ${level}`;
if (meta.module) {
logEntry += ` [${meta.module}]`;
}
if (meta.component) {
logEntry += ` [${meta.component}]`;
}
logEntry += `: ${message}`;
return logEntry;
})
);
// Create transports
const transports = [
// Console transport for development
new winston.transports.Console({
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
format: consoleFormat,
}),
];
// Add file transports in production or when FOREST_DATA_DIR is set
if (process.env.NODE_ENV === 'production' || process.env.FOREST_DATA_DIR) {
transports.push(
// General log file
new winston.transports.File({
filename: path.join(logDir, 'learn-mcp.log'),
level: 'info',
format: fileFormat,
maxsize: 10 * 1024 * 1024, // 10MB
maxFiles: 5,
}),
// Error log file
new winston.transports.File({
filename: path.join(logDir, 'learn-mcp-errors.log'),
level: 'error',
format: fileFormat,
maxsize: 10 * 1024 * 1024, // 10MB
maxFiles: 3,
})
);
}
// Create the logger
const logger = winston.createLogger({
levels: LOG_LEVELS,
level: process.env.LOG_LEVEL || (process.env.NODE_ENV === 'production' ? 'info' : 'debug'),
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true })
),
transports,
// Prevent crashes on uncaught exceptions
exitOnError: false,
});
// Add module context to all log calls
const contextLogger = {
error: (message, meta = {}) => logger.error(message, { module, ...meta }),
warn: (message, meta = {}) => logger.warn(message, { module, ...meta }),
info: (message, meta = {}) => logger.info(message, { module, ...meta }),
debug: (message, meta = {}) => logger.debug(message, { module, ...meta }),
};
return contextLogger;
}