fix(telemetry): namespace tool_call span under :anubis_mcp - #244
Conversation
The tools/call telemetry span passed Telemetry.event_server_tool_call() directly to :telemetry.span/3, bypassing Anubis.Telemetry.execute/3 — the one place that prepends the :anubis_mcp prefix every other server event gets. Consumers attaching to [:anubis_mcp, :server, :tool_call, ...] per the library's own documented convention silently never received it. Fixes zoedsoupe#243.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughProblemTool-call telemetry events lacked the SolutionPrefix RationaleAligns telemetry with the documented convention and adds regression coverage for the namespaced Walkthrough
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…tadata (#246) ## Problem The `tool_call` telemetry span (`do_handle_request/4` for `"tools/call"` requests) always emits the same `:stop` metadata, `%{tool: tool_name}`, regardless of whether the wrapped call succeeded, returned a protocol-level error (e.g. tool not found, invalid params), or returned a `CallToolResult` with `isError: true`. The actual result of `module.handle_request(request, frame)` is available inside the span function, but it is discarded before being turned into span metadata — a telemetry handler attached to this event has no way to tell a successful tool call apart from a failed one without independently re-deriving that information elsewhere (e.g. by separately handling `[:anubis_mcp, :server, :error]`, which only fires for protocol-level errors, not for tool-level `isError: true` results). ## Solution Bind the result of `module.handle_request(request, frame)` and derive an `is_error` boolean from it before it becomes span metadata: - `{:error, _reason, _frame}` (protocol-level error) → `is_error: true` - `{:reply, %{"isError" => true}, _frame}` (tool-level error via `Response.error/2`) → `is_error: true` - anything else → `is_error: false` Added three regression tests covering all three branches (successful call, tool-level `isError: true`, protocol-level tool-not-found), attached directly to the `tool_call` `:stop` telemetry event. ## Rationale This was the smallest possible change that surfaces the missing signal: the result is already computed and in scope inside the span closure, so no additional work is done, only a cheap pattern match on a value that already exists. No public API, return value, or behavior changes — this only adds a new key to telemetry metadata, which is additive and non-breaking for existing handlers. I kept the derivation as a private function (`tool_call_error?/1`) rather than inlining it, since the two failure shapes it distinguishes (protocol error vs. tool-level `isError`) are exactly the two failure modes the MCP spec itself defines, and a named predicate makes that mapping explicit rather than embedding it in the span call. Related to #243 (unprefixed event name) but independent — this PR does not touch the event namespace, only the `:stop` metadata shape. Happy to rebase on top of #244 if that merges first.
🚀 Want to release this? --- ## [1.11.0](v1.10.0...v1.11.0) (2026-07-29) ### Features * **session_store:** supervise Redis store subtree to make restarts race-free ([#242](#242)) ([48c8c1a](48c8c1a)) ### Bug Fixes * **prompts:** wrap prompt content objects and map system_message to user role ([#234](#234)) ([2451bb7](2451bb7)) * **server:** make title option compile in use Anubis.Server.Component ([b04feed](b04feed)) * **server:** use restart :temporary for session processes ([#240](#240)) ([30bc4f5](30bc4f5)) * **sse:** buffer partial events across Finch chunks ([#245](#245)) ([beea2f6](beea2f6)) * Stream the client SSE GET instead of buffering it (server push never delivered) ([#231](#231)) ([a722c1b](a722c1b)) * **telemetry:** expose tool call success/failure in tool_call span metadata ([#246](#246)) ([ace5ecb](ace5ecb)) * **telemetry:** include client_info in initialize response event metadata ([#248](#248)) ([74ed457](74ed457)) * **telemetry:** namespace tool_call span under :anubis_mcp ([#244](#244)) ([561a96b](561a96b)) ### Tests * **server:** fix stale tool_call event name and function_exported? loading races ([7247d2c](7247d2c)) * **transport:** synchronize held SSE plug with Bypass teardown ([93c5a34](93c5a34)) ### Continuous Integration * fix dialyzer plt caching ([8424439](8424439)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
Summary
Fixes #243.
Anubis.Server.Session'stool_calltelemetry span was firing under[:server, :tool_call, :start | :stop | :exception]— without the:anubis_mcpnamespace prefix every other server-side telemetry event in the library gets.Root cause
Every telemetry call site in
lib/anubis/server/session.exgoes throughAnubis.Telemetry.execute/3, which prepends the prefix:Anubis.Telemetry.event_server_tool_call/0— like every siblingevent_*function — is documented as returning "the event name, excluding the:anubis_mcpprefix", i.e. meant to be passed throughexecute/3. The one:telemetry.span/3call wrappingtools/call, however, passed this value straight to:telemetry.span/3, bypassingexecute/3entirely — so the real events fired were unprefixed.Fix
One-line change: prepend
:anubis_mcpto the span's event prefix, matching every other event in the file.Testing
test/anubis/server/session_async_dispatch_test.exs(describe "tool_call telemetry") attaching to[:anubis_mcp, :server, :tool_call, :stop]and asserting it fires on a realtools/calldispatch through a liveSession.mix test→ 941 tests, 35 doctests, 0 failures.mix lint(format --check-formatted, credo --strict, dialyzer) → clean.Impact
Any application instrumenting Anubis's native telemetry via the documented
[:anubis_mcp | ...]convention would silently receive zero tool-call metrics/spans while correctly receiving every other server event (request,response,error,init, etc) — no error, no warning, just a metric that never fires. Discovered while wiring Datadog metrics for tool-call latency in a downstream application.