All files / lib logger.ts

87.5% Statements 49/56
83.33% Branches 30/36
100% Functions 7/7
90.9% Lines 40/44

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103                                    556x   556x 556x 556x 550x 549x             5x     2x         2x 4x   2x   3x               556x 549x 95x 306x 549x     7x 7x       7x 4x 4x 3x   1x 1x 2x   2x 2x         7x 7x   7x 7x   3x     1x   1x       43x 309x 1x 95x 151x        
// Structured logger for the Hono API. The single allowed sink for route-side
// observability — Biome blocks raw `console.*` under `src/routes/**`.
//
// API: `logger.info(...args)`, `logger.warn(...args)`, `logger.error(...args)`
// mirrors `console.log/warn/error` so route call sites can migrate by name.
//
// - In dev/test, prints `[level] <args…>` to the console.
// - In preview/production (Cloudflare Workers), emits a single JSON line per
//   call: `{"level":"...","message":"...","extra":[...]}` so Workers logpush
//   can ingest cleanly. The first string arg becomes `message`; remaining
//   args are stashed in `extra` (Errors serialized to plain objects).
//
// The logger never throws. If the environment is unreadable, it falls back to
// dev formatting (verbose but harmless).
 
type LogLevel = "info" | "warn" | "error";
 
function isDevEnv(): boolean {
	try {
		// biome-ignore lint/suspicious/noExplicitAny: globalThis.process is optional on Workers
		const env = (globalThis as any).process?.env;
		Iif (!env) return true;
		if (env.NODE_ENV === "production") return false;
		if (env.ENVIRONMENT === "preview" || env.ENVIRONMENT === "production") return false;
		return true;
	} catch {
		return true;
	}
}
 
function serialize(value: unknown): unknown {
	if (value instanceof Error) {
		// `name` is inherited from Error.prototype (not an own property in V8), so capture it explicitly.
		// Custom properties (code, status, details, etc.) are own properties — picked up by getOwnPropertyNames.
		const obj: Record<string, unknown> = {
			name: value.name,
			message: value.message,
			stack: value.stack,
		};
		for (const key of Object.getOwnPropertyNames(value)) {
			Iif (!(key in obj)) obj[key] = (value as unknown as Record<string, unknown>)[key];
		}
		return obj;
	}
	return value;
}
 
// The Biome `noConsole` rule is scoped via overrides to `src/routes/**` only,
// so the raw `console.*` calls below are not flagged here. No suppression
// comments are needed (and adding them produces a `suppressions/unused`
// error in CI).
function emit(level: LogLevel, args: unknown[]): void {
	if (isDevEnv()) {
		if (level === "error") console.error(...args);
		else if (level === "warn") console.warn(...args);
		else console.log(...args);
		return;
	}
 
	try {
		const [first, ...rest] = args;
		let message: string;
		let extra: unknown[] | undefined;
 
		if (typeof first === "string") {
			message = first;
			if (rest.length > 0) extra = rest.map(serialize);
		} else if (first instanceof Error) {
			// Put the Error in extra; use its message as the log message
			message = first.message;
			extra = args.map(serialize);
		} else if (first !== undefined) {
			// Structured object passed directly — avoid double-encoding into message
			message = "[Structured Log]";
			extra = args.map(serialize);
		} else E{
			message = "";
		}
 
		const entry: Record<string, unknown> = { level, message };
		if (extra !== undefined) entry.extra = extra;
 
		const line = JSON.stringify(entry);
		if (level === "error") console.error(lineI);
		else if (level === "warn") console.warn(line);
		else console.log(line);
	} catch {
		// Fallback if JSON.stringify throws (circular refs, BigInt, etc.)
		Iif (level === "error") console.error(...argsI);
		else if (level === "warn") console.warn(...args);
		else console.log(...args);
	}
}
 
export const logger = {
	info: (...args: unknown[]) => emit("info", args),
	log: (...args: unknown[]) => emit("info", args),
	warn: (...args: unknown[]) => emit("warn", args),
	error: (...args: unknown[]) => emit("error", args),
};
 
export type Logger = typeof logger;