81 lines
2.1 KiB
TypeScript

import { redactLogArgs, redactValue } from "@/lib/observability/redaction";
type LogLevel = "debug" | "info" | "warn" | "error" | "silent";
const LEVEL_ORDER: Record<LogLevel, number> = {
debug: 10,
info: 20,
warn: 30,
error: 40,
silent: 50,
};
const SENSITIVE_EVENT_PATTERN =
/(authorization|body|cookie|header|image url|messages received|password|prompt|request body|session|token)/i;
function resolveLogLevel(): LogLevel {
const configured = String(process.env.LOG_LEVEL || "").toLowerCase();
if (configured in LEVEL_ORDER) return configured as LogLevel;
return process.env.NODE_ENV === "production" ? "info" : "debug";
}
function shouldLog(level: Exclude<LogLevel, "silent">) {
return LEVEL_ORDER[level] >= LEVEL_ORDER[resolveLogLevel()];
}
function normalizeEvent(event: string) {
return event
.trim()
.replace(/^\[[^\]]+\]\s*/, "")
.replace(/[^a-zA-Z0-9]+/g, "_")
.replace(/^_+|_+$/g, "")
.toLowerCase()
.slice(0, 80) || "event";
}
function writeLog(level: Exclude<LogLevel, "silent">, scope: string, event: string, args: unknown[]) {
if (!shouldLog(level)) return;
const payload = {
level,
scope,
event: normalizeEvent(event),
message: event,
timestamp: new Date().toISOString(),
...(args.length > 0
? {
meta: SENSITIVE_EVENT_PATTERN.test(event)
? { omitted: true, argCount: args.length }
: redactLogArgs(args),
}
: {}),
};
const serialized = JSON.stringify(redactValue(payload));
if (level === "error") {
console.error(serialized);
return;
}
if (level === "warn") {
console.warn(serialized);
return;
}
console.log(serialized);
}
export function createLogger(scope: string) {
return {
debug(event: string, ...args: unknown[]) {
writeLog("debug", scope, event, args);
},
info(event: string, ...args: unknown[]) {
writeLog("info", scope, event, args);
},
warn(event: string, ...args: unknown[]) {
writeLog("warn", scope, event, args);
},
error(event: string, ...args: unknown[]) {
writeLog("error", scope, event, args);
},
};
}