export type LogLevel = "debug" | "info" | "warn" | "error"; export interface LogFields { [key: string]: unknown; } export interface Logger { debug: (event: string, fields?: LogFields) => void; info: (event: string, fields?: LogFields) => void; warn: (event: string, fields?: LogFields) => void; error: (event: string, fields?: LogFields) => void; child: (fields: LogFields) => Logger; } const LEVELS: Record = { debug: 10, info: 20, warn: 30, error: 40, }; export const parseLogLevel = (input?: string | null): LogLevel => { const value = input?.toLowerCase(); if (value === "debug" || value === "info" || value === "warn" || value === "error") { return value; } return "info"; }; const normalizeFields = (fields: LogFields): LogFields => Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined)); const write = (minLevel: LogLevel, level: LogLevel, baseFields: LogFields, event: string, fields: LogFields): void => { if (LEVELS[level] < LEVELS[minLevel]) { return; } const entry = JSON.stringify({ ts: new Date().toISOString(), level, event, ...normalizeFields(baseFields), ...normalizeFields(fields), }); if (level === "error") { console.error(entry); return; } if (level === "warn") { console.warn(entry); return; } console.log(entry); }; // Builds a logger with a captured level and base fields. The level lives // on the closure so two loggers built at different levels do not race // through a shared module-global mutable variable. Callers in the worker // pull `c.var.log` (set by middleware in `src/worker/index.ts`) and // .child({...}) for component-local fields. export const createLogger = (level: LogLevel, baseFields: LogFields = {}): Logger => ({ debug: (event, fields = {}) => write(level, "debug", baseFields, event, fields), info: (event, fields = {}) => write(level, "info", baseFields, event, fields), warn: (event, fields = {}) => write(level, "warn", baseFields, event, fields), error: (event, fields = {}) => write(level, "error", baseFields, event, fields), child: (fields) => createLogger(level, { ...baseFields, ...fields }), }); // Stack traces from third-party throws (Better Auth, Drizzle, the // oauth-provider plugin) can interpolate values tessera must not log: // secrets, bearer tokens, OAuth client secrets, session cookies, // password material, invite tokens, signing keys, or raw Turnstile // responses. Only attach `errorStack` at debug level so production // (info) does not log it. export const errorContext = (error: unknown, level: LogLevel): LogFields => { if (error instanceof Error) { const ctx: LogFields = { errorName: error.name, errorMessage: error.message, }; if (LEVELS.debug >= LEVELS[level]) { ctx.errorStack = error.stack; } return ctx; } return { errorMessage: String(error), }; };