-
Notifications
You must be signed in to change notification settings - Fork 571
fix(ai): Support distributed tracing for MCP tool calls #5271
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -100,13 +100,87 @@ def normalize_message_roles(messages: "list[dict[str, Any]]") -> "list[dict[str, | |
|
|
||
|
|
||
| def get_start_span_function() -> "Callable[..., Any]": | ||
| """ | ||
| Determine whether to create a span or transaction for AI operations. | ||
|
|
||
| Checks in priority order: | ||
| 1. MCP request context has sentry-trace headers: create transaction continuing the trace | ||
| 2. Transaction exists in current scope: create child span | ||
| 3. Otherwise: create new transaction | ||
|
|
||
| This ensures MCP tool calls properly continue distributed traces while maintaining | ||
| correct span hierarchies for local operations. | ||
| """ | ||
| # Check for distributed trace headers in MCP request context for cross-service tracing | ||
| try: | ||
| from mcp.server.lowlevel.server import request_ctx # type: ignore[import-not-found] | ||
|
|
||
| ctx = request_ctx.get() | ||
| if ctx and hasattr(ctx, "request") and ctx.request is not None: | ||
| request = ctx.request | ||
| if hasattr(request, "headers"): | ||
| sentry_trace = request.headers.get("sentry-trace") | ||
| if sentry_trace: | ||
| return _create_transaction_from_mcp_headers | ||
| except (ImportError, LookupError): | ||
| # MCP not installed or no request context, fall through to normal logic | ||
| pass | ||
|
|
||
| # Normal logic: create span if transaction exists, otherwise create transaction | ||
| current_span = sentry_sdk.get_current_span() | ||
| transaction_exists = ( | ||
| current_span is not None and current_span.containing_transaction is not None | ||
| ) | ||
| return sentry_sdk.start_span if transaction_exists else sentry_sdk.start_transaction | ||
|
|
||
|
|
||
| def _create_transaction_from_mcp_headers( | ||
| op: "Optional[str]" = None, | ||
| name: "Optional[str]" = None, | ||
| origin: str = "manual", | ||
| **kwargs: "Any", | ||
| ) -> "Any": | ||
| """ | ||
| Create transaction continuing distributed trace from MCP request headers. | ||
|
|
||
| Extracts sentry-trace and baggage headers from MCP request context and uses | ||
| continue_trace() to create a transaction inheriting trace_id and parent_span_id. | ||
| Ensures MCP tool executions join the distributed trace while remaining transactions | ||
| for MCP Insights dashboard compatibility. | ||
| """ | ||
| try: | ||
| from mcp.server.lowlevel.server import request_ctx # type: ignore[import-not-found] | ||
|
|
||
| ctx = request_ctx.get() | ||
| if ctx and hasattr(ctx, "request") and ctx.request is not None: | ||
| request = ctx.request | ||
| if hasattr(request, "headers"): | ||
| headers = {} | ||
|
|
||
| # Extract trace propagation headers | ||
| sentry_trace = request.headers.get("sentry-trace") | ||
| baggage = request.headers.get("baggage") | ||
|
|
||
| if sentry_trace: | ||
| headers["sentry-trace"] = sentry_trace | ||
| if baggage: | ||
| headers["baggage"] = baggage | ||
|
|
||
| # Use continue_trace to create transaction inheriting trace context | ||
| isolation_scope = sentry_sdk.get_isolation_scope() | ||
| transaction = isolation_scope.continue_trace( | ||
| environ_or_headers=headers, op=op, name=name, origin=origin | ||
| ) | ||
|
|
||
| # Start transaction on scope so it becomes active | ||
| return sentry_sdk.start_transaction(transaction=transaction) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. kwargs ignored in MCP headers success pathThe Additional Locations (1) |
||
| except Exception as e: | ||
| logger.debug("Could not create transaction from MCP headers: %s", e) | ||
|
|
||
| # Fallback: create new transaction if headers unavailable | ||
| return sentry_sdk.start_transaction(op=op, name=name, origin=origin, **kwargs) | ||
|
|
||
|
|
||
| def _truncate_single_message_content_if_present( | ||
| message: "Dict[str, Any]", max_chars: int | ||
| ) -> "Dict[str, Any]": | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AttributeError uncaught when headers attribute is None
In
get_start_span_function(), the code useshasattr(request, "headers")to check if the attribute exists, then immediately callsrequest.headers.get("sentry-trace"). Ifheadersexists but isNone, this raises anAttributeErrorthat isn't caught by theexcept (ImportError, LookupError)handler. The similar code in_create_transaction_from_mcp_headers()catches allException, but this function would fail and propagate the error.