fix(observability): make the engine's log routing correct and race-free - #1028
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes an observability gap in the Spirit engine slog filter so engine INFO+ lines are always recorded into the apply log stream even when the deployment’s process log level would otherwise filter them out, while still preserving the deployment’s configured verbosity for stdout/stderr logs.
Changes:
- Adjust
spiritLogFilter.Enabledto return true when either the apply log stream or the process logger would consume the record. - Prevent “apply-log-only” records from being emitted to the process logs when the handler’s level would reject them.
- Expand unit tests to distinguish apply-log routing vs process-log emission.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| pkg/engine/spirit/logger.go | Updates slog handler filtering so apply-log routing is independent of process log level and avoids increasing deployment log volume. |
| pkg/engine/spirit/logger_test.go | Adds tests covering routing below process log level and the “no callback installed” case; updates logger setup helper. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…ss log level The Spirit log filter serves two consumers — the apply log stream and the process logs — but answered Enabled for only the second. slog skips building the record entirely when a handler reports false, so a deployment running its own logs above info recorded no engine lines at all for any apply: no copy progress, no checksum lines, nothing in a failed apply's summary comment. The apply log stream an operator reads was emptied by a setting about stdout. Each consumer now decides for itself. A line is built when either wants it, routed to the stream when an apply is being driven, and emitted to the process logs only at the level that deployment configured — so an apply cannot raise a deployment's log volume, and a quiet deployment cannot silence an apply.
e73df56 to
ee49d5f
Compare
…tomically The Spirit log filter read the engine's onLog callback and debugLogs flag through raw pointers while engine methods wrote them under the engine mutex. A drive that unwires its apply-log callback while a runner is still logging raced with the filter's read, and the callback's check-then-call window could dereference a cleared slot. Both now live in atomic slots, and the filter loads the callback once per record and calls through that copy. Atomics rather than the engine mutex: the filter runs on the Spirit runner's logging path, where taking the engine lock would invite reentrancy against the engine methods that already hold it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ee49d5f to
ead7fe5
Compare
|
🤖 Review findings - created by Kiran's code review agent - for pull/1028, ead7fe5. Verdict: 3 findings — all general suggestions (no blocking/non-blocking); 31/31 CI checks pass at head ead7fe5 (Build, Unit Tests, Integration Tests, Lint, all E2E/LocalScale variants), and local repro of General suggestions
The one thing that could have broken, verifiedThe riskiest mechanism is the load-once-then-call pattern in Verified correct
This review was generated by Claude Code (claude-fable-5). |
…-line floor Adds an engine-level test for the runtime debug toggle — the only consumer of a debug line is the process logs, so turning it on admits those lines and turning it off stops them, while the apply log stream records none of them. The concurrent unwiring test now routes one line before the race starts, so its upper bound cannot be satisfied by a filter that never calls through its callback slot at all.
|
🤖 Thanks — all three addressed or answered in 1. Loose assertion in the concurrency test — agreed, fixed, but not quite the way you suggested. You're right that 2. No test for debug-toggle delivery — added. 3. Title — already covers both fixes. The title was renamed to "make the engine's log routing correct and race-free" on Aug 15, two days before this review ran, so the review was reading the original "record the engine's log lines regardless of process log level". "Routing correct" is the This reply was generated by Claude Code (claude-opus-5). |
Why this matters
The apply log stream is what an operator reads when a schema change goes wrong — from the CLI, and from the summary comment on the PR. Most of it comes from the engine: copy progress, checksum results, the fatal line. Two bugs sat between those lines and the stream.
The callback was read unsynchronized. The filter read the engine's log callback through a raw pointer while engine methods wrote it under the engine mutex. A finished drive unwires that callback, and a Spirit runner winding down is still logging through the same filter — so the write races the read on every apply, and the check-then-call window can dereference a slot cleared in between.
The process log level decided for both consumers. The filter feeds two: the apply log stream and the process logs. It answered
Enabledfor only the second, and slog skips building the record entirely when a handler says no — so on a process logging above info, the routing never ran and the apply recorded nothing from the engine.Nothing runs above info today, so no stream is empty right now. The trap is that turning the level up, the ordinary way to cut noise, silently empties the surface operators triage from. It is worse for an embedder, whose own logger sets that threshold outside SchemaBot entirely.
What it does
The callback and debug toggle move into atomic slots, and the filter loads the callback once per record and calls through that copy. Atomics rather than the engine mutex: the filter runs on Spirit's logging path, where taking the engine lock would invite reentrancy.
Each consumer then decides for itself. A record is built if either wants it, routed to the apply log stream whenever an apply is being driven, and passed to the process logs only at their configured level. That second part is load-bearing — widening
Enabledalone would push every driven apply's info lines to a deployment's stdout, trading lost signal for unasked-for volume. Debug lines stay the process logs' call alone.🤖 Generated with Claude Code