Skip to content

Add OpenTelemetry tracing and metrics for MCP tool calls - #24

Merged
3dgiordano merged 4 commits into
developmentfrom
OTEL_SUPPORT
Jun 29, 2026
Merged

Add OpenTelemetry tracing and metrics for MCP tool calls#24
3dgiordano merged 4 commits into
developmentfrom
OTEL_SUPPORT

Conversation

@diego-ferrand

@diego-ferrand diego-ferrand commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

This pull request introduces OpenTelemetry-based telemetry support to the perfecto-mcp project, enabling distributed tracing and metrics collection for tool calls. The changes include the addition of a new telemetry.py module, integration of telemetry initialization into the main application, updates to dependencies and build configuration, and comprehensive unit tests for the telemetry logic.

Telemetry Integration and Implementation:

  • Added a new telemetry.py module that initializes OpenTelemetry tracing and metrics, provides helper functions for exporting to OTLP (gRPC or HTTP), and wraps tool calls with tracing and metrics instrumentation. The module is robust to missing dependencies and can be disabled via environment variables. (telemetry.py)

  • Integrated telemetry initialization by calling init_telemetry("perfecto-mcp", __version__) at application startup in main.py. (main.py) [1] [2]

Dependency and Build Updates:

  • Added OpenTelemetry dependencies (opentelemetry-api, opentelemetry-sdk, opentelemetry-exporter-otlp) to pyproject.toml, and registered the new telemetry module for inclusion in the package. Also added a development dependency group for pytest and configured Python path for testing. (pyproject.toml) [1] [2]

  • Updated the PyInstaller build process to include all necessary OpenTelemetry modules and submodules, ensuring the telemetry functionality is packaged correctly. (build.py)

Testing:

  • Added a comprehensive test suite for the telemetry module, covering initialization, error handling, protocol selection, and instrumentation logic. (tests/test_telemetry.py)

Documentation:

  • Added comments to the Dockerfile to clarify telemetry defaults and how to override or disable them via environment variables. (Dockerfile)

Comment thread telemetry.py
Comment on lines +111 to +130
def _get_meta(ctx: Any) -> dict:
try:
return ctx.request_context.request.params.meta or {}
except Exception:
return {}


def _extract_trace_context(meta: dict):
if not meta:
return None
try:
from opentelemetry.propagate import extract
carrier = {}
if "traceparent" in meta:
carrier["traceparent"] = meta["traceparent"]
if "tracestate" in meta:
carrier["tracestate"] = meta["tracestate"]
return extract(carrier) if carrier else None
except Exception:
return None

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.

Hey @diego-ferrand I was interiorizing with the PR and I got a pretty big alert on how are we handling the meta withing the context.
This is so far the research I could put through:

_extract_trace_context never extracts the parent context, so server spans are never linked to the caller's trace. Local spans/metrics still work, which is why this
fails quietly rather than erroring.

Root cause: request.params.meta is not a dict — it's the SDK's RequestParams.Meta Pydantic model (alias="_meta", extra="allow"). The _meta.meta
mapping is handled correctly by the alias, but traceparent/tracestate arrive as model attributes, not dict keys. The current code does dict-style access on it:

if "traceparent" in meta:                 # always False on a BaseModel
    carrier["traceparent"] = meta["traceparent"]   # 'Meta' object is not subscriptable

So carrier is always empty → extract() is never called → parent_ctx is always None.
Reproduced against the real SDK type:

type(meta): mcp.types.RequestParams.Meta
"traceparent" in meta  => False
meta["traceparent"]    => TypeError: 'Meta' object is not subscriptable
_extract_trace_context => None

Suggested fix — normalize to a dict first (handles both the Pydantic-model and plain-dict shapes, so we don't have to assume the client/transport):

def _extract_trace_context(meta):
    if not meta:
        return None
    if hasattr(meta, "model_dump"):
        meta = meta.model_dump()
    ...

model_dump() surfaces the extras correctly:

{'progressToken': None, 'traceparent': '00-...-01', 'tracestate': 'rojo=...'}

Tests: tests/test_telemetry.py currently has no coverage for traceparent / _get_meta / _extract_trace_context, which is how this regressed without anything
failing. A MagicMock ctx would fake in/[] and mask the bug, so the test should build the actual SDK type.
Non-breaking for everything else, but blocking for the OTEL propagation goal — it defeats the whole point of passing traceparent.

@3dgiordano
3dgiordano merged commit fcb1585 into development Jun 29, 2026
5 checks passed
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.

3 participants