Skip to content

feat: emit tool-call duration histogram metric#38

Draft
rahulcrl wants to merge 2 commits into
mainfrom
add_otel_metrics
Draft

feat: emit tool-call duration histogram metric#38
rahulcrl wants to merge 2 commits into
mainfrom
add_otel_metrics

Conversation

@rahulcrl

@rahulcrl rahulcrl commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Stacked on #28. Addresses the review feedback there about emitting tool-call latency as an OTel histogram so MCP performance can be measured off the metrics pipeline, with accuracy that survives log sampling.

Changes

  • Export mcp.tool.call.duration (unit s) via a new ToolCallMetrics middleware, keyed by mcp.tool.name and mcp.tool.call.outcome (success, error, tool_error).
  • Wire a MeterProvider into otel.Setup behind the same opt-in gate: stdoutmetric in file mode, otlpmetricgrpc in OTLP mode, flushed on shutdown.
  • Extract a shared toolName helper so the span and metrics middleware agree on the unknown_tool fallback.

Testing

  • middleware/metrics_test.go: manual-reader assertions for all three outcomes plus the non-tool-method no-op path.
  • otel/otel_test.go: file-mode Setup flushes the recorded histogram on shutdown.
  • go test ./... passes.

rahulcrl added 2 commits July 6, 2026 18:23
Off by default. Opt in via CRDB_MCP_OTEL_FILE (JSON spans to a file) or
OTEL_EXPORTER_OTLP_ENDPOINT (OTLP gRPC to any collector, honoring
standard OTEL_* env vars). Neither set: providers stay no-op.

A receiving middleware wraps each MCP tools/call in a span named after
the tool. db/adapter hand-instruments Query and Exec with SQL child
spans; db.query.text is redacted via cockroachdb-parser
(FmtAnonymize + FmtHideConstants), matching managed-service, so no user
data lands in span attributes. Zap records also mirror to the OTel log
pipeline via the otelzap bridge; trace-context propagation onto log
records is a follow-up.
Export mcp.tool.call.duration (seconds) keyed by tool name and outcome
via the existing opt-in OTel pipeline. Histogram accuracy survives log
sampling, so operators get p50/p95/p99, throughput, and error rate even
with tool-call logs dropped.
@rahulcrl
rahulcrl marked this pull request as draft July 6, 2026 13:11
Comment thread otel/otel.go
return nil, nil, nil, errors.Wrap(err, "build stdout log exporter")
return nil, nil, nil, nil, errors.Wrap(err, "build stdout log exporter")
}
me, err := stdoutmetric.New(stdoutmetric.WithWriter(f))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟢 Confidence: 42 (Low)

Bug (inconsistent cleanup): When stdoutmetric.New() fails, only the file is closed — the already-created te (trace exporter) and le (log exporter) are leaked. Contrast with the OTLP path (lines 155-159) which correctly shuts down all previously-created exporters on failure. Consider adding _ = te.Shutdown(ctx) and _ = le.Shutdown(ctx) (or at minimum closing them) before the return, matching the OTLP error-handling pattern. Note: the same leak existed pre-PR for te when stdoutlog.New() failed (line 135), but this PR extends the pattern to a third exporter.

Comment thread middleware/metrics.go
// tool name and outcome. Unlike per-call logs, histogram accuracy survives
// log sampling, so operators can derive p50/p95/p99, throughput, and error
// rate even with tool-call logs dropped. No-op until otel.Setup installs an
// exporter.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟢 Confidence: 20 (Low)

Potential high-cardinality issue: mcp.tool.name is derived from the request's Name field via toolName(req). Since this middleware runs before the tool handler dispatches (and validates) the tool name, a misbehaving client could send arbitrary/fabricated tool names, creating unbounded metric label cardinality in the histogram. Over time this can exhaust memory in the MeterProvider. Consider validating the tool name against a known set of registered tools, or adding a cardinality guard (e.g., mapping unknown tool names to a single "unregistered" bucket). In practice MCP clients are typically trusted local processes, so the risk is low.

@rahulcrl
rahulcrl force-pushed the add_otel_tracing branch from 3dee420 to 2a9532e Compare July 6, 2026 13:14
@rahulcrl rahulcrl mentioned this pull request Jul 6, 2026
3 tasks
@rahulcrl
rahulcrl force-pushed the add_otel_tracing branch from 2a9532e to e3e5e0c Compare July 6, 2026 13:23
Base automatically changed from add_otel_tracing to main July 7, 2026 05:40
@cockroachlabs-cla-agent

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

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