/** * Logger utility with consistent formatting and log levels * Supports console and Loki logging */ import { loggingConfig, lokiConfig } from '@stock-bot/config'; // Singleton Loki client let lokiClient: LokiClient | null = null; function getLokiClient(): LokiClient { if (!lokiClient) { lokiClient = new LokiClient(); } return lokiClient; } export class Logger { constructor(private serviceName: string, private level: LogLevel = LogLevel.INFO) {} debug(message: string, ...args: any[]): void { this.log(LogLevel.DEBUG, message, ...args); } info(message: string, ...args: any[]): void { this.log(LogLevel.INFO, message, ...args); } warn(message: string, ...args: any[]): void { this.log(LogLevel.WARN, message, ...args); } error(message: string, ...args: any[]): void { this.log(LogLevel.ERROR, message, ...args); } private log(level: LogLevel, message: string, ...args: any[]): void { if (level < this.level) return; const timestamp = new Date().toISOString(); const levelStr = LogLevel[level].padEnd(5); const formattedArgs = args.length ? this.formatArgs(args) : ''; const fullMessage = `${message}${formattedArgs}`; const logMessage = `[${timestamp}] [${levelStr}] [${this.serviceName}] ${fullMessage}`; // Console logging if (loggingConfig.LOG_CONSOLE) { switch (level) { case LogLevel.ERROR: console.error(logMessage); break; case LogLevel.WARN: console.warn(logMessage); break; case LogLevel.INFO: console.info(logMessage); break; case LogLevel.DEBUG: default: console.debug(logMessage); break; } } // Loki logging try { const loki = getLokiClient(); loki.log(LogLevel[level].toLowerCase(), fullMessage, this.serviceName); } catch (error) { console.error('Failed to send log to Loki:', error); } } private formatArgs(args: any[]): string { try { return args.map(arg => { if (arg instanceof Error) { return ` ${arg.message}\n${arg.stack}`; } else if (typeof arg === 'object') { return ` ${JSON.stringify(arg)}`; } else { return ` ${arg}`; } }).join(''); } catch (error) { return ` [Error formatting log arguments: ${error}]`; } } setLevel(level: LogLevel): void { this.level = level; } } export enum LogLevel { DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3 } /** * Create a new logger instance */ export function createLogger(serviceName: string, level: LogLevel = LogLevel.INFO): Logger { return new Logger(serviceName, level); } export class LokiClient { private batchQueue: any[] = []; private flushInterval: NodeJS.Timeout; private lokiUrl: string; private authHeader?: string; constructor() { const { LOKI_HOST, LOKI_PORT, LOKI_USERNAME, LOKI_PASSWORD } = lokiConfig; this.lokiUrl = `http://${LOKI_HOST}:${LOKI_PORT}/loki/api/v1/push`; if (LOKI_USERNAME && LOKI_PASSWORD) { const authString = Buffer.from(`${LOKI_USERNAME}:${LOKI_PASSWORD}`).toString('base64'); this.authHeader = `Basic ${authString}`; } this.flushInterval = setInterval( () => this.flush(), lokiConfig.LOKI_FLUSH_INTERVAL_MS || 1000 // Default to 1 second if not set ); } async log(level: string, message: string, serviceName: string, labels: Record = {}) { const timestamp = Date.now() * 1000000; // Loki expects nanoseconds this.batchQueue.push({ streams: [{ stream: { level, service: serviceName, ...lokiConfig.LOKI_DEFAULT_LABELS ? JSON.parse(lokiConfig.LOKI_DEFAULT_LABELS) : {}, ...labels, }, values: [[`${timestamp}`, message]], }], }); if (this.batchQueue.length >= lokiConfig.LOKI_BATCH_SIZE) { await this.flush(); } } private async flush() { if (this.batchQueue.length === 0) return; try { const headers: Record = { 'Content-Type': 'application/json', }; if (this.authHeader) { headers['Authorization'] = this.authHeader; } const response = await fetch(this.lokiUrl, { method: 'POST', headers, body: JSON.stringify({ streams: this.batchQueue.flatMap(batch => batch.streams), }), }); if (!response.ok) { console.error(`Failed to send logs to Loki: ${response.status} ${response.statusText}`); const text = await response.text(); if (text) { console.error(text); } } } catch (error) { console.error('Error sending logs to Loki:', error); } finally { this.batchQueue = []; } } async destroy() { clearInterval(this.flushInterval); return this.flush(); } }