import { join } from 'node:path'; import { extractError, formatEntry, redactCtx } from './formatter'; import { RotatingFileSink } from './sinks'; import { type LogContext, type LogEntry, type LogLevel, type LogPayload, type Logger, type LoggingConfig, type RootLogger, levelEnabled, } from './types'; const ROOT_SYMBOL = Symbol.for('kimi.logger.root'); class RootLoggerImpl implements RootLogger { private config: LoggingConfig | undefined; private globalSink: RotatingFileSink | undefined; isConfigured(): boolean { return this.config !== undefined; } getConfig(): LoggingConfig | undefined { return this.config; } configure(config: LoggingConfig): Promise { if (this.config !== undefined && sameLoggingConfig(this.config, config)) { return Promise.resolve(); } const oldGlobalSink = this.globalSink; this.config = config; this.globalSink = makeGlobalSink(config); return oldGlobalSink?.close() ?? Promise.resolve(); } async flush(): Promise { if (this.globalSink === undefined) return true; return this.globalSink.flush(); } flushSync(): void { this.globalSink?.flushSync(); } emit(entry: LogEntry): void { const config = this.config; if (config === undefined || config.level === 'off') return; if (!levelEnabled(config.level, entry.level)) return; const formatted = formatEntry(entry); if (formatted.dropped) return; this.globalSink?.enqueue(formatted.text + '\n'); } async __shutdownForTest(): Promise { const close = this.globalSink?.close(); this.globalSink = undefined; this.config = undefined; await close; } } function getRootInternal(): RootLoggerImpl { const globalAny = globalThis as Record; const existing = globalAny[ROOT_SYMBOL]; if (existing instanceof RootLoggerImpl) return existing; const fresh = new RootLoggerImpl(); globalAny[ROOT_SYMBOL] = fresh; return fresh; } export function getRootLogger(): RootLogger { return getRootInternal(); } export function flushDiagnosticLogs(): Promise { return getRootInternal().flush(); } export function flushDiagnosticLogsSync(): void { getRootInternal().flushSync(); } class LoggerImpl implements Logger { constructor(private readonly boundCtx: LogContext) {} error(message: string, payload?: LogPayload): void { this.emitAt('error', message, payload); } warn(message: string, payload?: LogPayload): void { this.emitAt('warn', message, payload); } info(message: string, payload?: LogPayload): void { this.emitAt('info', message, payload); } debug(message: string, payload?: LogPayload): void { this.emitAt('debug', message, payload); } createChild(ctx: LogContext): Logger { return new LoggerImpl({ ...this.boundCtx, ...ctx }); } private emitAt( level: Exclude, message: string, payload: LogPayload, ): void { const root = getRootInternal(); if (!root.isConfigured()) return; try { const { ctx: payloadCtx, error } = resolvePayload(payload); const ctx = mergeCtx(payloadCtx, this.boundCtx); root.emit({ t: Date.now(), level, msg: message, ctx, error, }); } catch { } } } function makeGlobalSink(config: LoggingConfig): RotatingFileSink | undefined { if (config.level === 'off') return undefined; return new RotatingFileSink({ path: config.globalLogPath, maxBytes: config.globalMaxBytes, files: config.globalFiles, }); } function sameLoggingConfig(a: LoggingConfig, b: LoggingConfig): boolean { return ( a.level === b.level && a.globalLogPath === b.globalLogPath && a.globalMaxBytes === b.globalMaxBytes && a.globalFiles === b.globalFiles && a.sessionMaxBytes === b.sessionMaxBytes && a.sessionFiles === b.sessionFiles ); } function resolvePayload( payload: LogPayload, ): { ctx: LogContext | undefined; error: LogEntry['error'] } { if (payload === undefined || payload === null) { return { ctx: undefined, error: undefined }; } if (payload instanceof Error) { return { ctx: undefined, error: extractError(payload) }; } if (typeof payload === 'object') { const obj = payload as Record; if (obj['error'] instanceof Error) { const { error: errValue, ...rest } = obj; return { ctx: rest as LogContext, error: extractError(errValue) }; } return { ctx: obj as LogContext, error: undefined }; } if ( typeof payload === 'string' || typeof payload === 'number' || typeof payload === 'boolean' || typeof payload === 'bigint' || typeof payload === 'symbol' ) { return { ctx: { reason: String(payload) }, error: undefined }; } if (typeof payload === 'function') { const reason = payload.name === '' ? '[Function]' : `[Function: ${payload.name}]`; return { ctx: { reason }, error: undefined }; } return { ctx: { reason: Object.prototype.toString.call(payload) }, error: undefined }; } function mergeCtx( payloadCtx: LogContext | undefined, boundCtx: LogContext, ): LogContext | undefined { const boundHasKeys = Object.keys(boundCtx).length > 0; if (!boundHasKeys) return payloadCtx; if (payloadCtx === undefined) return { ...boundCtx }; return { ...payloadCtx, ...boundCtx }; } export const log: Logger = new LoggerImpl({}); export function redact(value: T): T { if (value === null || typeof value !== 'object') return value; return redactCtx({ value: value as unknown })['value'] as T; } export async function __resetRootLoggerForTest(): Promise { const globalAny = globalThis as Record; const existing = globalAny[ROOT_SYMBOL]; if (existing instanceof RootLoggerImpl) { await existing.__shutdownForTest(); } globalAny[ROOT_SYMBOL] = undefined; } export function resolveGlobalLogPath(homeDir: string): string { return join(homeDir, 'logs', 'kimi-code.log'); }