From 8a013b0e626cec532fbcd81936773474186e414b Mon Sep 17 00:00:00 2001 From: Yuzu Date: Mon, 13 Jul 2026 16:59:30 +0700 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20a=20leveled=20logger?= =?UTF-8?q?=20with=20control-char=20sanitization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/logger.ts | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/logger.ts diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 0000000..27b2cc6 --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,58 @@ +export type LogLevel = "debug" | "info" | "warn" | "error"; + +const LEVEL_ORDER: Record = { + debug: 10, + info: 20, + warn: 30, + error: 40, +}; + +function resolveThreshold(): number { + const raw = (globalThis.process.env.LOG_LEVEL ?? "info") + .trim() + .toLowerCase(); + return LEVEL_ORDER[raw as LogLevel] ?? LEVEL_ORDER.info; +} + +let threshold = resolveThreshold(); + +export function sanitize(value: unknown, maxLength = 200): string { + let str = + typeof value === "string" + ? value + : value === undefined + ? "" + : String(value); + // eslint-disable-next-line no-control-regex + str = str.replace(/[\x00-\x1f\x7f-\x9f]/g, "�"); + if (str.length > maxLength) str = str.slice(0, maxLength) + "…"; + return str; +} + +function emit( + level: LogLevel, + stream: NodeJS.WriteStream, + args: unknown[] +): void { + if (LEVEL_ORDER[level] < threshold) return; + const ts = new Date().toISOString(); + const line = + `[${ts}] ${level.toUpperCase().padEnd(5)} ` + + args + .map((a) => (typeof a === "string" ? a : JSON.stringify(a))) + .join(" "); + stream.write(line + "\n"); +} + +export const logger = { + debug: (...args: unknown[]) => + emit("debug", globalThis.process.stdout, args), + info: (...args: unknown[]) => emit("info", globalThis.process.stdout, args), + warn: (...args: unknown[]) => emit("warn", globalThis.process.stderr, args), + error: (...args: unknown[]) => + emit("error", globalThis.process.stderr, args), + refresh: () => { + threshold = resolveThreshold(); + }, + sanitize, +};