Add configurable structured logging

Adds a leveled JSON logger (error/warn/info/verbose/debug, controlled via
LOG_LEVEL) wired into the Express API (access log, error middleware,
crash handlers, memory heartbeat) and the Next.js server (page-request
proxy, instrumentation crash handlers, heartbeat). LOG_LEVEL is exposed
through compose.yaml, and both services now rotate their Docker logs
(json-file, 20m x 10 files) instead of growing unbounded. Intended to
capture long-running diagnostic data for the intermittent 502s seen on
the Docker host.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Julian Appel 2026-08-06 19:45:39 +02:00
parent f26c000007
commit ea3c02cd6b
12 changed files with 317 additions and 6 deletions

83
tests/logger.test.ts Normal file
View file

@ -0,0 +1,83 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { createLogger, resolveLogLevel } from "../src/shared/logging/logger.js";
describe("resolveLogLevel", () => {
it("defaults to info for missing or unknown values", () => {
assert.equal(resolveLogLevel(undefined), "info");
assert.equal(resolveLogLevel(""), "info");
assert.equal(resolveLogLevel("nonsense"), "info");
});
it("accepts known levels case-insensitively", () => {
assert.equal(resolveLogLevel("DEBUG"), "debug");
assert.equal(resolveLogLevel(" verbose "), "verbose");
assert.equal(resolveLogLevel("warn"), "warn");
});
});
describe("createLogger", () => {
it("suppresses levels below the configured threshold", () => {
const infoLines: string[] = [];
const originalLog = console.log;
console.log = (line: string) => {
infoLines.push(line);
};
try {
const logger = createLogger("test", { level: "warn" });
logger.info("should not appear");
logger.debug("should not appear");
logger.verbose("should not appear");
} finally {
console.log = originalLog;
}
assert.equal(infoLines.length, 0);
});
it("emits enabled levels as structured JSON with scope and meta", () => {
const errorLines: string[] = [];
const originalError = console.error;
console.error = (line: string) => {
errorLines.push(line);
};
try {
const logger = createLogger("test", { level: "warn" });
logger.warn("something happened", { code: 42 });
} finally {
console.error = originalError;
}
assert.equal(errorLines.length, 1);
const parsed = JSON.parse(errorLines[0]);
assert.equal(parsed.level, "warn");
assert.equal(parsed.scope, "test");
assert.equal(parsed.message, "something happened");
assert.equal(parsed.code, 42);
assert.equal(typeof parsed.timestamp, "string");
});
it("routes debug/verbose/info to console.log and error/warn to console.error", () => {
const logLines: string[] = [];
const errorLines: string[] = [];
const originalLog = console.log;
const originalError = console.error;
console.log = (line: string) => {
logLines.push(line);
};
console.error = (line: string) => {
errorLines.push(line);
};
try {
const logger = createLogger("test", { level: "debug" });
logger.debug("d");
logger.verbose("v");
logger.info("i");
logger.warn("w");
logger.error("e");
} finally {
console.log = originalLog;
console.error = originalError;
}
assert.equal(logLines.length, 3);
assert.equal(errorLines.length, 2);
});
});