From 2c80340039a631f62b8131155d50568953a9cdda Mon Sep 17 00:00:00 2001 From: Dorian Date: Wed, 6 May 2026 17:06:49 +0100 Subject: [PATCH] fix(api): tame noisy logs and overlapping polls --- apps/api/src/datum/poller.ts | 9 ++- apps/api/src/logger.ts | 126 ++++++++++++++++++++++++++++++++++- apps/api/src/server.ts | 12 +++- 3 files changed, 144 insertions(+), 3 deletions(-) diff --git a/apps/api/src/datum/poller.ts b/apps/api/src/datum/poller.ts index 6a84fe1..27eddbd 100644 --- a/apps/api/src/datum/poller.ts +++ b/apps/api/src/datum/poller.ts @@ -44,6 +44,7 @@ let lastSnapshot: DatumSnapshot = { error: { code: "NOT_YET_POLLED", message: "Poller has not run yet" }, }; let timer: NodeJS.Timeout | null = null; +let pollInFlight = false; function formatErr(err: unknown): string { if (!(err instanceof Error)) return String(err); @@ -167,7 +168,13 @@ async function pollOnce(): Promise { export function startPoller(): void { if (timer) return; const tick = async (): Promise => { - lastSnapshot = await pollOnce(); + if (pollInFlight) return; + pollInFlight = true; + try { + lastSnapshot = await pollOnce(); + } finally { + pollInFlight = false; + } }; void tick(); timer = setInterval(() => void tick(), config.datum.pollIntervalMs); diff --git a/apps/api/src/logger.ts b/apps/api/src/logger.ts index a0b3121..69ca839 100644 --- a/apps/api/src/logger.ts +++ b/apps/api/src/logger.ts @@ -1,6 +1,33 @@ import { pino } from "pino"; +import { Writable } from "node:stream"; import { config } from "./config.js"; +const levelNames: Record = { + 10: "trace", + 20: "debug", + 30: "info", + 40: "warn", + 50: "error", + 60: "fatal", +}; + +let buffered = ""; + +const logStream = new Writable({ + write(chunk, _encoding, callback) { + buffered += chunk.toString(); + const lines = buffered.split("\n"); + buffered = lines.pop() ?? ""; + + for (const line of lines) { + if (!line) continue; + process.stdout.write(`${formatLogLine(line)}\n`); + } + + callback(); + }, +}); + export const logger = pino({ level: config.logLevel, redact: { @@ -15,4 +42,101 @@ export const logger = pino({ censor: "[REDACTED]", }, base: { app: "gashboard" }, -}); +}, logStream); + +function formatLogLine(line: string): string { + try { + const record = JSON.parse(line) as Record; + const handled = new Set([ + "level", + "time", + "pid", + "hostname", + "app", + "req", + "res", + "responseTime", + "port", + "datum", + "allowedNpubs", + "staticDir", + "url", + "reason", + "err", + "msg", + ]); + const parts: string[] = [ + `time=${formatTime(record.time)}`, + `level=${formatLevel(record.level)}`, + ]; + + pushField(parts, "app", record.app); + pushRequestFields(parts, record.req); + pushResponseFields(parts, record.res); + pushField(parts, "responseTime", record.responseTime); + pushField(parts, "port", record.port); + pushField(parts, "datum", record.datum); + pushField(parts, "allowedNpubs", record.allowedNpubs); + pushField(parts, "staticDir", record.staticDir); + pushField(parts, "url", record.url); + pushField(parts, "reason", record.reason); + + if (record.err && typeof record.err === "object") { + const err = record.err as Record; + pushField(parts, "err", err.message ?? err.type ?? record.err); + } + + for (const [key, value] of Object.entries(record)) { + if (handled.has(key)) continue; + if (!isScalar(value)) continue; + pushField(parts, key, value); + } + + pushField(parts, "msg", record.msg); + return parts.join(" "); + } catch { + return line; + } +} + +function formatTime(value: unknown): string { + if (typeof value === "number") return new Date(value).toISOString(); + if (typeof value === "string") return value; + return new Date().toISOString(); +} + +function formatLevel(value: unknown): string { + if (typeof value === "number") return levelNames[value] ?? String(value); + if (typeof value === "string") return value; + return "info"; +} + +function pushRequestFields(parts: string[], value: unknown): void { + if (!value || typeof value !== "object") return; + const req = value as Record; + pushField(parts, "method", req.method); + pushField(parts, "path", req.url); + pushField(parts, "remote", req.remoteAddress); +} + +function pushResponseFields(parts: string[], value: unknown): void { + if (!value || typeof value !== "object") return; + const res = value as Record; + pushField(parts, "status", res.statusCode); +} + +function pushField(parts: string[], key: string, value: unknown): void { + if (value === undefined || value === null || value === "") return; + parts.push(`${key}=${formatValue(value)}`); +} + +function isScalar(value: unknown): value is string | number | boolean { + return typeof value === "string" || typeof value === "number" || typeof value === "boolean"; +} + +function formatValue(value: unknown): string { + const raw = typeof value === "string" ? value : JSON.stringify(value); + if (!raw) return '""'; + if (/^[A-Za-z0-9_./:@+-]+$/.test(raw)) return raw; + return JSON.stringify(raw); +} diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts index dbcc016..e3aa767 100644 --- a/apps/api/src/server.ts +++ b/apps/api/src/server.ts @@ -43,7 +43,17 @@ export function buildApp() { } app.use(express.json({ limit: "32kb" })); - app.use(pinoHttp({ logger })); + app.use( + pinoHttp({ + logger, + autoLogging: { + ignore: (req) => + req.url === "/healthz" || + req.url === "/favicon.ico" || + req.url.startsWith("/assets/"), + }, + }), + ); app.get("/healthz", (_req, res) => { res.json({ ok: true });