stock-bot/libs/logger/src/logger.ts
2025-06-07 10:07:49 -04:00

203 lines
5 KiB
TypeScript

/**
* 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<string, pino.Logger>();
/**
* 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';