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"
|
CHOKIDAR_USEPOLLING: "true"
|
||||||
LOG_LEVEL: "${LOG_LEVEL:-info}"
|
LOG_LEVEL: "${LOG_LEVEL:-info}"
|
||||||
init: true
|
init: true
|
||||||
|
restart: unless-stopped
|
||||||
logging:
|
logging:
|
||||||
driver: json-file
|
driver: json-file
|
||||||
options:
|
options:
|
||||||
|
|
@ -53,6 +54,7 @@ services:
|
||||||
NEXT_TELEMETRY_DISABLED: "1"
|
NEXT_TELEMETRY_DISABLED: "1"
|
||||||
LOG_LEVEL: "${LOG_LEVEL:-info}"
|
LOG_LEVEL: "${LOG_LEVEL:-info}"
|
||||||
init: true
|
init: true
|
||||||
|
restart: unless-stopped
|
||||||
logging:
|
logging:
|
||||||
driver: json-file
|
driver: json-file
|
||||||
options:
|
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
|
einem 502 über einen längeren Zeitraum nachzuvollziehen. Für die Detailsuche
|
||||||
`LOG_LEVEL=debug` setzen; das protokolliert zusätzlich den Start jeder
|
`LOG_LEVEL=debug` setzen; das protokolliert zusätzlich den Start jeder
|
||||||
API-Anfrage und macht damit hängende (nie abgeschlossene) Requests sichtbar.
|
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
|
## Voraussetzungen für ein späteres Produktionssetup
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,13 @@ export function registerNodeInstrumentation() {
|
||||||
logger.info("web server starting", { pid: process.pid });
|
logger.info("web server starting", { pid: process.pid });
|
||||||
|
|
||||||
process.on("uncaughtException", (error) => {
|
process.on("uncaughtException", (error) => {
|
||||||
logger.error("uncaught exception", toErrorMeta(error));
|
logger.error("uncaught exception, exiting", toErrorMeta(error));
|
||||||
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on("unhandledRejection", (reason) => {
|
process.on("unhandledRejection", (reason) => {
|
||||||
logger.error("unhandled rejection", toErrorMeta(reason));
|
logger.error("unhandled rejection, exiting", toErrorMeta(reason));
|
||||||
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,14 @@ import { createLogger } from "./shared/logging/logger";
|
||||||
const logger = createLogger("web:navigation");
|
const logger = createLogger("web:navigation");
|
||||||
|
|
||||||
export function proxy(request: NextRequest) {
|
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", {
|
logger.info("page request", {
|
||||||
method: request.method,
|
method: request.method,
|
||||||
path: request.nextUrl.pathname,
|
path: request.nextUrl.pathname,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,15 @@ app.use((req, res, next) => {
|
||||||
else if (res.statusCode >= 400) logger.warn("request completed", meta);
|
else if (res.statusCode >= 400) logger.warn("request completed", meta);
|
||||||
else logger.info("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();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -46,11 +55,13 @@ app.use("/api/project-devices", projectDeviceRouter);
|
||||||
app.use(errorMiddleware);
|
app.use(errorMiddleware);
|
||||||
|
|
||||||
process.on("uncaughtException", (error) => {
|
process.on("uncaughtException", (error) => {
|
||||||
logger.error("uncaught exception", toErrorMeta(error));
|
logger.error("uncaught exception, exiting", toErrorMeta(error));
|
||||||
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on("unhandledRejection", (reason) => {
|
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"));
|
process.on("SIGTERM", () => logger.info("received SIGTERM"));
|
||||||
|
|
|
||||||
|
|
@ -47,13 +47,19 @@ export function createLogger(
|
||||||
if (LEVEL_SEVERITY[level] > LEVEL_SEVERITY[threshold]) {
|
if (LEVEL_SEVERITY[level] > LEVEL_SEVERITY[threshold]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const line = JSON.stringify({
|
const timestamp = new Date().toISOString();
|
||||||
timestamp: new Date().toISOString(),
|
let line: string;
|
||||||
|
try {
|
||||||
|
line = JSON.stringify({ timestamp, level, scope, message, ...meta });
|
||||||
|
} catch {
|
||||||
|
line = JSON.stringify({
|
||||||
|
timestamp,
|
||||||
level,
|
level,
|
||||||
scope,
|
scope,
|
||||||
message,
|
message,
|
||||||
...meta,
|
logError: "failed to serialize log metadata",
|
||||||
});
|
});
|
||||||
|
}
|
||||||
if (level === "error" || level === "warn") {
|
if (level === "error" || level === "warn") {
|
||||||
console.error(line);
|
console.error(line);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue