repositories / will
will
owned by admin
src/shared/log.ts
Rawconst SENSITIVE = /token|key|secret|password|authorization|credential/i
/** Redact values whose field names look sensitive; used by the structured logger. */
export function redactFields(fields: Record<string, unknown>): Record<string, unknown> {
const out: Record<string, unknown> = {}
for (const [k, v] of Object.entries(fields)) {
out[k] = SENSITIVE.test(k) ? '[redacted]' : v
}
return out
}
export type LogLevel = 'debug' | 'info' | 'warn' | 'error'
export interface Logger {
debug(message: string, fields?: Record<string, unknown>): void
info(message: string, fields?: Record<string, unknown>): void
warn(message: string, fields?: Record<string, unknown>): void
error(message: string, fields?: Record<string, unknown>): void
}
export interface LogContext {
component: string
revision?: string
}
/**
* Structured JSON logger on stdout/stderr.
* Spec: operational logs must never contain secrets, chat bodies, or raw tool output;
* callers pass only metadata, and sensitive-looking keys are masked defensively.
*/
export function createLogger(ctx: LogContext): Logger {
const write = (level: LogLevel, message: string, fields?: Record<string, unknown>) => {
const entry = {
ts: new Date().toISOString(),
level,
component: ctx.component,
...(ctx.revision ? { revision: ctx.revision } : {}),
message,
...(fields ? redactFields(fields) : {}),
}
const line = JSON.stringify(entry)
if (level === 'error') process.stderr.write(`${line}\n`)
else process.stdout.write(`${line}\n`)
}
return {
debug: (m, f) => write('debug', m, f),
info: (m, f) => write('info', m, f),
warn: (m, f) => write('warn', m, f),
error: (m, f) => write('error', m, f),
}
}