Expose telemetry attributes on span start

🤖 Generated with Codex
This commit is contained in:
zzstoatzz 2026-07-09 15:16:05 -05:00
commit e3941e23b1
3 changed files with 109 additions and 38 deletions

View file

@ -23,24 +23,25 @@ def client_span(
Automatically records any exception on the span and sets error status.
"""
attrs: dict[str, str] = {
# MCP semantic conventions
"mcp.method.name": method,
# FastMCP-specific attributes
"fastmcp.component.key": component_key,
}
if session_id is not None:
attrs["mcp.session.id"] = session_id
if resource_uri:
attrs["mcp.resource.uri"] = resource_uri
if tool_name is not None:
attrs["gen_ai.tool.name"] = tool_name
if prompt_name is not None:
attrs["gen_ai.prompt.name"] = prompt_name
tracer = get_tracer()
with tracer.start_as_current_span(name, kind=SpanKind.CLIENT) as span:
if span.is_recording():
attrs: dict[str, str] = {
# MCP semantic conventions
"mcp.method.name": method,
# FastMCP-specific attributes
"fastmcp.component.key": component_key,
}
if session_id is not None:
attrs["mcp.session.id"] = session_id
if resource_uri:
attrs["mcp.resource.uri"] = resource_uri
if tool_name is not None:
attrs["gen_ai.tool.name"] = tool_name
if prompt_name is not None:
attrs["gen_ai.prompt.name"] = prompt_name
span.set_attributes(attrs)
with tracer.start_as_current_span(
name, kind=SpanKind.CLIENT, attributes=attrs
) as span:
try:
yield span
except Exception as e:

View file

@ -119,22 +119,20 @@ def seam_span(method: str, server_name: str) -> Generator[Span, None, None]:
rejections *before* the high-level path (auth, not-found, middleware vetoes)
that would otherwise produce no SERVER span at all are recorded here.
"""
attrs = {
SEAM_SPAN_MARKER: True,
"mcp.method.name": method,
"fastmcp.server.name": server_name,
**get_auth_span_attributes(),
**get_session_span_attributes(),
}
tracer = get_tracer()
with tracer.start_as_current_span(
method,
context=_get_parent_trace_context(),
kind=SpanKind.SERVER,
attributes=attrs,
) as span:
if span.is_recording():
span.set_attribute(SEAM_SPAN_MARKER, True)
span.set_attributes(
{
"mcp.method.name": method,
"fastmcp.server.name": server_name,
**get_auth_span_attributes(),
**get_session_span_attributes(),
}
)
token = _active_seam_span.set(span)
try:
yield span
@ -201,9 +199,8 @@ def server_span(
name,
context=_get_parent_trace_context(),
kind=SpanKind.SERVER,
attributes=attrs,
) as span:
if span.is_recording():
span.set_attributes(attrs)
try:
yield span
except Exception as e:
@ -223,16 +220,15 @@ def delegate_span(
Used by FastMCPProvider when delegating to mounted servers.
Automatically records any exception on the span and sets error status.
"""
attrs: dict[str, str] = {
"fastmcp.provider.type": provider_type,
"fastmcp.component.key": component_key,
}
if method is not None:
attrs["mcp.method.name"] = method
tracer = get_tracer()
with tracer.start_as_current_span(f"delegate {name}") as span:
if span.is_recording():
attrs: dict[str, str] = {
"fastmcp.provider.type": provider_type,
"fastmcp.component.key": component_key,
}
if method is not None:
attrs["mcp.method.name"] = method
span.set_attributes(attrs)
with tracer.start_as_current_span(f"delegate {name}", attributes=attrs) as span:
try:
yield span
except Exception as e:

View file

@ -0,0 +1,74 @@
import pytest
from opentelemetry.context import Context
from opentelemetry.sdk.trace import Span, SpanProcessor, TracerProvider
from fastmcp.client.telemetry import client_span
from fastmcp.server.telemetry import delegate_span, seam_span, server_span
class OnStartRecorder(SpanProcessor):
def __init__(self) -> None:
self.attributes: dict[str, dict[str, object]] = {}
def on_start(self, span: Span, parent_context: Context | None = None) -> None:
self.attributes[span.name] = dict(span.attributes or {})
def test_known_span_attributes_are_available_on_start(
monkeypatch: pytest.MonkeyPatch,
) -> None:
recorder = OnStartRecorder()
provider = TracerProvider()
provider.add_span_processor(recorder)
tracer = provider.get_tracer("test")
monkeypatch.setattr("fastmcp.client.telemetry.get_tracer", lambda: tracer)
monkeypatch.setattr("fastmcp.server.telemetry.get_tracer", lambda: tracer)
with client_span(
"client test",
method="tools/call",
component_key="tool:echo@",
tool_name="echo",
):
pass
with server_span(
"server test",
method="tools/call",
server_name="test-server",
component_type="tool",
component_key="tool:echo@",
tool_name="echo",
):
pass
with seam_span("initialize test", server_name="test-server"):
pass
with delegate_span(
"delegate test",
provider_type="LocalProvider",
component_key="tool:echo@",
method="tools/call",
):
pass
assert recorder.attributes["client test"] == {
"mcp.method.name": "tools/call",
"fastmcp.component.key": "tool:echo@",
"gen_ai.tool.name": "echo",
}
assert recorder.attributes["server test"] == {
"mcp.method.name": "tools/call",
"fastmcp.server.name": "test-server",
"fastmcp.component.type": "tool",
"fastmcp.component.key": "tool:echo@",
"gen_ai.tool.name": "echo",
}
assert recorder.attributes["initialize test"] == {
"fastmcp.span.seam": True,
"mcp.method.name": "initialize test",
"fastmcp.server.name": "test-server",
}
assert recorder.attributes["delegate delegate test"] == {
"fastmcp.provider.type": "LocalProvider",
"fastmcp.component.key": "tool:echo@",
"mcp.method.name": "tools/call",
}