Skip to content

Commit 91fd1be

Browse files
committed
feat: Add OpenTelemetry tracing support
Adds OpenTelemetry integration for distributed tracing: - Client: OpenTelemetryClientTransport decorator wrapping all client operations with span creation and context propagation via ClientTransportWrapper SPI - Server: OpenTelemetryRequestHandlerDecorator CDI decorator for request handlers - Common: A2AObservabilityNames for standard observability attributes - Integration tests with comprehensive coverage for client and server tracing - Updated HelloWorld examples with OTEL configuration and documentation Fixes a2aproject#388 Signed-off-by: Emmanuel Hugonnet <ehugonne@redhat.com>
1 parent 5d2cd75 commit 91fd1be

File tree

63 files changed

+4436
-178
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+4436
-178
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ release.properties
77
.flattened-pom.xml
88
*.args
99

10+
#Claude
11+
CLAUDE.md
12+
1013
# Eclipse
1114
.project
1215
.classpath
@@ -21,6 +24,7 @@ bin/
2124

2225
# NetBeans
2326
nb-configuration.xml
27+
nbactions.xml
2428

2529
# Visual Studio Code
2630
.vscode
@@ -54,3 +58,4 @@ nbactions.xml
5458
.serena/
5559
.bob/
5660
claudedocs
61+
.bob/

boms/extras/pom.xml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,26 @@
3939
<artifactId>a2a-java-sdk-http-client-vertx</artifactId>
4040
<version>${project.version}</version>
4141
</dependency>
42+
<dependency>
43+
<groupId>${project.groupId}</groupId>
44+
<artifactId>a2a-java-sdk-opentelemetry-common</artifactId>
45+
<version>${project.version}</version>
46+
</dependency>
47+
<dependency>
48+
<groupId>${project.groupId}</groupId>
49+
<artifactId>a2a-java-sdk-opentelemetry-client</artifactId>
50+
<version>${project.version}</version>
51+
</dependency>
52+
<dependency>
53+
<groupId>${project.groupId}</groupId>
54+
<artifactId>a2a-java-sdk-opentelemetry-client-propagation</artifactId>
55+
<version>${project.version}</version>
56+
</dependency>
57+
<dependency>
58+
<groupId>${project.groupId}</groupId>
59+
<artifactId>a2a-java-sdk-opentelemetry-server</artifactId>
60+
<version>${project.version}</version>
61+
</dependency>
4262
<dependency>
4363
<groupId>${project.groupId}</groupId>
4464
<artifactId>a2a-java-extras-task-store-database-jpa</artifactId>

boms/extras/src/it/extras-usage-test/pom.xml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,22 @@
4848
<groupId>io.github.a2asdk</groupId>
4949
<artifactId>a2a-java-sdk-http-client-vertx</artifactId>
5050
</dependency>
51+
<dependency>
52+
<groupId>io.github.a2asdk</groupId>
53+
<artifactId>a2a-java-sdk-opentelemetry-common</artifactId>
54+
</dependency>
55+
<dependency>
56+
<groupId>io.github.a2asdk</groupId>
57+
<artifactId>a2a-java-sdk-opentelemetry-client</artifactId>
58+
</dependency>
59+
<dependency>
60+
<groupId>io.github.a2asdk</groupId>
61+
<artifactId>a2a-java-sdk-opentelemetry-client-propagation</artifactId>
62+
</dependency>
63+
<dependency>
64+
<groupId>io.github.a2asdk</groupId>
65+
<artifactId>a2a-java-sdk-opentelemetry-server</artifactId>
66+
</dependency>
5167
<dependency>
5268
<groupId>io.github.a2asdk</groupId>
5369
<artifactId>a2a-java-extras-task-store-database-jpa</artifactId>

boms/extras/src/it/extras-usage-test/src/main/java/io/a2a/test/ExtrasBomVerifier.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ public class ExtrasBomVerifier extends DynamicBomVerifier {
1717
"tck/", // TCK test suite
1818
"tests/", // Integration tests
1919
"extras/queue-manager-replicated/tests-multi-instance/", // Test harness applications
20-
"extras/queue-manager-replicated/tests-single-instance/" // Test harness applications
20+
"extras/queue-manager-replicated/tests-single-instance/", // Test harness applications
21+
"extras/opentelemetry/integration-tests/" // Test harness applications
2122
// Note: extras/ production modules are NOT in this list - we want to verify those classes load
2223
);
2324

client/base/src/main/java/io/a2a/client/ClientBuilder.java

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,27 @@
66
import java.util.List;
77
import java.util.Map;
88
import java.util.ServiceLoader;
9+
import java.util.ServiceLoader.Provider;
910
import java.util.function.BiConsumer;
1011
import java.util.function.Consumer;
12+
import java.util.stream.Collectors;
1113

1214
import io.a2a.client.config.ClientConfig;
1315
import io.a2a.client.transport.spi.ClientTransport;
1416
import io.a2a.client.transport.spi.ClientTransportConfig;
1517
import io.a2a.client.transport.spi.ClientTransportConfigBuilder;
1618
import io.a2a.client.transport.spi.ClientTransportProvider;
19+
import io.a2a.client.transport.spi.ClientTransportWrapper;
1720
import io.a2a.spec.A2AClientException;
1821
import io.a2a.spec.AgentCard;
1922
import io.a2a.spec.AgentInterface;
2023
import io.a2a.spec.TransportProtocol;
2124
import org.jspecify.annotations.NonNull;
2225
import org.jspecify.annotations.Nullable;
2326

27+
import org.slf4j.Logger;
28+
import org.slf4j.LoggerFactory;
29+
2430
/**
2531
* Builder for creating instances of {@link Client} to communicate with A2A agents.
2632
* <p>
@@ -96,6 +102,7 @@ public class ClientBuilder {
96102

97103
private static final Map<String, ClientTransportProvider<? extends ClientTransport, ? extends ClientTransportConfig<?>>> transportProviderRegistry = new HashMap<>();
98104
private static final Map<Class<? extends ClientTransport>, String> transportProtocolMapping = new HashMap<>();
105+
private static final Logger LOGGER = LoggerFactory.getLogger(ClientBuilder.class);
99106

100107
static {
101108
ServiceLoader<ClientTransportProvider> loader = ServiceLoader.load(ClientTransportProvider.class);
@@ -318,7 +325,7 @@ private ClientTransport buildClientTransport() throws A2AClientException {
318325
throw new A2AClientException("Missing required TransportConfig for " + agentInterface.protocolBinding());
319326
}
320327

321-
return clientTransportProvider.create(clientTransportConfig, agentCard, agentInterface);
328+
return wrap(clientTransportProvider.create(clientTransportConfig, agentCard, agentInterface), clientTransportConfig);
322329
}
323330

324331
private Map<String, String> getServerPreferredTransports() throws A2AClientException {
@@ -373,10 +380,55 @@ private AgentInterface findBestClientTransport() throws A2AClientException {
373380
if (transportProtocol == null || transportUrl == null) {
374381
throw new A2AClientException("No compatible transport found");
375382
}
376-
if (! transportProviderRegistry.containsKey(transportProtocol)) {
383+
if (!transportProviderRegistry.containsKey(transportProtocol)) {
377384
throw new A2AClientException("No client available for " + transportProtocol);
378385
}
379386

380387
return new AgentInterface(transportProtocol, transportUrl);
381388
}
389+
390+
/**
391+
* Wraps the transport with all available transport wrappers discovered via ServiceLoader.
392+
* Wrappers are applied in reverse priority order (lowest priority first) to build a stack
393+
* where the highest priority wrapper is the outermost layer.
394+
*
395+
* @param transport the base transport to wrap
396+
* @param clientTransportConfig the transport configuration
397+
* @return the wrapped transport (or original if no wrappers are available/applicable)
398+
*/
399+
private ClientTransport wrap(ClientTransport transport, ClientTransportConfig<? extends ClientTransport> clientTransportConfig) {
400+
ServiceLoader<ClientTransportWrapper> wrapperLoader = ServiceLoader.load(ClientTransportWrapper.class);
401+
402+
// Collect all wrappers, sort by priority, then reverse for stack application
403+
List<ClientTransportWrapper> wrappers = wrapperLoader.stream().map(Provider::get)
404+
.sorted()
405+
.collect(Collectors.toList());
406+
407+
if (wrappers.isEmpty()) {
408+
LOGGER.debug("No client transport wrappers found via ServiceLoader");
409+
return transport;
410+
}
411+
LOGGER.debug(wrappers.size() + " client transport wrappers found via ServiceLoader");
412+
413+
// Reverse to apply lowest priority first (building stack with highest priority outermost)
414+
java.util.Collections.reverse(wrappers);
415+
416+
// Apply wrappers to build stack
417+
ClientTransport wrapped = transport;
418+
for (ClientTransportWrapper wrapper : wrappers) {
419+
try {
420+
ClientTransport newWrapped = wrapper.wrap(wrapped, clientTransportConfig);
421+
if (newWrapped != wrapped) {
422+
LOGGER.debug("Applied transport wrapper: {} (priority: {})",
423+
wrapper.getClass().getName(), wrapper.priority());
424+
}
425+
wrapped = newWrapped;
426+
} catch (Exception e) {
427+
LOGGER.warn("Failed to apply transport wrapper {}: {}",
428+
wrapper.getClass().getName(), e.getMessage(), e);
429+
}
430+
}
431+
432+
return wrapped;
433+
}
382434
}

client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransportConfig.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
* @see A2AHttpClient
4949
* @see io.a2a.client.http.JdkA2AHttpClient
5050
*/
51-
public class RestTransportConfig extends ClientTransportConfig<RestTransport> {
51+
public class RestTransportConfig extends ClientTransportConfig<RestTransport> {
5252

5353
private final @Nullable A2AHttpClient httpClient;
5454

client/transport/spi/src/main/java/io/a2a/client/transport/spi/ClientTransportConfig.java

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package io.a2a.client.transport.spi;
22

33
import java.util.ArrayList;
4+
5+
import java.util.HashMap;
46
import java.util.List;
7+
import java.util.Map;
58

69
import io.a2a.client.transport.spi.interceptors.ClientCallInterceptor;
10+
import java.util.Collections;
711

812
/**
913
* Base configuration class for A2A client transport protocols.
@@ -34,7 +38,8 @@
3438
*/
3539
public abstract class ClientTransportConfig<T extends ClientTransport> {
3640

37-
protected List<ClientCallInterceptor> interceptors = new ArrayList<>();
41+
protected List<ClientCallInterceptor> interceptors = Collections.emptyList();
42+
protected Map<String, ? extends Object> parameters = Collections.emptyMap();
3843

3944
/**
4045
* Set the list of request/response interceptors.
@@ -63,4 +68,28 @@ public void setInterceptors(List<ClientCallInterceptor> interceptors) {
6368
public List<ClientCallInterceptor> getInterceptors() {
6469
return java.util.Collections.unmodifiableList(interceptors);
6570
}
71+
72+
/**
73+
* Set the Map of config parameters.
74+
* The provided map is copied to prevent external modifications from affecting
75+
* this configuration.
76+
*
77+
* @param parameters the map of parameters to use (will be copied)
78+
*/
79+
public void setParameters(Map<String, ? extends Object > parameters) {
80+
this.parameters = new HashMap<>(parameters);
81+
}
82+
83+
84+
/**
85+
* Get the list of configured parameters.
86+
* <p>
87+
* Returns an unmodifiable view of the parameters map. Attempting to modify
88+
* the returned map will throw {@link UnsupportedOperationException}.
89+
*
90+
* @return an unmodifiable map of configured parameters (never null, but may be empty)
91+
*/
92+
public Map<String, ? extends Object > getParameters() {
93+
return java.util.Collections.unmodifiableMap(parameters);
94+
}
6695
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package io.a2a.client.transport.spi;
2+
3+
/**
4+
* Service provider interface for wrapping client transports with additional functionality.
5+
* Implementations can add cross-cutting concerns like tracing, metrics, logging, etc.
6+
*
7+
* <p>Wrappers are discovered via Java's ServiceLoader mechanism. To register a wrapper,
8+
* create a file {@code META-INF/services/io.a2a.client.transport.spi.ClientTransportWrapper}
9+
* containing the fully qualified class name of your implementation.
10+
*
11+
* <p>Wrappers are sorted by priority in descending order (highest priority first).
12+
* This interface implements {@link Comparable} to enable natural sorting.
13+
*
14+
* <p>Example implementation:
15+
* <pre>{@code
16+
* public class TracingWrapper implements ClientTransportWrapper {
17+
* @Override
18+
* public ClientTransport wrap(ClientTransport transport, ClientTransportConfig<?> config) {
19+
* if (config.getParameters().containsKey("tracer")) {
20+
* return new TracingTransport(transport, (Tracer) config.getParameters().get("tracer"));
21+
* }
22+
* return transport;
23+
* }
24+
*
25+
* @Override
26+
* public int priority() {
27+
* return 100; // Higher priority = wraps earlier (outermost)
28+
* }
29+
* }
30+
* }</pre>
31+
*/
32+
public interface ClientTransportWrapper extends Comparable<ClientTransportWrapper> {
33+
34+
/**
35+
* Wraps the given transport with additional functionality.
36+
*
37+
* <p>Implementations should check the configuration to determine if they should
38+
* actually wrap the transport. If the wrapper is not applicable (e.g., required
39+
* configuration is missing), return the original transport unchanged.
40+
*
41+
* @param transport the transport to wrap
42+
* @param config the transport configuration, may contain wrapper-specific parameters
43+
* @return the wrapped transport, or the original if wrapping is not applicable
44+
*/
45+
ClientTransport wrap(ClientTransport transport, ClientTransportConfig<?> config);
46+
47+
/**
48+
* Returns the priority of this wrapper. Higher priority wrappers are applied first
49+
* (wrap the transport earlier, resulting in being the outermost wrapper).
50+
*
51+
* <p>Default priority is 0. Suggested ranges:
52+
* <ul>
53+
* <li>1000+ : Critical infrastructure (security, authentication)
54+
* <li>500-999: Observability (tracing, metrics, logging)
55+
* <li>100-499: Enhancement (caching, retry logic)
56+
* <li>0-99: Optional features
57+
* </ul>
58+
*
59+
* @return the priority value, higher values = higher priority
60+
*/
61+
default int priority() {
62+
return 0;
63+
}
64+
65+
/**
66+
* Compares this wrapper with another based on priority.
67+
* Returns a negative integer, zero, or a positive integer as this wrapper
68+
* has higher priority than, equal to, or lower priority than the specified wrapper.
69+
*
70+
* <p>Note: This comparison is reversed (higher priority comes first) to enable
71+
* natural sorting in descending priority order.
72+
*
73+
* @param other the wrapper to compare to
74+
* @return negative if this has higher priority, positive if lower, zero if equal
75+
*/
76+
@Override
77+
default int compareTo(ClientTransportWrapper other) {
78+
// Reverse comparison: higher priority should come first
79+
return Integer.compare(other.priority(), this.priority());
80+
}
81+
}

0 commit comments

Comments
 (0)