Skip to content
File

Blob: src/worker/logger.ts

typescript91 lines
1export type LogLevel = "debug" | "info" | "warn" | "error";
2 
3export interface LogFields {
4 [key: string]: unknown;
5}
6 
7export interface Logger {
8 debug: (event: string, fields?: LogFields) => void;
9 info: (event: string, fields?: LogFields) => void;
10 warn: (event: string, fields?: LogFields) => void;
11 error: (event: string, fields?: LogFields) => void;
12 child: (fields: LogFields) => Logger;
13}
14 
15const LEVELS: Record<LogLevel, number> = {
16 debug: 10,
17 info: 20,
18 warn: 30,
19 error: 40,
20};
21 
22export const parseLogLevel = (input?: string | null): LogLevel => {
23 const value = input?.toLowerCase();
24 if (value === "debug" || value === "info" || value === "warn" || value === "error") {
25 return value;
26 }
27 return "info";
28};
29 
30const normalizeFields = (fields: LogFields): LogFields =>
31 Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
32 
33const write = (minLevel: LogLevel, level: LogLevel, baseFields: LogFields, event: string, fields: LogFields): void => {
34 if (LEVELS[level] < LEVELS[minLevel]) {
35 return;
36 }
37 
38 const entry = JSON.stringify({
39 ts: new Date().toISOString(),
40 level,
41 event,
42 ...normalizeFields(baseFields),
43 ...normalizeFields(fields),
44 });
45 
46 if (level === "error") {
47 console.error(entry);
48 return;
49 }
50 if (level === "warn") {
51 console.warn(entry);
52 return;
53 }
54 console.log(entry);
55};
56 
57// Builds a logger with a captured level and base fields. The level lives
58// on the closure so two loggers built at different levels do not race
59// through a shared module-global mutable variable. Callers in the worker
60// pull `c.var.log` (set by middleware in `src/worker/index.ts`) and
61// .child({...}) for component-local fields.
62export const createLogger = (level: LogLevel, baseFields: LogFields = {}): Logger => ({
63 debug: (event, fields = {}) => write(level, "debug", baseFields, event, fields),
64 info: (event, fields = {}) => write(level, "info", baseFields, event, fields),
65 warn: (event, fields = {}) => write(level, "warn", baseFields, event, fields),
66 error: (event, fields = {}) => write(level, "error", baseFields, event, fields),
67 child: (fields) => createLogger(level, { ...baseFields, ...fields }),
68});
69 
70// Stack traces from third-party throws (Better Auth, Drizzle, the
71// oauth-provider plugin) can interpolate values tessera must not log:
72// secrets, bearer tokens, OAuth client secrets, session cookies,
73// password material, invite tokens, signing keys, or raw Turnstile
74// responses. Only attach `errorStack` at debug level so production
75// (info) does not log it.
76export const errorContext = (error: unknown, level: LogLevel): LogFields => {
77 if (error instanceof Error) {
78 const ctx: LogFields = {
79 errorName: error.name,
80 errorMessage: error.message,
81 };
82 if (LEVELS.debug >= LEVELS[level]) {
83 ctx.errorStack = error.stack;
84 }
85 return ctx;
86 }
87 return {
88 errorMessage: String(error),
89 };
90};