Spring Boot auto-configuration for observability of Spring AI chat model operations through Micrometer metrics and distributed tracing
Spring Boot auto-configuration module that provides comprehensive observability capabilities for Spring AI chat model operations through Micrometer metrics collection and distributed tracing integration.
org.springframework.ai:spring-ai-autoconfigure-model-chat-observation:1.1.2This auto-configuration module automatically configures observation handlers for monitoring and tracing Spring AI chat model interactions when added to a Spring Boot application. It seamlessly integrates with Spring Boot Actuator and Micrometer to provide:
The module activates automatically when ChatModel is on the classpath and adapts its configuration based on available dependencies (MeterRegistry, Tracer).
This module leverages Spring Boot's auto-configuration mechanism to provide zero-configuration observability for Spring AI chat operations. Understanding its architecture helps explain how it integrates seamlessly into Spring Boot applications.
Spring Boot automatically discovers this module through the standard auto-configuration mechanism:
META-INF Registration: The module registers itself in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:
org.springframework.ai.model.chat.observation.autoconfigure.ChatObservationAutoConfigurationConditional Activation: The auto-configuration only activates when:
ChatModel.class is on the classpath (from spring-ai-client-chat module)ObservationAutoConfiguration from Spring Boot ActuatorNo Explicit Configuration Required: Developers simply add the Maven dependency - Spring Boot handles the rest.
The module implements a sophisticated two-path strategy based on the presence of distributed tracing:
When io.micrometer.tracing.Tracer class and bean are present:
ChatObservationAutoConfiguration
├── ChatModelMeterObservationHandler (always, if MeterRegistry present)
└── TracerPresentObservationConfiguration
├── TracingAwareLoggingObservationHandler<ChatModelObservationContext> (for prompts, if enabled)
├── TracingAwareLoggingObservationHandler<ChatModelObservationContext> (for completions, if enabled)
└── ErrorLoggingObservationHandler (if enabled)Benefits: Logs include trace IDs and span IDs for correlation in distributed systems.
When io.micrometer.tracing.Tracer class is not on the classpath:
ChatObservationAutoConfiguration
├── ChatModelMeterObservationHandler (always, if MeterRegistry present)
└── TracerNotPresentObservationConfiguration
├── ChatModelPromptContentObservationHandler (for prompts, if enabled)
└── ChatModelCompletionObservationHandler (for completions, if enabled)Benefits: Simpler logging without tracing overhead for non-distributed applications.
The module creates beans in this order:
spring.ai.chat.observations.*MeterRegistry)All beans use @ConditionalOnMissingBean, allowing custom implementations to override defaults.
Spring Boot Application
└── Spring Boot Actuator
├── ObservationRegistry (core observation infrastructure)
├── MeterRegistry (metrics collection)
└── Observation Handlers
└── ChatModelMeterObservationHandler (registered by this module)
└── Collects metrics to MeterRegistry
└── Exposed via /actuator/metrics endpointsSpring Boot Application
└── Micrometer Tracing
├── Tracer (trace context propagation)
└── Observation Handlers
├── TracingAwareLoggingObservationHandler (prompts)
├── TracingAwareLoggingObservationHandler (completions)
└── ErrorLoggingObservationHandler
└── All logs include trace/span IDsChatModel Implementation (e.g., OpenAiChatModel)
└── Executes chat operations
└── Creates ChatModelObservationContext
└── Triggers all registered ObservationHandlers
├── ChatModelMeterObservationHandler → Metrics
├── Prompt logging handler → Logs (if enabled)
├── Completion logging handler → Logs (if enabled)
└── Error logging handler → Error logs (if enabled)application.properties
├── spring.ai.chat.observations.log-prompt=true
├── spring.ai.chat.observations.log-completion=true
└── spring.ai.chat.observations.include-error-logging=true
↓
ChatObservationProperties (bound by @ConfigurationProperties)
↓
@ConditionalOnProperty annotations on bean methods
↓
Selective bean creation based on property values<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-observation</artifactId>
<version>1.1.2</version>
</dependency>Required Dependencies (typically provided by Spring Boot starters):
<!-- Core Spring AI chat client -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-client-chat</artifactId>
</dependency>
<!-- Spring Boot Actuator for observability infrastructure -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>Optional Dependencies (enables additional features):
<!-- For distributed tracing support -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing</artifactId>
</dependency>This module primarily works through Spring Boot auto-configuration and does not require explicit imports in most cases. However, for programmatic access to configuration or custom handlers:
// For programmatic access to observation properties
import org.springframework.ai.model.chat.observation.autoconfigure.ChatObservationProperties;
// For creating custom observation handlers (from spring-ai-client-chat module)
import org.springframework.ai.chat.observation.ChatModelMeterObservationHandler;
import org.springframework.ai.chat.observation.ChatModelPromptContentObservationHandler;
import org.springframework.ai.chat.observation.ChatModelCompletionObservationHandler;
import org.springframework.ai.model.observation.ErrorLoggingObservationHandler;
import org.springframework.ai.observation.TracingAwareLoggingObservationHandler;Note: The auto-configuration class (ChatObservationAutoConfiguration) is automatically discovered and applied by Spring Boot. You do not need to import or reference it directly unless creating custom configurations.
This module works through Spring Boot auto-configuration. Simply add the dependency and configure properties as needed.
With just the module on the classpath and Spring Boot Actuator available:
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MyAiApplication {
public static void main(String[] args) {
SpringApplication.run(MyAiApplication.class, args);
}
}The auto-configuration will automatically register:
ChatModelMeterObservationHandler for metrics collection (if MeterRegistry is present)No explicit configuration required - metrics are collected automatically.
Security Warning: These settings may expose sensitive information in logs and traces. Use with caution.
# application.properties
# Enable logging of prompt content (security warning issued at startup)
spring.ai.chat.observations.log-prompt=true
# Enable logging of completion content (security warning issued at startup)
spring.ai.chat.observations.log-completion=true
# Enable error logging across multiple model contexts
spring.ai.chat.observations.include-error-logging=trueOr in YAML:
# application.yml
spring:
ai:
chat:
observations:
log-prompt: true # Default: false
log-completion: true # Default: false
include-error-logging: true # Default: falseWhen micrometer-tracing is on the classpath and a Tracer bean is configured:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<dependency>
<groupId>io.zipkin.reporter2</groupId>
<artifactId>zipkin-reporter-brave</artifactId>
</dependency>The module automatically registers tracing-aware observation handlers:
TracingAwareLoggingObservationHandler for prompt content (when enabled)TracingAwareLoggingObservationHandler for completion content (when enabled)ErrorLoggingObservationHandler for errors (when enabled)The module uses Spring Boot's conditional auto-configuration to adapt to the runtime environment.
package org.springframework.ai.model.chat.observation.autoconfigure;
@AutoConfiguration(afterName = "org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration")
@ConditionalOnClass(ChatModel.class)
@EnableConfigurationProperties(ChatObservationProperties.class)
public class ChatObservationAutoConfiguration {
// Bean definitions (see below)
}Activation Conditions:
ObservationAutoConfigurationChatModel.class on classpathChatObservationProperties configuration bindingBean Definitions:
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean(MeterRegistry.class)
ChatModelMeterObservationHandler chatModelMeterObservationHandler(ObjectProvider<MeterRegistry> meterRegistry);Creates a meter-based observation handler for collecting metrics about chat model operations (execution time, token usage, etc.). Only created when:
ChatModelMeterObservationHandler beanMeterRegistry bean is available@Bean
@ConditionalOnMissingBean(value = ChatModelPromptContentObservationHandler.class,
name = "chatModelPromptContentObservationHandler")
@ConditionalOnProperty(prefix = "spring.ai.chat.observations",
name = "log-prompt",
havingValue = "true")
TracingAwareLoggingObservationHandler<ChatModelObservationContext>
chatModelPromptContentObservationHandler(Tracer tracer);Available when Tracer is present. Wraps prompt logging with distributed tracing context. Only created when:
spring.ai.chat.observations.log-prompt=trueTracer class and bean are availableSecurity: Logs warning at startup about potential sensitive data exposure.
@Bean
@ConditionalOnMissingBean(value = ChatModelCompletionObservationHandler.class,
name = "chatModelCompletionObservationHandler")
@ConditionalOnProperty(prefix = "spring.ai.chat.observations",
name = "log-completion",
havingValue = "true")
TracingAwareLoggingObservationHandler<ChatModelObservationContext>
chatModelCompletionObservationHandler(Tracer tracer);Available when Tracer is present. Wraps completion logging with distributed tracing context. Only created when:
spring.ai.chat.observations.log-completion=trueTracer class and bean are availableSecurity: Logs warning at startup about potential sensitive data exposure.
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "spring.ai.chat.observations",
name = "include-error-logging",
havingValue = "true")
ErrorLoggingObservationHandler errorLoggingObservationHandler(Tracer tracer);Available when Tracer is present. Logs errors across multiple observation context types. Only created when:
spring.ai.chat.observations.include-error-logging=trueErrorLoggingObservationHandler beanTracer class and bean are availableSupported Context Types:
EmbeddingModelObservationContext - Embedding model operationsImageModelObservationContext - Image model operationsChatModelObservationContext - Chat model operationsChatClientObservationContext - Chat client operationsAdvisorObservationContext - Advisor operations@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "spring.ai.chat.observations",
name = "log-prompt",
havingValue = "true")
ChatModelPromptContentObservationHandler chatModelPromptContentObservationHandler();Available when Tracer is NOT present. Provides basic prompt logging without tracing. Only created when:
spring.ai.chat.observations.log-prompt=trueChatModelPromptContentObservationHandler beanTracer class is NOT on classpathSecurity: Logs warning at startup about potential sensitive data exposure.
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "spring.ai.chat.observations",
name = "log-completion",
havingValue = "true")
ChatModelCompletionObservationHandler chatModelCompletionObservationHandler();Available when Tracer is NOT present. Provides basic completion logging without tracing. Only created when:
spring.ai.chat.observations.log-completion=trueChatModelCompletionObservationHandler beanTracer class is NOT on classpathSecurity: Logs warning at startup about potential sensitive data exposure.
All observation behavior is controlled through configuration properties.
package org.springframework.ai.model.chat.observation.autoconfigure;
@ConfigurationProperties("spring.ai.chat.observations")
public class ChatObservationProperties {
public static final String CONFIG_PREFIX = "spring.ai.chat.observations";
private boolean logCompletion = false;
private boolean logPrompt = false;
private boolean includeErrorLogging = false;
// Getters and setters
public boolean isLogCompletion();
public void setLogCompletion(boolean logCompletion);
public boolean isLogPrompt();
public void setLogPrompt(boolean logPrompt);
public boolean isIncludeErrorLogging();
public void setIncludeErrorLogging(boolean includeErrorLogging);
}Property: logCompletion
booleanfalsespring.ai.chat.observations.log-completionisLogCompletion(), setLogCompletion(boolean)Property: logPrompt
booleanfalsespring.ai.chat.observations.log-promptisLogPrompt(), setLogPrompt(boolean)Property: includeErrorLogging
booleanfalsespring.ai.chat.observations.include-error-loggingisIncludeErrorLogging(), setIncludeErrorLogging(boolean)The module enforces null-safety at the package level:
@NonNullApi
@NonNullFields
package org.springframework.ai.model.chat.observation.autoconfigure;@Nullable@NullableDefault setup with metrics only (no sensitive data logging):
# No configuration needed - metrics collection is automaticAutomatically collects:
Access metrics via Spring Boot Actuator endpoints:
GET /actuator/metrics/gen.ai.client.operation
GET /actuator/metrics/gen.ai.client.token.usageEnable all logging for debugging (development only):
# application-dev.properties
spring.ai.chat.observations.log-prompt=true
spring.ai.chat.observations.log-completion=true
spring.ai.chat.observations.include-error-logging=trueWarning Messages logged at startup:
WARN ... You have enabled logging out the prompt content with the risk of exposing
sensitive or private information. Please, be careful!
WARN ... You have enabled logging out the completion content with the risk of exposing
sensitive or private information. Please, be careful!Production setup with tracing but no content logging:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency># application-prod.properties
spring.ai.chat.observations.log-prompt=false
spring.ai.chat.observations.log-completion=false
spring.ai.chat.observations.include-error-logging=trueProvides:
Override default handlers with custom implementations:
import org.springframework.ai.chat.observation.ChatModelMeterObservationHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.micrometer.core.instrument.MeterRegistry;
@Configuration
public class CustomObservationConfig {
@Bean
public ChatModelMeterObservationHandler chatModelMeterObservationHandler(
MeterRegistry meterRegistry) {
// Custom meter handler with additional tags or behavior
return new ChatModelMeterObservationHandler(meterRegistry) {
// Override methods to customize behavior
};
}
}The auto-configuration respects @ConditionalOnMissingBean, so your custom bean takes precedence.
Access configuration properties programmatically:
import org.springframework.ai.model.chat.observation.autoconfigure.ChatObservationProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class ObservationStatusChecker {
@Autowired
private ChatObservationProperties properties;
public void checkObservationSettings() {
if (properties.isLogPrompt()) {
// Prompt logging is enabled
System.out.println("Prompt logging: ENABLED (security risk)");
}
if (properties.isLogCompletion()) {
// Completion logging is enabled
System.out.println("Completion logging: ENABLED (security risk)");
}
if (properties.isIncludeErrorLogging()) {
// Error logging is enabled
System.out.println("Error logging: ENABLED");
}
}
}Implement a custom logging handler that filters sensitive data:
import org.springframework.ai.chat.observation.ChatModelPromptContentObservationHandler;
import org.springframework.ai.chat.observation.ChatModelObservationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.micrometer.observation.Observation;
@Configuration
public class FilteredLoggingConfig {
@Bean
public ChatModelPromptContentObservationHandler chatModelPromptContentObservationHandler() {
return new ChatModelPromptContentObservationHandler() {
@Override
public void onStart(ChatModelObservationContext context) {
// Filter or redact sensitive information before logging
String prompt = extractPromptContent(context);
String filtered = redactSensitiveData(prompt);
logFiltered(filtered);
}
private String extractPromptContent(ChatModelObservationContext context) {
if (context.getRequest() != null &&
context.getRequest().getInstructions() != null) {
return context.getRequest().getInstructions().toString();
}
return "";
}
private String redactSensitiveData(String content) {
// Implement filtering logic (e.g., regex patterns for emails, SSNs, etc.)
return content.replaceAll("\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b", "[EMAIL]")
.replaceAll("\\b\\d{3}-\\d{2}-\\d{4}\\b", "[SSN]");
}
private void logFiltered(String content) {
// Custom logging implementation
}
};
}
}Configure custom meters with additional tags for alerting:
import org.springframework.ai.chat.observation.ChatModelMeterObservationHandler;
import org.springframework.ai.chat.observation.ChatModelObservationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
@Configuration
public class CustomMetricsConfig {
@Bean
public ChatModelMeterObservationHandler chatModelMeterObservationHandler(
MeterRegistry meterRegistry) {
return new ChatModelMeterObservationHandler(meterRegistry) {
@Override
public void onStop(ChatModelObservationContext context) {
super.onStop(context);
// Add custom metrics with additional tags
if (context.getResponse() != null) {
Tags tags = Tags.of(
Tag.of("model", context.getModelName()),
Tag.of("provider", context.getModelProvider()),
Tag.of("status", "success")
);
meterRegistry.counter("ai.chat.requests", tags).increment();
}
}
@Override
public void onError(ChatModelObservationContext context) {
super.onError(context);
// Track errors separately
Tags tags = Tags.of(
Tag.of("model", context.getModelName()),
Tag.of("provider", context.getModelProvider()),
Tag.of("status", "error")
);
meterRegistry.counter("ai.chat.requests", tags).increment();
}
};
}
}Enable different logging levels based on Spring profiles:
import org.springframework.ai.chat.observation.ChatModelPromptContentObservationHandler;
import org.springframework.ai.chat.observation.ChatModelCompletionObservationHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@Configuration
public class ProfileBasedObservationConfig {
@Bean
@Profile("dev")
public ChatModelPromptContentObservationHandler devPromptObservationHandler() {
// Full logging in development
return new ChatModelPromptContentObservationHandler();
}
@Bean
@Profile("dev")
public ChatModelCompletionObservationHandler devCompletionObservationHandler() {
// Full logging in development
return new ChatModelCompletionObservationHandler();
}
// No logging beans for production profile
// Metrics-only configuration will be used
}Corresponding application properties:
# application-dev.properties
spring.ai.chat.observations.log-prompt=true
spring.ai.chat.observations.log-completion=true
spring.profiles.active=dev
# application-prod.properties
spring.ai.chat.observations.log-prompt=false
spring.ai.chat.observations.log-completion=false
spring.profiles.active=prod# Chat Observation Configuration
spring.ai.chat.observations.log-prompt=false
spring.ai.chat.observations.log-completion=false
spring.ai.chat.observations.include-error-logging=true
# Actuator configuration for metrics exposure
management.endpoints.web.exposure.include=health,info,metrics,prometheus
management.metrics.export.prometheus.enabled=true
# Tracing configuration (if using Zipkin)
management.tracing.sampling.probability=1.0
management.zipkin.tracing.endpoint=http://localhost:9411/api/v2/spansspring:
ai:
chat:
observations:
log-prompt: false # Default: false
log-completion: false # Default: false
include-error-logging: true # Default: false
management:
endpoints:
web:
exposure:
include:
- health
- info
- metrics
- prometheus
metrics:
export:
prometheus:
enabled: true
tracing:
sampling:
probability: 1.0
zipkin:
tracing:
endpoint: http://localhost:9411/api/v2/spansDifferent settings for different environments:
# application.properties (common defaults)
spring.ai.chat.observations.log-prompt=false
spring.ai.chat.observations.log-completion=false
spring.ai.chat.observations.include-error-logging=false# application-dev.properties (development)
spring.ai.chat.observations.log-prompt=true
spring.ai.chat.observations.log-completion=true
spring.ai.chat.observations.include-error-logging=true# application-prod.properties (production)
spring.ai.chat.observations.log-prompt=false
spring.ai.chat.observations.log-completion=false
spring.ai.chat.observations.include-error-logging=true# configmap.yaml for Kubernetes deployment
apiVersion: v1
kind: ConfigMap
metadata:
name: ai-app-config
namespace: production
data:
application.properties: |
# Production observability settings
spring.ai.chat.observations.log-prompt=false
spring.ai.chat.observations.log-completion=false
spring.ai.chat.observations.include-error-logging=true
# Metrics export to Prometheus
management.endpoints.web.exposure.include=health,metrics,prometheus
management.metrics.export.prometheus.enabled=true
# Distributed tracing to Jaeger
management.tracing.sampling.probability=0.1
management.otlp.tracing.endpoint=http://jaeger-collector:4318/v1/tracesWhen the module is active, the following metrics are automatically collected (provided by Spring AI core):
gen.ai.client.operation
gen.ai.client.token.usage
When exported to Prometheus:
gen_ai_client_operation_seconds_count{...}
gen_ai_client_operation_seconds_sum{...}
gen_ai_client_operation_seconds_max{...}
gen_ai_client_token_usage_total{token_type="input",...}
gen_ai_client_token_usage_total{token_type="output",...}import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class MetricsAnalyzer {
@Autowired
private MeterRegistry meterRegistry;
public void analyzeOperationMetrics() {
Timer timer = meterRegistry.find("gen.ai.client.operation").timer();
if (timer != null) {
long count = timer.count();
double meanMs = timer.mean(java.util.concurrent.TimeUnit.MILLISECONDS);
double maxMs = timer.max(java.util.concurrent.TimeUnit.MILLISECONDS);
System.out.println("Total operations: " + count);
System.out.println("Average latency: " + meanMs + " ms");
System.out.println("Max latency: " + maxMs + " ms");
}
}
public void analyzeTokenUsage() {
meterRegistry.find("gen.ai.client.token.usage")
.counters()
.forEach(counter -> {
String tokenType = counter.getId().getTag("token_type");
double totalTokens = counter.count();
System.out.println("Token type: " + tokenType +
", Total: " + totalTokens);
});
}
}The error logging handler supports multiple observation context types from Spring AI:
ChatModelObservationContext
ChatClientObservationContext
AdvisorObservationContext
EmbeddingModelObservationContext
ImageModelObservationContext
The module integrates seamlessly with Spring Boot Actuator:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>Metrics Endpoint: /actuator/metrics
Query specific metrics:
# Get chat operation metrics
curl http://localhost:8080/actuator/metrics/gen.ai.client.operation
# Get token usage metrics
curl http://localhost:8080/actuator/metrics/gen.ai.client.token.usagePrometheus Endpoint: /actuator/prometheus
# Get all metrics in Prometheus format
curl http://localhost:8080/actuator/prometheus | grep gen_aiWhile this module doesn't provide health indicators directly, the observation data can be used by custom health indicators:
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
@Component
public class ChatModelHealthIndicator implements HealthIndicator {
private final MeterRegistry meterRegistry;
public ChatModelHealthIndicator(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@Override
public Health health() {
Timer timer = meterRegistry.find("gen.ai.client.operation").timer();
if (timer != null && timer.count() > 0) {
double avgLatencyMs = timer.mean(java.util.concurrent.TimeUnit.MILLISECONDS);
if (avgLatencyMs < 1000) {
return Health.up()
.withDetail("avgLatencyMs", avgLatencyMs)
.build();
} else {
return Health.degraded()
.withDetail("avgLatencyMs", avgLatencyMs)
.withDetail("message", "High latency detected")
.build();
}
}
return Health.unknown().build();
}
}import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
@Component
public class AdvancedChatModelHealthIndicator implements HealthIndicator {
private final MeterRegistry meterRegistry;
private static final double LATENCY_THRESHOLD_MS = 2000.0;
private static final double TOKEN_RATE_THRESHOLD = 10000.0; // tokens per minute
public AdvancedChatModelHealthIndicator(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@Override
public Health health() {
Timer operationTimer = meterRegistry.find("gen.ai.client.operation").timer();
Counter tokenCounter = meterRegistry.find("gen.ai.client.token.usage").counter();
if (operationTimer == null) {
return Health.unknown()
.withDetail("message", "No chat operations observed yet")
.build();
}
double avgLatencyMs = operationTimer.mean(java.util.concurrent.TimeUnit.MILLISECONDS);
double maxLatencyMs = operationTimer.max(java.util.concurrent.TimeUnit.MILLISECONDS);
long operationCount = operationTimer.count();
Health.Builder healthBuilder = Health.up();
healthBuilder.withDetail("operationCount", operationCount)
.withDetail("avgLatencyMs", avgLatencyMs)
.withDetail("maxLatencyMs", maxLatencyMs);
if (tokenCounter != null) {
double totalTokens = tokenCounter.count();
healthBuilder.withDetail("totalTokensUsed", totalTokens);
// Estimate token rate (rough approximation)
if (operationCount > 0) {
double avgTokensPerOperation = totalTokens / operationCount;
healthBuilder.withDetail("avgTokensPerOperation", avgTokensPerOperation);
}
}
// Determine health status
if (avgLatencyMs > LATENCY_THRESHOLD_MS) {
return healthBuilder.down()
.withDetail("issue", "Average latency exceeds threshold")
.withDetail("threshold", LATENCY_THRESHOLD_MS)
.build();
} else if (avgLatencyMs > LATENCY_THRESHOLD_MS * 0.7) {
return healthBuilder.status("DEGRADED")
.withDetail("warning", "Latency approaching threshold")
.build();
}
return healthBuilder.build();
}
}When Micrometer Tracing is available, the module automatically creates tracing-aware handlers.
<!-- Brave (Zipkin) tracing bridge -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<!-- Zipkin reporter -->
<dependency>
<groupId>io.zipkin.reporter2</groupId>
<artifactId>zipkin-reporter-brave</artifactId>
</dependency>Or for OpenTelemetry:
<!-- OpenTelemetry tracing bridge -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<!-- OpenTelemetry exporter -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-zipkin</artifactId>
</dependency># Enable tracing
management.tracing.sampling.probability=1.0
# Zipkin endpoint
management.zipkin.tracing.endpoint=http://localhost:9411/api/v2/spans
# Propagation format
management.tracing.propagation.type=w3cWith tracing enabled, each chat operation creates a span with:
Logged content (when enabled) includes trace context:
TraceId: 5f3e8d9a7b2c1f4e SpanId: 9a7b2c1f Prompt: [prompt content]
TraceId: 5f3e8d9a7b2c1f4e SpanId: 9a7b2c1f Completion: [completion content]import io.micrometer.tracing.Span;
import io.micrometer.tracing.Tracer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class TracingExample {
@Autowired(required = false)
private Tracer tracer;
public void performTracedOperation() {
if (tracer != null) {
Span currentSpan = tracer.currentSpan();
if (currentSpan != null) {
String traceId = currentSpan.context().traceId();
String spanId = currentSpan.context().spanId();
System.out.println("Current trace: " + traceId);
System.out.println("Current span: " + spanId);
// Add custom tags to the span
currentSpan.tag("custom.tag", "value");
currentSpan.event("custom.event");
}
}
}
}import io.micrometer.tracing.Span;
import io.micrometer.tracing.Tracer;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class TracedChatService {
@Autowired
private ChatModel chatModel;
@Autowired(required = false)
private Tracer tracer;
public ChatResponse callWithCustomSpan(Prompt prompt) {
if (tracer != null) {
Span customSpan = tracer.nextSpan().name("custom.chat.operation");
try (Tracer.SpanInScope ws = tracer.withSpan(customSpan.start())) {
// Add metadata to span
customSpan.tag("prompt.size", String.valueOf(
prompt.getInstructions().size()));
ChatResponse response = chatModel.call(prompt);
// Add response metadata
if (response.getResult() != null) {
customSpan.tag("response.generated", "true");
}
return response;
} catch (Exception e) {
customSpan.error(e);
throw e;
} finally {
customSpan.end();
}
} else {
return chatModel.call(prompt);
}
}
}The module is secure by default:
falseWhen enabling content logging, warnings are logged at startup:
WARN o.s.a.m.c.o.a.ChatObservationAutoConfiguration : You have enabled logging out
the prompt content with the risk of exposing sensitive or private information.
Please, be careful!
WARN o.s.a.m.c.o.a.ChatObservationAutoConfiguration : You have enabled logging out
the completion content with the risk of exposing sensitive or private information.
Please, be careful!@Configuration
public class ActuatorSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.securityMatcher(EndpointRequest.toAnyEndpoint())
.authorizeHttpRequests(authorize -> authorize
.anyRequest().hasRole("ACTUATOR")
)
.httpBasic(withDefaults());
return http.build();
}
}@Bean
public ChatModelPromptContentObservationHandler chatModelPromptContentObservationHandler() {
return new ChatModelPromptContentObservationHandler() {
// Override to sanitize sensitive data before logging
};
}import org.springframework.ai.chat.observation.ChatModelPromptContentObservationHandler;
import org.springframework.ai.chat.observation.ChatModelObservationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SecureLoggingConfig {
@Bean
public ChatModelPromptContentObservationHandler chatModelPromptContentObservationHandler() {
return new ChatModelPromptContentObservationHandler() {
@Override
public void onStart(ChatModelObservationContext context) {
if (context.getRequest() != null) {
String content = extractAndRedact(context.getRequest().toString());
logRedacted(content);
}
}
private String extractAndRedact(String content) {
// Redact email addresses
content = content.replaceAll(
"\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b",
"[EMAIL_REDACTED]");
// Redact phone numbers
content = content.replaceAll(
"\\b\\d{3}[-.]?\\d{3}[-.]?\\d{4}\\b",
"[PHONE_REDACTED]");
// Redact SSN
content = content.replaceAll(
"\\b\\d{3}-\\d{2}-\\d{4}\\b",
"[SSN_REDACTED]");
// Redact credit card numbers
content = content.replaceAll(
"\\b\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}\\b",
"[CC_REDACTED]");
// Redact API keys (common patterns)
content = content.replaceAll(
"\\b[A-Za-z0-9_-]{20,}\\b",
"[API_KEY_REDACTED]");
return content;
}
private void logRedacted(String content) {
// Use appropriate logger
System.out.println("[REDACTED PROMPT] " + content);
}
};
}
}If your application processes regulated data (GDPR, HIPAA, PCI-DSS):
import org.springframework.ai.chat.observation.ChatModelCompletionObservationHandler;
import org.springframework.ai.chat.observation.ChatModelObservationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@Configuration
@Profile("gdpr-compliant")
public class GdprCompliantObservationConfig {
// No prompt or completion logging beans
// Only metrics without PII
@Bean
public ObservationAuditLogger observationAuditLogger() {
return new ObservationAuditLogger();
}
public static class ObservationAuditLogger {
public void logAccess(String userId, String action) {
// Log access to observation data for audit trail
// Store in append-only audit log
System.out.println("AUDIT: User " + userId +
" performed " + action +
" at " + java.time.Instant.now());
}
}
}Problem: No metrics visible in actuator endpoints
Solutions:
spring-boot-starter-actuator is on classpathmanagement.endpoints.web.exposure.include=metricsMeterRegistry bean exists:
@Autowired
private MeterRegistry meterRegistry; // Should not be nullChatModel is on classpath and auto-configuration activatedDiagnostic Code:
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class MetricsDiagnostics implements CommandLineRunner {
@Autowired(required = false)
private MeterRegistry meterRegistry;
@Override
public void run(String... args) {
if (meterRegistry == null) {
System.err.println("ERROR: MeterRegistry not available. " +
"Add spring-boot-starter-actuator dependency.");
return;
}
System.out.println("MeterRegistry available: " +
meterRegistry.getClass().getName());
// Check for AI metrics
boolean hasAiMetrics = meterRegistry.find("gen.ai.client.operation")
.timer() != null;
if (!hasAiMetrics) {
System.out.println("WARNING: No AI metrics found. " +
"Ensure ChatModel is being used.");
} else {
System.out.println("SUCCESS: AI metrics are being collected.");
}
}
}Problem: Trace context not propagated or handlers not created
Solutions:
micrometer-tracing dependency is presentTracer bean is configured:
@Autowired
private Tracer tracer; // Should not be nullmanagement.tracing.sampling.probability=1.0logging.level.org.springframework.boot.autoconfigure=DEBUGDiagnostic Code:
import io.micrometer.tracing.Tracer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class TracingDiagnostics implements CommandLineRunner {
@Autowired(required = false)
private Tracer tracer;
@Override
public void run(String... args) {
if (tracer == null) {
System.out.println("INFO: Tracer not available. " +
"Tracing is disabled. " +
"Add micrometer-tracing dependency if needed.");
return;
}
System.out.println("Tracer available: " + tracer.getClass().getName());
// Test span creation
try {
var span = tracer.nextSpan().name("test").start();
var traceId = span.context().traceId();
span.end();
System.out.println("SUCCESS: Tracing is working. Test trace ID: " +
traceId);
} catch (Exception e) {
System.err.println("ERROR: Tracing failed: " + e.getMessage());
}
}
}Problem: Prompts/completions not logged despite enabled properties
Solutions:
spring.ai.chat.observations.log-prompt=true
spring.ai.chat.observations.log-completion=truelog-prompts vs log-prompt)logging.level.org.springframework.ai=DEBUGDiagnostic Code:
import org.springframework.ai.model.chat.observation.autoconfigure.ChatObservationProperties;
import org.springframework.ai.chat.observation.ChatModelPromptContentObservationHandler;
import org.springframework.ai.chat.observation.ChatModelCompletionObservationHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
@Component
public class LoggingDiagnostics implements CommandLineRunner {
@Autowired
private ChatObservationProperties properties;
@Autowired
private ApplicationContext context;
@Override
public void run(String... args) {
System.out.println("=== Chat Observation Configuration ===");
System.out.println("Log Prompt: " + properties.isLogPrompt());
System.out.println("Log Completion: " + properties.isLogCompletion());
System.out.println("Include Error Logging: " +
properties.isIncludeErrorLogging());
// Check for handler beans
boolean hasPromptHandler = context.containsBean(
"chatModelPromptContentObservationHandler");
boolean hasCompletionHandler = context.containsBean(
"chatModelCompletionObservationHandler");
System.out.println("Prompt Handler Bean: " + hasPromptHandler);
System.out.println("Completion Handler Bean: " + hasCompletionHandler);
if (properties.isLogPrompt() && !hasPromptHandler) {
System.err.println("WARNING: Prompt logging enabled but handler " +
"bean not found!");
}
if (properties.isLogCompletion() && !hasCompletionHandler) {
System.err.println("WARNING: Completion logging enabled but " +
"handler bean not found!");
}
}
}Problem: Module present but no beans created
Solutions:
ChatModel is on classpath:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-client-chat</artifactId>
</dependency>debug=true@EnableAutoConfiguration exclusions:
@SpringBootApplication(exclude = {ChatObservationAutoConfiguration.class}) // Don't do thisDiagnostic Code:
import org.springframework.ai.model.chat.observation.autoconfigure.ChatObservationAutoConfiguration;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
@Component
public class AutoConfigurationDiagnostics implements CommandLineRunner {
private final ApplicationContext context;
public AutoConfigurationDiagnostics(ApplicationContext context) {
this.context = context;
}
@Override
public void run(String... args) {
// Check if auto-configuration class is loaded
boolean autoConfigPresent = context.containsBean(
"chatObservationAutoConfiguration");
System.out.println("ChatObservationAutoConfiguration present: " +
autoConfigPresent);
// List all observation-related beans
System.out.println("\n=== Observation Beans ===");
String[] beanNames = context.getBeanNamesForType(
io.micrometer.observation.ObservationHandler.class);
for (String name : beanNames) {
System.out.println("- " + name + ": " +
context.getBean(name).getClass().getName());
}
if (beanNames.length == 0) {
System.err.println("WARNING: No ObservationHandler beans found. " +
"Auto-configuration may not be active.");
}
}
}Problem: Custom beans not taking effect or unexpected beans created
Solution: Verify bean naming and conditions
import org.springframework.ai.chat.observation.ChatModelMeterObservationHandler;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import io.micrometer.core.instrument.MeterRegistry;
@Configuration
public class CustomObservationOverrideConfig {
// This bean will override the auto-configured one
@Bean
@Primary // Mark as primary if there are multiple candidates
public ChatModelMeterObservationHandler customChatModelMeterObservationHandler(
MeterRegistry meterRegistry) {
return new ChatModelMeterObservationHandler(meterRegistry) {
// Custom implementation
};
}
// Diagnostic bean to verify override
@Bean
public CommandLineRunner verifyBeanOverride(
ChatModelMeterObservationHandler handler) {
return args -> {
System.out.println("Active ChatModelMeterObservationHandler: " +
handler.getClass().getName());
};
}
}This section defines the key types used in this module's API. Most of these types are defined in other Spring AI modules (spring-ai-client-chat, spring-ai-model) and are provided here for reference to ensure complete understanding of the API surface.
These types are defined in org.springframework.ai:spring-ai-client-chat and org.springframework.ai:spring-ai-model modules:
package org.springframework.ai.chat.observation;
/**
* Observation handler that collects meter-based metrics for chat model operations.
* Automatically tracks execution time, token usage, and operation counts.
* Integrates with Micrometer's MeterRegistry for metrics collection.
*/
public class ChatModelMeterObservationHandler
implements ObservationHandler<ChatModelObservationContext> {
/**
* Creates a meter observation handler with the provided meter registry.
*
* @param meterRegistry the Micrometer meter registry for collecting metrics
*/
public ChatModelMeterObservationHandler(MeterRegistry meterRegistry);
// ObservationHandler methods for lifecycle management
public boolean supportsContext(Observation.Context context);
public void onStart(ChatModelObservationContext context);
public void onStop(ChatModelObservationContext context);
public void onError(ChatModelObservationContext context);
}Method Details:
supportsContext(Observation.Context context): Returns true if this handler supports the given context type (ChatModelObservationContext)onStart(ChatModelObservationContext context): Called when chat operation starts. Initializes timing measurement.onStop(ChatModelObservationContext context): Called when chat operation completes successfully. Records execution time and token usage metrics.onError(ChatModelObservationContext context): Called when chat operation fails. Records error metrics.package org.springframework.ai.chat.observation;
/**
* Observation handler that logs prompt content during chat model operations.
* WARNING: Enabling this handler may expose sensitive information in logs.
*/
public class ChatModelPromptContentObservationHandler
implements ObservationHandler<ChatModelObservationContext> {
/**
* Creates a prompt content logging handler.
*/
public ChatModelPromptContentObservationHandler();
// ObservationHandler methods for lifecycle management
public boolean supportsContext(Observation.Context context);
public void onStart(ChatModelObservationContext context);
}Method Details:
supportsContext(Observation.Context context): Returns true for ChatModelObservationContext instancesonStart(ChatModelObservationContext context): Called at operation start. Extracts and logs prompt content from context.getRequest().Usage Pattern:
// Access prompt content in custom handler
@Override
public void onStart(ChatModelObservationContext context) {
if (context.getRequest() != null) {
List<Message> messages = context.getRequest().getInstructions();
for (Message msg : messages) {
String content = msg.getContent();
// Log or process prompt content
}
}
}package org.springframework.ai.chat.observation;
/**
* Observation handler that logs completion content during chat model operations.
* WARNING: Enabling this handler may expose sensitive information in logs.
*/
public class ChatModelCompletionObservationHandler
implements ObservationHandler<ChatModelObservationContext> {
/**
* Creates a completion content logging handler.
*/
public ChatModelCompletionObservationHandler();
// ObservationHandler methods for lifecycle management
public boolean supportsContext(Observation.Context context);
public void onStop(ChatModelObservationContext context);
}Method Details:
supportsContext(Observation.Context context): Returns true for ChatModelObservationContext instancesonStop(ChatModelObservationContext context): Called when operation completes. Extracts and logs completion content from context.getResponse().Usage Pattern:
// Access completion content in custom handler
@Override
public void onStop(ChatModelObservationContext context) {
if (context.getResponse() != null &&
context.getResponse().getResult() != null) {
String completion = context.getResponse().getResult()
.getOutput().getContent();
// Log or process completion content
}
}package org.springframework.ai.model.observation;
/**
* Observation handler that logs errors across multiple observation context types.
* Supports ChatModelObservationContext, EmbeddingModelObservationContext,
* ImageModelObservationContext, ChatClientObservationContext, and AdvisorObservationContext.
*/
public class ErrorLoggingObservationHandler implements ObservationHandler<Observation.Context> {
/**
* Creates an error logging handler with the provided tracer.
*
* @param tracer the Micrometer tracing tracer for trace correlation
*/
public ErrorLoggingObservationHandler(Tracer tracer);
// ObservationHandler methods for lifecycle management
public boolean supportsContext(Observation.Context context);
public void onError(Observation.Context context);
}Method Details:
supportsContext(Observation.Context context): Returns true for supported AI model context typesonError(Observation.Context context): Called when operation fails. Logs error with trace context if available.Supported Context Type Check Pattern:
@Override
public boolean supportsContext(Observation.Context context) {
return context instanceof ChatModelObservationContext ||
context instanceof EmbeddingModelObservationContext ||
context instanceof ImageModelObservationContext ||
context instanceof ChatClientObservationContext ||
context instanceof AdvisorObservationContext;
}package org.springframework.ai.observation;
/**
* Generic observation handler that wraps logging functionality with distributed tracing context.
* Logs content with trace IDs and span IDs for correlation in distributed systems.
*
* @param <T> the observation context type
*/
public class TracingAwareLoggingObservationHandler<T extends Observation.Context>
implements ObservationHandler<T> {
/**
* Creates a tracing-aware logging handler.
*
* @param tracer the Micrometer tracing tracer
* @param logFunction the function to extract content to log from context
* @param logMessage the log message template
*/
public TracingAwareLoggingObservationHandler(
Tracer tracer,
Function<T, String> logFunction,
String logMessage
);
// ObservationHandler methods for lifecycle management
public boolean supportsContext(Observation.Context context);
public void onStart(T context);
public void onStop(T context);
}Method Details:
constructor: Accepts a tracer, extraction function, and log message templatelogFunction: Function that extracts content to log from the context (e.g., context -> context.getRequest().toString())logMessage: Template for log output (may include placeholders for trace/span IDs)onStart(T context) / onStop(T context): Called at appropriate lifecycle points, extracts trace context and logs with correlation IDsFactory Pattern for Creating Tracing Handlers:
// Example of creating a tracing-aware handler for prompts
TracingAwareLoggingObservationHandler<ChatModelObservationContext> promptHandler =
new TracingAwareLoggingObservationHandler<>(
tracer,
context -> {
if (context.getRequest() != null) {
return context.getRequest().getInstructions().toString();
}
return "";
},
"Prompt content: %s"
);These context types carry observation metadata and are defined in various Spring AI modules:
package org.springframework.ai.chat.observation;
/**
* Observation context for chat model operations.
* Contains metadata about the chat request, response, and model configuration.
*/
public class ChatModelObservationContext extends Observation.Context {
/**
* Gets the chat request prompt.
*
* @return the Prompt containing user messages and instructions
*/
public Prompt getRequest();
/**
* Gets the chat response.
*
* @return the ChatResponse containing AI-generated content and metadata
*/
public ChatResponse getResponse();
/**
* Gets the chat options/configuration.
*
* @return ChatOptions with model parameters (temperature, max tokens, etc.)
*/
public ChatOptions getChatOptions();
// Additional metadata methods
/**
* Gets the AI model provider name.
*
* @return provider identifier (e.g., "openai", "anthropic", "azure")
*/
public String getModelProvider();
/**
* Gets the specific model name being used.
*
* @return model identifier (e.g., "gpt-4", "claude-3-opus")
*/
public String getModelName();
/**
* Sets the request prompt.
*
* @param request the Prompt to set
*/
public void setRequest(Prompt request);
/**
* Sets the response.
*
* @param response the ChatResponse to set
*/
public void setResponse(ChatResponse response);
}Usage Pattern:
// Accessing context data in custom handler
@Override
public void onStop(ChatModelObservationContext context) {
Prompt request = context.getRequest();
ChatResponse response = context.getResponse();
if (request != null && response != null) {
int inputMessages = request.getInstructions().size();
String output = response.getResult().getOutput().getContent();
System.out.println("Model: " + context.getModelName());
System.out.println("Provider: " + context.getModelProvider());
System.out.println("Input messages: " + inputMessages);
System.out.println("Output length: " + output.length());
}
}package org.springframework.ai.chat.client.observation;
/**
* Observation context for ChatClient API operations.
* Captured when using the fluent ChatClient interface.
*/
public class ChatClientObservationContext extends Observation.Context {
/**
* Gets the chat client request.
*
* @return the ChatClient.Request with call configuration
*/
public ChatClient.Request getRequest();
/**
* Gets the chat client response.
* Response type varies based on call method (ChatResponse, String, Entity, etc.)
*
* @return the response object from the chat client call
*/
public Object getResponse();
/**
* Sets the request.
*
* @param request the ChatClient.Request to set
*/
public void setRequest(ChatClient.Request request);
/**
* Sets the response.
*
* @param response the response object to set
*/
public void setResponse(Object response);
}Usage Pattern:
// Handling ChatClient observations
@Override
public void onStop(ChatClientObservationContext context) {
Object response = context.getResponse();
// Response type depends on ChatClient call method
if (response instanceof ChatResponse) {
ChatResponse chatResponse = (ChatResponse) response;
// Process ChatResponse
} else if (response instanceof String) {
String stringResponse = (String) response;
// Process String response
}
}package org.springframework.ai.chat.client.advisor.observation;
/**
* Observation context for advisor execution in the chat client.
* Tracks advisor chain operations and transformations.
*/
public class AdvisorObservationContext extends Observation.Context {
/**
* Gets the advisor name.
*
* @return identifier for the advisor being executed
*/
public String getAdvisorName();
/**
* Gets the advisor type.
*
* @return type classification of the advisor (e.g., "request", "response")
*/
public String getAdvisorType();
/**
* Sets the advisor name.
*
* @param advisorName the advisor identifier
*/
public void setAdvisorName(String advisorName);
/**
* Sets the advisor type.
*
* @param advisorType the advisor type classification
*/
public void setAdvisorType(String advisorType);
}Usage Pattern:
// Tracking advisor execution
@Override
public void onStop(AdvisorObservationContext context) {
String advisorName = context.getAdvisorName();
String advisorType = context.getAdvisorType();
System.out.println("Advisor executed: " + advisorName +
" (type: " + advisorType + ")");
}package org.springframework.ai.embedding.observation;
/**
* Observation context for embedding model operations.
* Contains metadata about embedding requests and responses.
*/
public class EmbeddingModelObservationContext extends Observation.Context {
/**
* Gets the embedding request.
*
* @return EmbeddingRequest with text inputs and options
*/
public EmbeddingRequest getRequest();
/**
* Gets the embedding response.
*
* @return EmbeddingResponse with generated embeddings
*/
public EmbeddingResponse getResponse();
/**
* Sets the embedding request.
*
* @param request the EmbeddingRequest to set
*/
public void setRequest(EmbeddingRequest request);
/**
* Sets the embedding response.
*
* @param response the EmbeddingResponse to set
*/
public void setResponse(EmbeddingResponse response);
/**
* Gets the model provider name.
*
* @return provider identifier
*/
public String getModelProvider();
/**
* Gets the model name.
*
* @return model identifier
*/
public String getModelName();
}package org.springframework.ai.image.observation;
/**
* Observation context for image generation model operations.
* Contains metadata about image generation requests and responses.
*/
public class ImageModelObservationContext extends Observation.Context {
/**
* Gets the image generation request.
*
* @return ImageRequest with prompts and generation options
*/
public ImageRequest getRequest();
/**
* Gets the image generation response.
*
* @return ImageResponse with generated images
*/
public ImageResponse getResponse();
/**
* Sets the image request.
*
* @param request the ImageRequest to set
*/
public void setRequest(ImageRequest request);
/**
* Sets the image response.
*
* @param response the ImageResponse to set
*/
public void setResponse(ImageResponse response);
/**
* Gets the model provider name.
*
* @return provider identifier
*/
public String getModelProvider();
/**
* Gets the model name.
*
* @return model identifier
*/
public String getModelName();
}These types are from the Micrometer observability library:
package io.micrometer.core.instrument;
/**
* Registry for creating and managing meters (timers, counters, gauges).
* Part of Micrometer Core - the metrics collection library.
* Typically auto-configured by Spring Boot Actuator.
*/
public interface MeterRegistry {
/**
* Creates or retrieves a timer for measuring operation duration.
*
* @param name the metric name
* @param tags optional tags for categorization
* @return Timer instance for recording time measurements
*/
Timer timer(String name, String... tags);
/**
* Creates or retrieves a counter for counting events.
*
* @param name the metric name
* @param tags optional tags for categorization
* @return Counter instance for incrementing counts
*/
Counter counter(String name, String... tags);
/**
* Searches for meters by name.
*
* @param name the metric name to search for
* @return Search instance for querying meters
*/
Search find(String name);
/**
* Creates or retrieves a gauge for measuring current value.
*
* @param name the metric name
* @param obj the object to observe
* @param valueFunction function to extract gauge value
* @return the observed object
*/
<T> T gauge(String name, T obj, ToDoubleFunction<T> valueFunction);
/**
* Returns all registered meters.
*
* @return list of all Meter instances
*/
List<Meter> getMeters();
}Common Usage Patterns:
// Recording timer measurements
Timer timer = meterRegistry.timer("operation.duration",
"operation", "chat",
"model", "gpt-4");
timer.record(() -> {
// Operation to time
});
// Incrementing counters
Counter counter = meterRegistry.counter("operation.count",
"operation", "chat",
"status", "success");
counter.increment();
// Creating gauges
meterRegistry.gauge("queue.size",
Tags.of("queue", "requests"),
myQueue,
Queue::size);package io.micrometer.tracing;
/**
* Interface for distributed tracing systems.
* Part of Micrometer Tracing - enables distributed trace context propagation.
* Available when micrometer-tracing dependency is present.
*/
public interface Tracer {
/**
* Gets the current span in the trace.
*
* @return current Span or null if no span is active
*/
Span currentSpan();
/**
* Creates a new span with the given name.
* The span becomes current when started.
*
* @return new Span builder
*/
Span nextSpan();
/**
* Creates a new child span from the current span.
*
* @param parent the parent span
* @return new Span builder with parent context
*/
Span nextSpan(Span parent);
/**
* Gets the current trace context.
*
* @return TraceContext for current trace or null
*/
TraceContext currentTraceContext();
/**
* Creates a scope that makes the given span current.
* Must be closed to restore previous span.
*
* @param span the span to make current
* @return SpanInScope that must be closed
*/
SpanInScope withSpan(Span span);
}Common Usage Patterns:
// Creating and using spans
Span span = tracer.nextSpan().name("my.operation");
try (Tracer.SpanInScope ws = tracer.withSpan(span.start())) {
// Operation code here
span.tag("custom.tag", "value");
span.event("milestone.reached");
// Get trace identifiers
String traceId = span.context().traceId();
String spanId = span.context().spanId();
} catch (Exception e) {
span.error(e);
throw e;
} finally {
span.end();
}
// Accessing current span
Span current = tracer.currentSpan();
if (current != null) {
current.tag("operation", "chat");
}These types are from the Spring Framework:
package org.springframework.beans.factory;
/**
* A variant of ObjectFactory designed for injection points that allow
* for optional dependency injection and lazy access.
* Part of Spring Framework's dependency injection system.
*
* @param <T> the type of object provided
*/
public interface ObjectProvider<T> extends ObjectFactory<T>, Iterable<T> {
/**
* Returns an instance (possibly shared or independent) of the object.
* Throws NoSuchBeanDefinitionException if not available.
*
* @return an instance of the bean
* @throws BeansException if the bean could not be created
*/
T getObject() throws BeansException;
/**
* Returns an instance if available, or null otherwise.
*
* @return an instance or null
*/
T getIfAvailable();
/**
* Returns an instance if available, or the provided default otherwise.
*
* @param defaultSupplier supplier for default value
* @return an instance or default value
*/
T getIfAvailable(Supplier<T> defaultSupplier);
/**
* Returns an instance if available, otherwise returns result from supplier.
*
* @return an instance or computed default
*/
T getIfUnique();
/**
* Executes action if an instance is available.
*
* @param action the action to perform
*/
void ifAvailable(Consumer<T> action);
/**
* Returns stream of all matching beans.
*
* @return stream of instances
*/
Stream<T> stream();
/**
* Returns stream of all matching beans sorted by order.
*
* @return ordered stream of instances
*/
Stream<T> orderedStream();
}Usage in Auto-Configuration:
@Bean
public ChatModelMeterObservationHandler handler(
ObjectProvider<MeterRegistry> meterRegistryProvider) {
// Get registry if available, or throw exception
MeterRegistry registry = meterRegistryProvider.getObject();
// Or get if available with default
MeterRegistry registryOrDefault = meterRegistryProvider.getIfAvailable(
() -> new SimpleMeterRegistry());
// Or perform action only if available
meterRegistryProvider.ifAvailable(registry -> {
// Configure registry
});
return new ChatModelMeterObservationHandler(registry);
}package org.springframework.ai.chat.model;
/**
* Core interface for chat/completion models in Spring AI.
* Implemented by various AI model providers (OpenAI, Anthropic, etc.).
* Presence of this class on the classpath triggers auto-configuration activation.
*/
public interface ChatModel extends Model<Prompt, ChatResponse>, StreamingModel<Prompt, ChatResponse> {
/**
* Generates a chat response for the given prompt.
* Blocking synchronous operation.
*
* @param prompt the input prompt with messages and options
* @return ChatResponse containing generated text and metadata
*/
ChatResponse call(Prompt prompt);
/**
* Generates a streaming chat response for the given prompt.
* Returns a reactive stream of response chunks.
*
* @param prompt the input prompt with messages and options
* @return Flux of ChatResponse chunks
*/
Flux<ChatResponse> stream(Prompt prompt);
/**
* Gets the default options for this chat model.
*
* @return ChatOptions with model-specific defaults
*/
default ChatOptions getDefaultOptions() {
return ChatOptions.builder().build();
}
}Implementation Example:
@Service
public class MyChatService {
private final ChatModel chatModel;
public MyChatService(ChatModel chatModel) {
this.chatModel = chatModel;
}
public String chat(String userMessage) {
Prompt prompt = new Prompt(userMessage);
ChatResponse response = chatModel.call(prompt);
return response.getResult().getOutput().getContent();
}
public Flux<String> chatStream(String userMessage) {
Prompt prompt = new Prompt(userMessage);
return chatModel.stream(prompt)
.map(response -> response.getResult().getOutput().getContent());
}
}This module creates beans of the following types, which are defined in other Spring AI modules:
ChatModel
org.springframework.ai.chat.modelChatModelMeterObservationHandler
org.springframework.ai.chat.observationChatModelPromptContentObservationHandler
log-prompt=true and tracing not availableorg.springframework.ai.chat.observationChatModelCompletionObservationHandler
log-completion=true and tracing not availableorg.springframework.ai.chat.observationChatModelObservationContext
org.springframework.ai.chat.observationErrorLoggingObservationHandler
include-error-logging=trueorg.springframework.ai.model.observationTracingAwareLoggingObservationHandler
org.springframework.ai.observationMeterRegistry
io.micrometer.core.instrumentTracer
io.micrometer.tracingThis module version 1.1.2 is compatible with:
Spring Boot: 3.5.x Spring AI: 1.1.x Java: 17+ Micrometer: (version managed by Spring Boot) Micrometer Tracing: (version managed by Spring Boot, optional)
For Spring Boot 4.x compatibility, use Spring AI 2.x.
Java 17 Requirements:
Spring Boot 3.x Features:
When implementing custom observation handlers, always check for null values:
@Override
public void onStop(ChatModelObservationContext context) {
// Defensive null checking pattern
if (context == null) {
return;
}
ChatResponse response = context.getResponse();
if (response == null) {
logger.warn("Chat operation completed with null response");
return;
}
Generation result = response.getResult();
if (result == null || result.getOutput() == null) {
logger.warn("Chat response has no result or output");
return;
}
String content = result.getOutput().getContent();
if (content != null && !content.isEmpty()) {
// Process content
}
}When dealing with high concurrency, ensure thread-safe metrics collection:
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@Component
public class ConcurrentMetricsHandler {
private final ConcurrentHashMap<String, AtomicLong> requestCounts =
new ConcurrentHashMap<>();
public void recordRequest(String modelName) {
requestCounts
.computeIfAbsent(modelName, k -> new AtomicLong(0))
.incrementAndGet();
}
public long getRequestCount(String modelName) {
AtomicLong count = requestCounts.get(modelName);
return count != null ? count.get() : 0;
}
}For streaming responses, observation handlers need special consideration:
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.prompt.Prompt;
import reactor.core.publisher.Flux;
@Service
public class StreamingObservationService {
private final ChatModel chatModel;
private final MeterRegistry meterRegistry;
public Flux<String> chatWithObservation(Prompt prompt) {
Timer.Sample sample = Timer.start(meterRegistry);
AtomicLong tokenCount = new AtomicLong(0);
return chatModel.stream(prompt)
.doOnNext(response -> {
// Count tokens in each chunk
if (response.getResult() != null) {
tokenCount.incrementAndGet();
}
})
.doOnComplete(() -> {
// Record metrics when stream completes
sample.stop(meterRegistry.timer("ai.stream.duration"));
meterRegistry.counter("ai.stream.tokens").increment(tokenCount.get());
})
.doOnError(error -> {
sample.stop(meterRegistry.timer("ai.stream.duration",
"status", "error"));
})
.map(response -> response.getResult().getOutput().getContent());
}
}Use observed metrics to implement rate limiting:
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Component;
@Component
public class MetricsBasedRateLimiter {
private final MeterRegistry meterRegistry;
private static final double MAX_REQUESTS_PER_MINUTE = 60;
public MetricsBasedRateLimiter(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
public boolean allowRequest() {
var counter = meterRegistry.find("gen.ai.client.operation").counter();
if (counter == null) {
return true; // No metrics yet, allow
}
// Simple rate check (production would use sliding window)
double count = counter.count();
double ratePerMinute = estimateRatePerMinute(count);
return ratePerMinute < MAX_REQUESTS_PER_MINUTE;
}
private double estimateRatePerMinute(double totalCount) {
// Simplified estimation - production would track time windows
return totalCount; // Replace with actual rate calculation
}
}Handle graceful degradation when optional dependencies are absent:
@Configuration
public class GracefulObservationConfig {
@Bean
@ConditionalOnMissingBean(MeterRegistry.class)
public MeterRegistry fallbackMeterRegistry() {
return new SimpleMeterRegistry();
}
@Bean
@ConditionalOnMissingBean(name = "chatModelMeterObservationHandler")
public ObservationHandler<ChatModelObservationContext> noOpHandler() {
return new ObservationHandler<ChatModelObservationContext>() {
@Override
public boolean supportsContext(Observation.Context context) {
return context instanceof ChatModelObservationContext;
}
@Override
public void onStart(ChatModelObservationContext context) {
// No-op when metrics infrastructure unavailable
}
};
}
}