OpenTelemetry
OpenTelemetry observability integration for the A2A Java SDK with distributed tracing and context propagation for servers and clients.
OpenTelemetry
Adds distributed tracing and context propagation to A2A servers and clients using OpenTelemetry.
Features
- Distributed Tracing: Automatic span creation for all A2A protocol methods
- Context Propagation: OpenTelemetry trace context propagation across async operations
- Request/Response Logging: Optional extraction of request and response data into spans
- Error Tracking: Automatic error status and error type attributes on failures
- Metrics:
gen_ai.client.operation.durationhistogram on the client side;gen_ai.agent.a2a.streaming.durationhistogram for full streaming lifecycle on the server side
Modules
| Module | Artifact ID | Description |
|---|---|---|
| Common | a2a-java-sdk-opentelemetry-common |
Shared utilities and constants |
| Server | a2a-java-sdk-opentelemetry-server |
Server-side tracing |
| Client | a2a-java-sdk-opentelemetry-client |
Client-side instrumentation |
| Client Propagation | a2a-java-sdk-opentelemetry-client-propagation |
Context propagation for async client operations |
Server
The OpenTelemetryRequestHandlerDecorator wraps the default request handler to create spans for every A2A protocol method, with automatic error tracking.
Add Dependency
<dependency>
<groupId>org.a2aproject.sdk</groupId>
<artifactId>a2a-java-sdk-opentelemetry-server</artifactId>
</dependency>
Architecture
Client Request
|
OpenTelemetryRequestHandlerDecorator
| (creates span)
Default RequestHandler
|
Agent Execution (with context propagation)
|
Response
HTTP Request (with trace headers)
|
OpenTelemetry extracts context
|
Span created for A2A method
|
ManagedExecutor propagates context
|
Async agent execution (maintains trace context)
|
Response (with trace headers)
Streaming Span Lifecycle
Streaming methods (message/stream and tasks/resubscribe) follow a two-span model to correctly represent the full duration of a streaming response.
Span 1 — method span is created when the request arrives and ended as soon as the publisher is returned to the transport layer. It captures request attributes and the initial outcome (success or synchronous error).
Per-event child spans (<method>-event) are created for each streaming event published via onNext(). Each child span carries a link back to Span 1 and is made the current span during delegate execution, so any nested telemetry is correctly parented.
Span 2 — end span (<method>-end) is created when the stream terminates — either on completion or error. It also carries a link to Span 1 for correlation.
sequenceDiagram
participant C as Client
participant D as OTel Decorator
participant H as RequestHandler
participant W as WrappedPublisher
C->>D: onMessageSendStream(params)
D->>D: create Span₁ (SERVER)
D->>H: onMessageSendStream(params)
H-->>D: publisher
D->>D: capture Span₁.context
D->>D: Span₁.setStatus(OK) · Span₁.end()
D-->>C: WrappedPublisher
note over D: Span₁ closed immediately
loop for each streaming event
W->>W: create child span (link→Span₁)
W-->>C: onNext(event)
W->>W: child span.end()
end
alt stream completes normally
W->>W: create Span₂ (SERVER, link→Span₁)
W->>W: Span₂.setStatus(OK) · Span₂.end()
W-->>C: onComplete()
else stream errors
W->>W: create Span₂ (SERVER, link→Span₁)
W->>W: Span₂.setStatus(ERROR) · Span₂.end()
W-->>C: onError(throwable)
end
Each per-event child span carries:
| Attribute | Value |
|---|---|
gen_ai.agent.a2a.streaming-event |
Event kind identifier ("message", "task", "statusUpdate", "artifactUpdate") |
gen_ai.agent.a2a.task.status.state |
Task status state (for TaskStatusUpdateEvent and Task events) |
gen_ai.agent.a2a.task_id |
Task identifier (when available from the event) |
Metrics
The server decorator records the following histogram instruments:
| Instrument | Type | Unit | Description |
|---|---|---|---|
gen_ai.agent.a2a.streaming.duration |
Histogram | s |
Duration from stream initiation to last event, for message/stream and tasks/resubscribe |
Attributes on gen_ai.agent.a2a.streaming.duration:
| Attribute | Value |
|---|---|
gen_ai.agent.a2a.operation.name |
A2A method name (message/stream or tasks/resubscribe) |
gen_ai.system |
a2a |
error.type |
error (only on stream failure) |
Meter injection uses CDI Instance<Meter>, so the histogram is silently skipped if no Meter bean is available in the container.
Context-Aware Async Executor
Note: The
AsyncManagedExecutorProduceris provided by the Quarkus reference server (reference/common), not the OpenTelemetry module. It is documented here because it enables context propagation (including trace context) across async boundaries.
The reference server replaces the default AsyncExecutorProducer with AsyncManagedExecutorProducer:
- Priority 20: takes precedence over the default executor (priority 10)
- Automatic activation: no configuration needed — included automatically in the Quarkus reference server
- Context propagation: uses MicroProfile Context Propagation to maintain trace context and CDI request context across async boundaries
Note: Unlike the default
AsyncExecutorProducer, theAsyncManagedExecutorProducerdoes not use thea2a.executor.*configuration properties. Pool sizing is controlled by the container'sManagedExecutorconfiguration.
Quarkus Configuration
quarkus.thread-pool.core-threads=10
quarkus.thread-pool.max-threads=50
quarkus.thread-pool.queue-size=100
For other runtimes, consult your MicroProfile Context Propagation implementation documentation.
Span Attributes
The following attributes are automatically added to spans (defined in A2AObservabilityNames):
| Attribute | Description |
|---|---|
gen_ai.agent.a2a.operation.name |
The A2A method name (e.g., message/send, tasks/get) |
gen_ai.agent.a2a.task_id |
Task identifier (when available) |
gen_ai.agent.a2a.context_id |
Context/conversation identifier (when available) |
gen_ai.agent.a2a.message_id |
Message identifier (when available) |
gen_ai.agent.a2a.role |
Message role (when available) |
gen_ai.agent.a2a.extensions |
Comma-separated extension names (when present) |
gen_ai.agent.a2a.parts.number |
Number of message parts |
gen_ai.agent.a2a.config_id |
Push notification config identifier (when available) |
gen_ai.agent.a2a.request |
Full request parameters (only if extraction enabled) |
gen_ai.agent.a2a.response |
Full response data (only if extraction enabled) |
error.type |
Error message (on failures) |
Push Notification Delivery Tracing
When the server delivers a push notification to a client webhook, OpenTelemetryPushNotificationSenderDecorator creates a CLIENT span named SendPushNotification. This span is independent of the original request span but carries the gen_ai.agent.a2a.task_id attribute, allowing correlation by task ID in your observability tool. The span status reflects the actual delivery outcome: OK when all notifications succeed, ERROR when one or more deliveries fail (with error.type set to delivery_failed) or when an exception is thrown.
The module also provides W3C trace-context propagation for outbound push notification HTTP requests via the @Instrumented CDI qualifier and OpenTelemetryHttpClientProducer.
How it works
The OpenTelemetryHttpClientProducer CDI bean produces an @Instrumented A2AHttpClient — a wrapper that injects traceparent and tracestate headers into every outbound HTTP request. BasePushNotificationSender resolves the HTTP client in this order:
- An
@Instrumented A2AHttpClientbean (if available — e.g. the one produced by this module) - An unqualified
A2AHttpClientbean (if the application provides one) A2AHttpClientFactory.create(A2AHttpClientConfiguration.DEFAULT)as last resort
The @Instrumented qualifier (defined in server-common) marks HTTP client beans that have been wrapped with observability instrumentation. It ensures the instrumented client does not compete with the application's own A2AHttpClient bean in default CDI resolution — the two coexist, and the instrumented variant wraps the application's bean as its delegate.
Custom HTTP client compatibility
If your application provides a custom A2AHttpClient CDI bean (e.g. for mTLS, proxy, or custom timeouts), the OpenTelemetry wrapper preserves that configuration by delegating to your bean rather than replacing it. No additional annotations or configuration are needed — just produce your bean as usual:
@Produces
@ApplicationScoped
public A2AHttpClient myHttpClient() {
// your custom client (mTLS, proxy, auth, etc.)
}
The OpenTelemetry module automatically wraps it with trace-context injection.
Context-propagating executor
The producer also creates an @Instrumented Executor (via Context.taskWrapping()) that ensures the OTel trace context survives the async boundary when push notifications are dispatched to multiple recipients in parallel. This allows the receiving webhook to join the distributed trace.
BasePushNotificationSender resolves the dispatch executor in this order:
- An
@Instrumented Executorbean (if available — e.g. the one produced by this module) - A
@Named("a2a.pushNotificationDispatch") Executorbean (if the application provides one) ForkJoinPool.commonPool()as last resort
Custom dispatch executor compatibility
If your application provides a custom @Named("a2a.pushNotificationDispatch") Executor CDI bean (e.g. for custom thread pool sizing), the OpenTelemetry module preserves that configuration by wrapping it with Context.taskWrapping() rather than replacing it. Just produce your bean as usual:
@Produces
@ApplicationScoped
@Named("a2a.pushNotificationDispatch")
public Executor myDispatchExecutor() {
// your custom thread pool
}
The OpenTelemetry module automatically wraps it with context propagation.
| Attribute | Description |
|---|---|
gen_ai.agent.a2a.operation.name |
SendPushNotification |
gen_ai.agent.a2a.task_id |
Task identifier |
gen_ai.agent.a2a.context_id |
Context identifier (when available) |
gen_ai.agent.a2a.push_notification.event_kind |
The type of streaming event delivered (e.g. TaskStatusUpdateEvent) |
gen_ai.agent.a2a.response |
Full event payload (only if extraction enabled) |
error.type |
delivery_failed when one or more deliveries fail, or the exception message when an exception is thrown |
Request/Response Extraction
Enable request and response data extraction in spans using JVM system properties:
# Extract request parameters into span attributes (disabled by default)
-Dorg.a2aproject.sdk.server.extract.request=true
# Extract response data into span attributes (disabled by default)
-Dorg.a2aproject.sdk.server.extract.response=true
Warning: Extracting request/response data may expose sensitive information in traces. Use with caution in production environments.
Client
Instrumentation
Adds OpenTelemetry spans and metrics to A2A client operations:
<dependency>
<groupId>org.a2aproject.sdk</groupId>
<artifactId>a2a-java-sdk-opentelemetry-client</artifactId>
</dependency>
Metrics
The client transport wrapper is auto-discovered via ServiceLoader — just adding the dependency enables tracing. To also enable gen_ai.client.operation.duration metrics, pass a Meter via the transport config:
config.setParameters(Map.of(
OpenTelemetryClientTransportWrapper.OTEL_TRACER_KEY, openTelemetry.getTracer("my-service"),
OpenTelemetryClientTransportWrapper.OTEL_METER_KEY, openTelemetry.getMeter("my-service")
));
If no Meter is provided, the histogram is silently skipped.
| Instrument | Type | Unit | Description |
|---|---|---|---|
gen_ai.client.operation.duration |
Histogram | s |
Duration of each A2A client operation call |
Attributes on gen_ai.client.operation.duration:
| Attribute | Value |
|---|---|
gen_ai.agent.a2a.operation.name |
A2A method name (e.g., message/send, tasks/get) |
gen_ai.system |
a2a |
error.type |
error (only on failure) |
Context Propagation
For trace context propagation across async client boundaries:
<dependency>
<groupId>org.a2aproject.sdk</groupId>
<artifactId>a2a-java-sdk-opentelemetry-client-propagation</artifactId>
</dependency>
Troubleshooting
Context Not Propagating
Symptom: Spans in async tasks are not linked to parent spans.
Solution: Ensure the OpenTelemetry server module is included and check logs for:
Initializing ManagedExecutor for async operations with CDI context propagation
ManagedExecutor Not Available
Symptom: IllegalStateException: ManagedExecutor not injected - ensure MicroProfile Context Propagation is available
Solution: Ensure your runtime provides MicroProfile Context Propagation. For Quarkus, add:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-context-propagation</artifactId>
</dependency>
Performance Impact
- Disable request/response extraction in production unless needed
- Configure sampling rate to reduce trace volume
- Ensure your OpenTelemetry collector is properly sized
Best Practices
- Configure appropriate sampling rates for production environments
- Disable request/response extraction if handling sensitive data
- Add service name and version as resource attributes
- Use batch processors in your OpenTelemetry collector to reduce network overhead
- Monitor the OpenTelemetry collector's health and performance
Dependencies
- MicroProfile Telemetry 2.0.1+
- MicroProfile Context Propagation 1.3+
- OpenTelemetry API
- A2A Server Common