fix(api): tame noisy logs and overlapping polls

This commit is contained in:
Dorian
2026-05-06 17:06:49 +01:00
parent aae760331c
commit 2c80340039
3 changed files with 144 additions and 3 deletions

View File

@@ -44,6 +44,7 @@ let lastSnapshot: DatumSnapshot = {
error: { code: "NOT_YET_POLLED", message: "Poller has not run yet" }, error: { code: "NOT_YET_POLLED", message: "Poller has not run yet" },
}; };
let timer: NodeJS.Timeout | null = null; let timer: NodeJS.Timeout | null = null;
let pollInFlight = false;
function formatErr(err: unknown): string { function formatErr(err: unknown): string {
if (!(err instanceof Error)) return String(err); if (!(err instanceof Error)) return String(err);
@@ -167,7 +168,13 @@ async function pollOnce(): Promise<DatumSnapshot> {
export function startPoller(): void { export function startPoller(): void {
if (timer) return; if (timer) return;
const tick = async (): Promise<void> => { const tick = async (): Promise<void> => {
lastSnapshot = await pollOnce(); if (pollInFlight) return;
pollInFlight = true;
try {
lastSnapshot = await pollOnce();
} finally {
pollInFlight = false;
}
}; };
void tick(); void tick();
timer = setInterval(() => void tick(), config.datum.pollIntervalMs); timer = setInterval(() => void tick(), config.datum.pollIntervalMs);

View File

@@ -1,6 +1,33 @@
import { pino } from "pino"; import { pino } from "pino";
import { Writable } from "node:stream";
import { config } from "./config.js"; import { config } from "./config.js";
const levelNames: Record<number, string> = {
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({ export const logger = pino({
level: config.logLevel, level: config.logLevel,
redact: { redact: {
@@ -15,4 +42,101 @@ export const logger = pino({
censor: "[REDACTED]", censor: "[REDACTED]",
}, },
base: { app: "gashboard" }, base: { app: "gashboard" },
}); }, logStream);
function formatLogLine(line: string): string {
try {
const record = JSON.parse(line) as Record<string, unknown>;
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<string, unknown>;
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<string, unknown>;
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<string, unknown>;
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);
}

View File

@@ -43,7 +43,17 @@ export function buildApp() {
} }
app.use(express.json({ limit: "32kb" })); 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) => { app.get("/healthz", (_req, res) => {
res.json({ ok: true }); res.json({ ok: true });