Add GenAI semantic-convention attributes to OpenTelemetryMiddleware (#2970)

This commit is contained in:
Marcelo Trylesinski 2026-06-25 16:43:54 +02:00 • committed by GitHub
commit 1b1abf6ab6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -8,28 +8,32 @@ from pydantic import ValidationError
from mcp.server.context import CallNext, HandlerResult, ServerMiddleware, ServerRequestContext
from mcp.shared._otel import extract_trace_context, otel_span
from mcp.shared.exceptions import MCPError
from mcp.types import INVALID_PARAMS, CallToolResult
class OpenTelemetryMiddleware(ServerMiddleware[Any]):
"""Context-tier middleware that wraps each inbound message in an OpenTelemetry span.
Span name `"MCP handle <method> [<target>]"`, `mcp.method.name` attribute, W3C
trace context extracted from `params._meta` (SEP-414), and an ERROR status if
the handler raises. Requests and notifications both get a span;
`jsonrpc.request.id` is set only when `ctx.request_id` is present (notifications
have none).
"""
"""Context-tier middleware that wraps each inbound message in an OpenTelemetry span."""
async def __call__(self, ctx: ServerRequestContext[Any, Any], call_next: CallNext) -> HandlerResult:
name = ctx.params.get("name") if ctx.params else None
target = name if isinstance(name, str) else None
attributes: dict[str, Any] = {"mcp.method.name": ctx.method}
attributes: dict[str, Any] = {
"mcp.method.name": ctx.method,
"mcp.protocol.version": ctx.protocol_version,
}
if ctx.request_id is not None:
attributes["jsonrpc.request.id"] = str(ctx.request_id)
if ctx.method == "tools/call":
attributes["gen_ai.operation.name"] = "execute_tool"
if target is not None:
attributes["gen_ai.tool.name"] = target
elif ctx.method == "prompts/get" and target is not None:
attributes["gen_ai.prompt.name"] = target
with otel_span(
name=f"MCP handle {ctx.method}{f' {target}' if target else ''}",
name=f"{ctx.method}{f' {target}' if target else ''}",
kind=SpanKind.SERVER,
attributes=attributes,
context=extract_trace_context(ctx.meta),
@ -37,15 +41,28 @@ class OpenTelemetryMiddleware(ServerMiddleware[Any]):
set_status_on_exception=False,
) as span:
try:
return await call_next(ctx)
result = await call_next(ctx)
except MCPError as e:
code = str(e.error.code)
span.set_attributes({"error.type": code, "rpc.response.status_code": code})
span.set_status(StatusCode.ERROR, e.error.message)
raise
except ValidationError:
# Mirror the sanitized wire response; pydantic messages carry client input.
code = str(INVALID_PARAMS)
span.set_attributes({"error.type": code, "rpc.response.status_code": code})
span.set_status(StatusCode.ERROR, "Invalid request parameters")
raise
except Exception as e:
span.set_attribute("error.type", type(e).__qualname__)
span.record_exception(e)
span.set_status(StatusCode.ERROR, str(e))
raise
if ctx.method == "tools/call":
match result:
case CallToolResult(is_error=True) | {"isError": True} | {"is_error": True}:
span.set_attribute("error.type", "tool_error")
span.set_status(StatusCode.ERROR)
case _:
pass
return result