385 lines
9.5 KiB
TypeScript
385 lines
9.5 KiB
TypeScript
/**
|
|
* Enhanced Pino-based logger with Loki integration 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
|
|
* - 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>();
|
|
|
|
/**
|
|
* Create transport configuration for Pino based on options
|
|
*/
|
|
function createTransports(serviceName: string, options?: {
|
|
enableConsole?: boolean;
|
|
enableFile?: boolean;
|
|
enableLoki?: boolean;
|
|
}): any {
|
|
const {
|
|
enableConsole = loggingConfig.LOG_CONSOLE,
|
|
enableFile = loggingConfig.LOG_FILE,
|
|
enableLoki = true
|
|
} = options || {};
|
|
|
|
const targets: any[] = [];
|
|
|
|
// Console transport with pretty formatting
|
|
if (enableConsole) {
|
|
targets.push({
|
|
target: 'pino-pretty',
|
|
level: loggingConfig.LOG_LEVEL, options: {
|
|
colorize: true,
|
|
translateTime: 'yyyy-mm-dd HH:MM:ss.l',
|
|
ignore: 'pid,hostname',
|
|
messageFormat: '[{service}] {msg}'
|
|
}
|
|
});
|
|
}
|
|
|
|
// File transport for general logs
|
|
if (enableFile) {
|
|
targets.push({
|
|
target: 'pino/file',
|
|
level: loggingConfig.LOG_LEVEL,
|
|
options: {
|
|
destination: `${loggingConfig.LOG_FILE_PATH}/${serviceName}.log`,
|
|
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) {
|
|
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,
|
|
}
|
|
});
|
|
}
|
|
|
|
return {
|
|
targets
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create or retrieve a logger instance for a specific service
|
|
*/
|
|
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)!;
|
|
}
|
|
|
|
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
|
|
}
|
|
};
|
|
|
|
// Only add transport if targets exist to avoid worker thread issues
|
|
if (transport && transport.targets && transport.targets.length > 0) {
|
|
loggerConfig.transport = transport;
|
|
}
|
|
|
|
return pino(loggerConfig);
|
|
}
|
|
|
|
|
|
/**
|
|
* Enhanced Logger class with convenience methods and flexible message handling
|
|
*/
|
|
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;
|
|
this.context = context;
|
|
this.pino = createLogger(serviceName, options);
|
|
} /**
|
|
* Flexible log method that accepts string or object messages
|
|
*/
|
|
log(level: LogLevel, message: string | object, metadata?: LogMetadata): void {
|
|
const logData = {
|
|
...this.context,
|
|
...metadata,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
|
|
if (typeof message === 'string') {
|
|
(this.pino as any)[level](logData, message);
|
|
} else {
|
|
(this.pino as any)[level]({ ...logData, ...message });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Debug level logging
|
|
*/
|
|
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
|
|
*/
|
|
error(message: string | object, error?: Error | any, metadata?: LogMetadata): void {
|
|
const logData: LogMetadata = { ...metadata };
|
|
|
|
if (error) {
|
|
if (error instanceof Error) {
|
|
logData.error = {
|
|
name: error.name,
|
|
message: error.message,
|
|
stack: loggingConfig.LOG_ERROR_STACK ? error.stack : undefined
|
|
};
|
|
} else {
|
|
logData.error = error;
|
|
}
|
|
}
|
|
|
|
this.log('error', message, logData);
|
|
}
|
|
|
|
/**
|
|
* HTTP request logging
|
|
*/
|
|
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'
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
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();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a default logger instance for a service
|
|
*/
|
|
export function getLogger(serviceName: string, context?: LogContext): Logger {
|
|
return new Logger(serviceName, context);
|
|
}
|
|
|
|
/**
|
|
* Shutdown all logger instances gracefully
|
|
*/
|
|
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 types for convenience
|
|
export type { LogLevel, LogContext, LogMetadata } from './types';
|