/** * @sim/logger * * Framework-agnostic logging utilities for the Sim platform. * Provides standardized console logging with environment-aware configuration. */ import chalk from 'chalk' import { getRequestContext } from './request-context' /** * LogLevel enum defines the severity levels for logging * * DEBUG: Detailed information, typically useful only for diagnosing problems * INFO: Confirmation that things are working as expected * WARN: Indication that something unexpected happened * ERROR: Error events that might still allow the application to continue running */ export enum LogLevel { DEBUG = 'DEBUG', INFO = 'INFO', WARN = 'WARN', ERROR = 'ERROR', } /** * Logger configuration options */ export interface LoggerConfig { /** Minimum log level to display */ logLevel?: LogLevel | string /** Whether to colorize output */ colorize?: boolean /** Whether logging is enabled */ enabled?: boolean } /** * Metadata key-value pairs attached to a logger instance. * Included automatically in every log line produced by that logger. */ export type LoggerMetadata = Record const getNodeEnv = (): string => { if (typeof process !== 'undefined' && process.env) { return process.env.NODE_ENV || 'development' } return 'development' } const getLogLevel = (): string | undefined => { if (typeof process !== 'undefined' && process.env) { return process.env.LOG_LEVEL } return undefined } /** * Get the minimum log level from environment variable or use defaults * - Development: DEBUG (show all logs) * - Production: ERROR (only show errors, but can be overridden by LOG_LEVEL env var) * - Test: ERROR (only show errors in tests) */ const getMinLogLevel = (): LogLevel => { const logLevelEnv = getLogLevel() if (logLevelEnv && Object.values(LogLevel).includes(logLevelEnv as LogLevel)) { return logLevelEnv as LogLevel } const nodeEnv = getNodeEnv() switch (nodeEnv) { case 'development': return LogLevel.DEBUG case 'production': return LogLevel.ERROR case 'test': return LogLevel.ERROR default: return LogLevel.DEBUG } } /** * Configuration for different environments */ const getLogConfig = () => { const nodeEnv = getNodeEnv() const minLevel = getMinLogLevel() switch (nodeEnv) { case 'development': return { enabled: true, minLevel, colorize: true, } case 'production': return { enabled: true, minLevel, colorize: false, } case 'test': return { enabled: false, minLevel, colorize: false, } default: return { enabled: true, minLevel, colorize: true, } } } /** * Format objects for logging */ const formatObject = (obj: unknown, isDev: boolean): string => { try { if (obj instanceof Error) { const errorObj: Record = { message: obj.message, stack: isDev ? obj.stack : undefined, name: obj.name, } for (const key of Object.keys(obj)) { if (!(key in errorObj)) { errorObj[key] = (obj as unknown as Record)[key] } } return JSON.stringify(errorObj, null, isDev ? 2 : 0) } return JSON.stringify(obj, null, isDev ? 2 : 0) } catch { return '[Circular or Non-Serializable Object]' } } /** * Logger class for standardized console logging * * Provides methods for logging at different severity levels * and handles formatting, colorization, and environment-specific behavior. */ export class Logger { private module: string private config: ReturnType private isDev: boolean private metadata: LoggerMetadata = {} /** * Create a new logger for a specific module * @param module The name of the module (e.g., 'OpenAIProvider', 'AgentBlockHandler') * @param overrideConfig Optional configuration overrides */ constructor(module: string, overrideConfig?: LoggerConfig) { this.module = module this.config = getLogConfig() this.isDev = getNodeEnv() === 'development' // Apply overrides if provided if (overrideConfig) { if (overrideConfig.logLevel !== undefined) { const level = typeof overrideConfig.logLevel === 'string' ? (overrideConfig.logLevel as LogLevel) : overrideConfig.logLevel if (Object.values(LogLevel).includes(level)) { this.config.minLevel = level } } if (overrideConfig.colorize !== undefined) { this.config.colorize = overrideConfig.colorize } if (overrideConfig.enabled !== undefined) { this.config.enabled = overrideConfig.enabled } } } /** * Creates a child logger with additional metadata merged in. * The child inherits this logger's module name, config, and existing metadata. * New metadata keys override existing ones with the same name. */ withMetadata(metadata: LoggerMetadata): Logger { const child = Object.create(Logger.prototype) as Logger child.module = this.module child.config = this.config child.isDev = this.isDev child.metadata = { ...this.metadata, ...metadata } return child } /** * Determines if a log at the given level should be displayed */ private shouldLog(level: LogLevel): boolean { if (!this.config.enabled) return false if (getNodeEnv() === 'production' && typeof window !== 'undefined') { return false } const levels = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR] const minLevelIndex = levels.indexOf(this.config.minLevel) const currentLevelIndex = levels.indexOf(level) return currentLevelIndex >= minLevelIndex } /** * Format arguments for logging, converting objects to JSON strings */ private formatArgs(args: unknown[]): unknown[] { return args.map((arg) => { if (arg === null || arg === undefined) return arg if (typeof arg === 'object') return formatObject(arg, this.isDev) return arg }) } /** * Internal method to log a message with the specified level */ private log(level: LogLevel, message: string, ...args: unknown[]) { if (!this.shouldLog(level)) return const timestamp = new Date().toISOString() const formattedArgs = this.formatArgs(args) const reqCtx = getRequestContext() const effectiveMetadata = reqCtx ? { requestId: reqCtx.requestId, method: reqCtx.method, path: reqCtx.path, ...this.metadata, } : this.metadata const metadataEntries = Object.entries(effectiveMetadata).filter(([_, v]) => v !== undefined) const metadataStr = metadataEntries.length > 0 ? ` {${metadataEntries.map(([k, v]) => `${k}=${v}`).join(' ')}}` : '' if (this.config.colorize) { let levelColor: (text: string) => string const moduleColor = chalk.cyan const timestampColor = chalk.gray switch (level) { case LogLevel.DEBUG: levelColor = chalk.blue break case LogLevel.INFO: levelColor = chalk.green break case LogLevel.WARN: levelColor = chalk.yellow break case LogLevel.ERROR: levelColor = chalk.red break } const coloredMeta = metadataStr ? ` ${chalk.magenta(metadataStr.trim())}` : '' const coloredPrefix = `${timestampColor(`[${timestamp}]`)} ${levelColor(`[${level}]`)} ${moduleColor(`[${this.module}]`)}${coloredMeta}` if (level === LogLevel.ERROR) { console.error(coloredPrefix, message, ...formattedArgs) } else { console.log(coloredPrefix, message, ...formattedArgs) } } else { // Structured JSON for production — CloudWatch Log Insights auto-parses JSON lines const entry: Record = { timestamp, level, module: this.module, message, } for (const [k, v] of metadataEntries) { entry[k] = v } // Merge extra args into the entry for (const arg of args) { if ( arg !== null && arg !== undefined && typeof arg === 'object' && !(arg instanceof Error) ) { Object.assign(entry, arg) } else if (arg instanceof Error) { entry.error = arg.message entry.stack = arg.stack } else if (arg !== null && arg !== undefined) { entry.extra = arg } } const line = JSON.stringify(entry) if (level === LogLevel.ERROR) { console.error(line) } else { console.log(line) } } } /** * Log a debug message * * Use for detailed information useful during development and debugging. * These logs are only shown in development environment by default. */ debug(message: string, ...args: unknown[]) { this.log(LogLevel.DEBUG, message, ...args) } /** * Log an info message * * Use for general information about application operation. */ info(message: string, ...args: unknown[]) { this.log(LogLevel.INFO, message, ...args) } /** * Log a warning message * * Use for potentially problematic situations that don't cause operation failure. */ warn(message: string, ...args: unknown[]) { this.log(LogLevel.WARN, message, ...args) } /** * Log an error message * * Use for error events that might still allow the application to continue. */ error(message: string, ...args: unknown[]) { this.log(LogLevel.ERROR, message, ...args) } } /** * Create a logger for a specific module * * @example * ```typescript * import { createLogger } from '@sim/logger' * * const logger = createLogger('MyComponent') * * logger.debug('Initializing component', { props }) * logger.info('Component mounted') * logger.warn('Deprecated prop used', { propName }) * logger.error('Failed to fetch data', error) * ``` * * @param module The name of the module * @param config Optional configuration overrides * @returns A Logger instance */ export function createLogger(module: string, config?: LoggerConfig): Logger { return new Logger(module, config) } export type { RequestContext } from './request-context' export { getRequestContext, runWithRequestContext } from './request-context'