cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

132 lines
3.7 KiB
TypeScript

import { enrichLogContext, getLogContext, runWithLogContext } from "./context.js";
import { redactBindings, redactSensitiveText } from "./redaction.js";
import { serializeError } from "./serialize-error.js";
import { guardedFlush, guardedWrite } from "./sink-guard.js";
import type {
CreateLoggerOptions,
EmitLevel,
LogBindings,
LogEvent,
Logger,
LogLevel,
LogSink,
} from "./types.js";
import { LOG_LEVELS } from "./types.js";
const RESERVED = new Set(["timestamp", "level", "message", "error", "service.name"]);
const DEFAULT_FLUSH_MS = 2_000;
class LoggerImpl implements Logger {
readonly level: LogLevel;
private readonly service: string;
private readonly sinks: LogSink[];
private readonly localBindings: LogBindings;
constructor(options: CreateLoggerOptions) {
this.service = options.service;
this.level = options.level ?? "info";
this.sinks = options.sinks;
this.localBindings = options.bindings ?? {};
}
debug(message: string, bindings?: LogBindings): void {
this.emit("debug", message, bindings);
}
info(message: string, bindings?: LogBindings): void {
this.emit("info", message, bindings);
}
warn(message: string, bindings?: LogBindings): void {
this.emit("warn", message, bindings);
}
error(message: string, errorOrBindings?: unknown, bindings?: LogBindings): void {
if (errorOrBindings === undefined) {
this.emit("error", message);
return;
}
if (isPlainBindings(errorOrBindings)) {
this.emit("error", message, { ...errorOrBindings, ...bindings });
return;
}
this.emit("error", message, bindings, errorOrBindings);
}
child(bindings: LogBindings): Logger {
return new LoggerImpl({
service: this.service,
level: this.level,
sinks: this.sinks,
bindings: { ...this.localBindings, ...bindings },
});
}
withContext<T>(bindings: LogBindings, fn: () => T): T {
return runWithLogContext(bindings, fn);
}
enrich(bindings: LogBindings): void {
enrichLogContext(bindings);
}
async flush(options?: { timeoutMs?: number }): Promise<void> {
const timeoutMs = options?.timeoutMs ?? DEFAULT_FLUSH_MS;
const flushing = Promise.all(this.sinks.map((sink) => guardedFlush(sink.flush?.bind(sink))));
await Promise.race([
flushing,
new Promise<void>((resolve) => {
setTimeout(resolve, timeoutMs).unref?.();
}),
]);
}
private emit(level: EmitLevel, message: string, bindings?: LogBindings, error?: unknown): void {
if (LOG_LEVELS[level] < LOG_LEVELS[this.level]) return;
const merged = redactBindings({
...this.localBindings,
...getLogContext(),
...bindings,
});
const event: LogEvent = {
timestamp: new Date().toISOString(),
level,
message: redactSensitiveText(message),
"service.name": this.service,
};
for (const [key, value] of Object.entries(merged)) {
if (RESERVED.has(key) || value === undefined) continue;
event[key] = value;
}
if (error !== undefined) event.error = serializeError(error);
for (const sink of this.sinks) {
guardedWrite(() => sink.write(event));
}
}
}
export function createLogger(options: CreateLoggerOptions): Logger {
return new LoggerImpl(options);
}
function isPlainBindings(value: unknown): value is LogBindings {
if (value === null || typeof value !== "object") return false;
if (Array.isArray(value) || value instanceof Error) return false;
const proto = Object.getPrototypeOf(value);
return proto === Object.prototype || proto === null;
}
let installed: Logger = createLogger({
service: "rakazo",
level: "off",
sinks: [],
});
export function installLogger(logger: Logger): void {
installed = logger;
}
export function getLogger(): Logger {
return installed;
}