improved logger

This commit is contained in:
Bojan Kucera 2025-06-07 10:07:49 -04:00
parent e8485dd140
commit 0f510bfa33
9 changed files with 110 additions and 1250 deletions

View file

@ -1,59 +1,47 @@
/**
* Enhanced Pino-based logger with Loki integration for Stock Bot platform
* Simplified Pino-based logger for Stock Bot platform
*
* Features:
* - High performance JSON logging with Pino
* - Multiple log levels (debug, info, warn, error, http, verbose, silly)
* - Console and file logging with pino-pretty formatting
* - Loki integration for centralized logging
* - Console, file, and Loki transports
* - Structured logging with metadata
* - Flexible message handling (string or object)
* - Service-specific context
* - TypeScript-friendly interfaces
*/
import pino from 'pino';
import { loggingConfig, lokiConfig } from '@stock-bot/config';
import type { LogLevel, LogContext, LogMetadata } from './types';
// Global logger instances cache
const loggerInstances = new Map<string, pino.Logger>();
// Simple cache for logger instances
const loggerCache = new Map<string, pino.Logger>();
/**
* Create transport configuration for Pino based on options
* Create transport configuration
*/
function createTransports(serviceName: string, options?: {
enableConsole?: boolean;
enableFile?: boolean;
enableLoki?: boolean;
}): any {
const {
enableConsole = loggingConfig.LOG_CONSOLE,
enableFile = loggingConfig.LOG_FILE,
enableLoki = true
} = options || {};
function createTransports(serviceName: string): any {
const targets: any[] = [];
// Console transport with pretty formatting
if (enableConsole) {
// 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',
ignore: 'pid,hostname,environment,version,service',
messageFormat: '[{service}] {msg}',
singleLine: true,
hideObject: false,
ignore: 'pid,hostname,service,environment,version',
errorLikeObjectKeys: ['err', 'error'],
errorProps: 'message,stack,name,code',
singleLine: false, // Allow multiline for better metadata display
hideObject: false, // Show metadata objects
}
});
}
// File transport for general logs
if (enableFile) {
// File transport
if (loggingConfig.LOG_FILE) {
targets.push({
target: 'pino/file',
level: loggingConfig.LOG_LEVEL,
@ -62,335 +50,153 @@ function createTransports(serviceName: string, options?: {
mkdir: true
}
});
// Separate error file if enabled
if (loggingConfig.LOG_ERROR_FILE) {
targets.push({
target: 'pino/file',
level: 'error',
options: {
destination: `${loggingConfig.LOG_FILE_PATH}/${serviceName}-error.log`,
mkdir: true
}
});
}
}
// Loki transport for centralized logging
if (enableLoki && lokiConfig.LOKI_HOST) {
// Loki transport
if (lokiConfig.LOKI_HOST) {
targets.push({
target: 'pino-loki',
level: loggingConfig.LOG_LEVEL,
options: {
batching: true,
interval: lokiConfig.LOKI_BATCH_WAIT,
host: lokiConfig.LOKI_URL || `http://${lokiConfig.LOKI_HOST}:${lokiConfig.LOKI_PORT}`,
basicAuth: lokiConfig.LOKI_USERNAME && lokiConfig.LOKI_PASSWORD
? {
username: lokiConfig.LOKI_USERNAME,
password: lokiConfig.LOKI_PASSWORD
}
: undefined,
labels: {
service: serviceName,
environment: lokiConfig.LOKI_ENVIRONMENT_LABEL,
...(lokiConfig.LOKI_DEFAULT_LABELS ? JSON.parse(lokiConfig.LOKI_DEFAULT_LABELS) : {})
},
timeout: lokiConfig.LOKI_PUSH_TIMEOUT || 10000,
silenceErrors: false,
// Better JSON handling
replaceTimestamp: false,
environment: lokiConfig.LOKI_ENVIRONMENT_LABEL
}
}
});
}
return {
targets
};
return targets.length > 0 ? { targets } : null;
}
/**
* Create or retrieve a logger instance for a specific service
* Get or create pino logger
*/
export function createLogger(serviceName: string, options?: {
level?: LogLevel;
enableLoki?: boolean;
enableFile?: boolean;
enableConsole?: boolean;
}): pino.Logger {
const key = `${serviceName}-${JSON.stringify(options || {})}`;
if (loggerInstances.has(key)) {
return loggerInstances.get(key)!;
}
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
}
};
const logger = buildLogger(serviceName, options);
loggerInstances.set(key, logger);
return logger;
}
/**
* Build a Pino logger with all configured transports
*/
function buildLogger(serviceName: string, options?: {
level?: LogLevel;
enableLoki?: boolean;
enableFile?: boolean;
enableConsole?: boolean;
}): pino.Logger {
const {
level = loggingConfig.LOG_LEVEL as LogLevel,
enableLoki = true,
enableFile = loggingConfig.LOG_FILE,
enableConsole = loggingConfig.LOG_CONSOLE
} = options || {};
const transport = createTransports(serviceName, {
enableConsole,
enableFile,
enableLoki
});
const loggerConfig: pino.LoggerOptions = {
level: level,
timestamp: () => `,"timestamp":"${new Date().toISOString()}"`,
base: {
service: serviceName,
environment: loggingConfig.LOG_ENVIRONMENT,
version: loggingConfig.LOG_SERVICE_VERSION
if (transport) {
config.transport = transport;
}
};
// Only add transport if targets exist to avoid worker thread issues
if (transport && transport.targets && transport.targets.length > 0) {
loggerConfig.transport = transport;
loggerCache.set(serviceName, pino(config));
}
return pino(loggerConfig);
return loggerCache.get(serviceName)!;
}
/**
* Enhanced Logger class with convenience methods and flexible message handling
* Simplified Logger class
*/
export class Logger {
private pino: pino.Logger;
private serviceName: string;
private context: LogContext;
constructor(serviceName: string, context: LogContext = {}, options?: {
level?: LogLevel;
enableLoki?: boolean;
enableFile?: boolean;
enableConsole?: boolean;
}) {
this.serviceName = serviceName;
constructor(serviceName: string, context: LogContext = {}) {
this.pino = getPinoLogger(serviceName);
this.context = context;
this.pino = createLogger(serviceName, options);
}
/**
* Flexible log method that accepts string or object messages
* Core log method
*/
log(level: LogLevel, message: string | object, metadata?: LogMetadata): void {
const logData = {
...this.context,
...metadata,
};
private log(level: LogLevel, message: string | object, metadata?: LogMetadata): void {
const data = { ...this.context, ...metadata };
if (typeof message === 'string') {
(this.pino as any)[level](logData, message);
(this.pino as any)[level](data, message);
} else {
// For object messages, use the message object as the main log data
// and add metadata without overwriting properties from the message
const messageObj = message as Record<string, any>;
const combinedData = { ...logData };
// Add message properties that don't conflict with metadata
Object.keys(messageObj).forEach(key => {
if (!(key in combinedData)) {
combinedData[key] = messageObj[key];
}
});
(this.pino as any)[level](combinedData, messageObj.msg || messageObj.message || 'Object log');
(this.pino as any)[level]({ ...data, data: message }, 'Object logged');
}
}
/**
* Debug level logging
*/
// Simple log level methods
debug(message: string | object, metadata?: LogMetadata): void {
this.log('debug', message, metadata);
}
/**
* Verbose level logging
*/
verbose(message: string | object, metadata?: LogMetadata): void {
// Map verbose to debug level since Pino doesn't have verbose by default
this.log('debug', message, { ...metadata, originalLevel: 'verbose' });
}
/**
* Silly level logging
*/
silly(message: string | object, metadata?: LogMetadata): void {
// Map silly to debug level since Pino doesn't have silly by default
this.log('debug', message, { ...metadata, originalLevel: 'silly' });
}
/**
* Info level logging
*/
info(message: string | object, metadata?: LogMetadata): void {
this.log('info', message, metadata);
}
/**
* Warning level logging
*/
warn(message: string | object, metadata?: LogMetadata): void {
this.log('warn', message, metadata);
}
/**
* Error level logging with proper pino-pretty formatting
*/
error(message: string | object, error?: Error | any, metadata?: LogMetadata): void {
const logData: LogMetadata = { ...metadata };
error(message: string | object, metadata?: LogMetadata & { error?: any }): void {
const data = { ...metadata };
if (error) {
if (error instanceof Error) {
// Use 'err' key for pino-pretty compatibility
logData.err = error;
} else {
logData.error = error;
}
// Handle any type of error automatically
if (data.error) {
data.err = this.normalizeError(data.error);
delete data.error;
}
this.log('error', message, logData);
this.log('error', message, data);
}
/**
* HTTP request logging
* Normalize any error type to a structured format
*/
http(message: string | object, requestData?: {
method?: string;
url?: string;
statusCode?: number;
responseTime?: number;
userAgent?: string;
ip?: string;
}): void {
if (!loggingConfig.LOG_HTTP_REQUESTS) return;
this.log('info', message, {
request: requestData,
type: 'http_request'
});
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)
};
}
/**
* Performance/timing logging
*/
performance(message: string | object, timing: {
operation: string;
duration: number;
startTime?: number;
endTime?: number;
}): void {
if (!loggingConfig.LOG_PERFORMANCE) return;
this.log('info', message, {
performance: timing,
type: 'performance'
});
}
/**
* Business event logging
*/
business(message: string | object, event: {
type: string;
entity?: string;
action?: string;
result?: 'success' | 'failure' | 'partial';
amount?: number;
symbol?: string;
}): void {
this.log('info', message, {
business: event,
type: 'business_event'
});
}
/**
* Security event logging
*/
security(message: string | object, event: {
type: 'authentication' | 'authorization' | 'access' | 'vulnerability';
user?: string;
resource?: string;
action?: string;
result?: 'success' | 'failure';
ip?: string;
severity?: 'low' | 'medium' | 'high' | 'critical';
}): void {
this.log('warn', message, {
security: event,
type: 'security_event'
});
}
/**
* Create a child logger with additional context
* Create child logger with additional context
*/
child(context: LogContext): Logger {
return new Logger(this.serviceName, { ...this.context, ...context });
}
/**
* Add persistent context to this logger
*/
addContext(context: LogContext): void {
this.context = { ...this.context, ...context };
}
/**
* Get the underlying Pino logger
*/
getPinoLogger(): pino.Logger {
return this.pino;
}
/**
* Gracefully close all transports
*/
async close(): Promise<void> {
// Pino doesn't require explicit closing like Winston
// But we can flush any pending logs
this.pino.flush();
return new Logger((this.pino.bindings() as any).service, { ...this.context, ...context });
}
}
/**
* Create a default logger instance for a service
* Main factory function
*/
export function getLogger(serviceName: string, context?: LogContext): Logger {
return new Logger(serviceName, context);
}
/**
* Shutdown all logger instances gracefully
* Keep backward compatibility
*/
export async function shutdownLoggers(): Promise<void> {
// Flush all logger instances
const flushPromises = Array.from(loggerInstances.values()).map(logger => {
logger.flush();
return Promise.resolve();
});
await Promise.all(flushPromises);
loggerInstances.clear();
export function createLogger(serviceName: string): pino.Logger {
return getPinoLogger(serviceName);
}
// Export types for convenience