Skip to content

Commit d5cfb1d

Browse files
committed
feat: Add telemetry support.
* Adding an annotation that can be used by CDI interceptor to create spans on current exchanges. * Adding support for client wrappers to be able to add client side spans. * Updating the helloworld example to use opentelemetry. Fixing issue #388 Signed-off-by: Emmanuel Hugonnet <[email protected]>
1 parent fcc02ec commit d5cfb1d

File tree

40 files changed

+1334
-71
lines changed

40 files changed

+1334
-71
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ pom.xml.versionsBackup
66
release.properties
77
.flattened-pom.xml
88

9+
#Claude
10+
CLAUDE.md
11+
912
# Eclipse
1013
.project
1114
.classpath
@@ -20,6 +23,7 @@ bin/
2023

2124
# NetBeans
2225
nb-configuration.xml
26+
nbactions.xml
2327

2428
# Visual Studio Code
2529
.vscode

boms/extras/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@
3434
<artifactId>a2a-java-extras-common</artifactId>
3535
<version>${project.version}</version>
3636
</dependency>
37+
<dependency>
38+
<groupId>${project.groupId}</groupId>
39+
<artifactId>a2a-java-sdk-opentelemetry</artifactId>
40+
<version>${project.version}</version>
41+
</dependency>
3742
<dependency>
3843
<groupId>${project.groupId}</groupId>
3944
<artifactId>a2a-java-extras-task-store-database-jpa</artifactId>

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@
4444
<groupId>io.github.a2asdk</groupId>
4545
<artifactId>a2a-java-extras-common</artifactId>
4646
</dependency>
47+
<dependency>
48+
<groupId>io.github.a2asdk</groupId>
49+
<artifactId>a2a-java-sdk-opentelemetry</artifactId>
50+
</dependency>
4751
<dependency>
4852
<groupId>io.github.a2asdk</groupId>
4953
<artifactId>a2a-java-extras-task-store-database-jpa</artifactId>

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

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,32 @@
55
import io.a2a.client.transport.spi.ClientTransportConfig;
66
import io.a2a.client.transport.spi.ClientTransportConfigBuilder;
77
import io.a2a.client.transport.spi.ClientTransportProvider;
8+
import io.a2a.client.transport.spi.ClientTransportWrapper;
89
import io.a2a.spec.A2AClientException;
910
import io.a2a.spec.AgentCard;
1011
import io.a2a.spec.AgentInterface;
1112
import io.a2a.spec.TransportProtocol;
13+
import org.jspecify.annotations.NonNull;
14+
import org.jspecify.annotations.Nullable;
15+
import org.slf4j.Logger;
16+
import org.slf4j.LoggerFactory;
1217

1318
import java.util.ArrayList;
1419
import java.util.HashMap;
1520
import java.util.LinkedHashMap;
1621
import java.util.List;
1722
import java.util.Map;
1823
import java.util.ServiceLoader;
24+
import java.util.ServiceLoader.Provider;
1925
import java.util.function.BiConsumer;
2026
import java.util.function.Consumer;
21-
import org.jspecify.annotations.NonNull;
22-
import org.jspecify.annotations.Nullable;
27+
import java.util.stream.Collectors;
2328

2429
public class ClientBuilder {
2530

2631
private static final Map<String, ClientTransportProvider<? extends ClientTransport, ? extends ClientTransportConfig<?>>> transportProviderRegistry = new HashMap<>();
2732
private static final Map<Class<? extends ClientTransport>, String> transportProtocolMapping = new HashMap<>();
33+
private static final Logger LOGGER = LoggerFactory.getLogger(ClientBuilder.class);
2834

2935
static {
3036
ServiceLoader<ClientTransportProvider> loader = ServiceLoader.load(ClientTransportProvider.class);
@@ -37,7 +43,8 @@ public class ClientBuilder {
3743
private final AgentCard agentCard;
3844

3945
private final List<BiConsumer<ClientEvent, AgentCard>> consumers = new ArrayList<>();
40-
private @Nullable Consumer<Throwable> streamErrorHandler;
46+
private @Nullable
47+
Consumer<Throwable> streamErrorHandler;
4148
private ClientConfig clientConfig = new ClientConfig.Builder().build();
4249

4350
private final Map<Class<? extends ClientTransport>, ClientTransportConfig<? extends ClientTransport>> clientTransports = new LinkedHashMap<>();
@@ -105,7 +112,7 @@ private ClientTransport buildClientTransport() throws A2AClientException {
105112
throw new A2AClientException("Missing required TransportConfig for " + agentInterface.transport());
106113
}
107114

108-
return clientTransportProvider.create(clientTransportConfig, agentCard, agentInterface.url());
115+
return wrap(clientTransportProvider.create(clientTransportConfig, agentCard, agentInterface.url()), clientTransportConfig);
109116
}
110117

111118
private Map<String, String> getServerPreferredTransports() {
@@ -160,10 +167,50 @@ private AgentInterface findBestClientTransport() throws A2AClientException {
160167
if (transportProtocol == null || transportUrl == null) {
161168
throw new A2AClientException("No compatible transport found");
162169
}
163-
if (! transportProviderRegistry.containsKey(transportProtocol)) {
170+
if (!transportProviderRegistry.containsKey(transportProtocol)) {
164171
throw new A2AClientException("No client available for " + transportProtocol);
165172
}
166173

167174
return new AgentInterface(transportProtocol, transportUrl);
168175
}
176+
177+
/**
178+
* Wraps the transport with all available transport wrappers discovered via ServiceLoader.
179+
* Wrappers are applied in priority order (highest priority first).
180+
*
181+
* @param transport the base transport to wrap
182+
* @param clientTransportConfig the transport configuration
183+
* @return the wrapped transport (or original if no wrappers are available/applicable)
184+
*/
185+
private ClientTransport wrap(ClientTransport transport, ClientTransportConfig<? extends ClientTransport> clientTransportConfig) {
186+
ServiceLoader<ClientTransportWrapper> wrapperLoader = ServiceLoader.load(ClientTransportWrapper.class);
187+
188+
// Collect all wrappers and sort by natural order (uses Comparable implementation)
189+
List<ClientTransportWrapper> wrappers = wrapperLoader.stream().map(Provider::get)
190+
.sorted()
191+
.collect(Collectors.toList());
192+
193+
if (wrappers.isEmpty()) {
194+
LOGGER.debug("No client transport wrappers found via ServiceLoader");
195+
return transport;
196+
}
197+
198+
// Apply wrappers in priority order
199+
ClientTransport wrapped = transport;
200+
for (ClientTransportWrapper wrapper : wrappers) {
201+
try {
202+
ClientTransport newWrapped = wrapper.wrap(wrapped, clientTransportConfig);
203+
if (newWrapped != wrapped) {
204+
LOGGER.debug("Applied transport wrapper: {} (priority: {})",
205+
wrapper.getClass().getName(), wrapper.priority());
206+
}
207+
wrapped = newWrapped;
208+
} catch (Exception e) {
209+
LOGGER.warn("Failed to apply transport wrapper {}: {}",
210+
wrapper.getClass().getName(), e.getMessage(), e);
211+
}
212+
}
213+
214+
return wrapped;
215+
}
169216
}

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
@@ -4,7 +4,7 @@
44
import io.a2a.client.transport.spi.ClientTransportConfig;
55
import org.jspecify.annotations.Nullable;
66

7-
public class RestTransportConfig extends ClientTransportConfig<RestTransport> {
7+
public class RestTransportConfig extends ClientTransportConfig<RestTransport> {
88

99
private final @Nullable A2AHttpClient httpClient;
1010

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@
22

33
import io.a2a.client.transport.spi.interceptors.ClientCallInterceptor;
44
import java.util.ArrayList;
5+
import java.util.HashMap;
56

67
import java.util.List;
8+
import java.util.Map;
79

810
/**
911
* Configuration for an A2A client transport.
1012
*/
1113
public abstract class ClientTransportConfig<T extends ClientTransport> {
1214

1315
protected List<ClientCallInterceptor> interceptors = new ArrayList<>();
16+
protected Map<String, ? extends Object > parameters = new HashMap<>();
1417

1518
public void setInterceptors(List<ClientCallInterceptor> interceptors) {
1619
this.interceptors = new ArrayList<>(interceptors);
@@ -19,4 +22,12 @@ public void setInterceptors(List<ClientCallInterceptor> interceptors) {
1922
public List<ClientCallInterceptor> getInterceptors() {
2023
return interceptors;
2124
}
25+
26+
public void setParameters(Map<String, ? extends Object > parameters) {
27+
this.parameters = new HashMap<>(parameters);
28+
}
29+
30+
public Map<String, ? extends Object > getParameters() {
31+
return parameters;
32+
}
2233
}
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)