/** * Structured fatal-exit logging for the dev server. * * The most common cause of live-edit pod deaths is a bare Node exit code 1 with * no reason attached: the supervisor only sees the container exit code from * Kubernetes, and the gVisor runtime reports no CPU/memory/event-loop metrics, * so there is no way to tell why the process stopped. These helpers build one * log line, easy to search for, that says why the process is exiting. It is * written right before the process exits so the reason still lands in the pod * logs even when the batched telemetry export never gets flushed. * * The fields go in the message body as `key=value` markers instead of as * telemetry attributes, on purpose: at exit time the synchronous stdout write is * the only sink we can rely on, while the batched OTel log exporter is usually * dropped by `process.exit`. The existing warm-standby failure path in * dev-server.mts already puts its markers in the body for the same reason. And * for the same reason there is deliberately no OTel counter for fatal exits (a * counter increment would be dropped at exit); `FATAL_EXIT_EVENT` below is the * log token to query and alert on. * * Counting note for alerting: one crash can produce up to two of these lines. * When a JS handler runs and its own `process.exit(1)` runs before shutdown can * exit, the child emits a `handler=uncaughtException`/`unhandledRejection` line * AND the supervisor (CLI parent, or the warm entrypoint shell) emits a * `handler=containerExit` line for the same exit. A signal kill / OOM emits only * the `containerExit` line (no JS handler ran); a JS crash where shutdown exits * first with code 0 emits only the JS-handler line (the supervisor sees a clean * exit). A deliberate startup exit emits `handler=startupFailure` from the child * plus the supervisor's `containerExit` line, i.e. the same double shape. So a * raw `count(event=dev_server_fatal_exit)` over-counts the double case: * deduplicate by pod within a short time window for a true unique-crash count. * * Signal-kill field note: for a signal kill the cold parent emits * `exit_code=null signal=SIGKILL` (Node's `child.on("exit")` gives a null code + * the signal), while the warm entrypoint shell emits `exit_code=128+N * signal=SIGKILL` (its `wait` returns the raw 128+N status). Both are correct * for their context; query on the `signal` field, not `exit_code`, for * signal-kill alerts. */ /** * Where the fatal exit came from, so each kind can be told apart in the logs. * * uncaughtException / unhandledRejection A JS handler ran and knows the error * name/message/stack. * containerExit No JS handler ran (signal kill / OOM, * or the graceful-shutdown exit race); * the supervisor only knows the code + * signal. The CLI parent (dev-parent.mts) * and the warm entrypoint shell emit this * value. They cannot import this module, * so they repeat the same string. * startupFailure `dev()` decided at startup that it * cannot serve (sync/lock/setup failure, * or a dependency upgrade that may have * tainted the live tree in a disposable * sandbox) and is exiting deliberately. * No JS error handler is involved; the * adjacent `[dev-server] Startup failed * during …` line names the stage. * * Adding a VALUE here (as `startupFailure` was) needs no change in the two * emitters that cannot import this module — they each emit their own fixed * value. Only adding a FIELD does; see the SYNC note on `buildFatalExitLog`. */ type FatalExitHandler = "uncaughtException" | "unhandledRejection" | "containerExit" | "startupFailure"; /** Fixed token so a log-based metric or a log search can pick out these lines. */ export declare const FATAL_EXIT_EVENT = "dev_server_fatal_exit"; interface FatalExitFields { /** Where the fatal exit came from. */ handler: FatalExitHandler; /** * Exit code the process is about to use (JS handler path) or the code the * supervisor saw (container-exit path). `null` when the child was killed by a * signal and has no exit code. */ exitCode: number | null; /** Signal that killed the process; absent for a JS-level exit. */ signal?: string | null; /** Whether this pod was started as a warm-standby pod (entrypoint `--warm`). */ isWarm: boolean; } /** * Parse the entrypoint's warm-standby env var the same way * `scripts/dev-server-entrypoint.sh` does, so `is_warm` in the log matches the * pod's launch mode whether or not it was activated later. */ export declare function parseIsWarm(value: string | undefined): boolean; /** * Build the fatal-exit log message body. Every value is a simple primitive * (enum / number / boolean), so the line is safe to write as-is even on the CSB * log path. * * SYNC: this format is reproduced in two places that cannot import this module — * `buildFatalExitLog` in `packages/cli/packages/cli/src/commands/dev-parent.mts` * (the parent stays on `node:` builtins) and the `echo` line in * `scripts/dev-server-entrypoint.sh` (bash). A field added here must be added in * both, or `event=dev_server_fatal_exit` queries will miss one emitter. */ export declare function buildFatalExitLog(fields: FatalExitFields): string; export declare function isRecoverableShutdownError(reason: unknown): boolean; /** * Vite/esbuild build failures that should keep the dev-server process alive and * surface through `devServerStatus.serverErrors` instead of `process.exit(1)`. * * Deliberately narrow: anything not positively identified as a build failure * must stay fatal, so genuine crashes still exit and emit * `dev_server_fatal_exit`. Rollup/Vite plugin errors are matched by their * message rather than by structural fields like `id`/`plugin`/`loc`, which are * far too common to be evidence of a build failure on their own. */ export declare function isViteBuildError(reason: unknown): boolean; /** Best-effort text extraction for Vite/esbuild rejection reasons. */ export declare function extractViteBuildErrorMessage(reason: unknown): string; export {}; //# sourceMappingURL=fatal-exit.d.mts.map