Luigit
repositories / will

will

owned by admin

src/shared/log.ts

Raw
const 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),
  }
}