From c6126c13e96371bd14c06381bcd6c64e631a8502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Thu, 5 Mar 2026 10:22:49 +0100 Subject: [PATCH] feat: add backend logger matching frontend text format Co-Authored-By: Claude Sonnet 4.6 --- packages/backend/src/index.ts | 7 +-- packages/backend/src/lib/logger.ts | 91 ++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 packages/backend/src/lib/logger.ts diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 40967ba..f75cb1c 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -13,17 +13,14 @@ import { schema } from "./graphql/index"; import { buildContext } from "./graphql/context"; import { db } from "./db/connection"; import { redis } from "./lib/auth"; +import { logger } from "./lib/logger"; const PORT = parseInt(process.env.PORT || "4000"); const UPLOAD_DIR = process.env.UPLOAD_DIR || "/data/uploads"; const CORS_ORIGIN = process.env.CORS_ORIGIN || "http://localhost:3000"; async function main() { - const fastify = Fastify({ - logger: { - level: process.env.LOG_LEVEL || "info", - }, - }); + const fastify = Fastify({ loggerInstance: logger }); await fastify.register(fastifyCookie, { secret: process.env.COOKIE_SECRET || "change-me-in-production", diff --git a/packages/backend/src/lib/logger.ts b/packages/backend/src/lib/logger.ts new file mode 100644 index 0000000..993c00d --- /dev/null +++ b/packages/backend/src/lib/logger.ts @@ -0,0 +1,91 @@ +type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal"; + +const LEVEL_VALUES: Record = { + trace: 10, + debug: 20, + info: 30, + warn: 40, + error: 50, + fatal: 60, +}; + +function createLogger(bindings: Record = {}, initialLevel: LogLevel = "info") { + let currentLevel = initialLevel; + + function shouldLog(level: LogLevel): boolean { + return LEVEL_VALUES[level] >= LEVEL_VALUES[currentLevel]; + } + + function formatMessage(level: LogLevel, arg: unknown, msg?: string): string { + const timestamp = new Date().toISOString(); + + let message: string; + const meta: Record = { ...bindings }; + + if (typeof arg === "string") { + message = arg; + } else if (arg !== null && typeof arg === "object") { + // Pino-style: log(obj, msg?) — strip internal pino keys + const { msg: m, level: _l, time: _t, pid: _p, hostname: _h, req: _req, res: _res, reqId, ...rest } = arg as Record; + message = msg || (typeof m === "string" ? m : ""); + if (reqId) meta.reqId = reqId; + Object.assign(meta, rest); + } else { + message = String(arg ?? ""); + } + + const parts = [`[${timestamp}]`, `[${level.toUpperCase()}]`, message]; + let result = parts.join(" "); + + const metaEntries = Object.entries(meta).filter(([k]) => k !== "reqId"); + const reqId = meta.reqId; + if (reqId) result = `[${timestamp}] [${level.toUpperCase()}] [${reqId}] ${message}`; + + if (metaEntries.length > 0) { + result += " " + JSON.stringify(Object.fromEntries(metaEntries)); + } + + return result; + } + + function write(level: LogLevel, arg: unknown, msg?: string) { + if (!shouldLog(level)) return; + const formatted = formatMessage(level, arg, msg); + switch (level) { + case "trace": + case "debug": + console.debug(formatted); + break; + case "info": + console.info(formatted); + break; + case "warn": + console.warn(formatted); + break; + case "error": + case "fatal": + console.error(formatted); + break; + } + } + + return { + get level() { + return currentLevel; + }, + set level(l: string) { + currentLevel = l as LogLevel; + }, + trace: (arg: unknown, msg?: string) => write("trace", arg, msg), + debug: (arg: unknown, msg?: string) => write("debug", arg, msg), + info: (arg: unknown, msg?: string) => write("info", arg, msg), + warn: (arg: unknown, msg?: string) => write("warn", arg, msg), + error: (arg: unknown, msg?: string) => write("error", arg, msg), + fatal: (arg: unknown, msg?: string) => write("fatal", arg, msg), + silent: () => {}, + child: (newBindings: Record) => + createLogger({ ...bindings, ...newBindings }, currentLevel), + }; +} + +export const logger = createLogger({}, (process.env.LOG_LEVEL as LogLevel) || "info");