fix(#50): durable vLLM logs that survive restart + model logs - #51
Conversation
A vLLM EngineCore crash (issue #50) took the engine down, and by the time it was looked at a restart had wiped `docker logs` — so the crash trace was gone and the root cause could not be investigated for lack of data. This closes that observability gap (recovery/auto-restart is intentionally out of scope here). `model init` now scaffolds `mg-logwrap.sh`, bind-mounted as each vLLM service's entrypoint. It tees stdout+stderr to a per-boot file `<service>-<boot>.log` under a host-mounted log dir (`${MODEL_GEAR_LOG_DIR:-<deploy>/logs}` -> `/logs/model-gear`), then `exec`s the real command so vLLM stays the signal target (graceful SIGTERM) and the exit code / `restart:` policy are unchanged. Teeing at the process-I/O level captures BOTH python tracebacks AND native CUDA/C++ aborts; it falls back to a plain exec if logging can't be set up, so it never blocks serving. The crash boot is preserved as its own file across the restart that follows it. New read-only verb `model logs` reads the host files directly (works even after the container is gone): list boots, tail the latest, or `--previous` to tail the boot that crashed. Wired into the single-model and fleet (primary/embed/rerank) templates; init/serve/fleet-up pre-create the log dir user-owned. OTEL was evaluated and rejected for crash logs: vLLM's OTLP support is traces-only and a crash traceback is not a span; file capture is the right tool. See docs/durable-logs.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NnDfFkZkXz8C68hr3AA9Qa
PR Summary by Qodofix(#50): Durable vLLM logs that survive restart + Description
Diagram
High-Level Assessment
Files changed (20)
|
Code Review by Qodo
Context used✅ Compliance rules (platform):
35 rules✅ Skills:
|
SonarCloud:
- logs.py `cmd_logs`: extract `_emit_tail` / `_emit_listing` so it has a single
return (S3516) and its cognitive complexity drops from 18 to under 15 (S3776).
- _compose.py: use the `LOG_WRAPPER` constant for the template dict keys instead
of repeating the "mg-logwrap.sh" literal (S1192).
Colleague review:
- Set `MG_LOG_DIR=/logs/model-gear` explicitly in each vLLM service's
`environment:` so the in-container log path can't drift from the volume mount
and can't be silently mis-overridden.
- Comment the two `exec` uses in mg-logwrap.sh (redirect-only vs replace-shell).
- `model logs <svc> --previous` with only one boot now says "(only 1 boot —
showing latest)" instead of silently showing the latest as if it were the
crashed boot; add `only_boot` to the JSON.
- Add a drift-guard test linking the compose `${MODEL_GEAR_LOG_DIR:-./logs}`
default + `/logs/model-gear` mount to the Python `LOG_DIRNAME`.
322 tests pass; black/isort/flake8 clean; both composes pass `docker compose config`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NnDfFkZkXz8C68hr3AA9Qa
…ogs (Qodo) Qodo flagged that collect_logs() used is_file()/stat() (which follow symlinks) and tail_lines() then opened the path, so a symlink planted in the log dir and named like a boot log (e.g. vllm-x.log -> /etc/shadow) would be listed and tailed. mg-logwrap only ever writes regular per-boot files, so: - collect_logs(): skip any symlink (also covers the <service>-latest.log pointer). - tail_lines(): refuse to read through a symlink as defense in depth. - Tests for both: a planted symlink is neither listed nor read. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NnDfFkZkXz8C68hr3AA9Qa
|
Addressed the remaining review + SonarCloud findings: Qodo bug #2 — symlink traversal (security) → fixed in SonarCloud (3) → fixed in
Colleague (different-model) review folded into 324 tests pass; black/isort/flake8/bandit clean; both composes pass
|
|



What & why
A vLLM EngineCore crash (#50)
took the engine down on a tool-calling request, and by the time it was looked
at, a restart had wiped
docker logs— so the crash trace was gone and the rootcause could not be investigated for lack of data. This PR closes that
observability gap so the next crash leaves a durable, readable trace.
Per the issue owner's steer, auto-restart/autoheal is intentionally out of
scope here — the blocker was losing the data, not the recovery. Pinning the
EngineCore root cause (MTP speculative decoding + tools vs FP4) needs a controlled
repro with the durable trace in hand; this PR is that prerequisite.
How
model initnow scaffoldsmg-logwrap.sh, bind-mounted as each vLLMservice's entrypoint (the
command:arg list is unchanged). It:stdout+stderrto a per-boot file<service>-<boot>.logunder ahost-mounted log dir (
${MODEL_GEAR_LOG_DIR:-<deploy>/logs}→/logs/model-gear)and passes them through to the console (so
docker logsstill works);execs the real command, so vLLM stays the signal target (gracefuldocker stop) and the exit code /restart:policy are unchanged` (verified:exit code propagates through the wrapper);
below Python logging);
exec "$@"if logging can't be set up — it never blocksserving.
The crash boot is preserved as its own file across the restart that follows
it. Wired into the single-model and fleet (
primary/embed/rerank) composetemplates;
init/serve/fleet uppre-create the log dir user-owned so itis never created root-owned by the bind mount.
New verb:
model logs(read-only)Reads the host files directly, so it works even after the crashed container is
gone (
docker logswould not):OTEL — evaluated, rejected for crash logs
vLLM's OpenTelemetry support is traces-only (
--otlp-traces-endpoint); it hasno native OTLP log export, and a crash traceback is not a span (the engine
dies). OTEL log capture would need a Collector +
filelogsidecar reading the samestderr— new infra for no gain over a host file. So crash durability is done atthe file level; OTEL traces remain a future opt-in for request observability.
Details in
docs/durable-logs.md.Tests / validation
tests/test_cli_logs.pycoverscollect_logs/tail_lines,--previouscrash-boot recovery, JSON, empty/unknown-service paths;test_initextended for the scaffolded wrapper + per-service
MG_LOG_NAME.mg-logwrap.shverified against a throwaway localbashrun: capturesstdout+stderr, propagates the exit code, per-boot file +
latestsymlink, andthe unwritable-dir fallback still execs.
docker compose configwith the new entrypoint/volumes.Closes #50.
🤖 Generated with Claude Code