Skip to content

fix(telemetry): namespace tool_call span under :anubis_mcp - #244

Merged
zoedsoupe merged 1 commit into
zoedsoupe:mainfrom
acollado-g2:fix/tool-call-telemetry-namespace
Jul 29, 2026
Merged

zoedsoupe merged 1 commit into
zoedsoupe:mainfrom
acollado-g2:fix/tool-call-telemetry-namespace

Conversation

@acollado-g2

Copy link
Copy Markdown
Contributor

Summary

Fixes #243. Anubis.Server.Session's tool_call telemetry span was firing under [:server, :tool_call, :start | :stop | :exception] — without the :anubis_mcp namespace prefix every other server-side telemetry event in the library gets.

Root cause

Every telemetry call site in lib/anubis/server/session.ex goes through Anubis.Telemetry.execute/3, which prepends the prefix:

def execute(event_name, measurements, metadata) do
  :telemetry.execute([:anubis_mcp | event_name], measurements, metadata)
end

Anubis.Telemetry.event_server_tool_call/0 — like every sibling event_* function — is documented as returning "the event name, excluding the :anubis_mcp prefix", i.e. meant to be passed through execute/3. The one :telemetry.span/3 call wrapping tools/call, however, passed this value straight to :telemetry.span/3, bypassing execute/3 entirely — so the real events fired were unprefixed.

Fix

One-line change: prepend :anubis_mcp to the span's event prefix, matching every other event in the file.

:telemetry.span(
  [:anubis_mcp | Telemetry.event_server_tool_call()],
  %{tool: tool_name},
  fn -> {module.handle_request(request, frame), %{tool: tool_name}} end
)

Testing

  • Added a regression test in 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 real tools/call dispatch through a live Session.
  • Verified the new test fails without the fix (reverted the one-line change locally, test failed with "no matching message after 1000ms" — the unprefixed event doesn't match) and passes with it.
  • Full suite: 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.

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.
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9cc4b249-7017-42c7-8062-3d5e5772562c

📥 Commits

Reviewing files that changed from the base of the PR and between 2451bb7 and c9a3219.

📒 Files selected for processing (2)
  • lib/anubis/server/session.ex
  • test/anubis/server/session_async_dispatch_test.exs

📝 Walkthrough

Problem

Tool-call telemetry events lacked the :anubis_mcp namespace.

Solution

Prefix tool_call span events with :anubis_mcp.

Rationale

Aligns telemetry with the documented convention and adds regression coverage for the namespaced :stop event.

Walkthrough

Anubis.Server.Session now prefixes tools/call telemetry spans with :anubis_mcp. A test attaches a handler for the namespaced stop event, executes an "echo" tool call, and verifies the duration and tool metadata before detaching the handler.

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is complete, but it does not use the required Problem/Solution/Rationale template headings. Rewrite the PR description using the repository template sections: Problem, Solution, and Rationale, with the key details under each.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the telemetry namespace fix.
Linked Issues check ✅ Passed The change and regression test directly address issue #243 by namespacing the tool_call telemetry span.
Out of Scope Changes check ✅ Passed The only code and test changes are tightly scoped to the telemetry namespace fix and its regression coverage.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified 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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

zoedsoupe pushed a commit that referenced this pull request Jul 29, 2026
…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.
@zoedsoupe
zoedsoupe merged commit 561a96b into zoedsoupe:main Jul 29, 2026
13 checks passed
@zoedsoupe zoedsoupe mentioned this pull request Jul 29, 2026
zoedsoupe added a commit that referenced this pull request Jul 29, 2026
🚀 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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tool_call telemetry span missing :anubis_mcp namespace prefix (inconsistent with all other server events)

2 participants