/** * 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 { loggingConfig, lokiConfig } from '@stock-bot/config'; import type { LogLevel, LogContext, LogMetadata } from './types'; // Simple cache for logger instances const loggerCache = new Map(); /** * Create transport configuration */ function createTransports(serviceName: string): any { const targets: any[] = []; // const isDev = loggingConfig.LOG_ENVIRONMENT === 'development'; // Console transport if (loggingConfig.LOG_CONSOLE) { targets.push({ target: 'pino-pretty', level: loggingConfig.LOG_LEVEL, options: { colorize: true, translateTime: 'yyyy-mm-dd HH:MM:ss.l', messageFormat: '[{service}] {msg}', singleLine: true, hideObject: false, ignore: 'pid,hostname,service,environment,version', errorLikeObjectKeys: ['err', 'error'], errorProps: 'message,stack,name,code', } }); } // File transport if (loggingConfig.LOG_FILE) { targets.push({ target: 'pino/file', level: loggingConfig.LOG_LEVEL, options: { destination: `${loggingConfig.LOG_FILE_PATH}/${serviceName}.log`, mkdir: true } }); } // Loki transport if (lokiConfig.LOKI_HOST) { targets.push({ target: 'pino-loki', level: loggingConfig.LOG_LEVEL, options: { host: lokiConfig.LOKI_URL || `http://${lokiConfig.LOKI_HOST}:${lokiConfig.LOKI_PORT}`, labels: { service: serviceName, environment: lokiConfig.LOKI_ENVIRONMENT_LABEL } } }); } return targets.length > 0 ? { targets } : null; } /** * Get or create pino logger */ function getPinoLogger(serviceName: string): pino.Logger { if (!loggerCache.has(serviceName)) { const transport = createTransports(serviceName); const config: pino.LoggerOptions = { level: loggingConfig.LOG_LEVEL, base: { service: serviceName, environment: loggingConfig.LOG_ENVIRONMENT, version: loggingConfig.LOG_SERVICE_VERSION } }; if (transport) { config.transport = transport; } loggerCache.set(serviceName, pino(config)); } return loggerCache.get(serviceName)!; } /** * Simplified Logger class */ export class Logger { private pino: pino.Logger; private context: LogContext; constructor(serviceName: string, context: LogContext = {}) { this.pino = getPinoLogger(serviceName); this.context = context; } /** * 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 }): void { const data = { ...metadata }; // Handle any type of error automatically if (data.error) { data.err = this.normalizeError(data.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(context: LogContext): Logger { return new Logger((this.pino.bindings() as any).service, { ...this.context, ...context }); } } /** * Main factory function */ export function getLogger(serviceName: string, context?: LogContext): Logger { return new Logger(serviceName, context); } /** * Keep backward compatibility */ export function createLogger(serviceName: string): pino.Logger { return getPinoLogger(serviceName); } // Export types for convenience export type { LogLevel, LogContext, LogMetadata } from './types';