diff --git a/compose.yaml b/compose.yaml index d8d787e..6a5ec7c 100644 --- a/compose.yaml +++ b/compose.yaml @@ -13,6 +13,7 @@ services: CHOKIDAR_USEPOLLING: "true" LOG_LEVEL: "${LOG_LEVEL:-info}" init: true + restart: unless-stopped logging: driver: json-file options: @@ -53,6 +54,7 @@ services: NEXT_TELEMETRY_DISABLED: "1" LOG_LEVEL: "${LOG_LEVEL:-info}" init: true + restart: unless-stopped logging: driver: json-file options: diff --git a/docs/deployment.md b/docs/deployment.md index 5458a69..dfbb3f1 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -58,6 +58,15 @@ mit Laufzeit und Speicherverbrauch – nützlich, um Speicherlecks oder Hänger einem 502 über einen längeren Zeitraum nachzuvollziehen. Für die Detailsuche `LOG_LEVEL=debug` setzen; das protokolliert zusätzlich den Start jeder API-Anfrage und macht damit hängende (nie abgeschlossene) Requests sichtbar. +Ein `close`-Ereignis ohne vorheriges `finish` wird als `request aborted before +response finished` (`warn`) geloggt und zeigt damit vom Client oder einem +vorgeschalteten Proxy abgebrochene Verbindungen. + +Eine unbehandelte Exception oder Promise-Rejection wird geloggt und beendet +den jeweiligen Prozess anschließend bewusst (`process.exit(1)`), statt in +einem unbekannten Zustand weiterzulaufen. Beide Dienste laufen deshalb mit +`restart: unless-stopped`, damit Docker sie danach automatisch neu startet; +ohne diese Policy würde ein Crash den Dienst dauerhaft unerreichbar lassen. ## Voraussetzungen für ein späteres Produktionssetup diff --git a/src/instrumentation-node.ts b/src/instrumentation-node.ts index 0549375..760f6eb 100644 --- a/src/instrumentation-node.ts +++ b/src/instrumentation-node.ts @@ -7,11 +7,13 @@ export function registerNodeInstrumentation() { logger.info("web server starting", { pid: process.pid }); process.on("uncaughtException", (error) => { - logger.error("uncaught exception", toErrorMeta(error)); + logger.error("uncaught exception, exiting", toErrorMeta(error)); + process.exit(1); }); process.on("unhandledRejection", (reason) => { - logger.error("unhandled rejection", toErrorMeta(reason)); + logger.error("unhandled rejection, exiting", toErrorMeta(reason)); + process.exit(1); }); setInterval(() => { diff --git a/src/proxy.ts b/src/proxy.ts index 9b301d8..4607ec0 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -5,10 +5,14 @@ import { createLogger } from "./shared/logging/logger"; const logger = createLogger("web:navigation"); export function proxy(request: NextRequest) { - logger.info("page request", { - method: request.method, - path: request.nextUrl.pathname, - }); + // The Docker healthcheck hits "/" every few seconds with no User-Agent + // header; skip it so real navigation isn't drowned out in the logs. + if (request.headers.get("user-agent")) { + logger.info("page request", { + method: request.method, + path: request.nextUrl.pathname, + }); + } return NextResponse.next(); } diff --git a/src/server/index.ts b/src/server/index.ts index 851276a..fa3a74e 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -31,6 +31,15 @@ app.use((req, res, next) => { else if (res.statusCode >= 400) logger.warn("request completed", meta); else logger.info("request completed", meta); }); + res.on("close", () => { + if (!res.writableEnded) { + logger.warn("request aborted before response finished", { + method: req.method, + path: req.originalUrl, + durationMs: Date.now() - startedAt, + }); + } + }); next(); }); @@ -46,11 +55,13 @@ app.use("/api/project-devices", projectDeviceRouter); app.use(errorMiddleware); process.on("uncaughtException", (error) => { - logger.error("uncaught exception", toErrorMeta(error)); + logger.error("uncaught exception, exiting", toErrorMeta(error)); + process.exit(1); }); process.on("unhandledRejection", (reason) => { - logger.error("unhandled rejection", toErrorMeta(reason)); + logger.error("unhandled rejection, exiting", toErrorMeta(reason)); + process.exit(1); }); process.on("SIGTERM", () => logger.info("received SIGTERM")); diff --git a/src/shared/logging/logger.ts b/src/shared/logging/logger.ts index 423a7ad..38afb9d 100644 --- a/src/shared/logging/logger.ts +++ b/src/shared/logging/logger.ts @@ -47,13 +47,19 @@ export function createLogger( if (LEVEL_SEVERITY[level] > LEVEL_SEVERITY[threshold]) { return; } - const line = JSON.stringify({ - timestamp: new Date().toISOString(), - level, - scope, - message, - ...meta, - }); + const timestamp = new Date().toISOString(); + let line: string; + try { + line = JSON.stringify({ timestamp, level, scope, message, ...meta }); + } catch { + line = JSON.stringify({ + timestamp, + level, + scope, + message, + logError: "failed to serialize log metadata", + }); + } if (level === "error" || level === "warn") { console.error(line); } else {