/** * CDC Logger - Built-in logging with configurable levels * * Provides structured logging for CDC operations with: * - Multiple log levels (debug, info, warn, error) * - Structured output with timestamps and context * - Configurable output (console, custom handler) * - Memory-efficient circular buffer for event log */ /** * Log levels in order of verbosity */ export enum LogLevel { DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3, SILENT = 4, } /** * Log entry structure */ export interface LogEntry { /** Log level */ level: LogLevel /** Human-readable level name */ levelName: string /** Log message */ message: string /** ISO timestamp */ timestamp: string /** Subscription ID if applicable */ subscriptionId?: string /** Additional context data */ context?: Record /** Error if this is an error log */ error?: Error } /** * Custom log handler function type */ export type LogHandler = (entry: LogEntry) => void /** * Logger configuration */ export interface LoggerConfig { /** Minimum level to log (default: INFO) */ level?: LogLevel /** Custom log handler (default: console) */ handler?: LogHandler /** Whether to include timestamps (default: true) */ timestamps?: boolean /** Prefix for all log messages */ prefix?: string /** Whether to capture logs in memory for debugging (default: false) */ captureEvents?: boolean /** Maximum number of events to capture in memory (default: 100) */ maxCapturedEvents?: number } /** * Level name mapping */ const LEVEL_NAMES: Record = { [LogLevel.DEBUG]: 'DEBUG', [LogLevel.INFO]: 'INFO', [LogLevel.WARN]: 'WARN', [LogLevel.ERROR]: 'ERROR', [LogLevel.SILENT]: 'SILENT', } /** * Default console handler with color support */ function createConsoleHandler(useColors: boolean): LogHandler { const colors = { [LogLevel.DEBUG]: '\x1b[90m', // Gray [LogLevel.INFO]: '\x1b[36m', // Cyan [LogLevel.WARN]: '\x1b[33m', // Yellow [LogLevel.ERROR]: '\x1b[31m', // Red [LogLevel.SILENT]: '', } const reset = '\x1b[0m' return (entry: LogEntry) => { const parts: string[] = [] if (useColors) { parts.push(colors[entry.level]) } parts.push(`[${entry.levelName}]`) if (entry.timestamp) { parts.push(`[${entry.timestamp}]`) } if (entry.subscriptionId) { parts.push(`[sub:${entry.subscriptionId.substring(0, 8)}]`) } parts.push(entry.message) if (useColors) { parts.push(reset) } const message = parts.join(' ') switch (entry.level) { case LogLevel.DEBUG: console.debug(message, entry.context || '') break case LogLevel.INFO: console.info(message, entry.context || '') break case LogLevel.WARN: console.warn(message, entry.context || '') break case LogLevel.ERROR: console.error(message, entry.context || '', entry.error || '') break } } } /** * Circular buffer for event log capture */ class CircularBuffer { private buffer: T[] = [] private head = 0 private size = 0 constructor(private readonly capacity: number) {} push(item: T): void { if (this.size < this.capacity) { this.buffer.push(item) this.size++ } else { this.buffer[this.head] = item } this.head = (this.head + 1) % this.capacity } toArray(): T[] { if (this.size < this.capacity) { return [...this.buffer] } // Return items in order from oldest to newest return [...this.buffer.slice(this.head), ...this.buffer.slice(0, this.head)] } clear(): void { this.buffer = [] this.head = 0 this.size = 0 } get length(): number { return this.size } } /** * CDC Logger class */ export class CDCLogger { private level: LogLevel private handler: LogHandler private timestamps: boolean private prefix: string private capturedEvents: CircularBuffer | null = null constructor(config: LoggerConfig = {}) { this.level = config.level ?? LogLevel.INFO this.handler = config.handler ?? createConsoleHandler(typeof process !== 'undefined' && process.stdout?.isTTY === true) this.timestamps = config.timestamps ?? true this.prefix = config.prefix ?? '[CDC]' if (config.captureEvents) { this.capturedEvents = new CircularBuffer(config.maxCapturedEvents ?? 100) } } /** * Set the log level */ setLevel(level: LogLevel): void { this.level = level } /** * Get the current log level */ getLevel(): LogLevel { return this.level } /** * Set a custom log handler */ setHandler(handler: LogHandler): void { this.handler = handler } /** * Enable event capture */ enableCapture(maxEvents: number = 100): void { this.capturedEvents = new CircularBuffer(maxEvents) } /** * Disable event capture */ disableCapture(): void { this.capturedEvents = null } /** * Get captured log entries */ getCapturedLogs(): LogEntry[] { return this.capturedEvents?.toArray() ?? [] } /** * Clear captured logs */ clearCapturedLogs(): void { this.capturedEvents?.clear() } /** * Core log method */ private log( level: LogLevel, message: string, context?: { subscriptionId?: string data?: Record error?: Error } ): void { if (level < this.level) { return } const entry: LogEntry = { level, levelName: LEVEL_NAMES[level], message: this.prefix ? `${this.prefix} ${message}` : message, timestamp: this.timestamps ? new Date().toISOString() : '', } if (context?.subscriptionId !== undefined) { entry.subscriptionId = context.subscriptionId } if (context?.data !== undefined) { entry.context = context.data } if (context?.error !== undefined) { entry.error = context.error } // Capture if enabled this.capturedEvents?.push(entry) // Output via handler this.handler(entry) } /** * Log debug message */ debug( message: string, context?: { subscriptionId?: string; data?: Record } ): void { this.log(LogLevel.DEBUG, message, context) } /** * Log info message */ info( message: string, context?: { subscriptionId?: string; data?: Record } ): void { this.log(LogLevel.INFO, message, context) } /** * Log warning message */ warn( message: string, context?: { subscriptionId?: string; data?: Record } ): void { this.log(LogLevel.WARN, message, context) } /** * Log error message */ error( message: string, context?: { subscriptionId?: string data?: Record error?: Error } ): void { this.log(LogLevel.ERROR, message, context) } /** * Create a child logger with a specific subscription ID */ withSubscription(subscriptionId: string): SubscriptionLogger { return new SubscriptionLogger(this, subscriptionId) } } /** * Subscription-scoped logger */ export class SubscriptionLogger { constructor( private parent: CDCLogger, private subscriptionId: string ) {} debug(message: string, data?: Record): void { const ctx: { subscriptionId?: string; data?: Record } = { subscriptionId: this.subscriptionId } if (data !== undefined) { ctx.data = data } this.parent.debug(message, ctx) } info(message: string, data?: Record): void { const ctx: { subscriptionId?: string; data?: Record } = { subscriptionId: this.subscriptionId } if (data !== undefined) { ctx.data = data } this.parent.info(message, ctx) } warn(message: string, data?: Record): void { const ctx: { subscriptionId?: string; data?: Record } = { subscriptionId: this.subscriptionId } if (data !== undefined) { ctx.data = data } this.parent.warn(message, ctx) } error(message: string, data?: Record, error?: Error): void { const ctx: { subscriptionId?: string; data?: Record; error?: Error } = { subscriptionId: this.subscriptionId } if (data !== undefined) { ctx.data = data } if (error !== undefined) { ctx.error = error } this.parent.error(message, ctx) } } /** * Create a new CDC logger */ export function createCDCLogger(config?: LoggerConfig): CDCLogger { return new CDCLogger(config) } /** * Default shared logger instance */ export const defaultLogger = new CDCLogger({ level: LogLevel.INFO, })