Skip to main content
The Agent2Agent (A2A) protocol is an open standard for agent-to-agent communication: one agent (the client) sends tasks to another agent (the server) over HTTP, even when the two agents are built by different teams and deployed as separate services. Because the A2A SDKs ride on standard HTTP, OpenTelemetry trace context propagates across the A2A hop automatically: the calling agent injects the W3C 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 a BaggageSpanProcessor, 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 Node http 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:
Run the worker (any ASGI server such as 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 the x-bt-parent header differs per agent. With trace context propagating exactly as before, Openlayer links the per-project traces:
  1. 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.
  2. 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.
A few properties worth knowing:
  • 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.)
Expose the worker agent — wrapping the ASGI app directly keeps per-event spans and agent-card fetches out of your records:
worker.py
Consume it from the orchestrator. ADK’s 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
ADK-specific gotchas:
  • The agent-card URL must match the worker’s serving origin exactly — resolving a card served on localhost via 127.0.0.1 (or vice versa) fails with a card resolution error.
  • Set OTEL_PYTHON_EXCLUDED_URLS on 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 — install a2a-sdk[http-server] alongside it, as shown above.

Propagate session and user context with Baggage

Openlayer reads the openlayer.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:
  1. The calling agent puts the IDs in baggage (step 2 above).
  2. The instrumented HTTP client sends them in the baggage header alongside traceparent.
  3. Each agent’s BaggageSpanProcessor (step 1 above) copies them onto every span it creates.
The result: every span from every agent carries the session and user, with no Openlayer-specific code in the downstream agents.
Baggage travels in plaintext HTTP headers to every downstream service the instrumented client calls — including third-party APIs. Filter which keys you propagate (the snippet above only copies openlayer.*) and keep sensitive values out of baggage.

Tips

  • Exclude health checks and agent-card fetches. Server-side HTTP instrumentation turns every direct request — including /.well-known/agent-card.json polls 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 with ignoreIncomingRequestHook):
  • 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.