All files / lib logger.ts

91.66% Statements 33/36
91.66% Branches 22/24
100% Functions 7/7
92.85% Lines 26/28

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                                    545x   545x 545x 545x 541x 540x             3x 1x   2x               545x 540x 92x 302x 540x     5x 5x 545x 545x 5x 5x   3x     40x 304x 1x 92x 148x        
// 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) {
		return { name: value.name, message: value.message, stack: value.stack };
	}
	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;
	}
 
	const [first, ...rest] = args;
	const message = typeof first === "string" ? first : JSON.stringify(serialize(first));
	const entry: Record<string, unknown> = { level, message };
	if (rest.length > 0) entry.extra = rest.map(serialize);
	const line = JSON.stringify(entry);
	if (level === "error") console.error(lineI);
	else if (level === "warn") console.warn(line);
	else console.log(line);
}
 
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;