traceparent header,
the serving agent picks it up, and the spans from both processes share one trace ID.
This works with both the A2A Python SDK
(which additionally ships its own OTel telemetry) and the
A2A JavaScript SDK, where propagation comes from
the standard OTel HTTP instrumentations. Export both agents’ telemetry to Openlayer and
you get a single trace that starts at the user request in the calling agent and continues
through the serving agent’s LLM and tool calls — the full distributed picture of your
multi-agent system.
Agents that export to the same inference pipeline (see the
x-bt-parent
header below) merge into one trace — that is the recipe on this page, and it
works with no further setup. To give each agent its own project and
navigate between the linked traces, see One project per
agent: the instrumentation is
identical, only the export destination changes.Installation
1. Configure the OpenTelemetry exporter — in every agent
Each agent runs the standard OTel SDK and exports to Openlayer’s OTel endpoint. The snippet below also installs aBaggageSpanProcessor, which copies openlayer.*
baggage entries (session and
user IDs — see below) onto every span
the process creates.
In TypeScript, load the instrumentation before your application code so the
HTTP patches apply:
node --import ./instrumentation.js server.js. The
UndiciInstrumentation covers the global fetch the A2A client uses; the
HttpInstrumentation covers the Express server.2. Instrument the calling agent (A2A client)
The A2A client rides on the language’s standard HTTP stack —httpx in Python, fetch
(undici) in Node — so instrumenting that stack is what injects the traceparent and
baggage headers into the A2A request:
3. Instrument the serving agent (A2A server)
On the server side, instrumenting the HTTP server (Starlette in Python, Express via the Nodehttp module in TypeScript) is what extracts the incoming traceparent and
baggage headers, so every span this agent creates — including LLM calls made inside
your AgentExecutor — joins the caller’s trace:
uvicorn for Python; node --import ./instrumentation.js server.js for TypeScript). When the orchestrator handles a
user request, Openlayer shows one record whose trace tree starts at
handle_user_request, crosses the A2A hop, and continues through the worker agent’s
execution — LLM steps, token counts, and latency included:
One project per agent (linked traces)
Everything above merges the agents into one trace in one pipeline. When different teams own the agents — or you want per-agent records, tests, and dashboards — point each agent’s exporter at its own project’s pipeline instead. The instrumentation does not change at all: only thex-bt-parent header differs per agent.
With trace context propagating exactly as before, Openlayer links the per-project traces:
- The serving agent’s spans arrive with a parent that lives in another process (the caller). Openlayer promotes that subtree into a full record in the serving agent’s own project — with its own step tree, tokens, and cost — instead of leaving it unreachable.
- The severed parent edge is preserved, so the two records are linked directionally: the caller’s record shows a Continues in … chip and the serving agent’s record shows a Called from … chip. Each chip deep-links to the other record.
- Links are direct call edges only. An orchestrator that fans out to several agents shows one chip per agent it called; two agents that merely share a trace without calling each other are not linked.
- Works within one project too. A single project with a dedicated pipeline per agent behaves identically — chips are labeled with the peer pipeline’s name.
- Scoped to your workspace. Link lookups only ever consider pipelines in the same workspace, in projects the viewer can access.
Per-project linking relies on remote-root promotion, which is enabled per
workspace. Reach out to your Openlayer contact to turn it on; without it,
point all agents at one pipeline (the recipe above).
Using Google ADK agents over A2A
Google ADK agents speak A2A natively:to_a2a()
exposes an agent as an A2A server, and RemoteA2aAgent consumes one as a sub-agent. The
OTel setup from steps 1–3 applies unchanged, so an ADK orchestrator and an ADK worker in
separate projects produce linked traces with real invoke_agent, LLM, and tool steps on
both sides. (For tracing a single ADK app without A2A, the
Google ADK integration is the simpler path.)
worker.py
invoke_agent span for a remote agent carries no
content of its own (the request and response travel inside the A2A message body), so the
optional callback below stamps them onto the span with standard attributes — the handoff
step then shows exactly what was asked of the remote agent and what it answered:
orchestrator.py
- The agent-card URL must match the worker’s serving origin exactly — resolving a
card served on
localhostvia127.0.0.1(or vice versa) fails with a card resolution error. - Set
OTEL_PYTHON_EXCLUDED_URLSon the worker (the middleware above does it in code): otherwise the orchestrator’s agent-card fetch becomes a record of its own in the worker’s project. google-adk[a2a]does not pull the A2A server extra — installa2a-sdk[http-server]alongside it, as shown above.
Propagate session and user context with Baggage
Openlayer reads theopenlayer.session.id and openlayer.user.id span attributes to
group traces into sessions and users. In a multi-agent
system, only the entry-point agent knows those IDs — W3C Baggage is how they reach the
other agents:
- The calling agent puts the IDs in baggage (step 2 above).
- The instrumented HTTP client sends them in the
baggageheader alongsidetraceparent. - Each agent’s
BaggageSpanProcessor(step 1 above) copies them onto every span it creates.
Tips
-
Exclude health checks and agent-card fetches. Server-side HTTP instrumentation
turns every direct request — including
/.well-known/agent-card.jsonpolls and health checks — into a root span, which becomes a record in Openlayer. In Python, exclude those URLs via the environment (the TypeScript setup above already handles this withignoreIncomingRequestHook): -
Trim the A2A Python SDK’s internal spans. The Python SDK instruments its own
internals (event queues, dispatchers), which adds a couple dozen spans per message. If
you prefer leaner traces, disable them and rely on the
httpx/Starlette instrumentation for propagation (the JavaScript SDK emits no internal spans, so its traces are lean by default): -
Configuring the exporter via environment variables instead? Use
OTEL_EXPORTER_OTLP_ENDPOINT=https://api.openlayer.com/v1/otel(the SDK appends/v1/traces), as shown on the OpenTelemetry integration page.