Add missing observability tests

Some tests were missing from gh-3003, this change adds them

See gh-3003
This commit is contained in:
Jonatan Ivanov
2025-05-12 15:52:07 -07:00
committed by Christian Tzolov
parent 0bcb2e138b
commit 3fb66928f5
5 changed files with 754 additions and 37 deletions

View File

@@ -18,12 +18,20 @@ package org.springframework.ai.model.chat.client.autoconfigure;
import io.micrometer.tracing.Tracer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.ai.chat.client.observation.ChatClientObservationContext;
import org.springframework.ai.chat.client.observation.ChatClientPromptContentObservationHandler;
import org.springframework.ai.observation.TracingAwareLoggingObservationHandler;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Unit tests for {@link ChatClientAutoConfiguration} observability support.
@@ -32,22 +40,127 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Thomas Vitale
* @author Jonatan Ivanov
*/
@ExtendWith(OutputCaptureExtension.class)
class ChatClientObservationAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(ChatClientAutoConfiguration.class));
@Test
void promptContentHandlerDefault() {
this.contextRunner
.run(context -> assertThat(context).doesNotHaveBean(ChatClientPromptContentObservationHandler.class));
void promptContentHandlerNoTracer() {
this.contextRunner.withClassLoader(new FilteredClassLoader(Tracer.class))
.run(context -> assertThat(context).doesNotHaveBean(ChatClientPromptContentObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class));
}
@Test
void promptContentHandlerEnabled() {
void promptContentHandlerWithTracer() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.run(context -> assertThat(context).doesNotHaveBean(ChatClientPromptContentObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class));
}
@Test
void promptContentHandlerEnabledNoTracer(CapturedOutput output) {
this.contextRunner.withClassLoader(new FilteredClassLoader(Tracer.class))
.withPropertyValues("spring.ai.chat.client.observations.log-prompt=true")
.run(context -> assertThat(context).hasSingleBean(ChatClientPromptContentObservationHandler.class));
.run(context -> assertThat(context).hasSingleBean(ChatClientPromptContentObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class));
assertThat(output).contains(
"You have enabled logging out the ChatClient prompt content with the risk of exposing sensitive or private information. Please, be careful!");
}
@Test
void promptContentHandlerEnabledWithTracer(CapturedOutput output) {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.withPropertyValues("spring.ai.chat.client.observations.log-prompt=true")
.run(context -> assertThat(context).doesNotHaveBean(ChatClientPromptContentObservationHandler.class)
.hasSingleBean(TracingAwareLoggingObservationHandler.class));
assertThat(output).contains(
"You have enabled logging out the ChatClient prompt content with the risk of exposing sensitive or private information. Please, be careful!");
}
@Test
void promptContentHandlerDisabledNoTracer() {
this.contextRunner.withClassLoader(new FilteredClassLoader(Tracer.class))
.withPropertyValues("spring.ai.chat.client.observations.log-prompt=false")
.run(context -> assertThat(context).doesNotHaveBean(ChatClientPromptContentObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class));
}
@Test
void promptContentHandlerDisabledWithTracer() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.withPropertyValues("spring.ai.chat.client.observations.log-prompt=false")
.run(context -> assertThat(context).doesNotHaveBean(ChatClientPromptContentObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class));
}
@Test
void customChatClientPromptContentObservationHandlerNoTracer() {
this.contextRunner.withClassLoader(new FilteredClassLoader(Tracer.class))
.withUserConfiguration(CustomChatClientPromptContentObservationHandlerConfiguration.class)
.withPropertyValues("spring.ai.chat.client.observations.log-prompt=true")
.run(context -> assertThat(context).hasSingleBean(ChatClientPromptContentObservationHandler.class)
.hasBean("customChatClientPromptContentObservationHandler")
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class));
}
@Test
void customChatClientPromptContentObservationHandlerWithTracer() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.withUserConfiguration(CustomChatClientPromptContentObservationHandlerConfiguration.class)
.withPropertyValues("spring.ai.chat.client.observations.log-prompt=true")
.run(context -> assertThat(context).hasSingleBean(ChatClientPromptContentObservationHandler.class)
.hasBean("customChatClientPromptContentObservationHandler")
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class));
}
@Test
void customTracingAwareLoggingObservationHandler() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.withUserConfiguration(CustomTracingAwareLoggingObservationHandlerConfiguration.class)
.withPropertyValues("spring.ai.chat.client.observations.log-prompt=true")
.run(context -> {
assertThat(context).hasSingleBean(TracingAwareLoggingObservationHandler.class)
.hasBean("chatClientPromptContentObservationHandler")
.doesNotHaveBean(ChatClientPromptContentObservationHandler.class);
assertThat(context.getBean(TracingAwareLoggingObservationHandler.class))
.isSameAs(CustomTracingAwareLoggingObservationHandlerConfiguration.handlerInstance);
});
}
@Configuration(proxyBeanMethods = false)
static class TracerConfiguration {
@Bean
Tracer tracer() {
return mock(Tracer.class);
}
}
@Configuration(proxyBeanMethods = false)
static class CustomChatClientPromptContentObservationHandlerConfiguration {
@Bean
ChatClientPromptContentObservationHandler customChatClientPromptContentObservationHandler() {
return new ChatClientPromptContentObservationHandler();
}
}
@Configuration(proxyBeanMethods = false)
static class CustomTracingAwareLoggingObservationHandlerConfiguration {
static TracingAwareLoggingObservationHandler<ChatClientObservationContext> handlerInstance = new TracingAwareLoggingObservationHandler<>(
new ChatClientPromptContentObservationHandler(), null);
@Bean
TracingAwareLoggingObservationHandler<ChatClientObservationContext> chatClientPromptContentObservationHandler() {
return handlerInstance;
}
}
}

View File

@@ -19,15 +19,26 @@ package org.springframework.ai.model.chat.observation.autoconfigure;
import io.micrometer.core.instrument.composite.CompositeMeterRegistry;
import io.micrometer.tracing.Tracer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.ai.chat.client.observation.ChatClientObservationContext;
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.ChatModelPromptContentObservationHandler;
import org.springframework.ai.model.observation.ErrorLoggingObservationHandler;
import org.springframework.ai.observation.TracingAwareLoggingObservationHandler;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Unit tests for {@link ChatObservationAutoConfiguration}.
@@ -35,6 +46,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Thomas Vitale
* @author Jonatan Ivanov
*/
@ExtendWith(OutputCaptureExtension.class)
class ChatObservationAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
@@ -52,41 +64,308 @@ class ChatObservationAutoConfigurationTests {
}
@Test
void promptHandlerDefault() {
this.contextRunner
.run(context -> assertThat(context).doesNotHaveBean(ChatModelPromptContentObservationHandler.class));
void handlersNoTracer() {
this.contextRunner.withClassLoader(new FilteredClassLoader(Tracer.class))
.run(context -> assertThat(context).doesNotHaveBean(ChatModelPromptContentObservationHandler.class)
.doesNotHaveBean(ChatModelCompletionObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class)
.doesNotHaveBean(ErrorLoggingObservationHandler.class));
}
@Test
void promptHandlerEnabled() {
void handlersWithTracer() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.run(context -> assertThat(context).doesNotHaveBean(ChatModelPromptContentObservationHandler.class)
.doesNotHaveBean(ChatModelCompletionObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class)
.doesNotHaveBean(ErrorLoggingObservationHandler.class));
}
@Test
void promptContentHandlerEnabledNoTracer(CapturedOutput output) {
this.contextRunner.withClassLoader(new FilteredClassLoader(Tracer.class))
.withPropertyValues("spring.ai.chat.observations.log-prompt=true")
.run(context -> assertThat(context).hasSingleBean(ChatModelPromptContentObservationHandler.class));
.run(context -> assertThat(context).hasSingleBean(ChatModelPromptContentObservationHandler.class)
.doesNotHaveBean(ChatModelCompletionObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class)
.doesNotHaveBean(ErrorLoggingObservationHandler.class));
assertThat(output).contains(
"You have enabled logging out the prompt content with the risk of exposing sensitive or private information. Please, be careful!");
}
@Test
void promptHandlerDisabled() {
this.contextRunner.withPropertyValues("spring.ai.chat.observations.log-prompt=false")
.run(context -> assertThat(context).doesNotHaveBean(ChatModelPromptContentObservationHandler.class));
void promptContentHandlerEnabledWithTracer(CapturedOutput output) {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.withPropertyValues("spring.ai.chat.observations.log-prompt=true")
.run(context -> assertThat(context).doesNotHaveBean(ChatModelPromptContentObservationHandler.class)
.doesNotHaveBean(ChatModelCompletionObservationHandler.class)
.hasSingleBean(TracingAwareLoggingObservationHandler.class)
.doesNotHaveBean(ErrorLoggingObservationHandler.class));
assertThat(output).contains(
"You have enabled logging out the prompt content with the risk of exposing sensitive or private information. Please, be careful!");
}
@Test
void completionHandlerDefault() {
this.contextRunner
.run(context -> assertThat(context).doesNotHaveBean(ChatModelCompletionObservationHandler.class));
void promptContentHandlerDisabledNoTracer() {
this.contextRunner.withClassLoader(new FilteredClassLoader(Tracer.class))
.withPropertyValues("spring.ai.chat.observations.log-prompt=false")
.run(context -> assertThat(context).doesNotHaveBean(ChatModelPromptContentObservationHandler.class)
.doesNotHaveBean(ChatModelCompletionObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class)
.doesNotHaveBean(ErrorLoggingObservationHandler.class));
}
@Test
void completionHandlerEnabled() {
void promptContentHandlerDisabledWithTracer() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.withPropertyValues("spring.ai.chat.observations.log-prompt=false")
.run(context -> assertThat(context).doesNotHaveBean(ChatModelPromptContentObservationHandler.class)
.doesNotHaveBean(ChatModelCompletionObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class)
.doesNotHaveBean(ErrorLoggingObservationHandler.class));
}
@Test
void completionHandlerEnabledNoTracer(CapturedOutput output) {
this.contextRunner.withClassLoader(new FilteredClassLoader(Tracer.class))
.withPropertyValues("spring.ai.chat.observations.log-completion=true")
.run(context -> assertThat(context).hasSingleBean(ChatModelCompletionObservationHandler.class));
.run(context -> assertThat(context).doesNotHaveBean(ChatModelPromptContentObservationHandler.class)
.hasSingleBean(ChatModelCompletionObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class)
.doesNotHaveBean(ErrorLoggingObservationHandler.class));
assertThat(output).contains(
"You have enabled logging out the completion content with the risk of exposing sensitive or private information. Please, be careful!");
}
@Test
void completionHandlerDisabled() {
this.contextRunner.withPropertyValues("spring.ai.chat.observations.log-completion=false")
.run(context -> assertThat(context).doesNotHaveBean(ChatModelCompletionObservationHandler.class));
void completionHandlerEnabledWithTracer(CapturedOutput output) {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.withPropertyValues("spring.ai.chat.observations.log-completion=true")
.run(context -> assertThat(context).doesNotHaveBean(ChatModelPromptContentObservationHandler.class)
.doesNotHaveBean(ChatModelCompletionObservationHandler.class)
.hasSingleBean(TracingAwareLoggingObservationHandler.class)
.doesNotHaveBean(ErrorLoggingObservationHandler.class));
assertThat(output).contains(
"You have enabled logging out the completion content with the risk of exposing sensitive or private information. Please, be careful!");
}
@Test
void completionHandlerDisabledNoTracer() {
this.contextRunner.withClassLoader(new FilteredClassLoader(Tracer.class))
.withPropertyValues("spring.ai.chat.observations.log-completion=false")
.run(context -> assertThat(context).doesNotHaveBean(ChatModelPromptContentObservationHandler.class)
.doesNotHaveBean(ChatModelCompletionObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class)
.doesNotHaveBean(ErrorLoggingObservationHandler.class));
}
@Test
void completionHandlerDisabledWithTracer() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.withPropertyValues("spring.ai.chat.observations.log-completion=false")
.run(context -> assertThat(context).doesNotHaveBean(ChatModelPromptContentObservationHandler.class)
.doesNotHaveBean(ChatModelCompletionObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class)
.doesNotHaveBean(ErrorLoggingObservationHandler.class));
}
@Test
void errorLoggingHandlerEnabledNoTracer() {
this.contextRunner.withClassLoader(new FilteredClassLoader(Tracer.class))
.withPropertyValues("spring.ai.chat.observations.include-error-logging=true")
.run(context -> assertThat(context).doesNotHaveBean(ChatModelPromptContentObservationHandler.class)
.doesNotHaveBean(ChatModelCompletionObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class)
.doesNotHaveBean(ErrorLoggingObservationHandler.class));
}
@Test
void errorLoggingHandlerEnabledWithTracer() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.withPropertyValues("spring.ai.chat.observations.include-error-logging=true")
.run(context -> assertThat(context).doesNotHaveBean(ChatModelPromptContentObservationHandler.class)
.doesNotHaveBean(ChatModelCompletionObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class)
.hasSingleBean(ErrorLoggingObservationHandler.class));
}
@Test
void errorLoggingHandlerDisabledNoTracer() {
this.contextRunner.withClassLoader(new FilteredClassLoader(Tracer.class))
.withPropertyValues("spring.ai.chat.observations.include-error-logging=false")
.run(context -> assertThat(context).doesNotHaveBean(ChatModelPromptContentObservationHandler.class)
.doesNotHaveBean(ChatModelCompletionObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class)
.doesNotHaveBean(ErrorLoggingObservationHandler.class));
}
@Test
void errorLoggingHandlerDisabledWithTracer() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.withPropertyValues("spring.ai.chat.observations.include-error-logging=false")
.run(context -> assertThat(context).doesNotHaveBean(ChatModelPromptContentObservationHandler.class)
.doesNotHaveBean(ChatModelCompletionObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class)
.doesNotHaveBean(ErrorLoggingObservationHandler.class));
}
@Test
void customChatModelPromptContentObservationHandlerNoTracer() {
this.contextRunner.withClassLoader(new FilteredClassLoader(Tracer.class))
.withUserConfiguration(CustomChatModelPromptContentObservationHandlerConfiguration.class)
.withPropertyValues("spring.ai.chat.observations.log-prompt=true")
.run(context -> assertThat(context).hasSingleBean(ChatModelPromptContentObservationHandler.class)
.hasBean("customChatModelPromptContentObservationHandler")
.doesNotHaveBean(ChatModelCompletionObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class)
.doesNotHaveBean(ErrorLoggingObservationHandler.class));
}
@Test
void customChatModelPromptContentObservationHandlerWithTracer() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.withUserConfiguration(CustomChatModelPromptContentObservationHandlerConfiguration.class)
.withPropertyValues("spring.ai.chat.observations.log-prompt=true")
.run(context -> assertThat(context).hasSingleBean(ChatModelPromptContentObservationHandler.class)
.hasBean("customChatModelPromptContentObservationHandler")
.doesNotHaveBean(ChatModelCompletionObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class)
.doesNotHaveBean(ErrorLoggingObservationHandler.class));
}
@Test
void customTracingAwareLoggingObservationHandlerForChatModelPromptContent() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.withUserConfiguration(
CustomTracingAwareLoggingObservationHandlerForChatModelPromptContentConfiguration.class)
.withPropertyValues("spring.ai.chat.observations.log-prompt=true")
.run(context -> {
assertThat(context).doesNotHaveBean(ChatModelPromptContentObservationHandler.class)
.doesNotHaveBean(ChatModelCompletionObservationHandler.class)
.hasSingleBean(TracingAwareLoggingObservationHandler.class)
.hasBean("chatModelPromptContentObservationHandler")
.doesNotHaveBean(ErrorLoggingObservationHandler.class);
assertThat(context.getBean(TracingAwareLoggingObservationHandler.class)).isSameAs(
CustomTracingAwareLoggingObservationHandlerForChatModelPromptContentConfiguration.handlerInstance);
});
}
@Test
void customChatModelCompletionObservationHandlerNoTracer() {
this.contextRunner.withClassLoader(new FilteredClassLoader(Tracer.class))
.withUserConfiguration(CustomChatModelCompletionObservationHandlerConfiguration.class)
.withPropertyValues("spring.ai.chat.observations.log-completion=true")
.run(context -> assertThat(context).doesNotHaveBean(ChatModelPromptContentObservationHandler.class)
.hasSingleBean(ChatModelCompletionObservationHandler.class)
.hasBean("customChatModelCompletionObservationHandler")
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class)
.doesNotHaveBean(ErrorLoggingObservationHandler.class));
}
@Test
void customChatModelCompletionObservationHandlerWithTracer() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.withUserConfiguration(CustomChatModelCompletionObservationHandlerConfiguration.class)
.withPropertyValues("spring.ai.chat.observations.log-completion=true")
.run(context -> assertThat(context).doesNotHaveBean(ChatModelPromptContentObservationHandler.class)
.hasSingleBean(ChatModelCompletionObservationHandler.class)
.hasBean("customChatModelCompletionObservationHandler")
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class)
.doesNotHaveBean(ErrorLoggingObservationHandler.class));
}
@Test
void customTracingAwareLoggingObservationHandlerForChatModelCompletion() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.withUserConfiguration(CustomTracingAwareLoggingObservationHandlerForChatModelCompletionConfiguration.class)
.withPropertyValues("spring.ai.chat.observations.log-completion=true")
.run(context -> {
assertThat(context).doesNotHaveBean(ChatModelPromptContentObservationHandler.class)
.doesNotHaveBean(ChatModelCompletionObservationHandler.class)
.hasSingleBean(TracingAwareLoggingObservationHandler.class)
.hasBean("chatModelCompletionObservationHandler")
.doesNotHaveBean(ErrorLoggingObservationHandler.class);
assertThat(context.getBean(TracingAwareLoggingObservationHandler.class)).isSameAs(
CustomTracingAwareLoggingObservationHandlerForChatModelCompletionConfiguration.handlerInstance);
});
}
@Test
void customErrorLoggingObservationHandler() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.withUserConfiguration(CustomErrorLoggingObservationHandlerConfiguration.class)
.withPropertyValues("spring.ai.chat.observations.include-error-logging=true")
.run(context -> assertThat(context).doesNotHaveBean(ChatModelPromptContentObservationHandler.class)
.doesNotHaveBean(ChatModelCompletionObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class)
.hasSingleBean(ErrorLoggingObservationHandler.class)
.hasBean("customErrorLoggingObservationHandler"));
}
@Configuration(proxyBeanMethods = false)
static class TracerConfiguration {
@Bean
Tracer tracer() {
return mock(Tracer.class);
}
}
@Configuration(proxyBeanMethods = false)
static class CustomChatModelPromptContentObservationHandlerConfiguration {
@Bean
ChatModelPromptContentObservationHandler customChatModelPromptContentObservationHandler() {
return new ChatModelPromptContentObservationHandler();
}
}
@Configuration(proxyBeanMethods = false)
static class CustomTracingAwareLoggingObservationHandlerForChatModelPromptContentConfiguration {
static TracingAwareLoggingObservationHandler<ChatModelObservationContext> handlerInstance = new TracingAwareLoggingObservationHandler<>(
new ChatModelPromptContentObservationHandler(), null);
@Bean
TracingAwareLoggingObservationHandler<ChatModelObservationContext> chatModelPromptContentObservationHandler() {
return handlerInstance;
}
}
@Configuration(proxyBeanMethods = false)
static class CustomChatModelCompletionObservationHandlerConfiguration {
@Bean
ChatModelCompletionObservationHandler customChatModelCompletionObservationHandler() {
return new ChatModelCompletionObservationHandler();
}
}
@Configuration(proxyBeanMethods = false)
static class CustomTracingAwareLoggingObservationHandlerForChatModelCompletionConfiguration {
static TracingAwareLoggingObservationHandler<ChatModelObservationContext> handlerInstance = new TracingAwareLoggingObservationHandler<>(
new ChatModelCompletionObservationHandler(), null);
@Bean
TracingAwareLoggingObservationHandler<ChatModelObservationContext> chatModelCompletionObservationHandler() {
return handlerInstance;
}
}
@Configuration(proxyBeanMethods = false)
static class CustomErrorLoggingObservationHandlerConfiguration {
@Bean
ErrorLoggingObservationHandler customErrorLoggingObservationHandler(Tracer tracer) {
return new ErrorLoggingObservationHandler(tracer, List.of(ChatClientObservationContext.class));
}
}
}

View File

@@ -18,13 +18,20 @@ package org.springframework.ai.model.image.observation.autoconfigure;
import io.micrometer.tracing.Tracer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.ai.image.observation.ImageModelObservationContext;
import org.springframework.ai.image.observation.ImageModelPromptContentObservationHandler;
import org.springframework.ai.observation.TracingAwareLoggingObservationHandler;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Unit tests for {@link ImageObservationAutoConfiguration}.
@@ -32,22 +39,129 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Thomas Vitale
* @author Jonatan Ivanov
*/
@ExtendWith(OutputCaptureExtension.class)
class ImageObservationAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(ImageObservationAutoConfiguration.class));
@Test
void promptHandlerDefault() {
this.contextRunner
.run(context -> assertThat(context).doesNotHaveBean(ImageModelPromptContentObservationHandler.class));
void imageModelPromptContentHandlerNoTracer() {
this.contextRunner.withClassLoader(new FilteredClassLoader(Tracer.class))
.run(context -> assertThat(context).doesNotHaveBean(ImageModelPromptContentObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class));
}
@Test
void promptHandlerEnabled() {
void imageModelPromptContentHandlerWithTracer() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.run(context -> assertThat(context).doesNotHaveBean(ImageModelPromptContentObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class));
}
@Test
void imageModelPromptContentHandlerEnabledNoTracer(CapturedOutput output) {
this.contextRunner.withClassLoader(new FilteredClassLoader(Tracer.class))
.withPropertyValues("spring.ai.image.observations.log-prompt=true")
.run(context -> assertThat(context).hasSingleBean(ImageModelPromptContentObservationHandler.class));
.run(context -> assertThat(context).hasSingleBean(ImageModelPromptContentObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class));
assertThat(output).contains(
"You have enabled logging out the image prompt content with the risk of exposing sensitive or private information. Please, be careful!");
}
@Test
void imageModelPromptContentHandlerEnabledWithTracer(CapturedOutput output) {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.withPropertyValues("spring.ai.image.observations.log-prompt=true")
.run(context -> assertThat(context).doesNotHaveBean(ImageModelPromptContentObservationHandler.class)
.hasSingleBean(TracingAwareLoggingObservationHandler.class));
assertThat(output).contains(
"You have enabled logging out the image prompt content with the risk of exposing sensitive or private information. Please, be careful!");
}
@Test
void imageModelPromptContentHandlerDisabledNoTracer() {
this.contextRunner.withClassLoader(new FilteredClassLoader(Tracer.class))
.withPropertyValues("spring.ai.image.observations.log-prompt=false")
.run(context -> assertThat(context).doesNotHaveBean(ImageModelPromptContentObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class));
}
@Test
void imageModelPromptContentHandlerDisabledWithTracer() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.withPropertyValues("spring.ai.image.observations.log-prompt=false")
.run(context -> assertThat(context).doesNotHaveBean(ImageModelPromptContentObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class));
}
@Test
void customChatClientPromptContentObservationHandlerNoTracer() {
this.contextRunner.withClassLoader(new FilteredClassLoader(Tracer.class))
.withUserConfiguration(CustomImageModelPromptContentObservationHandlerConfiguration.class)
.withPropertyValues("spring.ai.image.observations.log-prompt=true")
.run(context -> assertThat(context).hasSingleBean(ImageModelPromptContentObservationHandler.class)
.hasBean("customImageModelPromptContentObservationHandler")
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class));
}
@Test
void customChatClientPromptContentObservationHandlerWithTracer() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.withUserConfiguration(CustomImageModelPromptContentObservationHandlerConfiguration.class)
.withPropertyValues("spring.ai.image.observations.log-prompt=true")
.run(context -> assertThat(context).hasSingleBean(ImageModelPromptContentObservationHandler.class)
.hasBean("customImageModelPromptContentObservationHandler")
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class));
}
@Test
void customTracingAwareLoggingObservationHandler() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.withUserConfiguration(CustomTracingAwareLoggingObservationHandlerConfiguration.class)
.withPropertyValues("spring.ai.image.observations.log-prompt=true")
.run(context -> {
assertThat(context).doesNotHaveBean(ImageModelPromptContentObservationHandler.class)
.hasSingleBean(TracingAwareLoggingObservationHandler.class)
.hasBean("imageModelPromptContentObservationHandler");
assertThat(context.getBean(TracingAwareLoggingObservationHandler.class))
.isSameAs(CustomTracingAwareLoggingObservationHandlerConfiguration.handlerInstance);
});
}
@Configuration(proxyBeanMethods = false)
static class TracerConfiguration {
@Bean
Tracer tracer() {
return mock(Tracer.class);
}
}
@Configuration(proxyBeanMethods = false)
static class CustomImageModelPromptContentObservationHandlerConfiguration {
@Bean
ImageModelPromptContentObservationHandler customImageModelPromptContentObservationHandler() {
return new ImageModelPromptContentObservationHandler();
}
}
@Configuration(proxyBeanMethods = false)
static class CustomTracingAwareLoggingObservationHandlerConfiguration {
static TracingAwareLoggingObservationHandler<ImageModelObservationContext> handlerInstance = new TracingAwareLoggingObservationHandler<>(
new ImageModelPromptContentObservationHandler(), null);
@Bean
TracingAwareLoggingObservationHandler<ImageModelObservationContext> imageModelPromptContentObservationHandler() {
return handlerInstance;
}
}
}

View File

@@ -18,13 +18,20 @@ package org.springframework.ai.vectorstore.observation.autoconfigure;
import io.micrometer.tracing.Tracer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.ai.observation.TracingAwareLoggingObservationHandler;
import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext;
import org.springframework.ai.vectorstore.observation.VectorStoreQueryResponseObservationHandler;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Unit tests for {@link VectorStoreObservationAutoConfiguration}.
@@ -32,28 +39,127 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Christian Tzolov
* @author Jonatan Ivanov
*/
@ExtendWith(OutputCaptureExtension.class)
class VectorStoreObservationAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(VectorStoreObservationAutoConfiguration.class));
@Test
void queryResponseHandlerDefault() {
this.contextRunner
.run(context -> assertThat(context).doesNotHaveBean(VectorStoreQueryResponseObservationHandler.class));
void queryResponseHandlerNoTracer() {
this.contextRunner.withClassLoader(new FilteredClassLoader(Tracer.class))
.run(context -> assertThat(context).doesNotHaveBean(VectorStoreQueryResponseObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class));
}
@Test
void queryResponseHandlerEnabled() {
void queryResponseHandlerWithTracer() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.run(context -> assertThat(context).doesNotHaveBean(VectorStoreQueryResponseObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class));
}
@Test
void queryResponseHandlerEnabledNoTracer(CapturedOutput output) {
this.contextRunner.withClassLoader(new FilteredClassLoader(Tracer.class))
.withPropertyValues("spring.ai.vectorstore.observations.log-query-response=true")
.run(context -> assertThat(context).hasSingleBean(VectorStoreQueryResponseObservationHandler.class));
.run(context -> assertThat(context).hasSingleBean(VectorStoreQueryResponseObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class));
assertThat(output).contains(
"You have enabled logging out of the query response content with the risk of exposing sensitive or private information. Please, be careful!");
}
@Test
void queryResponseHandlerDisabled() {
this.contextRunner.withPropertyValues("spring.ai.vectorstore.observations.log-query-response=false")
.run(context -> assertThat(context).doesNotHaveBean(VectorStoreQueryResponseObservationHandler.class));
void queryResponseHandlerEnabledWithTracer(CapturedOutput output) {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.withPropertyValues("spring.ai.vectorstore.observations.log-query-response=true")
.run(context -> assertThat(context).doesNotHaveBean(VectorStoreQueryResponseObservationHandler.class)
.hasSingleBean(TracingAwareLoggingObservationHandler.class));
assertThat(output).contains(
"You have enabled logging out of the query response content with the risk of exposing sensitive or private information. Please, be careful!");
}
@Test
void queryResponseHandlerDisabledNoTracer() {
this.contextRunner.withClassLoader(new FilteredClassLoader(Tracer.class))
.withPropertyValues("spring.ai.vectorstore.observations.log-query-response=false")
.run(context -> assertThat(context).doesNotHaveBean(VectorStoreQueryResponseObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class));
}
@Test
void queryResponseHandlerDisabledWithTracer() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.withPropertyValues("spring.ai.vectorstore.observations.log-query-response=false")
.run(context -> assertThat(context).doesNotHaveBean(VectorStoreQueryResponseObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class));
}
@Test
void customQueryResponseHandlerNoTracer() {
this.contextRunner.withClassLoader(new FilteredClassLoader(Tracer.class))
.withUserConfiguration(CustomVectorStoreQueryResponseObservationHandlerConfiguration.class)
.withPropertyValues("spring.ai.vectorstore.observations.log-query-response=true")
.run(context -> assertThat(context).hasSingleBean(VectorStoreQueryResponseObservationHandler.class)
.hasBean("customVectorStoreQueryResponseObservationHandler")
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class));
}
@Test
void customQueryResponseHandlerWithTracer() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.withUserConfiguration(CustomVectorStoreQueryResponseObservationHandlerConfiguration.class)
.withPropertyValues("spring.ai.vectorstore.observations.log-query-response=true")
.run(context -> assertThat(context).hasSingleBean(VectorStoreQueryResponseObservationHandler.class)
.hasBean("customVectorStoreQueryResponseObservationHandler")
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class));
}
@Test
void customTracingAwareLoggingObservationHandler() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.withUserConfiguration(CustomTracingAwareLoggingObservationHandlerConfiguration.class)
.withPropertyValues("spring.ai.vectorstore.observations.log-query-response=true")
.run(context -> {
assertThat(context).doesNotHaveBean(VectorStoreQueryResponseObservationHandler.class)
.hasSingleBean(TracingAwareLoggingObservationHandler.class)
.hasBean("vectorStoreQueryResponseObservationHandler");
assertThat(context.getBean(TracingAwareLoggingObservationHandler.class))
.isSameAs(CustomTracingAwareLoggingObservationHandlerConfiguration.handlerInstance);
});
}
@Configuration(proxyBeanMethods = false)
static class TracerConfiguration {
@Bean
Tracer tracer() {
return mock(Tracer.class);
}
}
@Configuration(proxyBeanMethods = false)
static class CustomVectorStoreQueryResponseObservationHandlerConfiguration {
@Bean
VectorStoreQueryResponseObservationHandler customVectorStoreQueryResponseObservationHandler() {
return new VectorStoreQueryResponseObservationHandler();
}
}
@Configuration(proxyBeanMethods = false)
static class CustomTracingAwareLoggingObservationHandlerConfiguration {
static TracingAwareLoggingObservationHandler<VectorStoreObservationContext> handlerInstance = new TracingAwareLoggingObservationHandler<>(
new VectorStoreQueryResponseObservationHandler(), null);
@Bean
TracingAwareLoggingObservationHandler<VectorStoreObservationContext> vectorStoreQueryResponseObservationHandler() {
return handlerInstance;
}
}
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2023-2025 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.observation;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationHandler;
import io.micrometer.tracing.CurrentTraceContext;
import io.micrometer.tracing.Span;
import io.micrometer.tracing.TraceContext;
import io.micrometer.tracing.Tracer;
import io.micrometer.tracing.handler.TracingObservationHandler;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;
/**
* Tests for {@link TracingAwareLoggingObservationHandler}.
*
* @author Jonatan Ivanov
*/
@ExtendWith(MockitoExtension.class)
class TracingAwareLoggingObservationHandlerTests {
@Mock
private ObservationHandler<Observation.Context> delegate;
@Mock
private Tracer tracer;
@InjectMocks
private TracingAwareLoggingObservationHandler<Observation.Context> handler;
@Test
void callsShouldBeDelegated() {
Observation.Context context = new Observation.Context();
context.put(TracingObservationHandler.TracingContext.class, new TracingObservationHandler.TracingContext());
handler.onStart(context);
verify(delegate).onStart(context);
handler.onError(context);
verify(delegate).onError(context);
Observation.Event event = Observation.Event.of("test");
handler.onEvent(event, context);
verify(delegate).onEvent(event, context);
handler.onScopeOpened(context);
verify(delegate).onScopeOpened(context);
handler.onStop(context);
verify(delegate).onStop(context);
handler.onScopeClosed(context);
verify(delegate).onScopeClosed(context);
handler.onScopeReset(context);
verify(delegate).onScopeReset(context);
handler.supportsContext(context);
verify(delegate).supportsContext(context);
}
@Test
void spanShouldBeAvailableOnStop() {
Observation.Context observationContext = new Observation.Context();
TracingObservationHandler.TracingContext tracingContext = new TracingObservationHandler.TracingContext();
observationContext.put(TracingObservationHandler.TracingContext.class, tracingContext);
Span span = mock(Span.class);
tracingContext.setSpan(span);
TraceContext traceContext = mock(TraceContext.class);
CurrentTraceContext currentTraceContext = mock(CurrentTraceContext.class);
CurrentTraceContext.Scope scope = mock(CurrentTraceContext.Scope.class);
when(span.context()).thenReturn(traceContext);
when(tracer.currentTraceContext()).thenReturn(currentTraceContext);
when(currentTraceContext.maybeScope(traceContext)).thenReturn(scope);
handler.onStop(observationContext);
verify(scope).close();
verify(currentTraceContext).maybeScope(traceContext);
verify(delegate).onStop(observationContext);
}
}