errorConsumer = mock(Consumer.class);
+ A2AClientException expectedException = new A2AClientException("Streaming failed");
+ doThrow(expectedException).when(delegate).sendMessageStreaming(any(MessageSendParams.class), any(Consumer.class),
+ any(Consumer.class), any(ClientCallContext.class));
+
+ A2AClientException exception = assertThrows(A2AClientException.class,
+ () -> transport.sendMessageStreaming(request, eventConsumer, errorConsumer, context));
+
+ assertEquals(expectedException, exception);
+ verify(span).setStatus(StatusCode.ERROR, "Streaming failed");
+ verify(span).end();
+ }
+}
diff --git a/extras/opentelemetry/common/pom.xml b/extras/opentelemetry/common/pom.xml
new file mode 100644
index 000000000..d21655202
--- /dev/null
+++ b/extras/opentelemetry/common/pom.xml
@@ -0,0 +1,18 @@
+
+
+ 4.0.0
+
+
+ io.github.a2asdk
+ a2a-java-sdk-opentelemetry-parent
+ 1.0.0.Alpha2-SNAPSHOT
+
+
+ a2a-java-sdk-opentelemetry-common
+
+ A2A Java SDK :: Extras :: Opentelemetry :: Common
+ Common OpenTelemetry utilities for A2A Java SDK
+
+
diff --git a/extras/opentelemetry/common/src/main/java/io/a2a/extras/opentelemetry/A2AObservabilityNames.java b/extras/opentelemetry/common/src/main/java/io/a2a/extras/opentelemetry/A2AObservabilityNames.java
new file mode 100644
index 000000000..c2821d8a7
--- /dev/null
+++ b/extras/opentelemetry/common/src/main/java/io/a2a/extras/opentelemetry/A2AObservabilityNames.java
@@ -0,0 +1,23 @@
+package io.a2a.extras.opentelemetry;
+
+public interface A2AObservabilityNames {
+
+ String EXTRACT_REQUEST_SYS_PROPERTY = "io.a2a.server.extract.request";
+ String EXTRACT_RESPONSE_SYS_PROPERTY = "io.a2a.server.extract.response";
+
+ String ERROR_TYPE = "error.type";
+
+ String GENAI_PREFIX = "gen_ai.agent.a2a";
+ String GENAI_CONFIG_ID = GENAI_PREFIX + ".config_id";
+ String GENAI_CONTEXT_ID = GENAI_PREFIX + ".context_id"; //gen_ai.conversation.id
+ String GENAI_EXTENSIONS = GENAI_PREFIX + ".extensions";
+ String GENAI_MESSAGE_ID = GENAI_PREFIX + ".message_id";
+ String GENAI_OPERATION_NAME = GENAI_PREFIX + ".operation.name"; //gen_ai.agent.operation.name ?
+ String GENAI_PARTS_NUMBER = GENAI_PREFIX + ".parts.number";
+ String GENAI_PROTOCOL = GENAI_PREFIX + ".protocol";
+ String GENAI_STATUS = GENAI_PREFIX + ".status";
+ String GENAI_REQUEST = GENAI_PREFIX + ".request"; //gen_ai.input.messages ?
+ String GENAI_RESPONSE = GENAI_PREFIX + ".response"; // gen_ai.output.messages ?
+ String GENAI_ROLE = GENAI_PREFIX + ".role";
+ String GENAI_TASK_ID = GENAI_PREFIX + ".task_id";
+}
diff --git a/extras/opentelemetry/common/src/main/java/io/a2a/extras/opentelemetry/package-info.java b/extras/opentelemetry/common/src/main/java/io/a2a/extras/opentelemetry/package-info.java
new file mode 100644
index 000000000..8e7125631
--- /dev/null
+++ b/extras/opentelemetry/common/src/main/java/io/a2a/extras/opentelemetry/package-info.java
@@ -0,0 +1,8 @@
+/**
+ * Common OpenTelemetry utilities and shared components for A2A Java SDK.
+ *
+ * This package contains common utilities, constants, and helper classes
+ * used across both client and server OpenTelemetry integrations.
+ */
+@org.jspecify.annotations.NullMarked
+package io.a2a.extras.opentelemetry;
diff --git a/extras/opentelemetry/integration-tests/README.md b/extras/opentelemetry/integration-tests/README.md
new file mode 100644
index 000000000..a66fe8f6c
--- /dev/null
+++ b/extras/opentelemetry/integration-tests/README.md
@@ -0,0 +1,160 @@
+# OpenTelemetry Integration Tests (Quarkus-based)
+
+## Overview
+
+This module provides **Quarkus-based integration tests** for OpenTelemetry tracing in the A2A Java SDK, similar to the approach used in the [Quarkus OpenTelemetry quickstart](https://github.com/quarkusio/quarkus/tree/main/integration-tests/opentelemetry-quickstart).
+
+Unlike the previous mock-based tests, these are **real integration tests** that:
+- Start an actual Quarkus application
+- Expose a REST API with A2A agent endpoints
+- Make real HTTP requests
+- Validate that OpenTelemetry spans are created correctly
+
+## Architecture
+
+### Components
+
+1. **SimpleAgent** - A basic A2A agent implementation for testing
+ - Implements all RequestHandler methods
+ - Stores tasks in memory
+ - Provides simple echo responses for messages
+
+2. **AgentResource** - JAX-RS REST resource
+ - Exposes HTTP endpoints (`/a2a/tasks`, `/a2a/messages`, etc.)
+ - Delegates to the RequestHandler
+ - Creates ServerCallContext for each request
+
+3. **InstrumentedRequestHandler** - CDI Alternative
+ - Wraps SimpleAgent with OpenTelemetry decorator
+ - Delegates to the OpenTelemetry decorator for span creation
+ - Ensures spans are created for each operation
+
+4. **OpenTelemetryProducer** - CDI bean producer
+ - Creates the Tracer from Quarkus OpenTelemetry
+ - Produces the OpenTelemetryRequestHandlerDecorator (CDI decorator)
+ - Integrates with Quarkus OpenTelemetry extension
+
+## Test Strategy
+
+### Functional Tests (`OpenTelemetryIntegrationTest`)
+- Use `@QuarkusTest` annotation
+- Make real HTTP requests using REST Assured
+- Verify HTTP responses are correct
+- Ensure the application behaves correctly end-to-end
+
+### Tracing Tests (`OpenTelemetryTracingTest`)
+- Use `InMemorySpanExporter` to capture spans
+- Verify that HTTP requests create OpenTelemetry spans
+- Validate span names, kinds (CLIENT/SERVER), and status codes
+- Check that spans are properly ended
+
+## Current Status
+
+### ✅ Completed
+- Project structure and POM configuration
+- Quarkus dependencies and plugins (including opentelemetry-sdk-testing)
+- SimpleAgentExecutor following reference module pattern
+- TestAgentCardProducer with proper JSONRPC interface
+- InMemorySpanExporter producer for span validation
+- OpenTelemetryIntegrationTest using Client API
+- Tests compile and run (service loader issues resolved)
+
+### 🔨 In Progress / Known Issues
+
+1. **Test Execution Timeouts**
+ - Tests are timing out during message send operations
+ - Error: "Timeout waiting for consumption to complete for task test-task-1"
+ - Likely a configuration issue between client (non-streaming) and server (streaming capable)
+ - Need to investigate JSONRPC transport configuration
+
+2. **Span Validation**
+ - InMemorySpanExporter is configured but needs verification
+ - Some tests are not finding expected spans
+ - May need to configure span processor to route to InMemorySpanExporter
+
+## Running the Tests
+
+### Prerequisites
+```bash
+# Build all A2A SDK modules first
+mvn clean install -DskipTests
+```
+
+### Run Integration Tests
+```bash
+# From the integration-tests directory
+mvn clean verify
+
+# Or from the root
+mvn verify -pl extras/opentelemetry/integration-tests -am
+```
+
+### Run Specific Test
+```bash
+mvn test -Dtest=OpenTelemetryIntegrationTest
+```
+
+## Configuration
+
+### Application Properties
+- `src/main/resources/application.properties` - Runtime configuration
+- `src/test/resources/application.properties` - Test-specific configuration
+
+Key settings:
+```properties
+# OpenTelemetry
+quarkus.otel.sdk.disabled=false
+quarkus.otel.traces.enabled=true
+quarkus.otel.service.name=a2a-opentelemetry-integration-test
+
+# Test mode: use in-memory exporter
+quarkus.otel.traces.exporter=none
+```
+
+### beans.xml
+Located at `src/main/resources/META-INF/beans.xml`:
+- Enables CDI bean discovery
+- Configures alternatives (InstrumentedRequestHandler)
+
+## Next Steps
+
+To complete this integration test module:
+
+1. **Resolve CDI ambiguity**
+ - Add `@Named` or custom qualifier to SimpleAgent
+ - Or exclude DefaultRequestHandler from test classpath
+ - Or use `@Alternative` more effectively
+
+2. **Configure span exporter**
+ - Properly wire InMemorySpanExporter into Quarkus OpenTelemetry
+ - May need custom OTel SDK configuration
+
+3. **Add more test scenarios**
+ - Test error handling and error spans
+ - Test streaming operations
+ - Test context propagation across services
+ - Test span attributes and metadata
+
+4. **Performance testing** (optional)
+ - Measure overhead of OpenTelemetry instrumentation
+ - Verify spans don't impact performance significantly
+
+## Comparison with Quarkus Quickstart
+
+This implementation follows the same patterns as the Quarkus OpenTelemetry quickstart:
+- ✅ Uses `@QuarkusTest` for integration tests
+- ✅ Uses REST Assured for HTTP testing
+- ✅ Integrates with Quarkus OpenTelemetry extension
+- ✅ Uses in-memory span exporter for validation
+- ✅ Tests actual HTTP requests, not mocks
+
+Unlike the quickstart which is a standalone app, this module:
+- Tests A2A SDK-specific functionality
+- Validates OpenTelemetry CDI decorator integration
+- Focuses on A2A protocol operations (tasks, messages, etc.)
+
+## References
+
+- [Quarkus OpenTelemetry Guide](https://quarkus.io/guides/opentelemetry)
+- [Quarkus OpenTelemetry Quickstart](https://github.com/quarkusio/quarkus/tree/main/integration-tests/opentelemetry-quickstart)
+- [OpenTelemetry Java Documentation](https://opentelemetry.io/docs/languages/java/)
diff --git a/extras/opentelemetry/integration-tests/pom.xml b/extras/opentelemetry/integration-tests/pom.xml
new file mode 100644
index 000000000..6d2b4734a
--- /dev/null
+++ b/extras/opentelemetry/integration-tests/pom.xml
@@ -0,0 +1,136 @@
+
+
+ 4.0.0
+
+
+ io.github.a2asdk
+ a2a-java-sdk-opentelemetry-parent
+ 1.0.0.Alpha2-SNAPSHOT
+
+
+ a2a-java-sdk-opentelemetry-integration-tests
+
+ A2A Java SDK :: Extras :: OpenTelemetry :: Integration Tests
+ Quarkus-based integration tests for OpenTelemetry support in A2A Java SDK
+
+
+
+
+ ${project.groupId}
+ a2a-java-sdk-client
+
+
+ ${project.groupId}
+ a2a-java-sdk-reference-jsonrpc
+
+
+ ${project.groupId}
+ a2a-java-sdk-opentelemetry-server
+
+
+ ${project.groupId}
+ a2a-java-sdk-opentelemetry-client
+
+
+
+
+ io.quarkus
+ quarkus-opentelemetry
+
+
+
+
+ io.quarkus
+ quarkus-reactive-routes
+
+
+
+
+ jakarta.enterprise
+ jakarta.enterprise.cdi-api
+ provided
+
+
+
+
+ com.google.code.gson
+ gson
+
+
+
+
+ io.quarkus
+ quarkus-junit5
+ test
+
+
+ io.opentelemetry
+ opentelemetry-sdk-testing
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+ org.awaitility
+ awaitility
+ test
+
+
+
+
+
+
+ io.quarkus
+ quarkus-maven-plugin
+ true
+
+
+
+ build
+ generate-code
+ generate-code-tests
+
+
+
+
+ --add-opens=java.base/java.lang=ALL-UNNAMED
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ org.jboss.logmanager.LogManager
+ ${maven.home}
+
+ --add-opens java.base/java.lang=ALL-UNNAMED
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+
+
+
+ integration-test
+ verify
+
+
+
+
+
+ org.jboss.logmanager.LogManager
+ ${maven.home}
+
+ --add-opens java.base/java.lang=ALL-UNNAMED
+
+
+
+
+
+
diff --git a/extras/opentelemetry/integration-tests/src/main/java/io/a2a/extras/opentelemetry/it/A2ATestRoutes.java b/extras/opentelemetry/integration-tests/src/main/java/io/a2a/extras/opentelemetry/it/A2ATestRoutes.java
new file mode 100644
index 000000000..f6f84f6e1
--- /dev/null
+++ b/extras/opentelemetry/integration-tests/src/main/java/io/a2a/extras/opentelemetry/it/A2ATestRoutes.java
@@ -0,0 +1,228 @@
+package io.a2a.extras.opentelemetry.it;
+
+
+
+import static io.vertx.core.http.HttpHeaders.CONTENT_TYPE;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import io.a2a.jsonrpc.common.json.JsonUtil;
+import io.a2a.spec.Task;
+import io.a2a.spec.TaskArtifactUpdateEvent;
+import io.a2a.spec.TaskStatusUpdateEvent;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.Tracer;
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter;
+import io.opentelemetry.sdk.trace.data.SpanData;
+import io.quarkus.vertx.web.Body;
+import io.quarkus.vertx.web.Param;
+import io.quarkus.vertx.web.Route;
+import io.vertx.ext.web.RoutingContext;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.inject.Produces;
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Test routes for OpenTelemetry integration testing.
+ * Exposes test utilities via REST endpoints.
+ */
+@Singleton
+public class A2ATestRoutes {
+
+ private static final String APPLICATION_JSON = "application/json";
+ private static final String TEXT_PLAIN = "text/plain";
+ private static final Gson gson = new GsonBuilder().create();
+
+ @Inject
+ TestUtilsBean testUtilsBean;
+ @Inject
+ InMemorySpanExporter inMemorySpanExporter;
+
+ @Inject
+ Tracer tracer;
+
+ @Route(path = "/test/task", methods = {Route.HttpMethod.POST}, consumes = {APPLICATION_JSON}, type = Route.HandlerType.BLOCKING)
+ public void saveTask(@Body String body, RoutingContext rc) {
+ try {
+ Task task = JsonUtil.fromJson(body, Task.class);
+ testUtilsBean.saveTask(task);
+ rc.response()
+ .setStatusCode(200)
+ .end();
+ } catch (Throwable t) {
+ errorResponse(t, rc);
+ }
+ }
+
+ @Route(path = "/test/task/:taskId", methods = {Route.HttpMethod.GET}, produces = {APPLICATION_JSON}, type = Route.HandlerType.BLOCKING)
+ public void getTask(@Param String taskId, RoutingContext rc) {
+ try {
+ Task task = testUtilsBean.getTask(taskId);
+ if (task == null) {
+ rc.response()
+ .setStatusCode(404)
+ .end();
+ return;
+ }
+ rc.response()
+ .setStatusCode(200)
+ .putHeader(CONTENT_TYPE, APPLICATION_JSON)
+ .end(JsonUtil.toJson(task));
+
+ } catch (Throwable t) {
+ errorResponse(t, rc);
+ }
+ }
+
+ @Route(path = "/test/task/:taskId", methods = {Route.HttpMethod.DELETE}, type = Route.HandlerType.BLOCKING)
+ public void deleteTask(@Param String taskId, RoutingContext rc) {
+ try {
+ Task task = testUtilsBean.getTask(taskId);
+ if (task == null) {
+ rc.response()
+ .setStatusCode(404)
+ .end();
+ return;
+ }
+ testUtilsBean.deleteTask(taskId);
+ rc.response()
+ .setStatusCode(200)
+ .end();
+ } catch (Throwable t) {
+ errorResponse(t, rc);
+ }
+ }
+
+ @Route(path = "/test/queue/ensure/:taskId", methods = {Route.HttpMethod.POST})
+ public void ensureTaskQueue(@Param String taskId, RoutingContext rc) {
+ try {
+ testUtilsBean.ensureQueue(taskId);
+ rc.response()
+ .setStatusCode(200)
+ .end();
+ } catch (Throwable t) {
+ errorResponse(t, rc);
+ }
+ }
+
+ @Route(path = "/test/queue/enqueueTaskStatusUpdateEvent/:taskId", methods = {Route.HttpMethod.POST})
+ public void enqueueTaskStatusUpdateEvent(@Param String taskId, @Body String body, RoutingContext rc) {
+ try {
+ TaskStatusUpdateEvent event = JsonUtil.fromJson(body, TaskStatusUpdateEvent.class);
+ testUtilsBean.enqueueEvent(taskId, event);
+ rc.response()
+ .setStatusCode(200)
+ .end();
+ } catch (Throwable t) {
+ errorResponse(t, rc);
+ }
+ }
+
+ @Route(path = "/test/queue/enqueueTaskArtifactUpdateEvent/:taskId", methods = {Route.HttpMethod.POST})
+ public void enqueueTaskArtifactUpdateEvent(@Param String taskId, @Body String body, RoutingContext rc) {
+ try {
+ TaskArtifactUpdateEvent event = JsonUtil.fromJson(body, TaskArtifactUpdateEvent.class);
+ testUtilsBean.enqueueEvent(taskId, event);
+ rc.response()
+ .setStatusCode(200)
+ .end();
+ } catch (Throwable t) {
+ errorResponse(t, rc);
+ }
+ }
+
+ @Route(path = "/test/queue/childCount/:taskId", methods = {Route.HttpMethod.GET}, produces = {TEXT_PLAIN})
+ public void getChildQueueCount(@Param String taskId, RoutingContext rc) {
+ int count = testUtilsBean.getChildQueueCount(taskId);
+ rc.response()
+ .setStatusCode(200)
+ .end(String.valueOf(count));
+ }
+
+ @Route(path = "/hello", methods = {Route.HttpMethod.GET}, produces = {TEXT_PLAIN})
+ public void hello(RoutingContext rc) {
+ Span span = tracer.spanBuilder("hello").startSpan();
+ try (Scope scope = span.makeCurrent()) {
+ rc.response()
+ .setStatusCode(200)
+ .putHeader(CONTENT_TYPE, TEXT_PLAIN)
+ .end("Hello from Quarkus REST");
+ } finally {
+ span.end();
+ }
+ }
+
+ @Route(path = "/export", methods = {Route.HttpMethod.GET}, produces = {APPLICATION_JSON})
+ public void exportSpans(@Param String taskId, RoutingContext rc) {
+ List spans = inMemorySpanExporter.getFinishedSpanItems()
+ .stream()
+ .filter(sd -> !sd.getName().contains("export") && !sd.getName().contains("reset"))
+ .collect(Collectors.toList());
+ String json = gson.toJson(serialize(spans));
+ rc.response()
+ .setStatusCode(200)
+ .putHeader(CONTENT_TYPE, APPLICATION_JSON)
+ .end(json);
+ }
+
+ private JsonElement serialize(List spanDatas) {
+ JsonArray spans = new JsonArray(spanDatas.size());
+ for (SpanData spanData : spanDatas) {
+ JsonObject jsonObject = new JsonObject();
+
+ jsonObject.addProperty("spanId", spanData.getSpanId());
+ jsonObject.addProperty("traceId", spanData.getTraceId());
+ jsonObject.addProperty("name", spanData.getName());
+ jsonObject.addProperty("kind", spanData.getKind().name());
+ jsonObject.addProperty("ended", spanData.hasEnded());
+
+ jsonObject.addProperty("parentSpanId", spanData.getParentSpanContext().getSpanId());
+ jsonObject.addProperty("parent_spanId", spanData.getParentSpanContext().getSpanId());
+ jsonObject.addProperty("parent_traceId", spanData.getParentSpanContext().getTraceId());
+ jsonObject.addProperty("parent_remote", spanData.getParentSpanContext().isRemote());
+ jsonObject.addProperty("parent_valid", spanData.getParentSpanContext().isValid());
+
+ spanData.getAttributes().forEach((k, v) -> {
+ jsonObject.addProperty("attr_" + k.getKey(), v.toString());
+ });
+
+ spanData.getResource().getAttributes().forEach((k, v) -> {
+ jsonObject.addProperty("resource_" + k.getKey(), v.toString());
+ });
+ spans.add(jsonObject);
+ }
+
+ return spans;
+ }
+
+ @Route(path = "/reset", methods = {Route.HttpMethod.GET}, produces = {TEXT_PLAIN})
+ public void reset(@Param String taskId, RoutingContext rc) {
+ inMemorySpanExporter.reset();
+ rc.response().setStatusCode(200).end();
+ }
+
+ private void errorResponse(Throwable t, RoutingContext rc) {
+ t.printStackTrace();
+ rc.response()
+ .setStatusCode(500)
+ .putHeader(CONTENT_TYPE, TEXT_PLAIN)
+ .end();
+ }
+
+ @ApplicationScoped
+ static class InMemorySpanExporterProducer {
+
+ @Produces
+ @Singleton
+ InMemorySpanExporter inMemorySpanExporter() {
+ return InMemorySpanExporter.create();
+ }
+ }
+}
diff --git a/extras/opentelemetry/integration-tests/src/main/java/io/a2a/extras/opentelemetry/it/SimpleAgentExecutor.java b/extras/opentelemetry/integration-tests/src/main/java/io/a2a/extras/opentelemetry/it/SimpleAgentExecutor.java
new file mode 100644
index 000000000..6d33dd89c
--- /dev/null
+++ b/extras/opentelemetry/integration-tests/src/main/java/io/a2a/extras/opentelemetry/it/SimpleAgentExecutor.java
@@ -0,0 +1,41 @@
+package io.a2a.extras.opentelemetry.it;
+
+import io.a2a.A2A;
+import io.a2a.server.agentexecution.AgentExecutor;
+import io.a2a.server.agentexecution.RequestContext;
+import io.a2a.server.tasks.AgentEmitter;
+import io.a2a.spec.A2AError;
+import io.a2a.spec.TextPart;
+import jakarta.enterprise.context.ApplicationScoped;
+
+/**
+ * Simple AgentExecutor for integration testing.
+ * Echoes back the user's message and completes the task immediately.
+ */
+@ApplicationScoped
+public class SimpleAgentExecutor implements AgentExecutor {
+
+ @Override
+ public void execute(RequestContext context, AgentEmitter emitter) throws A2AError {
+ // If task doesn't exist, create it
+ if (context.getTask() == null) {
+ emitter.submit();
+ }
+
+ // Get the user's message
+ String userText = context.getMessage().parts().stream()
+ .filter(part -> part instanceof TextPart)
+ .map(part -> ((TextPart) part).text())
+ .findFirst()
+ .orElse("");
+
+ // Echo it back
+ String response = "Echo: " + userText;
+ emitter.complete(A2A.toAgentMessage(response));
+ }
+
+ @Override
+ public void cancel(RequestContext context, AgentEmitter emitter) throws A2AError {
+ emitter.cancel();
+ }
+}
diff --git a/extras/opentelemetry/integration-tests/src/main/java/io/a2a/extras/opentelemetry/it/TestAgentCardProducer.java b/extras/opentelemetry/integration-tests/src/main/java/io/a2a/extras/opentelemetry/it/TestAgentCardProducer.java
new file mode 100644
index 000000000..373169fac
--- /dev/null
+++ b/extras/opentelemetry/integration-tests/src/main/java/io/a2a/extras/opentelemetry/it/TestAgentCardProducer.java
@@ -0,0 +1,47 @@
+package io.a2a.extras.opentelemetry.it;
+
+import io.a2a.server.PublicAgentCard;
+import io.a2a.spec.AgentCapabilities;
+import io.a2a.spec.AgentCard;
+import io.a2a.spec.AgentInterface;
+import io.a2a.spec.AgentSkill;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.inject.Produces;
+
+import java.util.Collections;
+import java.util.List;
+
+
+/**
+ * Produces the AgentCard for integration testing.
+ */
+@ApplicationScoped
+public class TestAgentCardProducer {
+
+ @Produces
+ @PublicAgentCard
+ public AgentCard agentCard() {
+ return AgentCard.builder()
+ .name("OpenTelemetry Test Agent")
+ .description("Test agent for OpenTelemetry integration tests")
+ .supportedInterfaces(Collections.singletonList(
+ new AgentInterface("JSONRPC", "http://localhost:8081")
+ ))
+ .version("1.0.0-TEST")
+ .documentationUrl("http://example.com/test")
+ .capabilities(AgentCapabilities.builder()
+ .streaming(true)
+ .pushNotifications(false)
+ .build())
+ .defaultInputModes(Collections.singletonList("text"))
+ .defaultOutputModes(Collections.singletonList("text"))
+ .skills(Collections.singletonList(AgentSkill.builder()
+ .id("echo")
+ .name("Echo")
+ .description("Echoes back the user's message")
+ .tags(Collections.singletonList("test"))
+ .examples(List.of("hello", "test message"))
+ .build()))
+ .build();
+ }
+}
diff --git a/extras/opentelemetry/integration-tests/src/main/java/io/a2a/extras/opentelemetry/it/TestUtilsBean.java b/extras/opentelemetry/integration-tests/src/main/java/io/a2a/extras/opentelemetry/it/TestUtilsBean.java
new file mode 100644
index 000000000..594063171
--- /dev/null
+++ b/extras/opentelemetry/integration-tests/src/main/java/io/a2a/extras/opentelemetry/it/TestUtilsBean.java
@@ -0,0 +1,46 @@
+package io.a2a.extras.opentelemetry.it;
+
+import io.a2a.server.events.QueueManager;
+import io.a2a.server.tasks.TaskStore;
+import io.a2a.spec.Event;
+import io.a2a.spec.Task;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+/**
+ * Test utilities for OpenTelemetry integration tests.
+ * Allows direct manipulation of tasks and queues for testing.
+ */
+@ApplicationScoped
+public class TestUtilsBean {
+
+ @Inject
+ TaskStore taskStore;
+
+ @Inject
+ QueueManager queueManager;
+
+ public void saveTask(Task task) {
+ taskStore.save(task, false);
+ }
+
+ public Task getTask(String taskId) {
+ return taskStore.get(taskId);
+ }
+
+ public void deleteTask(String taskId) {
+ taskStore.delete(taskId);
+ }
+
+ public void ensureQueue(String taskId) {
+ queueManager.createOrTap(taskId);
+ }
+
+ public void enqueueEvent(String taskId, Event event) {
+ queueManager.get(taskId).enqueueEvent(event);
+ }
+
+ public int getChildQueueCount(String taskId) {
+ return queueManager.getActiveChildQueueCount(taskId);
+ }
+}
diff --git a/extras/opentelemetry/integration-tests/src/main/resources/META-INF/beans.xml b/extras/opentelemetry/integration-tests/src/main/resources/META-INF/beans.xml
new file mode 100644
index 000000000..5badbc80d
--- /dev/null
+++ b/extras/opentelemetry/integration-tests/src/main/resources/META-INF/beans.xml
@@ -0,0 +1,9 @@
+
+
+
+ io.a2a.extras.opentelemetry.it.InstrumentedRequestHandler
+
+
diff --git a/extras/opentelemetry/integration-tests/src/main/resources/application.properties b/extras/opentelemetry/integration-tests/src/main/resources/application.properties
new file mode 100644
index 000000000..ca0d2c4fd
--- /dev/null
+++ b/extras/opentelemetry/integration-tests/src/main/resources/application.properties
@@ -0,0 +1,28 @@
+# Quarkus configuration
+quarkus.application.name=a2a-otel-test
+
+# HTTP configuration
+quarkus.http.port=8081
+quarkus.http.test-port=8081
+
+# OpenTelemetry configuration
+quarkus.otel.sdk.disabled=false
+quarkus.otel.traces.enabled=true
+quarkus.otel.metrics.enabled=false
+quarkus.otel.logs.enabled=false
+
+quarkus.otel.instrument.vertx-http=false
+
+quarkus.otel.bsp.schedule.delay=0
+quarkus.otel.bsp.export.timeout=5s
+
+# Service name
+quarkus.otel.service.name=a2a-opentelemetry-integration-test
+
+# Propagators
+quarkus.otel.propagators=tracecontext
+
+# Logging
+quarkus.log.level=INFO
+quarkus.log.category."io.a2a".level=DEBUG
+quarkus.log.category."io.opentelemetry".level=DEBUG
diff --git a/extras/opentelemetry/integration-tests/src/test/java/io/a2a/extras/opentelemetry/it/BaseTest.java b/extras/opentelemetry/integration-tests/src/test/java/io/a2a/extras/opentelemetry/it/BaseTest.java
new file mode 100644
index 000000000..69304e10e
--- /dev/null
+++ b/extras/opentelemetry/integration-tests/src/test/java/io/a2a/extras/opentelemetry/it/BaseTest.java
@@ -0,0 +1,20 @@
+package io.a2a.extras.opentelemetry.it;
+
+import static io.restassured.RestAssured.get;
+
+import java.util.List;
+import java.util.Map;
+
+import io.restassured.common.mapper.TypeRef;
+
+public class BaseTest {
+
+ protected List