meld
Version:
Meld: A template language for LLM prompts
208 lines (186 loc) • 6.52 kB
text/typescript
import winston from 'winston';
import path from 'path';
import { loggingConfig } from '@core/config/logging.js';
// Add colors to Winston
winston.addColors(loggingConfig.colors);
// Create formatters
const consoleFormat = winston.format.combine(
winston.format.timestamp({ format: loggingConfig.format.timestamp }),
winston.format.colorize({ all: loggingConfig.format.colorize }),
winston.format.printf(({ level, message, timestamp, service, ...metadata }) => {
// In non-debug mode, use more concise output
if (process.env.DEBUG !== 'true') {
// Only show error messages, no debug/info/etc
if (level !== 'error') {
return '';
}
// For errors, include minimal context
return `Error: ${message}`;
}
// Full verbose output for debug mode
let msg = `${timestamp} [${level}]${service ? ` [${service}]` : ''} ${message}`;
if (Object.keys(metadata).length > 0) {
msg += '\n' + JSON.stringify(metadata, null, 2);
}
return msg;
})
);
const fileFormat = winston.format.combine(
winston.format.timestamp({ format: loggingConfig.format.timestamp }),
winston.format.json()
);
// Determine the log level based on environment variables
const getLogLevel = () => {
// Explicit LOG_LEVEL takes precedence
if (process.env.LOG_LEVEL) {
return process.env.LOG_LEVEL;
}
// During tests, respect TEST_LOG_LEVEL or default to silent for minimal output
if (process.env.NODE_ENV === 'test') {
return process.env.TEST_LOG_LEVEL || 'error';
}
// In debug mode use debug level
if (process.env.DEBUG === 'true') {
return 'debug';
}
// Otherwise use the default level
return loggingConfig.defaultLevel;
};
// Create the logger instance
export const logger = winston.createLogger({
level: getLogLevel(),
levels: loggingConfig.levels,
transports: [
// Console transport (but not during silent test)
...((process.env.NODE_ENV === 'test' && !process.env.TEST_LOG_LEVEL) ? [] : [
new winston.transports.Console({
format: consoleFormat
})
]),
// File transport for all logs
new winston.transports.File({
filename: path.join(loggingConfig.files.directory, loggingConfig.files.mainLog),
format: fileFormat,
maxsize: loggingConfig.files.maxSize,
maxFiles: loggingConfig.files.maxFiles,
tailable: loggingConfig.files.tailable
}),
// Separate file for errors
new winston.transports.File({
filename: path.join(loggingConfig.files.directory, loggingConfig.files.errorLog),
level: 'error',
format: fileFormat,
maxsize: loggingConfig.files.maxSize,
maxFiles: loggingConfig.files.maxFiles,
tailable: loggingConfig.files.tailable
})
]
});
// Export the Logger interface for type safety
export interface Logger {
error(message: string, context?: Record<string, unknown>): void;
warn(message: string, context?: Record<string, unknown>): void;
info(message: string, context?: Record<string, unknown>): void;
debug(message: string, context?: Record<string, unknown>): void;
trace(message: string, context?: Record<string, unknown>): void;
level: string;
}
// Create a service-specific logger factory
export function createServiceLogger(serviceName: keyof typeof loggingConfig.services): winston.Logger {
const serviceConfig = loggingConfig.services[serviceName];
// Determine the service-specific log level based on environment variables
const getServiceLogLevel = () => {
// Explicit LOG_LEVEL takes precedence
if (process.env.LOG_LEVEL) {
return process.env.LOG_LEVEL;
}
// During tests, respect TEST_LOG_LEVEL or default to error for minimal output
if (process.env.NODE_ENV === 'test') {
return process.env.TEST_LOG_LEVEL || 'error';
}
// In debug mode use debug level
if (process.env.DEBUG === 'true') {
return 'debug';
}
// Otherwise use the service's configured level
return serviceConfig.level;
};
const logger = winston.createLogger({
level: getServiceLogLevel(),
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
defaultMeta: { service: serviceName },
transports: [
// Only use console transport outside of tests
...(process.env.NODE_ENV === 'test' ? [] : [
new winston.transports.Console({
format: consoleFormat,
level: getServiceLogLevel()
})
])
]
});
// Add a method to update the log level
let currentLevel = serviceConfig.level;
Object.defineProperty(logger, 'level', {
get() {
return currentLevel;
},
set(newLevel: string) {
currentLevel = newLevel;
logger.transports.forEach(transport => {
transport.level = newLevel;
});
}
});
return logger;
}
// Ensure logs directory exists
import fs from 'fs';
if (!fs.existsSync(loggingConfig.files.directory)) {
fs.mkdirSync(loggingConfig.files.directory);
}
// Add a stream interface for use with other logging tools
export const logStream = {
write: (message: string): void => {
logger.info(message.trim());
}
};
// Create service loggers
export const stateLogger = createServiceLogger('state');
export const parserLogger = createServiceLogger('parser');
export const interpreterLogger = createServiceLogger('interpreter');
export const filesystemLogger = createServiceLogger('filesystem');
export const validationLogger = createServiceLogger('validation');
export const outputLogger = createServiceLogger('output');
export const pathLogger = createServiceLogger('path');
export const directiveLogger = createServiceLogger('directive');
export const circularityLogger = createServiceLogger('circularity');
export const resolutionLogger = createServiceLogger('resolution');
export const importLogger = createServiceLogger('import');
export const cliLogger = createServiceLogger('cli');
export const embedLogger = createServiceLogger('embed');
// Export default logger for general use
export default logger;
// Add file transport in production
if (process.env.NODE_ENV === 'production') {
const fileTransport = new winston.transports.File({
filename: 'logs/error.log',
level: 'error'
});
// Add to all loggers
[
cliLogger,
directiveLogger,
interpreterLogger,
parserLogger,
outputLogger,
filesystemLogger,
pathLogger,
stateLogger
].forEach(logger => {
logger.add(fileTransport);
});
}