Wrappers are discovered via Java's ServiceLoader mechanism. To register a wrapper, + * create a file {@code META-INF/services/io.a2a.client.transport.spi.ClientTransportWrapper} + * containing the fully qualified class name of your implementation. + * + *
Wrappers are sorted by priority in descending order (highest priority first). + * This interface implements {@link Comparable} to enable natural sorting. + * + *
Example implementation: + *
{@code
+ * public class TracingWrapper implements ClientTransportWrapper {
+ * @Override
+ * public ClientTransport wrap(ClientTransport transport, ClientTransportConfig> config) {
+ * if (config.getParameters().containsKey("tracer")) {
+ * return new TracingTransport(transport, (Tracer) config.getParameters().get("tracer"));
+ * }
+ * return transport;
+ * }
+ *
+ * @Override
+ * public int priority() {
+ * return 100; // Higher priority = wraps earlier (outermost)
+ * }
+ * }
+ * }
+ */
+public interface ClientTransportWrapper extends ComparableImplementations should check the configuration to determine if they should + * actually wrap the transport. If the wrapper is not applicable (e.g., required + * configuration is missing), return the original transport unchanged. + * + * @param transport the transport to wrap + * @param config the transport configuration, may contain wrapper-specific parameters + * @return the wrapped transport, or the original if wrapping is not applicable + */ + ClientTransport wrap(ClientTransport transport, ClientTransportConfig> config); + + /** + * Returns the priority of this wrapper. Higher priority wrappers are applied first + * (wrap the transport earlier, resulting in being the outermost wrapper). + * + *
Default priority is 0. Suggested ranges: + *
Note: This comparison is reversed (higher priority comes first) to enable
+ * natural sorting in descending priority order.
+ *
+ * @param other the wrapper to compare to
+ * @return negative if this has higher priority, positive if lower, zero if equal
+ */
+ @Override
+ default int compareTo(ClientTransportWrapper other) {
+ // Reverse comparison: higher priority should come first
+ return Integer.compare(other.priority(), this.priority());
+ }
+}
diff --git a/examples/helloworld/client/README.md b/examples/helloworld/client/README.md
index ac01c890f..7f484607c 100644
--- a/examples/helloworld/client/README.md
+++ b/examples/helloworld/client/README.md
@@ -41,9 +41,9 @@ The Python A2A server is part of the [a2a-samples](https://github.com/google-a2a
The server will start running on `http://localhost:9999`.
-## Run the Java A2A Client with JBang
+## Run the Java A2A Client
-The Java client can be run using JBang, which allows you to run Java source files directly without any manual compilation.
+The Java client can be run using either Maven or JBang.
### Build the A2A Java SDK
@@ -54,9 +54,23 @@ cd /path/to/a2a-java
mvn clean install
```
-### Using the JBang script
+### Option 1: Using Maven (Recommended)
-A JBang script is provided in the example directory to make running the client easy:
+Run the client using Maven's exec plugin:
+
+```bash
+cd examples/helloworld/client
+mvn exec:java
+```
+
+To enable OpenTelemetry with Maven:
+```bash
+mvn exec:java -Dopentelemetry=true
+```
+
+### Option 2: Using JBang
+
+A JBang script is provided for running the client without Maven:
1. Make sure you have JBang installed. If not, follow the [JBang installation guide](https://www.jbang.dev/documentation/guide/latest/installation.html).
@@ -70,20 +84,99 @@ A JBang script is provided in the example directory to make running the client e
jbang HelloWorldRunner.java
```
-This script automatically handles the dependencies and sources for you.
+To enable OpenTelemetry with JBang:
+```bash
+jbang -Dopentelemetry=true HelloWorldRunner.java
+```
## What the Example Does
The Java client (`HelloWorldClient.java`) performs the following actions:
1. Fetches the server's public agent card
-2. Fetches the server's extended agent card
+2. Fetches the server's extended agent card
3. Creates a client using the extended agent card that connects to the Python server at `http://localhost:9999`.
4. Sends a regular message asking "how much is 10 USD in INR?".
5. Prints the server's response.
6. Sends the same message as a streaming request.
7. Prints each chunk of the server's streaming response as it arrives.
+## Enable OpenTelemetry (Optional)
+
+The client includes support for distributed tracing with OpenTelemetry. To enable it:
+
+### Prerequisites
+
+**IMPORTANT**: The client expects an OpenTelemetry collector to be ready and accepting traces. You have two options:
+
+#### Option 1: Use the Java Server Example (Recommended)
+
+Instead of the Python server, use the Java server example which has built-in OpenTelemetry support:
+
+1. **Start the Java server with OpenTelemetry enabled**:
+ ```bash
+ cd examples/helloworld/server
+ mvn quarkus:dev -Popentelemetry
+ ```
+ This will:
+ - Start the server at `http://localhost:9999`
+ - Launch Grafana at `http://localhost:3001`
+ - Start OTLP collectors on ports 5317 (gRPC) and 5318 (HTTP)
+
+2. **Run the client with OpenTelemetry**:
+
+ Using Maven (from `examples/helloworld/client`):
+ ```bash
+ mvn exec:java -Dopentelemetry=true
+ ```
+
+ Or using JBang (from `examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/`):
+ ```bash
+ jbang -Dopentelemetry=true HelloWorldRunner.java
+ ```
+
+3. **View traces in Grafana**:
+ - Open `http://localhost:3001` (credentials: admin/admin)
+ - Go to "Explore" → select "Tempo" data source
+ - View distributed traces showing the full request flow from client to server
+
+#### Option 2: Use External OpenTelemetry Collector
+
+If you want to use the Python server with OpenTelemetry:
+
+1. **Start an OpenTelemetry collector** on port 5317 (e.g., using Docker):
+ ```bash
+ docker run -p 5317:4317 otel/opentelemetry-collector
+ ```
+
+2. **Run the Python server** as described above
+
+3. **Run the client with OpenTelemetry**:
+ ```bash
+ jbang -Dopentelemetry=true HelloWorldRunner.java
+ ```
+
+### What Gets Traced
+
+When OpenTelemetry is enabled, the client traces:
+- Agent card fetching (public and extended)
+- Message sending (blocking and streaming)
+- Task operations (get, cancel, list)
+- Push notification configuration operations
+- Connection and transport layer operations
+
+Client traces are automatically linked with server traces (when using the Java server), providing end-to-end visibility of the entire A2A protocol flow.
+
+### Configuration
+
+The client is configured to send traces to `http://localhost:5317` (OTLP gRPC endpoint). To use a different endpoint, modify the `initOpenTelemetry()` method in `HelloWorldClient.java`:
+
+```java
+OtlpGrpcSpanExporter.builder()
+ .setEndpoint("http://your-collector:4317")
+ .build()
+```
+
## Notes
- Make sure the Python server is running before starting the Java client.
diff --git a/examples/helloworld/client/pom.xml b/examples/helloworld/client/pom.xml
index 8f5b63406..408d3b3a8 100644
--- a/examples/helloworld/client/pom.xml
+++ b/examples/helloworld/client/pom.xml
@@ -12,7 +12,7 @@
This wrapper is automatically discovered via Java's ServiceLoader mechanism. + * To enable tracing, add a {@link Tracer} instance to the transport configuration: + *
{@code
+ * ClientTransportConfig config = new JSONRPCTransportConfig();
+ * config.setParameters(Map.of(
+ * OpenTelemetryClientTransportFactory.OTEL_TRACER_KEY,
+ * openTelemetry.getTracer("my-service")
+ * ));
+ * }
+ */
+public class OpenTelemetryClientTransportWrapper implements ClientTransportWrapper {
+
+ /**
+ * Configuration key for the OpenTelemetry Tracer instance.
+ * Value must be of type {@link Tracer}.
+ */
+ public static final String OTEL_TRACER_KEY = "io.a2a.extras.opentelemetry.Tracer";
+
+ @Override
+ public ClientTransport wrap(ClientTransport transport, ClientTransportConfig> config) {
+ Object tracerObj = config.getParameters().get(OTEL_TRACER_KEY);
+ if (tracerObj instanceof Tracer tracer) {
+ return new OpenTelemetryClientTransport(transport, tracer);
+ }
+ // No tracer configured, return unwrapped transport
+ return transport;
+ }
+
+ @Override
+ public int priority() {
+ // Observability/tracing should be in the middle priority range
+ // so it can observe other wrappers but doesn't interfere with security
+ return 500;
+ }
+}
diff --git a/extras/opentelemetry/src/main/java/io/a2a/extras/opentelemetry/SpanInterceptor.java b/extras/opentelemetry/src/main/java/io/a2a/extras/opentelemetry/SpanInterceptor.java
new file mode 100644
index 000000000..bd158f301
--- /dev/null
+++ b/extras/opentelemetry/src/main/java/io/a2a/extras/opentelemetry/SpanInterceptor.java
@@ -0,0 +1,93 @@
+package io.a2a.extras.opentelemetry;
+
+import io.a2a.server.interceptors.Kind;
+import io.a2a.server.interceptors.NoAttributeExtractor;
+import io.a2a.server.interceptors.Trace;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanBuilder;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.api.trace.StatusCode;
+import io.opentelemetry.api.trace.Tracer;
+import io.opentelemetry.context.Scope;
+import jakarta.annotation.Priority;
+import jakarta.inject.Inject;
+import jakarta.interceptor.AroundInvoke;
+import jakarta.interceptor.Interceptor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Map;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * Jakarta EE CDI interceptor for @Trace annotation.
+ * Integrates with OpenTelemetry to create spans for traced methods.
+ */
+@Trace()
+@Interceptor
+@Priority(Interceptor.Priority.APPLICATION)
+public class SpanInterceptor {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(SpanInterceptor.class);
+
+ @Inject
+ private Tracer tracer;
+
+ @AroundInvoke
+ public Object trace(jakarta.interceptor.InvocationContext jakartaContext) throws Exception {
+ // Convert Jakarta InvocationContext to our custom InvocationContext
+ io.a2a.server.interceptors.InvocationContext customContext
+ = new io.a2a.server.interceptors.InvocationContext(
+ jakartaContext.getTarget(),
+ jakartaContext.getMethod(),
+ jakartaContext.getParameters()
+ );
+
+ Kind kind = jakartaContext
+ .getMethod()
+ .getAnnotation(Trace.class)
+ .kind();
+ Class extends Supplierdiff --git a/spec/src/main/java/io/a2a/spec/Task.java b/spec/src/main/java/io/a2a/spec/Task.java index c2fd23d10..6e44e8b80 100644 --- a/spec/src/main/java/io/a2a/spec/Task.java +++ b/spec/src/main/java/io/a2a/spec/Task.java @@ -113,6 +113,11 @@ public String getKind() { return kind; } + @Override + public String toString() { + return "Task{" + "id=" + id + ", contextId=" + contextId + ", status=" + status + ", artifacts=" + artifacts + ", history=" + history + ", metadata=" + metadata + ", kind=" + kind + '}'; + } + /** * Builder for constructing immutable {@link Task} instances. *
diff --git a/transport/grpc/src/main/java/io/a2a/transport/grpc/context/GrpcContextKeys.java b/transport/grpc/src/main/java/io/a2a/transport/grpc/context/GrpcContextKeys.java
index 483daf7e8..3e432098f 100644
--- a/transport/grpc/src/main/java/io/a2a/transport/grpc/context/GrpcContextKeys.java
+++ b/transport/grpc/src/main/java/io/a2a/transport/grpc/context/GrpcContextKeys.java
@@ -1,43 +1,73 @@
package io.a2a.transport.grpc.context;
+import io.a2a.spec.GetTaskPushNotificationConfigRequest;
+import io.a2a.spec.CancelTaskRequest;
+import io.a2a.spec.DeleteTaskPushNotificationConfigRequest;
+import io.a2a.spec.GetTaskRequest;
+import io.a2a.spec.ListTaskPushNotificationConfigRequest;
+import io.a2a.spec.ListTasksRequest;
+import io.a2a.spec.SendMessageRequest;
+import io.a2a.spec.SendStreamingMessageRequest;
+import io.a2a.spec.SetTaskPushNotificationConfigRequest;
+import io.a2a.spec.TaskResubscriptionRequest;
import io.grpc.Context;
+import java.util.Map;
/**
* Shared gRPC context keys for A2A protocol data.
- *
+ *
* These keys provide access to gRPC context information similar to
* Python's grpc.aio.ServicerContext, enabling rich context access
* in service method implementations.
*/
public final class GrpcContextKeys {
-
+
/**
* Context key for storing the X-A2A-Extensions header value.
* Set by server interceptors and accessed by service handlers.
*/
- public static final Context.Key