/** * Simplified Pino-based logger for Stock Bot platform * * Features: * - High performance JSON logging with Pino * - Console, file, and Loki transports * - Structured logging with metadata * - Service-specific context */ import pino from 'pino'; import type { LogContext, LogLevel, LogMetadata, LoggerConfig } from './types'; // Simple cache for logger instances const loggerCache = new Map(); // Global config that can be set let globalConfig: LoggerConfig = { logLevel: 'info', logConsole: true, logFile: false, logFilePath: './logs', logLoki: false, environment: 'development', }; /** * Set global logger configuration */ export function setLoggerConfig(config: LoggerConfig): void { globalConfig = { ...globalConfig, ...config }; // Clear cache to force recreation with new config loggerCache.clear(); console.log('Logger config updated:', globalConfig.logLevel); } /** * Create transport configuration */ function createTransports(serviceName: string, config: LoggerConfig = globalConfig): any { const targets: any[] = []; // Console transport if (config.logConsole) { targets.push({ target: 'pino-pretty', level: config.logLevel || 'info', options: { colorize: true, translateTime: 'yyyy-mm-dd HH:MM:ss.l', messageFormat: '[{service}{childName}] {msg}', singleLine: true, hideObject: false, ignore: 'pid,hostname,service,environment,version,childName', errorLikeObjectKeys: ['err', 'error'], errorProps: 'message,stack,name,code', }, }); } // File transport if (config.logFile) { targets.push({ target: 'pino/file', level: config.logLevel || 'info', options: { destination: `${config.logFilePath}/${serviceName}.log`, mkdir: true, }, }); } // Loki transport if (config.logLoki && config.lokiHost) { targets.push({ target: 'pino-loki', level: config.logLevel || 'info', options: { host: config.lokiHost, labels: { service: serviceName, environment: config.environment || 'development', }, ignore: 'childName', ...(config.lokiUser && config.lokiPassword ? { basicAuth: { username: config.lokiUser, password: config.lokiPassword, }, } : {}), }, }); } return targets.length > 0 ? { targets } : null; } /** * Get or create pino logger */ function getPinoLogger(serviceName: string, config: LoggerConfig = globalConfig): pino.Logger { const cacheKey = `${serviceName}-${JSON.stringify(config)}`; if (!loggerCache.has(cacheKey)) { const transport = createTransports(serviceName, config); const loggerOptions: pino.LoggerOptions = { level: config.logLevel || 'info', base: { service: serviceName, environment: config.environment || 'development', version: '1.0.0', }, }; if (transport) { loggerOptions.transport = transport; } loggerCache.set(cacheKey, pino(loggerOptions)); } return loggerCache.get(cacheKey)!; } /** * Simplified Logger class */ export class Logger { private pino: pino.Logger; private context: LogContext; private serviceName: string; private childName?: string; constructor(serviceName: string, context: LogContext = {}, config?: LoggerConfig) { this.pino = getPinoLogger(serviceName, config); this.context = context; this.serviceName = serviceName; } /** * Core log method */ private log(level: LogLevel, message: string | object, metadata?: LogMetadata): void { const data = { ...this.context, ...metadata }; if (typeof message === 'string') { (this.pino as any)[level](data, message); } else { (this.pino as any)[level]({ ...data, data: message }, 'Object logged'); } } // Simple log level methods debug(message: string | object, metadata?: LogMetadata): void { this.log('debug', message, metadata); } info(message: string | object, metadata?: LogMetadata): void { this.log('info', message, metadata); } warn(message: string | object, metadata?: LogMetadata): void { this.log('warn', message, metadata); } error(message: string | object, metadata?: (LogMetadata & { error?: any }) | unknown): void { let data: any = {}; // Handle metadata parameter normalization if (metadata instanceof Error) { // Direct Error object as metadata data = { error: metadata }; } else if (metadata !== null && typeof metadata === 'object') { // Object metadata (including arrays, but not null) data = { ...metadata }; } else if (metadata !== undefined) { // Primitive values (string, number, boolean, etc.) data = { metadata }; } // Handle multiple error properties in metadata const errorKeys = ['error', 'err', 'primaryError', 'secondaryError']; errorKeys.forEach(key => { if (data[key]) { const normalizedKey = key === 'error' ? 'err' : `${key}_normalized`; data[normalizedKey] = this.normalizeError(data[key]); // Only delete the original 'error' key to maintain other error properties if (key === 'error') { delete data.error; } } }); this.log('error', message, data); } /** * Normalize any error type to a structured format */ private normalizeError(error: any): any { if (error instanceof Error) { return { name: error.name, message: error.message, stack: error.stack, }; } if (error && typeof error === 'object') { // Handle error-like objects return { name: error.name || 'UnknownError', message: error.message || error.toString(), ...(error.stack && { stack: error.stack }), ...(error.code && { code: error.code }), ...(error.status && { status: error.status }), }; } // Handle primitives (string, number, etc.) return { name: 'UnknownError', message: String(error), }; } /** * Create child logger with additional context */ child(serviceName: string, context?: LogContext): Logger { // Create child logger that shares the same pino instance with additional context const childLogger = Object.create(Logger.prototype); childLogger.serviceName = this.serviceName; childLogger.childName = serviceName; childLogger.context = { ...this.context, ...context }; const childBindings = { service: this.serviceName, childName: ' -> ' + serviceName, ...(context || childLogger.context), }; childLogger.pino = this.pino.child(childBindings); return childLogger; // } // childLogger.pino = this.pino.child(context || childLogger.context); // Let pino handle level inheritance naturally // return childLogger; } // Getters for service and context getServiceName(): string { return this.serviceName; } getChildName(): string | undefined { return this.childName; } } /** * Main factory function */ export function getLogger(serviceName: string, context?: LogContext, config?: LoggerConfig): Logger { return new Logger(serviceName, context, config); } /** * Gracefully shutdown all logger instances * This should be called during application shutdown to ensure all logs are flushed */ export async function shutdownLoggers(): Promise { const flushPromises = Array.from(loggerCache.values()).map(logger => { return new Promise(resolve => { if (typeof logger.flush === 'function') { logger.flush(err => { if (err) { console.error('Logger flush error:', err); } resolve(); }); } else { resolve(); } }); }); try { await Promise.allSettled(flushPromises); console.log('All loggers flushed successfully'); } catch (error) { console.error('Logger flush failed:', error); } finally { loggerCache.clear(); } } // Export types for convenience export type { LogLevel, LogContext, LogMetadata } from './types';