diff --git a/spring-ai-core/src/main/java/org/springframework/ai/model/observation/ErrorLoggingObservationHandler.java b/spring-ai-core/src/main/java/org/springframework/ai/model/observation/ErrorLoggingObservationHandler.java new file mode 100644 index 00000000000..21b92e75b33 --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/model/observation/ErrorLoggingObservationHandler.java @@ -0,0 +1,80 @@ +/* +* Copyright 2024 - 2024 the original author or authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* https://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package org.springframework.ai.model.observation; + +import java.util.List; +import java.util.function.Consumer; + +import org.springframework.util.Assert; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.Observation.Context; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.handler.TracingObservationHandler.TracingContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Christian Tzolov + * @since 1.0.0 + */ +@SuppressWarnings({ "rawtypes", "null" }) +public class ErrorLoggingObservationHandler implements ObservationHandler { + + private static final Logger logger = LoggerFactory.getLogger(ErrorLoggingObservationHandler.class); + + private final Tracer tracer; + + private final List> supportedContextTypes; + + private final Consumer errorConsumer; + + public ErrorLoggingObservationHandler(Tracer tracer, + List> supportedContextTypes) { + this(tracer, supportedContextTypes, context -> logger.error("Traced Error: ", context.getError())); + } + + public ErrorLoggingObservationHandler(Tracer tracer, + List> supportedContextTypes, Consumer errorConsumer) { + + Assert.notNull(tracer, "Tracer must not be null"); + Assert.notNull(supportedContextTypes, "SupportedContextTypes must not be null"); + Assert.notNull(errorConsumer, "ErrorConsumer must not be null"); + + this.tracer = tracer; + this.supportedContextTypes = supportedContextTypes; + this.errorConsumer = errorConsumer; + } + + @Override + public boolean supportsContext(Context context) { + return (context == null) ? false : this.supportedContextTypes.stream().anyMatch(clz -> clz.isInstance(context)); + } + + @Override + public void onError(Context context) { + if (context != null) { + TracingContext tracingContext = context.get(TracingContext.class); + if (tracingContext != null) { + try (var val = this.tracer.withSpan(tracingContext.getSpan())) { + this.errorConsumer.accept(context); + } + } + } + } + +} diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationContext.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationContext.java index 67f04f58f41..d12dd55adc1 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationContext.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationContext.java @@ -27,7 +27,7 @@ /** * Context used to store metadata for vector store operations. * - * @author Christian Tzolo + * @author Christian Tzolov * @author Thomas Vitale * @since 1.0.0 */ diff --git a/spring-ai-core/src/test/java/org/springframework/ai/observation/tracing/TracingHelperTests.java b/spring-ai-core/src/test/java/org/springframework/ai/observation/tracing/TracingHelperTests.java index 6fc0eeb12e7..aa5848370ba 100644 --- a/spring-ai-core/src/test/java/org/springframework/ai/observation/tracing/TracingHelperTests.java +++ b/spring-ai-core/src/test/java/org/springframework/ai/observation/tracing/TracingHelperTests.java @@ -1,18 +1,17 @@ package org.springframework.ai.observation.tracing; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; + import io.micrometer.tracing.Span; import io.micrometer.tracing.TraceContext; import io.micrometer.tracing.handler.TracingObservationHandler; import io.micrometer.tracing.otel.bridge.OtelCurrentTraceContext; import io.micrometer.tracing.otel.bridge.OtelTracer; import io.opentelemetry.api.OpenTelemetry; -import org.junit.jupiter.api.Test; -import org.springframework.ai.chat.observation.ChatModelObservationContentProcessor; - -import java.util.concurrent.TimeUnit; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; /** * Unit tests for {@link TracingHelper}. diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationAutoConfiguration.java index 8dfa9323a19..d751d5d1956 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationAutoConfiguration.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationAutoConfiguration.java @@ -15,16 +15,23 @@ */ package org.springframework.ai.autoconfigure.chat.observation; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.tracing.otel.bridge.OtelTracer; +import java.util.List; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.ai.chat.client.advisor.observation.AdvisorObservationContext; +import org.springframework.ai.chat.client.observation.ChatClientObservationContext; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.observation.ChatModelCompletionObservationFilter; import org.springframework.ai.chat.observation.ChatModelCompletionObservationHandler; import org.springframework.ai.chat.observation.ChatModelMeterObservationHandler; +import org.springframework.ai.chat.observation.ChatModelObservationContext; import org.springframework.ai.chat.observation.ChatModelPromptContentObservationFilter; import org.springframework.ai.chat.observation.ChatModelPromptContentObservationHandler; +import org.springframework.ai.embedding.observation.EmbeddingModelObservationContext; +import org.springframework.ai.image.observation.ImageModelObservationContext; +import org.springframework.ai.model.observation.ErrorLoggingObservationHandler; +import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -36,6 +43,10 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.otel.bridge.OtelTracer; + /** * Auto-configuration for Spring AI chat model observations. * @@ -113,6 +124,17 @@ ChatModelCompletionObservationFilter chatModelCompletionObservationFilter() { } + @Bean + @ConditionalOnBean(Tracer.class) + @ConditionalOnProperty(prefix = ChatObservationProperties.CONFIG_PREFIX, name = "include-error-logging", + havingValue = "true") + public ErrorLoggingObservationHandler errorLoggingObservationHandler(Tracer tracer) { + return new ErrorLoggingObservationHandler(tracer, + List.of(EmbeddingModelObservationContext.class, ImageModelObservationContext.class, + ChatModelObservationContext.class, ChatClientObservationContext.class, + AdvisorObservationContext.class, VectorStoreObservationContext.class)); + } + private static void logPromptContentWarning() { logger.warn( "You have enabled the inclusion of the prompt content in the observations, with the risk of exposing sensitive or private information. Please, be careful!"); diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationProperties.java index 1ae6637c2ab..750f4911265 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationProperties.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationProperties.java @@ -38,6 +38,11 @@ public class ChatObservationProperties { */ private boolean includePrompt = false; + /** + * Whether to include error logging in the observations. + */ + private boolean includeErrorLogging = false; + public boolean isIncludeCompletion() { return includeCompletion; } @@ -54,4 +59,12 @@ public void setIncludePrompt(boolean includePrompt) { this.includePrompt = includePrompt; } + public boolean isIncludeErrorLogging() { + return this.includeErrorLogging; + } + + public void setIncludeErrorLogging(boolean includeErrorLogging) { + this.includeErrorLogging = includeErrorLogging; + } + }