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

View file

@ -4,12 +4,36 @@ import { globalDeviceRouter } from "./routes/global-device.routes.js";
import { projectDeviceRouter } from "./routes/project-device.routes.js";
import { projectRouter } from "./routes/project.routes.js";
import { errorMiddleware } from "./middleware/error.middleware.js";
import { createLogger, toErrorMeta } from "../shared/logging/logger.js";
const logger = createLogger("api");
const app = express();
const port = Number(process.env.PORT || 3000);
const heartbeatIntervalMs = 5 * 60 * 1000;
app.use(express.json({ limit: "25mb" }));
app.use((req, res, next) => {
if (req.path === "/health") {
next();
return;
}
const startedAt = Date.now();
logger.debug("request started", { method: req.method, path: req.originalUrl });
res.on("finish", () => {
const meta = {
method: req.method,
path: req.originalUrl,
status: res.statusCode,
durationMs: Date.now() - startedAt,
};
if (res.statusCode >= 500) logger.error("request completed", meta);
else if (res.statusCode >= 400) logger.warn("request completed", meta);
else logger.info("request completed", meta);
});
next();
});
app.get("/health", (_req, res) => {
res.json({ ok: true });
});
@ -21,6 +45,26 @@ app.use("/api/project-devices", projectDeviceRouter);
app.use(errorMiddleware);
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
process.on("uncaughtException", (error) => {
logger.error("uncaught exception", toErrorMeta(error));
});
process.on("unhandledRejection", (reason) => {
logger.error("unhandled rejection", toErrorMeta(reason));
});
process.on("SIGTERM", () => logger.info("received SIGTERM"));
process.on("SIGINT", () => logger.info("received SIGINT"));
setInterval(() => {
const memory = process.memoryUsage();
logger.verbose("heartbeat", {
uptimeSeconds: Math.round(process.uptime()),
rssMb: Math.round(memory.rss / 1024 / 1024),
heapUsedMb: Math.round(memory.heapUsed / 1024 / 1024),
});
}, heartbeatIntervalMs).unref();
app.listen(port, () => {
logger.info("server started", { port });
});