diff --git a/core/deployment/pom.xml b/core/deployment/pom.xml
index a42bdd846..995235a18 100644
--- a/core/deployment/pom.xml
+++ b/core/deployment/pom.xml
@@ -112,6 +112,21 @@
quarkus-test-vertx
test
+
+ io.opentelemetry.instrumentation
+ opentelemetry-instrumentation-api
+ test
+
+
+ io.opentelemetry
+ opentelemetry-sdk-testing
+ test
+
+
+ io.quarkus
+ quarkus-junit5-mockito
+ test
+
diff --git a/core/deployment/src/test/java/io/quarkiverse/langchain4j/test/listeners/ListenersProcessorAbstractSpanChatModelListenerTest.java b/core/deployment/src/test/java/io/quarkiverse/langchain4j/test/listeners/ListenersProcessorAbstractSpanChatModelListenerTest.java
new file mode 100644
index 000000000..7829150e2
--- /dev/null
+++ b/core/deployment/src/test/java/io/quarkiverse/langchain4j/test/listeners/ListenersProcessorAbstractSpanChatModelListenerTest.java
@@ -0,0 +1,125 @@
+package io.quarkiverse.langchain4j.test.listeners;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+import java.util.HashMap;
+import java.util.List;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import dev.langchain4j.data.message.AiMessage;
+import dev.langchain4j.data.message.UserMessage;
+import dev.langchain4j.model.chat.listener.ChatModelErrorContext;
+import dev.langchain4j.model.chat.listener.ChatModelRequestContext;
+import dev.langchain4j.model.chat.listener.ChatModelResponseContext;
+import dev.langchain4j.model.chat.request.ChatRequest;
+import dev.langchain4j.model.chat.request.DefaultChatRequestParameters;
+import dev.langchain4j.model.chat.response.ChatResponse;
+import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter;
+import io.opentelemetry.sdk.trace.data.SpanData;
+import io.opentelemetry.sdk.trace.internal.data.ExceptionEventData;
+import io.quarkiverse.langchain4j.runtime.listeners.ChatModelSpanContributor;
+import io.quarkiverse.langchain4j.runtime.listeners.SpanChatModelListener;
+import io.quarkus.arc.All;
+
+abstract class ListenersProcessorAbstractSpanChatModelListenerTest {
+ @Inject
+ SpanChatModelListener spanChatModelListener;
+ @Inject
+ @All
+ List contributors;
+ @Inject
+ InMemorySpanExporter exporter;
+
+ static JavaArchive appWithInMemorySpanExporter() {
+ var applicationProperties = """
+ # Since using a InMemorySpanExporter inside our tests,
+ # these properties reduce the export timeout and schedule delay from the BatchSpanProcessor
+ quarkus.otel.bsp.schedule.delay=PT0.001S
+ quarkus.otel.bsp.max.queue.size=1
+ quarkus.otel.bsp.max.export.batch.size=1
+ """;
+ return ShrinkWrap.create(JavaArchive.class)
+ .addAsResource(new StringAsset(applicationProperties), "application.properties")
+ .addClasses(InMemorySpanExporterProducer.class);
+ }
+
+ @BeforeEach
+ void cleanupSpans() {
+ exporter.reset();
+ }
+
+ @Test
+ void shouldCreateSpanOnSuccess() {
+ var ctx = MockedContexts.create();
+
+ spanChatModelListener.onRequest(ctx.requestContext());
+ spanChatModelListener.onResponse(ctx.responseContext());
+
+ await().untilAsserted(() -> assertThat(exporter.getFinishedSpanItems()).hasSize(1));
+ var actualSpan = exporter.getFinishedSpanItems().get(0);
+ assertThat(actualSpan.getName()).isEqualTo("completion --mock-model-name--");
+ verifySuccessfulSpan(actualSpan);
+ }
+
+ protected void verifySuccessfulSpan(SpanData actualSpan) {
+ // nothing here
+ }
+
+ @Test
+ void shouldCreateAndEndSpanOnFailure() {
+ var ctx = MockedContexts.create();
+
+ spanChatModelListener.onRequest(ctx.requestContext());
+ spanChatModelListener.onError(ctx.errorContext());
+
+ await().untilAsserted(() -> assertThat(exporter.getFinishedSpanItems()).hasSize(1));
+ var actualSpan = exporter.getFinishedSpanItems().get(0);
+ assertThat(actualSpan.getName()).isEqualTo("completion --mock-model-name--");
+ assertThat(actualSpan.getEvents())
+ .hasSize(1)
+ .first()
+ .isInstanceOf(ExceptionEventData.class)
+ .extracting(ex -> ((ExceptionEventData) ex).getException().getMessage())
+ .isEqualTo("--failed--");
+ verifyFailedSpan(actualSpan);
+ }
+
+ protected void verifyFailedSpan(SpanData actualSpan) {
+ // nothing here
+ }
+
+ public static class InMemorySpanExporterProducer {
+ @ApplicationScoped
+ InMemorySpanExporter exporter() {
+ return InMemorySpanExporter.create();
+ }
+ }
+
+ record MockedContexts(
+ ChatModelRequestContext requestContext,
+ ChatModelResponseContext responseContext,
+ ChatModelErrorContext errorContext) {
+ static MockedContexts create() {
+ var attributes = new HashMap();
+ var request = ChatRequest.builder().messages(List.of(UserMessage.from("--test-message--")))
+ .parameters(DefaultChatRequestParameters.builder().modelName("--mock-model-name--").temperature(0.0)
+ .topP(0.0).build())
+ .build();
+ var response = ChatResponse.builder().aiMessage(AiMessage.from("--test-response--")).build();
+ var requestCtx = new ChatModelRequestContext(request, attributes);
+ var responseContext = new ChatModelResponseContext(response, request, attributes);
+ var errorCtx = new ChatModelErrorContext(
+ new RuntimeException("--failed--"), request, attributes);
+ return new MockedContexts(requestCtx, responseContext, errorCtx);
+ }
+ }
+}
diff --git a/core/deployment/src/test/java/io/quarkiverse/langchain4j/test/listeners/ListenersProcessorNoOpentelemetryTest.java b/core/deployment/src/test/java/io/quarkiverse/langchain4j/test/listeners/ListenersProcessorNoOpentelemetryTest.java
new file mode 100644
index 000000000..af2d014ca
--- /dev/null
+++ b/core/deployment/src/test/java/io/quarkiverse/langchain4j/test/listeners/ListenersProcessorNoOpentelemetryTest.java
@@ -0,0 +1,37 @@
+package io.quarkiverse.langchain4j.test.listeners;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.List;
+
+import jakarta.inject.Inject;
+
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkiverse.langchain4j.runtime.listeners.ChatModelSpanContributor;
+import io.quarkiverse.langchain4j.runtime.listeners.SpanChatModelListener;
+import io.quarkus.arc.All;
+import io.quarkus.test.QuarkusUnitTest;
+
+class ListenersProcessorNoOpentelemetryTest {
+ @RegisterExtension
+ static final QuarkusUnitTest unitTest = new QuarkusUnitTest()
+ .setArchiveProducer(
+ () -> ShrinkWrap.create(JavaArchive.class));
+
+ @Inject
+ @All
+ List spanChatModelListeners;
+ @Inject
+ @All
+ List chatModelSpanContributors;
+
+ @Test
+ void shouldNotHaveSpanChatModelListenerWhenNoOtel() {
+ assertThat(spanChatModelListeners).isEmpty();
+ assertThat(chatModelSpanContributors).isEmpty();
+ }
+}
diff --git a/core/deployment/src/test/java/io/quarkiverse/langchain4j/test/listeners/ListenersProcessorOnlySpanChatModelListenerTest.java b/core/deployment/src/test/java/io/quarkiverse/langchain4j/test/listeners/ListenersProcessorOnlySpanChatModelListenerTest.java
new file mode 100644
index 000000000..bc2929759
--- /dev/null
+++ b/core/deployment/src/test/java/io/quarkiverse/langchain4j/test/listeners/ListenersProcessorOnlySpanChatModelListenerTest.java
@@ -0,0 +1,27 @@
+package io.quarkiverse.langchain4j.test.listeners;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.maven.dependency.Dependency;
+import io.quarkus.test.QuarkusUnitTest;
+
+class ListenersProcessorOnlySpanChatModelListenerTest
+ extends ListenersProcessorAbstractSpanChatModelListenerTest {
+ @RegisterExtension
+ static final QuarkusUnitTest unitTest = new QuarkusUnitTest()
+ .setArchiveProducer(
+ ListenersProcessorAbstractSpanChatModelListenerTest::appWithInMemorySpanExporter)
+ .setForcedDependencies(
+ List.of(Dependency.of("io.quarkus", "quarkus-opentelemetry", "3.15.2")));
+
+ @Test
+ void shouldHaveSpanChatModelListenerWithoutContributors() {
+ assertThat(spanChatModelListener).isNotNull();
+ assertThat(contributors).isEmpty();
+ }
+}
diff --git a/core/deployment/src/test/java/io/quarkiverse/langchain4j/test/listeners/ListenersProcessorSingleChatModelSpanContributorTest.java b/core/deployment/src/test/java/io/quarkiverse/langchain4j/test/listeners/ListenersProcessorSingleChatModelSpanContributorTest.java
new file mode 100644
index 000000000..875a00f69
--- /dev/null
+++ b/core/deployment/src/test/java/io/quarkiverse/langchain4j/test/listeners/ListenersProcessorSingleChatModelSpanContributorTest.java
@@ -0,0 +1,74 @@
+package io.quarkiverse.langchain4j.test.listeners;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.List;
+
+import jakarta.enterprise.context.ApplicationScoped;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import dev.langchain4j.model.chat.listener.ChatModelErrorContext;
+import dev.langchain4j.model.chat.listener.ChatModelRequestContext;
+import dev.langchain4j.model.chat.listener.ChatModelResponseContext;
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.sdk.trace.data.SpanData;
+import io.quarkiverse.langchain4j.runtime.listeners.ChatModelSpanContributor;
+import io.quarkus.maven.dependency.Dependency;
+import io.quarkus.test.QuarkusUnitTest;
+
+class ListenersProcessorSingleChatModelSpanContributorTest
+ extends ListenersProcessorAbstractSpanChatModelListenerTest {
+ @RegisterExtension
+ static final QuarkusUnitTest unitTest = new QuarkusUnitTest()
+ .setArchiveProducer(
+ () -> appWithInMemorySpanExporter().addClasses(TestChatModelSpanContributor.class))
+ .setForcedDependencies(
+ List.of(Dependency.of("io.quarkus", "quarkus-opentelemetry", "3.15.2")));
+
+ @Test
+ void shouldHaveSpanChatModelListenerWitContributor() {
+ assertThat(spanChatModelListener).isNotNull();
+ assertThat(contributors).hasSize(1).first().isInstanceOf(TestChatModelSpanContributor.class);
+ }
+
+ @Override
+ protected void verifySuccessfulSpan(SpanData actualSpan) {
+ assertThat(actualSpan.getAttributes().get(AttributeKey.stringKey(("--custom-on-request--"))))
+ .isEqualTo("--value-on-request--");
+ assertThat(actualSpan.getAttributes().get(AttributeKey.stringKey(("--custom-on-response--"))))
+ .isEqualTo("--value-on-response--");
+ assertThat(actualSpan.getAttributes().get(AttributeKey.stringKey(("--custom-on-error--"))))
+ .isNull();
+ }
+
+ @Override
+ protected void verifyFailedSpan(SpanData actualSpan) {
+ assertThat(actualSpan.getAttributes().get(AttributeKey.stringKey(("--custom-on-request--"))))
+ .isEqualTo("--value-on-request--");
+ assertThat(actualSpan.getAttributes().get(AttributeKey.stringKey(("--custom-on-response--"))))
+ .isNull();
+ assertThat(actualSpan.getAttributes().get(AttributeKey.stringKey(("--custom-on-error--"))))
+ .isEqualTo("--value-on-error--");
+ }
+
+ @ApplicationScoped
+ public static class TestChatModelSpanContributor implements ChatModelSpanContributor {
+ @Override
+ public void onRequest(ChatModelRequestContext requestContext, Span currentSpan) {
+ currentSpan.setAttribute("--custom-on-request--", "--value-on-request--");
+ }
+
+ @Override
+ public void onResponse(ChatModelResponseContext responseContext, Span currentSpan) {
+ currentSpan.setAttribute("--custom-on-response--", "--value-on-response--");
+ }
+
+ @Override
+ public void onError(ChatModelErrorContext errorContext, Span currentSpan) {
+ currentSpan.setAttribute("--custom-on-error--", "--value-on-error--");
+ }
+ }
+}
diff --git a/core/deployment/src/test/java/io/quarkiverse/langchain4j/test/listeners/ListenersProcessorTwoChatModelSpanContributorsTest.java b/core/deployment/src/test/java/io/quarkiverse/langchain4j/test/listeners/ListenersProcessorTwoChatModelSpanContributorsTest.java
new file mode 100644
index 000000000..e8f15d2f8
--- /dev/null
+++ b/core/deployment/src/test/java/io/quarkiverse/langchain4j/test/listeners/ListenersProcessorTwoChatModelSpanContributorsTest.java
@@ -0,0 +1,157 @@
+package io.quarkiverse.langchain4j.test.listeners;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+import java.util.List;
+import java.util.function.BiConsumer;
+
+import jakarta.enterprise.context.ApplicationScoped;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import dev.langchain4j.model.chat.listener.ChatModelErrorContext;
+import dev.langchain4j.model.chat.listener.ChatModelRequestContext;
+import dev.langchain4j.model.chat.listener.ChatModelResponseContext;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.sdk.trace.data.SpanData;
+import io.opentelemetry.sdk.trace.internal.data.ExceptionEventData;
+import io.quarkiverse.langchain4j.runtime.listeners.ChatModelSpanContributor;
+import io.quarkus.maven.dependency.Dependency;
+import io.quarkus.test.QuarkusUnitTest;
+
+class ListenersProcessorTwoChatModelSpanContributorsTest
+ extends ListenersProcessorAbstractSpanChatModelListenerTest {
+ @RegisterExtension
+ static final QuarkusUnitTest unitTest = new QuarkusUnitTest()
+ .setArchiveProducer(
+ () -> appWithInMemorySpanExporter()
+ .addClasses(
+ FirstChatModelSpanContributor.class,
+ SecondChatModelSpanContributor.class))
+ .setForcedDependencies(
+ List.of(Dependency.of("io.quarkus", "quarkus-opentelemetry", "3.15.2")));
+
+ static BiConsumer onRequest;
+ static BiConsumer onResponse;
+ static BiConsumer onError;
+
+ @BeforeEach
+ void setupConsumers() {
+ onRequest = mock(BiConsumer.class);
+ onResponse = mock(BiConsumer.class);
+ onError = mock(BiConsumer.class);
+ }
+
+ @Test
+ void shouldHaveSpanChatModelListenerWitContributor() {
+ assertThat(spanChatModelListener).isNotNull();
+ assertThat(contributors)
+ .hasSize(2);
+ }
+
+ @Test
+ void shouldBeResilientToContributorFailuresOnRequest() {
+ var ctx = MockedContexts.create();
+ var failure = new RuntimeException("--failure-on-request-of-contributor--");
+ doThrow(failure).when(onRequest).accept(any(), any());
+
+ spanChatModelListener.onRequest(ctx.requestContext());
+ spanChatModelListener.onResponse(ctx.responseContext());
+
+ await().untilAsserted(() -> assertThat(exporter.getFinishedSpanItems()).hasSize(1));
+ verify(onRequest, times(2)).accept(any(), any());
+ var actualSpan = exporter.getFinishedSpanItems().get(0);
+ assertThat(actualSpan.getEvents())
+ .hasSize(2)
+ .extracting(ex -> ((ExceptionEventData) ex).getException().getMessage())
+ .contains("--failure-on-request-of-contributor--", "--failure-on-request-of-contributor--");
+ }
+
+ @Test
+ void shouldBeResilientToContributorFailuresOnResponse() {
+ var ctx = MockedContexts.create();
+ var failure = new RuntimeException("--failure-on-response-of-contributor--");
+ doThrow(failure).when(onResponse).accept(any(), any());
+
+ spanChatModelListener.onRequest(ctx.requestContext());
+ spanChatModelListener.onResponse(ctx.responseContext());
+
+ await().untilAsserted(() -> assertThat(exporter.getFinishedSpanItems()).hasSize(1));
+ verify(onResponse, times(2)).accept(any(), any());
+ var actualSpan = exporter.getFinishedSpanItems().get(0);
+ assertThat(actualSpan.getEvents())
+ .hasSize(2)
+ .extracting(ex -> ((ExceptionEventData) ex).getException().getMessage())
+ .contains(
+ "--failure-on-response-of-contributor--", "--failure-on-response-of-contributor--");
+ }
+
+ @Test
+ void shouldBeResilientToContributorFailuresOnError() {
+ var ctx = MockedContexts.create();
+ var failure = new RuntimeException("--failure-on-error-of-contributor--");
+ doThrow(failure).when(onError).accept(any(), any());
+
+ spanChatModelListener.onRequest(ctx.requestContext());
+ spanChatModelListener.onError(ctx.errorContext());
+
+ await().untilAsserted(() -> assertThat(exporter.getFinishedSpanItems()).hasSize(1));
+ verify(onError, times(2)).accept(any(), any());
+ var actualSpan = exporter.getFinishedSpanItems().get(0);
+ assertThat(actualSpan.getEvents())
+ .hasSize(3)
+ .extracting(ex -> ((ExceptionEventData) ex).getException().getMessage())
+ .contains(
+ "--failure-on-error-of-contributor--",
+ "--failure-on-error-of-contributor--",
+ ctx.errorContext().error().getMessage());
+ }
+
+ @Override
+ protected void verifySuccessfulSpan(SpanData actualSpan) {
+ verify(onRequest, times(2)).accept(any(), any());
+ verify(onResponse, times(2)).accept(any(), any());
+ verifyNoInteractions(onError);
+ }
+
+ @Override
+ protected void verifyFailedSpan(SpanData actualSpan) {
+ verify(onRequest, times(2)).accept(any(), any());
+ verifyNoInteractions(onResponse);
+ verify(onError, times(2)).accept(any(), any());
+ }
+
+ @ApplicationScoped
+ public static class FirstChatModelSpanContributor extends AbstractChatModelSpanContributor {
+ }
+
+ @ApplicationScoped
+ public static class SecondChatModelSpanContributor extends AbstractChatModelSpanContributor {
+ }
+
+ public static class AbstractChatModelSpanContributor implements ChatModelSpanContributor {
+ @Override
+ public void onRequest(ChatModelRequestContext requestContext, Span currentSpan) {
+ onRequest.accept(requestContext, currentSpan);
+ }
+
+ @Override
+ public void onResponse(ChatModelResponseContext responseContext, Span currentSpan) {
+ onResponse.accept(responseContext, currentSpan);
+ }
+
+ @Override
+ public void onError(ChatModelErrorContext errorContext, Span currentSpan) {
+ onError.accept(errorContext, currentSpan);
+ }
+ }
+}
diff --git a/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/listeners/ChatModelSpanContributor.java b/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/listeners/ChatModelSpanContributor.java
new file mode 100644
index 000000000..8d6384472
--- /dev/null
+++ b/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/listeners/ChatModelSpanContributor.java
@@ -0,0 +1,50 @@
+package io.quarkiverse.langchain4j.runtime.listeners;
+
+import dev.langchain4j.model.chat.listener.ChatModelErrorContext;
+import dev.langchain4j.model.chat.listener.ChatModelRequest;
+import dev.langchain4j.model.chat.listener.ChatModelRequestContext;
+import dev.langchain4j.model.chat.listener.ChatModelResponse;
+import dev.langchain4j.model.chat.listener.ChatModelResponseContext;
+import io.opentelemetry.api.trace.Span;
+
+/**
+ * Contributes custom attributes, events or other data to the spans created by {@link SpanChatModelListener}.
+ */
+public interface ChatModelSpanContributor {
+ /**
+ * Allows for custom data to be added to the span.
+ *
+ * @param requestContext The request context. It contains the {@link ChatModelRequest} and attributes.
+ * The attributes can be used to pass data between methods of this listener
+ * or between multiple listeners.
+ * @param currentSpan Span opened by {@link SpanChatModelListener}.
+ */
+ default void onRequest(ChatModelRequestContext requestContext, Span currentSpan) {
+ }
+
+ /**
+ * Allows for custom data to be added to the span.
+ *
+ * @param responseContext The response context.
+ * It contains {@link ChatModelResponse}, corresponding {@link ChatModelRequest} and attributes.
+ * The attributes can be used to pass data between methods of this listener
+ * or between multiple listeners.
+ * @param currentSpan Span opened by {@link SpanChatModelListener}.
+ */
+ default void onResponse(ChatModelResponseContext responseContext, Span currentSpan) {
+ }
+
+ /**
+ * Allows for custom data to be added to the span.
+ *
+ * @param errorContext The error context.
+ * It contains the error, corresponding {@link ChatModelRequest},
+ * partial {@link ChatModelResponse} (if available) and attributes.
+ * The attributes can be used to pass data between methods of this listener
+ * or between multiple listeners.
+ * @param currentSpan Span opened by {@link SpanChatModelListener}.
+ */
+ default void onError(ChatModelErrorContext errorContext, Span currentSpan) {
+ }
+
+}
diff --git a/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/listeners/SpanChatModelListener.java b/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/listeners/SpanChatModelListener.java
index 5dade6b6d..f0730eda9 100644
--- a/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/listeners/SpanChatModelListener.java
+++ b/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/listeners/SpanChatModelListener.java
@@ -1,5 +1,6 @@
package io.quarkiverse.langchain4j.runtime.listeners;
+import java.util.List;
import java.util.Map;
import jakarta.inject.Inject;
@@ -18,6 +19,7 @@
import io.opentelemetry.context.Scope;
import io.quarkiverse.langchain4j.cost.Cost;
import io.quarkiverse.langchain4j.cost.CostEstimatorService;
+import io.quarkus.arc.All;
/**
* Creates a span that follows the
@@ -33,11 +35,14 @@ public class SpanChatModelListener implements ChatModelListener {
private final Tracer tracer;
private final CostEstimatorService costEstimatorService;
+ private final List chatModelSpanContributors;
@Inject
- public SpanChatModelListener(Tracer tracer, CostEstimatorService costEstimatorService) {
+ public SpanChatModelListener(Tracer tracer, CostEstimatorService costEstimatorService,
+ @All List chatModelSpanContributors) {
this.tracer = tracer;
this.costEstimatorService = costEstimatorService;
+ this.chatModelSpanContributors = chatModelSpanContributors;
}
@Override
@@ -53,6 +58,7 @@ public void onRequest(ChatModelRequestContext requestContext) {
var attributes = requestContext.attributes();
attributes.put(OTEL_SCOPE_KEY_NAME, scope);
attributes.put(OTEL_SPAN_KEY_NAME, span);
+ notifyContributorsOnRequest(requestContext, span);
}
@Override
@@ -78,6 +84,7 @@ public void onResponse(ChatModelResponseContext responseContext) {
span.setAttribute("gen_ai.client.estimated_cost", costEstimate.toString());
}
}
+ notifyContributorsOnResponse(responseContext, span);
span.end();
} else {
// should never happen
@@ -92,6 +99,8 @@ public void onError(ChatModelErrorContext errorContext) {
Span span = (Span) attributes.get(OTEL_SPAN_KEY_NAME);
if (span != null) {
span.recordException(errorContext.error());
+ notifyContributorsOnError(errorContext, span);
+ span.end();
} else {
// should never happen
log.warn("No Span found in response");
@@ -112,4 +121,39 @@ private void safeCloseScope(Map