Files
elasticsearch/ts/core/observability/logger.ts

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()],
});