BREAKING CHANGE(core): Refactor to v3: introduce modular core/domain architecture, plugin system, observability and strict TypeScript configuration; remove legacy classes
This commit is contained in:
281
ts/core/observability/logger.ts
Normal file
281
ts/core/observability/logger.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
/**
|
||||
* 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()],
|
||||
});
|
||||
Reference in New Issue
Block a user