import { loadEnv } from "../config/env.js"; import type { Request } from "express"; import { otelContext, otelTrace } from "@factory/observability"; export interface Logger { child(bindings: Record): Logger; debug(msg: string, extra?: Record): void; info(msg: string, extra?: Record): void; warn(msg: string, extra?: Record): void; error(msg: string, extra?: Record): void; } class ConsoleLogger implements Logger { constructor(private readonly base: Record = {}) {} child(bindings: Record): Logger { return new ConsoleLogger({ ...this.base, ...bindings }); } private log(level: string, msg: string, extra?: Record): void { const env = safeEnv(); if (level === "debug" && env.NODE_ENV === "production") return; // TASK-0135: correlate every line with the ACTIVE OTel span (traceId/ // spanId — the requestId binding already comes from the logger's base / // child bindings, or is supplied by the caller via `extra`). Absent when // no span is active (or OTel disabled) — keys are omitted, never null. const line: Record = { level, time: new Date().toISOString(), msg, ...this.base, ...(extra ?? {}) }; const sc = otelTrace.getSpanContext(otelContext.active()); if (sc && sc.traceId && sc.spanId) { line.traceId = sc.traceId; line.spanId = sc.spanId; } const out = JSON.stringify(line); if (level === "error") process.stderr.write(out + "\n"); else if (level === "warn") process.stderr.write(out + "\n"); else process.stdout.write(out + "\n"); } debug(msg: string, extra?: Record): void { this.log("debug", msg, extra); } info(msg: string, extra?: Record): void { this.log("info", msg, extra); } warn(msg: string, extra?: Record): void { this.log("warn", msg, extra); } error(msg: string, extra?: Record): void { this.log("error", msg, extra); } } function safeEnv() { try { return loadEnv(); } catch { return { NODE_ENV: "development" }; } } export const logger: Logger = new ConsoleLogger({ service: "factory-api" }); export function requestId(req: Request): string { return req.id ?? "unknown"; }