Skip to content

feat(telemetry): expose tool call arguments/result in tools/call span (opt-in) - #281

Merged
zoedsoupe merged 4 commits into
zoedsoupe:mainfrom
acollado-g2:feature/opt-in-tool-call-payload-telemetry
Aug 10, 2026
Merged

zoedsoupe merged 4 commits into
zoedsoupe:mainfrom
acollado-g2:feature/opt-in-tool-call-payload-telemetry

Conversation

@acollado-g2

Copy link
Copy Markdown
Contributor

What

Adds tool call arguments and result to the [:anubis_mcp, :server, :tool_call] telemetry span metadata, gated behind a config flag that defaults to false.

Why

Anubis.Server.Session.Scheduler's tools/call handler already wraps execution in a :telemetry.span, but the span only ever carried the tool name and an error flag:

https://github.com/zoedsoupe/anubis-mcp/blob/main/lib/anubis/server/session/scheduler.ex#L184-L192

For anyone instrumenting a production MCP server, this means you can measure that a tool was called and whether it errored, but never what it was asked or what it returned — the two things that actually matter for debugging tool behavior or validating output quality over time. Both values are already in scope at the point the span is created (request["params"]["arguments"] and the computed result); they're just never surfaced.

This applies uniformly across protocol eras. I checked V2026_07_28 (the new stateless dialect) against the legacy version modules — tools/call keeps the same name/arguments param shape in both — and the scheduler's span sits above the era distinction in the call stack, so this single change covers legacy and stateless servers without duplication.

Why opt-in, not default-on

Tool arguments and results are the highest-cardinality, highest-PII surface in the request lifecycle. This matches where the spec ecosystem has already landed: OpenTelemetry's GenAI semantic conventions define gen_ai.tool.call.arguments / gen_ai.tool.call.result on both the generic execute_tool span and MCP's own mcp.server/mcp.client span types, and both are marked opt_in — OTel's strictest requirement level.

So this PR follows that same posture: default off, one config flag, integrator opts in with full knowledge of the tradeoff.

Changes

  • lib/anubis/server/session/scheduler.ex — do_handle_request/4 for "tools/call" now conditionally includes arguments in the span's start metadata and result in its stop metadata, controlled by Application.get_env(:anubis_mcp, :telemetry_capture_tool_payload, false).
  • test/anubis/server/session_test.exs — new describe block with two tests: default behavior is unchanged (no arguments/result in span metadata when the flag is unset), and with the flag enabled, :telemetry.attach_many captures arguments on span start and result on span stop. Built on the existing TasksStubServer/Session harness, same pattern as the adjacent tool_call telemetry :is_error metadata block.
  • pages/testing.md — new "Observing tool calls" section documenting the flag, an example :telemetry.attach, and the PII/

… (opt-in)

The tools/call telemetry span ([:anubis_mcp, :server, :tool_call]) only
ever carried the tool name and an is_error flag, never the arguments a
tool was called with or the result it returned. Integrators building
observability around a production MCP server could measure that a tool
was called and whether it errored, but never what it was asked or what
was returned.

Add a config flag, :telemetry_capture_tool_payload, defaulting to
false, that opts a server into carrying arguments in the span's start
metadata and result in its stop metadata. This mirrors the opt_in
requirement level OpenTelemetry's GenAI semantic conventions assign to
the equivalent gen_ai.tool.call.arguments / gen_ai.tool.call.result
span attributes -- tool payloads are the highest-cardinality,
highest-PII surface in the request lifecycle, so capture must be an
explicit choice, never a default.

Applies uniformly across protocol eras: tools/call keeps the same
name/arguments shape in both the legacy version modules and the new
2026-07-28 stateless dialect, and the scheduler's span sits above the
era distinction in the call stack.

Adds test coverage for both the default-off and opt-in-on paths, and
documents the flag in pages/testing.md alongside the PII rationale.
@github-actions github-actions Bot added the needs-work PR failed automated quality checks; maintainer review needed label Aug 10, 2026
@github-actions

Copy link
Copy Markdown

Thanks for the PR! Some automated quality checks didn't pass - see the
action logs above for specifics. A maintainer will still take a look.
Common fixes: use a conventional title (fix:, feat:, docs: ...),
fill in the Problem/Solution/Rationale template, and keep the diff focused.

@acollado-g2 acollado-g2 changed the title feat(telemetry): expose tool call arguments/result in tools/call span… feat(telemetry): expose tool call arguments/result in tools/call span (opt-in) Aug 10, 2026
@coderabbitai

coderabbitai Bot commented Aug 10, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@acollado-g2, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 10 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e006cfac-90ab-4370-876e-9881080bb92f

📥 Commits

Reviewing files that changed from the base of the PR and between 6e2625f and dcff11e.

📒 Files selected for processing (3)
  • lib/anubis/telemetry.ex
  • pages/testing.md
  • test/anubis/server/session_test.exs
📝 Walkthrough

Problem

Tool-call telemetry did not capture request arguments or handler results.

Solution

Add opt-in payload capture through :telemetry_capture_tool_payload. Use shared span logic for synchronous and task-augmented tool calls. Keep capture disabled by default.

Rationale

Improve tool-call observability while limiting PII exposure. Add tests and document configuration and privacy considerations.

Walkthrough

Tool-call execution now uses Telemetry.span_tool_call/3 for synchronous and asynchronous handlers. The wrapper emits standardized telemetry events with tool metadata and error status. When enabled, it captures tool arguments and results. Payload capture remains disabled by default. Tests cover both capture modes and asynchronous failures. Documentation describes the event, configuration, metadata, attachments, and data-sensitivity guidance.

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the opt-in exposure of tool call arguments and results in telemetry spans.
Description check ✅ Passed The description explains the problem, solution, rationale, configuration, tests, and documentation changes, although it uses alternate headings and appears truncated.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f0dd3dd2-d7b0-4c25-8520-4b0187e939c2

📥 Commits

Reviewing files that changed from the base of the PR and between 986fca6 and 1204f3f.

📒 Files selected for processing (3)
  • lib/anubis/server/session/scheduler.ex
  • pages/testing.md
  • test/anubis/server/session_test.exs

Comment thread lib/anubis/server/session/scheduler.ex Outdated
Comment thread lib/anubis/server/session/scheduler.ex Outdated
Comment thread pages/testing.md
Comment thread test/anubis/server/session_test.exs Outdated
Comment thread test/anubis/server/session_test.exs
- Instrument the task-augmented tools/call worker path
  (Session.Tasks.spawn_worker/6), which previously bypassed the
  tool_call span entirely. Extract the span logic shared by both the
  synchronous scheduler dispatch and the async task worker into
  Telemetry.span_tool_call/3, so there is one implementation instead
  of two diverging copies.
- Move the :telemetry_capture_tool_payload default into a module
  attribute (@default_capture_tool_payload), matching this codebase's
  existing convention for configurable defaults.
- Fix the pages/testing.md example: arguments only exists in :start
  metadata and result/is_error only in :stop metadata, so a single
  :stop-only handler pattern-matching on all three would never match.
  Attach to both events instead, and note the task-augmented path is
  covered too.
- Restore the telemetry_capture_tool_payload application env correctly
  in the opt-in test: Application.get_env/2 can't distinguish 'unset'
  from 'explicitly nil', so an unset key was being persisted as nil on
  exit instead of actually deleted. Use fetch_env/2 to capture the
  original state and delete_env/2 to restore an absent key.
- Add a dedicated test proving the task-augmented path now emits the
  same telemetry span as the synchronous path.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/anubis/server/session_test.exs (1)

804-805: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

P3 — Test the unset configuration path.

Line 805 sets the flag to false. The test therefore verifies explicit disablement, not the default when the key is absent. Delete the application key before the request.

Proposed fix
- Application.put_env(:anubis_mcp, :telemetry_capture_tool_payload, false)
+ Application.delete_env(:anubis_mcp, :telemetry_capture_tool_payload)

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9483988f-9a22-4a1f-a031-9b0af8e8fab7

📥 Commits

Reviewing files that changed from the base of the PR and between 1204f3f and 6e2625f.

📒 Files selected for processing (6)
  • lib/anubis/server/session/scheduler.ex
  • lib/anubis/server/session/tasks.ex
  • lib/anubis/telemetry.ex
  • pages/testing.md
  • test/anubis/server/session_test.exs
  • test/anubis/server/tasks_test.exs

Comment thread lib/anubis/telemetry.ex
Comment thread pages/testing.md
… testing.md

- Add a Examples section to span_tool_call/3's @doc, matching the
  convention used by other modules with iex> examples in this
  codebase. Kept simple (no Frame struct, no map literal) to avoid
  custom-inspect and key-ordering mismatches in the example output.
- pages/testing.md's :telemetry.attach_many example called
  Logger.info/2 without require Logger; since Logger.info is a macro,
  copying the example as-is would emit a compile warning. Add the
  require.
The default-behavior test set the flag to false rather than leaving it
unset, so it exercised explicit disablement instead of the true
default when no one has configured anything. Delete the application
env key instead.
@zoedsoupe
zoedsoupe merged commit 3cd6d58 into zoedsoupe:main Aug 10, 2026
12 checks passed
@acollado-g2
acollado-g2 deleted the feature/opt-in-tool-call-payload-telemetry branch August 10, 2026 18:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-work PR failed automated quality checks; maintainer review needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants