You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Proposal] End-to-end tracing for agentic use cases (GenAI spans)
#3741
The gateway now serves LLM providers, LLM proxies, MCP proxies and A2A agents. This proposal makes one agent turn through the gateway show up as one trace, and adds OpenTelemetry GenAI spans that carry the model, token usage and tool for each call.
Add GenAI spans for LLM, MCP and A2A calls, following the OpenTelemetry GenAI and MCP semantic conventions.
No new component, control-plane API or database change is needed. This builds on the original tracing design in # .
What is the problem that we are trying to solve?
An agent turn through the gateway does not appear as one trace, and no span carries model, token or tool data.
The following was observed by running the gateway built from main (gateway-runtime 2026.09.24, Envoy 1.39.0) with tracing enabled and inspecting the traces in Jaeger.
Case
What happens today
Tracked in
Client sends no traceparent (also the LLM proxy's loopback hop)
Envoy's spans and the policy engine's spans form two separate traces
No client traceparent: two unrelated traces
Trace A Trace B
ingress (Envoy, SERVER) external_processing.process (policy engine, SERVER)
├─ ext_proc egress (Envoy, CLIENT) ├─ process_request_headers
└─ router egress (Envoy, CLIENT) ├─ policy.request.<name>
└─ … all flat
Client sends traceparent: one trace, wrong shape
client span
├─ ingress (Envoy, SERVER)
│ ├─ ext_proc egress (Envoy, CLIENT) ← the policy engine should be under this
│ └─ router egress (Envoy, CLIENT)
└─ external_processing.process (policy engine) ← sibling of ingress
Who are we solving the problem for?
Engineers who run agents through the AI Gateway. They need one trace per agent turn that shows each model call and tool call, its tokens and latency, and any policy that blocked or changed it.
Secondarily, teams that display gateway spans next to agent-side spans, such as WSO2 Agent Manager.
Why should it be solved?
Agent turns now cross several gateway hops: LLM proxy, provider, MCP proxy, A2A agent. Without one trace, latency and failures cannot be attributed to a hop.
Other gateways, such as Agent Router (formerly Envoy AI Gateway) and agentgateway, already emit OpenTelemetry GenAI spans.
The data already exists. The gateway extracts the model, token usage and MCP method for analytics today; tracing mostly needs to put that data on spans.
Proposed solution
The policy engine emits OpenTelemetry GenAI spans from data it already holds, and trace context handling is fixed so that all gateway spans form one tree. Envoy stays the source of HTTP spans.
flowchart LR
client["Agent or client<br/>sends traceparent"]
ctrl["Gateway controller<br/>injects the genai-tracing system policy"]
subgraph runtime["Gateway runtime (one container)"]
envoy["Envoy router<br/>ingress, ext_proc and upstream spans<br/>(unchanged)"]
subgraph pe["Policy engine (root span parented on Envoy's span)"]
rec["GenAI span recorder (new)<br/>chat, tools/call, invoke_agent spans"]
chain["Policy chain<br/>policy spans; side-call CLIENT spans (new)"]
end
end
subgraph up["Upstreams"]
llm["LLM provider"]
mcp["MCP server"]
a2a["A2A agent"]
end
col["OTel Collector"]
be["Any OTLP backend"]
client --> envoy
envoy -- "ext_proc stream<br/>traceparent in gRPC metadata" --> pe
envoy -- "traceparent header<br/>MCP _meta rewritten" --> up
ctrl --> pe
envoy -. "OTLP" .-> col
pe -. "OTLP" .-> col
col --> be
Loading
1. Trace continuity (prerequisite)
Change
Issue
Parent the policy-engine root span on the traceparent that Envoy injects into each per-request ext_proc gRPC stream. Use the HTTP headers only when Envoy tracing is off.
Envoy 1.39 creates a CLIENT span for the ext_proc call and injects its context into the stream's metadata (async_client_impl.cc). It opens one stream per HTTP request (ext_proc.cc). The policy engine reads the downstream HTTP headers instead, which carry only the client's context.
2. GenAI spans
A new span recorder in the policy-engine kernel opens one span per AI operation. The gateway controller injects a genai-tracing system policy on LLM, MCP and Agent routes, the same way it injects the analytics system policy, so that the policy engine sees the response stream.
Spans follow the OpenTelemetry GenAI and MCP semantic conventions, pinned at commit 06ec68e (schema gen-ai-dev/1.42.0-dev). Gateway-only facts use wso2.* keys, reusing the keys the OTel analytics publisher already emits where they exist.
gen_ai.operation.name=invoke_agent, gen_ai.agent.name, gen_ai.agent.version, gen_ai.conversation.id (the A2A contextId)
A2A resolver
Existing policy-engine spans keep their names and gain wso2.ai.proxy, wso2.guardrail.name and wso2.policy.action.
Rules:
Operation names. The standard values are used: chat, text_completion, embeddings, generate_content, execute_tool, invoke_agent. For LLM routes these are the values the OTel analytics publisher already derives from the route.
Span timing. A GenAI span runs from when the request leaves the policy chain to the last response byte. Gateway time and model time therefore show separately, and a streamed response closes at stream end.
LLM proxies. Only provider routes emit the inference span. An LLM proxy's own route never does, because its upstream is the loopback provider route, so tokens are reported once.
MCP context.
When params._meta carries a context in the same trace as the HTTP request, the MCP span nests under the gateway's span.
Otherwise the MCP span is parented on the _meta context and linked to the HTTP context, as the MCP conventions specify.
Before forwarding, the gateway rewrites _meta.traceparent and _meta.tracestate to its own span. These keys are reserved for this by MCP SEP-414.
Guardrails. No OpenTelemetry convention exists yet (semantic-conventions-genai PR 427 is open). wso2.* keys are used until it merges.
Content. Prompts, completions and tool arguments are not recorded by default. When enabled, they are captured after the masking and guardrail policies have run, and truncated.
Policy denials keep today's behaviour: the HTTP status and terminal.reason are recorded, and the span status stays unset for 4xx, as the OpenTelemetry HTTP conventions specify.
3. Target trace
One agent turn: an LLM call through an LLM proxy, then an MCP tool call. Envoy's ext_proc span above each policy-engine root is omitted for readability.
4. Configuration
One new block under the existing [tracing] section. The Helm chart mirrors it under gateway.config.tracing.genai.
[tracing]
enabled = truesampling_rate = 0.1# existing; the policy engine now follows Envoy's decision
[tracing.genai]
enabled = true# GenAI, MCP and A2A spans; default true when tracing is oncapture_content = false# prompts, completions, tool input and output (opt-in)max_content_bytes = 16384# truncate captured content
5. Compatibility with WSO2 Agent Manager
Agent Manager instruments agents in-process with Traceloop, and its trace parser reads the standard gen_ai.* names. The gateway therefore needs no Agent Manager specific keys: it emits the standard names, sets error.type on failures, and never emits traceloop.* keys.
For gateway spans to appear inside an agent's trace in the Agent Manager console, two further things are needed, both listed under open questions: the calling agent's identifiers on the spans, and export to Agent Manager's collector.
Alternatives considered
Alternative
Why it was not chosen
Put GenAI attributes on Envoy's own spans, using custom_tags fed from dynamic metadata
Tags are strings only in Envoy 1.39, but token counts must be integers. The tag list is fixed per listener, and Envoy cannot rewrite MCP params._meta.
Most OpenTelemetry backends, and Agent Manager, read gen_ai.*. An OpenTelemetry Collector can remap these spans to OpenInference if a user needs it.
A new tracing sidecar or component
The policy engine already has the OpenTelemetry SDK, the request context and the extracted data.
Challenges and how the design handles them
Convention churn. Every GenAI and MCP convention is at Development stability, and names have changed since semantic conventions v1.41.0 (for example cache_creation became cache_write). A commit is pinned, and every key lives in one Go package, because no official Go package exists after semconv/v1.41.0. GenAI metrics wait until the token metrics settle.
Privacy. Content capture is opt-in. Inbound traceparent, _meta and baggage are used only to correlate spans, never as identity.
Token double counting. An agent's SDK span and the gateway's span can both report usage. Backends can separate them by service.name; Agent Manager counts usage only on the outermost span.
Performance. Token counts need the response body. The policy engine already reads it when analytics, token rate-limit or cost policies run; otherwise enabling GenAI spans adds one pass-through streamed read. Collapsing per-chunk spans reduces the span count on streams.
Envoy owns the upstream span. The provider's parent is Envoy's router egress span, and the GenAI span sits beside it in the same trace.
MCP clients that send context only in _meta. Envoy's HTTP spans for those requests land in a separate trace that the MCP span links to, because Envoy picks its trace before the request body is read.
Backward compatibility. Existing span names stay, and GenAI spans are additive. The visible changes are that policy-engine spans move under Envoy's span, and streamed responses produce fewer spans.
Content capture: is a gateway-wide switch enough, or is a per-proxy setting needed? A per-proxy setting changes the control-plane API.
Agent Manager integration: how does the gateway get each agent's identifiers, and which export route should it use? Agent Manager's collector accepts OTLP over HTTP only, while the gateway exports OTLP over gRPC today.
Should the gateway forward traceparent to A2A agents by default? An agent that adopts it gets the gateway's span as its parent.
Should GenAI metrics be exported as OTLP metrics or as Prometheus series?
Routes that carry telemetry (for example an OTLP ingest route deployed on the gateway) would themselves be traced. Should such routes be excluded from tracing, as health-check routes are today?
Area/AIGatewayAI Gateway runtime/control plane related issues
1 participant
Heading
Bold
Italic
Quote
Code
Link
Numbered list
Unordered list
Task list
Attach files
Mention
Reference
Menu
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Summary
The gateway now serves LLM providers, LLM proxies, MCP proxies and A2A agents. This proposal makes one agent turn through the gateway show up as one trace, and adds OpenTelemetry GenAI spans that carry the model, token usage and tool for each call.
It has two parts:
No new component, control-plane API or database change is needed. This builds on the original tracing design in # .
What is the problem that we are trying to solve?
An agent turn through the gateway does not appear as one trace, and no span carries model, token or tool data.
The following was observed by running the gateway built from
main(gateway-runtime2026.09.24, Envoy 1.39.0) with tracing enabled and inspecting the traces in Jaeger.traceparent(also the LLM proxy's loopback hop)traceparentingressspan, not a child of Envoy's ext_proc spantraceparentwith the sampled flag00tools/callparams._metais ignored and forwarded unchangedgen_ai.*attributes on spans. They exist only on the OTel analytics log records, which carry no trace IDTrees observed today:
Who are we solving the problem for?
Engineers who run agents through the AI Gateway. They need one trace per agent turn that shows each model call and tool call, its tokens and latency, and any policy that blocked or changed it.
Secondarily, teams that display gateway spans next to agent-side spans, such as WSO2 Agent Manager.
Why should it be solved?
Proposed solution
The policy engine emits OpenTelemetry GenAI spans from data it already holds, and trace context handling is fixed so that all gateway spans form one tree. Envoy stays the source of HTTP spans.
flowchart LR client["Agent or client<br/>sends traceparent"] ctrl["Gateway controller<br/>injects the genai-tracing system policy"] subgraph runtime["Gateway runtime (one container)"] envoy["Envoy router<br/>ingress, ext_proc and upstream spans<br/>(unchanged)"] subgraph pe["Policy engine (root span parented on Envoy's span)"] rec["GenAI span recorder (new)<br/>chat, tools/call, invoke_agent spans"] chain["Policy chain<br/>policy spans; side-call CLIENT spans (new)"] end end subgraph up["Upstreams"] llm["LLM provider"] mcp["MCP server"] a2a["A2A agent"] end col["OTel Collector"] be["Any OTLP backend"] client --> envoy envoy -- "ext_proc stream<br/>traceparent in gRPC metadata" --> pe envoy -- "traceparent header<br/>MCP _meta rewritten" --> up ctrl --> pe envoy -. "OTLP" .-> col pe -. "OTLP" .-> col col --> be1. Trace continuity (prerequisite)
traceparentthat Envoy injects into each per-request ext_proc gRPC stream. Use the HTTP headers only when Envoy tracing is off.ParentBased, so it follows Envoy's sampling decision.traceIdandspanIdto the OTel analytics log records.service.versionfrom the build.Envoy 1.39 creates a CLIENT span for the ext_proc call and injects its context into the stream's metadata (
async_client_impl.cc). It opens one stream per HTTP request (ext_proc.cc). The policy engine reads the downstream HTTP headers instead, which carry only the client's context.2. GenAI spans
A new span recorder in the policy-engine kernel opens one span per AI operation. The gateway controller injects a
genai-tracingsystem policy on LLM, MCP and Agent routes, the same way it injects the analytics system policy, so that the policy engine sees the response stream.Spans follow the OpenTelemetry GenAI and MCP semantic conventions, pinned at commit
06ec68e(schemagen-ai-dev/1.42.0-dev). Gateway-only facts usewso2.*keys, reusing the keys the OTel analytics publisher already emits where they exist.{operation} {model}, for examplechat gpt-4ogen_ai.operation.name,gen_ai.provider.name,gen_ai.request.model,gen_ai.response.model,gen_ai.response.id,gen_ai.response.finish_reasons,gen_ai.usage.input_tokens,gen_ai.usage.output_tokens,gen_ai.usage.cache_read.input_tokens,gen_ai.usage.reasoning.output_tokens,gen_ai.request.stream,gen_ai.response.time_to_first_chunk,server.address,error.typesdk/ai/llmusage{mcp.method.name} {target}, for exampletools/call get_invoicemcp.method.name,gen_ai.operation.name=execute_tool(tool calls only),gen_ai.tool.name,mcp.protocol.version,jsonrpc.request.id,rpc.response.status_code,error.typeinvoke_agent {agent}gen_ai.operation.name=invoke_agent,gen_ai.agent.name,gen_ai.agent.version,gen_ai.conversation.id(the A2AcontextId)Existing policy-engine spans keep their names and gain
wso2.ai.proxy,wso2.guardrail.nameandwso2.policy.action.Rules:
chat,text_completion,embeddings,generate_content,execute_tool,invoke_agent. For LLM routes these are the values the OTel analytics publisher already derives from the route.params._metacarries a context in the same trace as the HTTP request, the MCP span nests under the gateway's span._metacontext and linked to the HTTP context, as the MCP conventions specify._meta.traceparentand_meta.tracestateto its own span. These keys are reserved for this by MCP SEP-414.wso2.*keys are used until it merges.terminal.reasonare recorded, and the span status stays unset for 4xx, as the OpenTelemetry HTTP conventions specify.3. Target trace
One agent turn: an LLM call through an LLM proxy, then an MCP tool call. Envoy's ext_proc span above each policy-engine root is omitted for readability.
4. Configuration
One new block under the existing
[tracing]section. The Helm chart mirrors it undergateway.config.tracing.genai.5. Compatibility with WSO2 Agent Manager
Agent Manager instruments agents in-process with Traceloop, and its trace parser reads the standard
gen_ai.*names. The gateway therefore needs no Agent Manager specific keys: it emits the standard names, setserror.typeon failures, and never emitstraceloop.*keys.For gateway spans to appear inside an agent's trace in the Agent Manager console, two further things are needed, both listed under open questions: the calling agent's identifiers on the spans, and export to Agent Manager's collector.
Alternatives considered
custom_tagsfed from dynamic metadataparams._meta.gen_ai.*. An OpenTelemetry Collector can remap these spans to OpenInference if a user needs it.Challenges and how the design handles them
cache_creationbecamecache_write). A commit is pinned, and every key lives in one Go package, because no official Go package exists aftersemconv/v1.41.0. GenAI metrics wait until the token metrics settle.traceparent,_metaandbaggageare used only to correlate spans, never as identity.service.name; Agent Manager counts usage only on the outermost span.router egressspan, and the GenAI span sits beside it in the same trace._meta. Envoy's HTTP spans for those requests land in a separate trace that the MCP span links to, because Envoy picks its trace before the request body is read.Delivery plan
gateway-runtime/policy-engine/internal:kernel/extproc.go,tracing/tracer.go,executor/chain.go,kernel/resolution.go,analytics/publishers/otel.gogenai-tracingsystem policy,[tracing.genai]configuration, LLM proxy rule, time to first chunksdk/ai/llmusage,gateway-controller/pkg/utils/system_policies.go,gateway/configs/config-template.toml, Helm chart_metaread and rewrite,invoke_agentspaninternal/resolver/mcp.go,internal/resolver/a2a.gohttpkit/httpclient/client.go, guardrail policies inwso2/gateway-controllersNo tracing integration tests exist today. Each phase adds tests that run the gateway against an OTLP sink and assert the span tree and parent IDs.
Open questions
IsSpanSafeAttributerule keeps tool names and JSON-RPC IDs off spans, but the MCP conventions require both. Should the conventions be followed on the new MCP span only ([Task]: Decide which resolver attributes may be recorded on spans (IsSpanSafeAttribute is defined but never applied) #3737)?traceparentto A2A agents by default? An agent that adopts it gets the gateway's span as its parent.References
service.versionattribute #2096, Submit traces from Policies #854, [Task]: check the frequent "No valid trace context extracted" #3026_metaAll reactions