forked from jappel/leistungsbilanz-ts
Fix crash handling and log noise in logging feature
Review of the logging work from the previous commit turned up a real correctness issue: the uncaughtException/unhandledRejection handlers logged the error but let the process keep running, which silently disabled Node's default crash-on-fatal-error behavior and could leave a zombie process serving broken requests instead of restarting. Both processes now log and then exit(1); restart: unless-stopped is added to both services so Docker actually brings them back up. Also: harden the logger against JSON.stringify throwing on non-serializable meta, log aborted (closed-before-finished) requests in the API access log, and stop the Next.js proxy from logging the Docker healthcheck's request to "/" every few seconds so real navigation events aren't drowned out. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
ea3c02cd6b
commit
fa96be2d42
6 changed files with 49 additions and 15 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -5,10 +5,14 @@ import { createLogger } from "./shared/logging/logger";
|
|||
const logger = createLogger("web:navigation");
|
||||
|
||||
export function proxy(request: NextRequest) {
|
||||
// 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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
|
|
|||
|
|
@ -47,13 +47,19 @@ export function createLogger(
|
|||
if (LEVEL_SEVERITY[level] > LEVEL_SEVERITY[threshold]) {
|
||||
return;
|
||||
}
|
||||
const line = JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
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,
|
||||
...meta,
|
||||
logError: "failed to serialize log metadata",
|
||||
});
|
||||
}
|
||||
if (level === "error" || level === "warn") {
|
||||
console.error(line);
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue