export type Logger = { info(obj: object, msg?: string): void; warn(obj: object, msg?: string): void; error(obj: object, msg?: string): void; fatal(obj: object, msg?: string): void; }; const LEVELS = { info: 30, warn: 40, error: 50, fatal: 60 } as const; const threshold = (): number => { const raw = ( globalThis as { process?: { env?: Record } } ).process?.env?.["LOG_LEVEL"]; return LEVELS[raw as keyof typeof LEVELS] ?? LEVELS.info; }; const emit = (level: keyof typeof LEVELS) => (obj: object, msg?: string): void => { if (LEVELS[level] < threshold()) return; console.log( JSON.stringify({ level: LEVELS[level], time: Date.now(), component: "pinboard", ...obj, msg, }), ); }; /** Mutable so a binding can install a real logger via `setLogger`. */ export const logger: Logger = { info: emit("info"), warn: emit("warn"), error: emit("error"), fatal: emit("fatal"), }; export const setLogger = (impl: Logger): void => { logger.info = impl.info.bind(impl); logger.warn = impl.warn.bind(impl); logger.error = impl.error.bind(impl); logger.fatal = impl.fatal.bind(impl); }; // Raw Error properties are non-enumerable and serialize to {}; always log // thrown values as { err: serializeError(e) }. export const serializeError = ( error: unknown, ): { name?: string; message: string; stack?: string | undefined } => { if (error instanceof Error) { return { name: error.name, message: error.message, stack: error.stack }; } return { message: String(error) }; };