ward-mcp containers ship no logs: 1 line in 12h across every ward-mcp pod on the fleet #78
Labels
No labels
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
coherence-core
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
qa-fixture
role/ai
role/creator
role/design
role/director
role/engineer
role/exec
role/human
role/ops
role/qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-flight-deck/mcp-beaver#78
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
Generated
ward-mcpcontainers emit essentially nothing to stdout or stderr. Over 12h on ser8 SigNoz (2026-08-16 13:08Z to 2026-08-17 01:08Z), grouping all logs wherek8s.container.name = "ward-mcp"by namespace returns exactly one row:aws-ssm-mcp- 1 log lineThat is one line, fleet-wide, across every ward-mcp pod in every namespace.
Scale of the silence
The two sirens namespaces alone run 22 MCP pods, all
Running, allrestart_count 0, all roughly 19h old:sirens-deep- bluesky, demo-discord, exa, forgejo, gbif, gutendex, openlibrary, playwright, steam-storefront, steam-web-api, tvmazesirens-echo- discord, exa, forgejo, gbif, gutendex, openlibrary, playwright, steam-storefront, steam-web-api, tvmazeNone of them contributed a single log line. Grouping the
sirens-deepnamespace by container returns onlysirens-echo(the app) andts(the tailscale sidecar).Why this is the image and not collection
The otel-agent collects
/var/log/pods/*node-wide and demonstrably works in these exact namespaces: it picks up the app containers and everytssidecar in the same pods. A ward-mcp container sitting beside atscontainer that is being collected rules out a namespace or collector-scope gap. The containers are silent at the source.Flagging the alternative for completeness: if ward-mcp does write logs and something downstream discards them, this belongs in coilyco-bridge/deploy instead. The evidence above points at the image, but I have not read the ward-mcp source.
Why it matters
These MCPs fail. Over the same window, as measured from the caller side in sirens-deep and sirens-echo:
Every one of those failures had to be inferred from client spans, because the server that actually knows what went wrong says nothing. Playwright rejecting
browser_navigatein 16 milliseconds is a server-side decision, and there is no server-side record of it anywhere.Ask
Have generated ward-mcp servers log at minimum: startup and bound config, each tool call with its outcome, and every refusal or validation rejection with its reason. Structured JSON would be ideal, since the ser8 ingest already promotes JSON bodies and maps
levelonto OTel severity (10-json-bodyin deployservices/signoz-pipelines/), so JSON output gets correct severity with no new parser.Filed from an ops investigation into 12h of sirens-echo / sirens-deep errors. Related: the caller-side span-status gap is coilyco-gaming/sirens-echo#873, and the demo-discord failure rate is filed against coilyco-bridge/deploy.
Fixed on
mainin9911c12. Your read was right and it was the image, not collection.What the source said
Three
fmt.Fprintf(os.Stderr, ...)calls in the whole tree - one startup banner per serve mode - and nothing anywhere per call. That is the one line you measured, exactly. The startup banners fired at pod start ~19h ago, outside your 12h window, which is why even those were absent.What lands
JSON to stderr through
log/slog, one object per line, so the ingest promotes the body and mapslevelonto OTel severity with no new parser - as you suggested.Real output from a server built off this commit:
Against your three asks:
tool,outcome,duration_ms, plustrace_idandspan_idwhen a span is active, so a line joins to the trace you were forced to infer from. Applied atregisterToolrather than inNew, so grants, the info tool, withheld stubs, the SSM readers and the upstream proxy are covered by construction rather than by remembering.outcome=tool_errorat WARN. A handler failure isoutcome=handler_errorat ERROR. The Playwright case you named - a server-side rejection in 16ms with no server-side record - now writes a line.One thing that needs your eye, because it moves a boundary
docs/FEATURES.mdrecords that upstream URLs are never captured, and a refusal reason embeds one. Silently logging the raw error would have widened that boundary without saying so, and reason-with-no-detail would not have answered this issue.The line I drew: a reason keeps each URL's scheme, host and path and drops its query, marked
?<redacted>rather than silently gone. That is not arbitrary -pinwrites query parameters and only query parameters, andauthwrites a header, so dropping the query removes exactly the surfaces a credential can reach, while a 404 stays attributable to an endpoint. The exception is written into the Safe attributes entry rather than left implicit.Narrowed in the other direction while I was there: the upstream proxy's startup line no longer prints the upstream URL at all. The ask did not need it, and it was the one place a URL was already going to stdout unredacted.
Reasons are bounded at 512 characters, verified against reddit - whose 403 is a full HTML page that would otherwise have gone into one log line.
MCP_BEAVER_LOG_LEVELtakesdebug|info|warn|error, defaulting to info.What this does not do
It does not explain your four failing servers. It makes the next twelve hours legible rather than retroactively explaining the last twelve. Worth re-running your SigNoz query once these images roll -
playwright 0 ok / 10 failedat 16.8ms p95 should now say why on the server side, andopenlibraryat 30.005s p95 looks like the 60s request bound being hit from the caller's side rather than a refusal at all.