feat: add backend logger matching frontend text format

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 10:22:49 +01:00
parent fd4050a49f
commit c6126c13e9
2 changed files with 93 additions and 5 deletions

View File

@@ -13,17 +13,14 @@ import { schema } from "./graphql/index";
import { buildContext } from "./graphql/context"; import { buildContext } from "./graphql/context";
import { db } from "./db/connection"; import { db } from "./db/connection";
import { redis } from "./lib/auth"; import { redis } from "./lib/auth";
import { logger } from "./lib/logger";
const PORT = parseInt(process.env.PORT || "4000"); const PORT = parseInt(process.env.PORT || "4000");
const UPLOAD_DIR = process.env.UPLOAD_DIR || "/data/uploads"; const UPLOAD_DIR = process.env.UPLOAD_DIR || "/data/uploads";
const CORS_ORIGIN = process.env.CORS_ORIGIN || "http://localhost:3000"; const CORS_ORIGIN = process.env.CORS_ORIGIN || "http://localhost:3000";
async function main() { async function main() {
const fastify = Fastify({ const fastify = Fastify({ loggerInstance: logger });
logger: {
level: process.env.LOG_LEVEL || "info",
},
});
await fastify.register(fastifyCookie, { await fastify.register(fastifyCookie, {
secret: process.env.COOKIE_SECRET || "change-me-in-production", secret: process.env.COOKIE_SECRET || "change-me-in-production",

View File

@@ -0,0 +1,91 @@
type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal";
const LEVEL_VALUES: Record<LogLevel, number> = {
trace: 10,
debug: 20,
info: 30,
warn: 40,
error: 50,
fatal: 60,
};
function createLogger(bindings: Record<string, unknown> = {}, 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<string, unknown> = { ...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<string, unknown>;
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<string, unknown>) =>
createLogger({ ...bindings, ...newBindings }, currentLevel),
};
}
export const logger = createLogger({}, (process.env.LOG_LEVEL as LogLevel) || "info");