270 lines
7.4 KiB
TypeScript
270 lines
7.4 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 { LogContext, LogLevel, LogMetadata } from './types';
|
|
|
|
// Simple cache for logger instances
|
|
const loggerCache = new Map<string, pino.Logger>();
|
|
console.log('Logger cache initialized: ', loggingConfig.LOG_LEVEL);
|
|
/**
|
|
* 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, // Only show errors on console
|
|
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 (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,
|
|
},
|
|
ignore: 'childName',
|
|
},
|
|
});
|
|
}
|
|
|
|
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;
|
|
private serviceName: string;
|
|
private childName?: string;
|
|
|
|
constructor(serviceName: string, context: LogContext = {}) {
|
|
this.pino = getPinoLogger(serviceName);
|
|
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): Logger {
|
|
return new Logger(serviceName, context);
|
|
}
|
|
|
|
/**
|
|
* Gracefully shutdown all logger instances
|
|
* This should be called during application shutdown to ensure all logs are flushed
|
|
*/
|
|
export async function shutdownLoggers(): Promise<void> {
|
|
const flushPromises = Array.from(loggerCache.values()).map(logger => {
|
|
return new Promise<void>(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';
|