When a vLLM container restarts or is recreated, its docker logs are gone. That
is exactly how the EngineCore crash trace in
issue #50 vanished before
anyone could read it: the server 500'd on a tool-calling request, the engine went
down, and by the time the box was looked at, a restart had wiped the logs — so
the root cause could not be investigated for lack of data.
lobes fixes the observability gap, not (yet) the crash itself: it makes each vLLM service's output durable so the next crash leaves a trace you can read. Pinning the EngineCore root cause (MTP speculative decoding + tools vs FP4) needs a controlled repro with that durable trace in hand — durable logs is the prerequisite that unblocks it.
lobes init scaffolds mg-logwrap.sh next to docker-compose.yml. Each
vLLM service bind-mounts it as the container entrypoint, so the real
command: (the vllm serve … arg list, unchanged) arrives at the wrapper as
"$@". The wrapper:
- opens a per-boot file
<service>-<ISO8601>.logunder the host-mounted log dir and points<service>-latest.logat it; - tees
stdout+stderrto that file and passes them through to the console (sodocker logskeeps working too); execs the real command, so vLLM stays the signal target —docker stopstill drains it gracefully (SIGTERM reaches vLLM, not a shell), and the container's exit code is vLLM's (sorestart: unless-stoppedis unaffected).
Because it tees at the process-I/O level — below Python's logging — it captures
both Python tracebacks and native stderr aborts (CUDA / C++ / OOM),
which is the class of crash that most needs investigating and which Python-level
file logging would miss. If anything about logging fails (no log dir, read-only
mount, no bash), the wrapper falls back to a plain exec "$@" — logging can
never stop the model from serving.
| Host | In container | |
|---|---|---|
| Log dir | ${MODEL_GEAR_LOG_DIR:-<deploy>/logs} |
/logs/lobes |
| Single model | …/logs/vllm-<boot>.log |
/logs/lobes/vllm-<boot>.log |
| Fleet gears | …/logs/{primary,embed,rerank}-<boot>.log |
/logs/lobes/<svc>-<boot>.log |
The host dir is created (user-owned) by lobes init, lobes serve, and
lobes fleet up before compose bind-mounts it, so the logs are never
root-owned. Per-boot files mean the crash boot is preserved as its own file
and never overwritten by the restart that follows it.
lobes logs is read-only and reads the host files directly, so it works even
after the crashed container is gone (docker logs would not):
lobes logs # list per-boot files (newest first) + the log dir
lobes logs vllm # tail the latest boot for a service
lobes logs vllm --previous # tail the boot BEFORE the latest — i.e. the crashed
# boot, after a restart created a fresh healthy one
lobes logs primary -n 200 # more lines (fleet service)
lobes logs --json # structured listing
The --previous flag is the #50 investigation path: after a crash+restart, the
latest boot is the healthy one — --previous tails the boot that actually
crashed.
Per-boot files accumulate across restarts. They are plain files under the host log dir; prune old ones with a one-liner, e.g. keep the newest 20 per service:
ls -1t <deploy>/logs/vllm-*.log | tail -n +21 | xargs -r rmOpenTelemetry was considered first. vLLM's OTEL support is traces-only
(--otlp-traces-endpoint, request spans) — it has no native OTLP log export,
and a crash traceback is not a span (the engine dies), so OTEL tracing would not
capture the very thing #50 needs. Capturing logs via OTEL would require an OTEL
Collector + filelog receiver sidecar reading the same stderr plus a backend
to store it — significant new infrastructure for no gain over a host file. So
crash durability is done at the file level; OTEL traces remain a future
opt-in for request observability (latency/token spans), a separate concern from
crash logs.
- Wrapped: the vLLM generate/embed/rerank services (single-model
vllm, fleetprimary/embed/rerank). - Not changed: the model's serving flags/behaviour, the
restart:policy, or the healthcheck. No docker-socket mounts, no new runtime dependencies. Auto-restart / autoheal is intentionally out of scope here (see #50) — this PR makes the crash investigable; recovery is a separate decision.