282 lines
6.4 KiB
TypeScript
282 lines
6.4 KiB
TypeScript
|
|
/**
|
||
|
|
* Log levels in order of severity
|
||
|
|
*/
|
||
|
|
export enum LogLevel {
|
||
|
|
DEBUG = 'debug',
|
||
|
|
INFO = 'info',
|
||
|
|
WARN = 'warn',
|
||
|
|
ERROR = 'error',
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Log level priorities for filtering
|
||
|
|
*/
|
||
|
|
const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
|
||
|
|
[LogLevel.DEBUG]: 0,
|
||
|
|
[LogLevel.INFO]: 1,
|
||
|
|
[LogLevel.WARN]: 2,
|
||
|
|
[LogLevel.ERROR]: 3,
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Structured log entry
|
||
|
|
*/
|
||
|
|
export interface LogEntry {
|
||
|
|
level: LogLevel;
|
||
|
|
message: string;
|
||
|
|
timestamp: Date;
|
||
|
|
context?: Record<string, unknown>;
|
||
|
|
correlationId?: string;
|
||
|
|
error?: Error;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Log transport interface for custom log handlers
|
||
|
|
*/
|
||
|
|
export interface LogTransport {
|
||
|
|
log(entry: LogEntry): void | Promise<void>;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Console transport with colored output
|
||
|
|
*/
|
||
|
|
export class ConsoleTransport implements LogTransport {
|
||
|
|
private readonly colors = {
|
||
|
|
debug: '\x1b[36m', // Cyan
|
||
|
|
info: '\x1b[32m', // Green
|
||
|
|
warn: '\x1b[33m', // Yellow
|
||
|
|
error: '\x1b[31m', // Red
|
||
|
|
reset: '\x1b[0m',
|
||
|
|
};
|
||
|
|
|
||
|
|
log(entry: LogEntry): void {
|
||
|
|
const color = this.colors[entry.level];
|
||
|
|
const reset = this.colors.reset;
|
||
|
|
const timestamp = entry.timestamp.toISOString();
|
||
|
|
const level = entry.level.toUpperCase().padEnd(5);
|
||
|
|
|
||
|
|
let message = `${color}[${timestamp}] ${level}${reset} ${entry.message}`;
|
||
|
|
|
||
|
|
if (entry.correlationId) {
|
||
|
|
message += ` ${color}[correlation: ${entry.correlationId}]${reset}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (entry.context && Object.keys(entry.context).length > 0) {
|
||
|
|
message += `\n Context: ${JSON.stringify(entry.context, null, 2)}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (entry.error) {
|
||
|
|
message += `\n Error: ${entry.error.message}`;
|
||
|
|
if (entry.error.stack) {
|
||
|
|
message += `\n${entry.error.stack}`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log(message);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* JSON transport for structured logging
|
||
|
|
*/
|
||
|
|
export class JsonTransport implements LogTransport {
|
||
|
|
log(entry: LogEntry): void {
|
||
|
|
const jsonEntry = {
|
||
|
|
level: entry.level,
|
||
|
|
message: entry.message,
|
||
|
|
timestamp: entry.timestamp.toISOString(),
|
||
|
|
...(entry.correlationId && { correlationId: entry.correlationId }),
|
||
|
|
...(entry.context && { context: entry.context }),
|
||
|
|
...(entry.error && {
|
||
|
|
error: {
|
||
|
|
message: entry.error.message,
|
||
|
|
name: entry.error.name,
|
||
|
|
stack: entry.error.stack,
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
};
|
||
|
|
|
||
|
|
console.log(JSON.stringify(jsonEntry));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Logger configuration
|
||
|
|
*/
|
||
|
|
export interface LoggerConfig {
|
||
|
|
/** Minimum log level to output */
|
||
|
|
level: LogLevel;
|
||
|
|
|
||
|
|
/** Log transports */
|
||
|
|
transports: LogTransport[];
|
||
|
|
|
||
|
|
/** Default context to include in all logs */
|
||
|
|
defaultContext?: Record<string, unknown>;
|
||
|
|
|
||
|
|
/** Whether to include timestamp */
|
||
|
|
includeTimestamp?: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Structured logger with context and correlation support
|
||
|
|
*
|
||
|
|
* @example
|
||
|
|
* ```typescript
|
||
|
|
* const logger = new Logger({
|
||
|
|
* level: LogLevel.INFO,
|
||
|
|
* transports: [new ConsoleTransport()],
|
||
|
|
* defaultContext: { service: 'elasticsearch-client' }
|
||
|
|
* });
|
||
|
|
*
|
||
|
|
* logger.info('Connected to Elasticsearch', { node: 'localhost:9200' });
|
||
|
|
*
|
||
|
|
* const childLogger = logger.withContext({ operation: 'bulk-index' });
|
||
|
|
* childLogger.debug('Processing batch', { size: 1000 });
|
||
|
|
* ```
|
||
|
|
*/
|
||
|
|
export class Logger {
|
||
|
|
private config: LoggerConfig;
|
||
|
|
private context: Record<string, unknown>;
|
||
|
|
private correlationId?: string;
|
||
|
|
|
||
|
|
constructor(config: Partial<LoggerConfig> = {}) {
|
||
|
|
this.config = {
|
||
|
|
level: config.level || LogLevel.INFO,
|
||
|
|
transports: config.transports || [new ConsoleTransport()],
|
||
|
|
defaultContext: config.defaultContext || {},
|
||
|
|
includeTimestamp: config.includeTimestamp !== false,
|
||
|
|
};
|
||
|
|
this.context = { ...this.config.defaultContext };
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create a child logger with additional context
|
||
|
|
*/
|
||
|
|
withContext(context: Record<string, unknown>): Logger {
|
||
|
|
const child = new Logger(this.config);
|
||
|
|
child.context = { ...this.context, ...context };
|
||
|
|
child.correlationId = this.correlationId;
|
||
|
|
return child;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create a child logger with correlation ID
|
||
|
|
*/
|
||
|
|
withCorrelation(correlationId: string): Logger {
|
||
|
|
const child = new Logger(this.config);
|
||
|
|
child.context = { ...this.context };
|
||
|
|
child.correlationId = correlationId;
|
||
|
|
return child;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create a child logger for a specific namespace
|
||
|
|
*/
|
||
|
|
child(namespace: string): Logger {
|
||
|
|
return this.withContext({ namespace });
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Log at DEBUG level
|
||
|
|
*/
|
||
|
|
debug(message: string, meta?: Record<string, unknown>): void {
|
||
|
|
this.log(LogLevel.DEBUG, message, meta);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Log at INFO level
|
||
|
|
*/
|
||
|
|
info(message: string, meta?: Record<string, unknown>): void {
|
||
|
|
this.log(LogLevel.INFO, message, meta);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Log at WARN level
|
||
|
|
*/
|
||
|
|
warn(message: string, meta?: Record<string, unknown>): void {
|
||
|
|
this.log(LogLevel.WARN, message, meta);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Log at ERROR level
|
||
|
|
*/
|
||
|
|
error(message: string, error?: Error, meta?: Record<string, unknown>): void {
|
||
|
|
this.log(LogLevel.ERROR, message, meta, error);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Internal log method
|
||
|
|
*/
|
||
|
|
private log(
|
||
|
|
level: LogLevel,
|
||
|
|
message: string,
|
||
|
|
meta?: Record<string, unknown>,
|
||
|
|
error?: Error
|
||
|
|
): void {
|
||
|
|
// Check if we should log this level
|
||
|
|
if (!this.shouldLog(level)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const entry: LogEntry = {
|
||
|
|
level,
|
||
|
|
message,
|
||
|
|
timestamp: new Date(),
|
||
|
|
context: { ...this.context, ...meta },
|
||
|
|
...(this.correlationId && { correlationId: this.correlationId }),
|
||
|
|
...(error && { error }),
|
||
|
|
};
|
||
|
|
|
||
|
|
// Send to all transports
|
||
|
|
for (const transport of this.config.transports) {
|
||
|
|
try {
|
||
|
|
const result = transport.log(entry);
|
||
|
|
// Handle async transports
|
||
|
|
if (result && typeof result.then === 'function') {
|
||
|
|
result.catch((err) => {
|
||
|
|
console.error('Transport error:', err);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Transport error:', err);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if a log level should be output
|
||
|
|
*/
|
||
|
|
private shouldLog(level: LogLevel): boolean {
|
||
|
|
return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[this.config.level];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Update logger configuration
|
||
|
|
*/
|
||
|
|
setLevel(level: LogLevel): void {
|
||
|
|
this.config.level = level;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Add a transport
|
||
|
|
*/
|
||
|
|
addTransport(transport: LogTransport): void {
|
||
|
|
this.config.transports.push(transport);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get current log level
|
||
|
|
*/
|
||
|
|
getLevel(): LogLevel {
|
||
|
|
return this.config.level;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Default logger instance
|
||
|
|
*/
|
||
|
|
export const defaultLogger = new Logger({
|
||
|
|
level: LogLevel.INFO,
|
||
|
|
transports: [new ConsoleTransport()],
|
||
|
|
});
|