Skip to content

feat(telemetry): every sample of one MCP tool call carries its operation id (#733, slice 3) - #967

Merged
cdeust merged 1 commit into
mainfrom
fix/733-operation-identity
Nov 10, 2026
Merged

cdeust merged 1 commit into
mainfrom
fix/733-operation-identity

Conversation

@cdeust

@cdeust cdeust commented Nov 10, 2026

Copy link
Copy Markdown
Owner

Summary

Issue #733, slice 3 (Refs #733, the issue stays open). Root cause: the samples one MCP tool call causes were joined only by op and ts (docs/audits/operation-consumption-inventory-2026-11-05.md, "No operation identity"). A nested handler's sample, a worker thread's sample and a sample recorded by work that outlives the call had nothing pointing to the call.

telemetry.capture_records() (opened by TelemetryMiddleware for every tools/call) now sets a fresh operation_id (uuid4 hex) in a ContextVar. record() stamps it on each TelemetrySample. Every sample recorded in that context carries the same id, and so does every sample recorded in a context copied from it: a run_in_executor worker or a task that records after the call has closed. A sample recorded outside a tool call (hooks, capture worker) carries operation_id: null, an explicit "not attributed", not an invented id. The field reaches telemetry.jsonl and the exporter's sample dict. The OTLP exporter reads only op and the outcome, so the id never becomes a metric attribute (unbounded cardinality, written in the TelemetryExporter contract).

record() delegates its capture-or-publish tail to _deliver() so it stays under the 40-line method rule.

Refs #733

Type of change

  • New feature (non-breaking change which adds functionality)

Test plan

  • New: tests_py/core/test_operation_identity_733.py (4 tests): nested and outer samples share one id; two calls get distinct ids; a worker-thread sample and a sample recorded after the call keep the caller's id; a sample outside a call has None. Removing the operation_id stamp, or resetting it per sample, fails them.
  • pytest tests_py/core tests_py/test_telemetry_middleware.py tests_py/handlers tests_py/hooks: 6735 passed, 276 skipped, 3 failed. All 3 fail the same way on main in this container: test_wiki_coverage_dashboard.py::test_unreadable_page_is_skipped_not_fatal (runs as root, so the file is still readable) and 2 cases of test_entry_auto_dispatch.py.
  • pytest tests_py/infrastructure tests_py/*.py tests_py/observability tests_py/server tests_py/invariants tests_py/architecture: the only failures are 4 sentence_transformers/torch tests. torch is not installed here because the proxy refuses download-r2.pytorch.org. This diff does not touch that code.
  • ruff check and ruff format --check (0.17.3, the version in uv.lock) on the changed files: clean. python scripts/check_craftsmanship.py: OK.

Audit notes

  • Not verified: pyright run locally; PostgreSQL paths (no server here; telemetry does not touch the store).
  • Remaining slices of feat: expose local operation receipts with attributable resource consumption #733: background capture (capture_processed, resident worker) does not run in the tool call's context, so it still carries null and needs the id passed explicitly through the spool; retry count; CPU time; provider usage beyond total_cost_usd.

Coding-standards compliance

  • §2.2 Layer dependency direction preserved (core uses only stdlib uuid, as core/wiki_identity.py does).
  • §4.1 / §4.2 300 / 40 limits (craftsmanship gate OK).
  • §9 No dead code.

Breaking changes

None. telemetry.jsonl lines gain one key, operation_id.


Generated by Claude Code

…ion id (#733, slice 3)

The samples a tool call caused were joined only by op and ts: a nested
handler's sample, a worker thread's and work that outlived the call had
nothing pointing to the call. capture_records() now sets a fresh
operation_id in a ContextVar, so record() stamps it on every sample of
that context and of contexts copied from it, including late ones. A
sample outside any tool call (hooks, capture worker) carries None.

Refs #733

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018kyZYuFrgtexu7G9TLud4P
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-10T08:44:53.558287Z 17db073 PR opened
🔒 Security Review ✅ Completed 2026-11-10T08:45:50.918897Z 17db073 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@claude

claude Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Claude finished @cdeust's task in 1m 48s —— View job


I'll analyze this and get back to you.

@claude

claude Bot commented Nov 10, 2026

Copy link
Copy Markdown

Review 1 of 3: PR #967, commit 17db073

I ran five reviews: correctness, security, tests, performance and docs. None found a Blocker.

What I checked:

  • publish_record copies the whole sample with **sample, so operation_id reaches telemetry.jsonl and the exporter.
  • OtelTelemetryExporter sets only {tool, status} as metric attributes, so the id adds no cardinality and is not sent off-host.
  • test_telemetry_record_contract.py:46 takes its key set from TelemetrySample.__annotations__, so it still passes with the new key.
  • The two ContextVars are reset in reverse order of setting, which is correct.
  • Nothing outside record() builds a TelemetrySample.

Notes, no action before merge

  • test_sample_outside_a_tool_call_has_no_operation passes even without _operation.reset(...). asyncio.run runs the middleware in a copied context, so the id never reaches the caller's context anyway. A synchronous with telemetry.capture_records(): pass before the record("hook") call would test the reset.
  • PRIVACY.md:100-106 lists the fields of a sample but does not mention operation_id. It could say the id is random, contains no content, and is null outside a tool call.
  • Nothing enforces "never a metric attribute" (telemetry.py:35). A test that pins the OTel attributes to {tool, status} would.

Verdict: approve

@cdeust
cdeust merged commit e2a9479 into main Nov 10, 2026
18 checks passed
@cdeust
cdeust deleted the fix/733-operation-identity branch November 10, 2026 19:13
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.

2 participants