Spring AI Chat Client provides a fluent API for building AI-powered applications with LLMs, supporting advisors, streaming, structured outputs, and conversation memory
Spring AI Chat Client integrates with Micrometer for comprehensive observability through metrics, traces, and logs. This enables monitoring of chat interactions, advisor performance, and request/response flows.
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.observation.ChatClientObservationContext;
import org.springframework.ai.chat.client.observation.ChatClientObservationConvention;
import org.springframework.ai.chat.client.observation.DefaultChatClientObservationConvention;
import org.springframework.ai.chat.client.observation.ChatClientObservationDocumentation;
import org.springframework.ai.chat.client.observation.ChatClientPromptContentObservationHandler;
import org.springframework.ai.chat.client.observation.ChatClientCompletionObservationHandler;
import org.springframework.ai.chat.client.advisor.observation.AdvisorObservationContext;
import org.springframework.ai.chat.client.advisor.observation.AdvisorObservationConvention;
import org.springframework.ai.chat.client.advisor.observation.DefaultAdvisorObservationConvention;
import org.springframework.ai.chat.client.advisor.observation.AdvisorObservationDocumentation;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.Observation;Chat Client creates observations for each request, recording metrics and traces.
Configure ChatClient with an ObservationRegistry to enable observations.
import io.micrometer.observation.ObservationRegistry;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
// Create with observation registry
ObservationRegistry registry = ObservationRegistry.create();
ChatClient client = ChatClient.create(chatModel, registry);
// Or with builder
ChatClient client = ChatClient.builder(chatModel)
.observationRegistry(registry)
.build();The observation context captures request and response metadata.
class ChatClientObservationContext extends Observation.Context {
static Builder builder();
ChatClientRequest getRequest();
ChatClientResponse getResponse();
void setResponse(ChatClientResponse response);
AiOperationMetadata getOperationMetadata();
List<Advisor> getAdvisors();
boolean isStream();
String getFormat();
interface Builder {
Builder request(ChatClientRequest request);
Builder format(String format);
Builder advisors(List<Advisor> advisors);
Builder stream(boolean stream);
ChatClientObservationContext build();
}
}Fields:
request - The ChatClientRequest being executedresponse - The ChatClientResponse (set after execution)operationMetadata - AI operation metadataadvisors - List of advisors in the chainstream - Whether this is a streaming requestformat - Output format from contextChat Client observations include low and high cardinality tags.
enum ChatClientObservationDocumentation implements ObservationDocumentation {
AI_CHAT_CLIENT;
enum LowCardinalityKeyNames implements KeyName {
SPRING_AI_KIND("spring.ai.kind"),
STREAM("spring.ai.chat.client.stream");
private final String key;
String asString();
}
enum HighCardinalityKeyNames implements KeyName {
CHAT_CLIENT_ADVISORS("spring.ai.chat.client.advisors"),
CHAT_CLIENT_CONVERSATION_ID("spring.ai.chat.client.conversation.id"),
CHAT_CLIENT_TOOL_NAMES("spring.ai.chat.client.tool.names");
private final String key;
String asString();
}
}Low Cardinality Tags (limited distinct values):
spring.ai.kind - Component type (always "chat.client")spring.ai.chat.client.stream - Streaming flag (true/false)High Cardinality Tags (many distinct values):
spring.ai.chat.client.advisors - Comma-separated advisor namesspring.ai.chat.client.conversation.id - Conversation identifierspring.ai.chat.client.tool.names - Comma-separated tool namesCustomize observation names and tags with a custom convention.
interface ChatClientObservationConvention
extends ObservationConvention<ChatClientObservationContext> {
}
class DefaultChatClientObservationConvention
implements ChatClientObservationConvention {
static final String DEFAULT_NAME = "spring.ai.chat.client";
DefaultChatClientObservationConvention();
DefaultChatClientObservationConvention(String name);
}Example:
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.observation.ChatClientObservationConvention;
import org.springframework.ai.chat.client.observation.ChatClientObservationContext;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.common.KeyValues;
// Custom convention
class CustomChatClientConvention implements ChatClientObservationConvention {
@Override
public String getName() {
return "my.app.chat.client";
}
@Override
public KeyValues getLowCardinalityKeyValues(
ChatClientObservationContext context
) {
return KeyValues.of(
"app.name", "my-app",
"stream", String.valueOf(context.isStream())
);
}
@Override
public KeyValues getHighCardinalityKeyValues(
ChatClientObservationContext context
) {
return KeyValues.of(
"request.format", context.getFormat() != null ?
context.getFormat() : "text"
);
}
}
// Configure client
ChatClient client = ChatClient.builder(chatModel)
.observationRegistry(registry)
.chatClientObservationConvention(new CustomChatClientConvention())
.build();Each advisor in the chain creates its own observation, enabling fine-grained monitoring.
class AdvisorObservationContext extends Observation.Context {
static Builder builder();
String getAdvisorName();
void setAdvisorName(String advisorName);
ChatClientRequest getChatClientRequest();
void setChatClientRequest(ChatClientRequest request);
int getOrder();
void setOrder(int order);
ChatClientResponse getChatClientResponse();
void setChatClientResponse(ChatClientResponse response);
interface Builder {
Builder advisorName(String advisorName);
Builder chatClientRequest(ChatClientRequest request);
Builder order(int order);
AdvisorObservationContext build();
}
}Fields:
advisorName - Name of the advisorchatClientRequest - Request being processedorder - Advisor order in chainchatClientResponse - Response (set after execution)enum AdvisorObservationDocumentation implements ObservationDocumentation {
AI_ADVISOR;
enum LowCardinalityKeyNames implements KeyName {
AI_OPERATION_TYPE("spring.ai.kind"),
AI_PROVIDER("spring.ai.ai-provider"),
SPRING_AI_KIND("spring.ai.kind"),
ADVISOR_NAME("spring.ai.advisor.name");
private final String key;
String asString();
}
enum HighCardinalityKeyNames implements KeyName {
ADVISOR_ORDER("spring.ai.advisor.order");
private final String key;
String asString();
}
}Low Cardinality Tags:
spring.ai.kind - Component type (always "advisor")spring.ai.ai-provider - AI provider namespring.ai.advisor.name - Advisor nameHigh Cardinality Tags:
spring.ai.advisor.order - Advisor order in chaininterface AdvisorObservationConvention
extends ObservationConvention<AdvisorObservationContext> {
}
class DefaultAdvisorObservationConvention
implements AdvisorObservationConvention {
static final String DEFAULT_NAME = "spring.ai.advisor";
DefaultAdvisorObservationConvention();
DefaultAdvisorObservationConvention(String name);
}Example:
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.observation.AdvisorObservationConvention;
import org.springframework.ai.chat.client.advisor.observation.AdvisorObservationContext;
import io.micrometer.common.KeyValues;
class CustomAdvisorConvention implements AdvisorObservationConvention {
@Override
public String getName() {
return "my.app.advisor";
}
@Override
public KeyValues getLowCardinalityKeyValues(
AdvisorObservationContext context
) {
return KeyValues.of(
"advisor.name", context.getAdvisorName(),
"app.component", "chat-client"
);
}
@Override
public KeyValues getHighCardinalityKeyValues(
AdvisorObservationContext context
) {
return KeyValues.of(
"advisor.order", String.valueOf(context.getOrder())
);
}
}
// Configure client
ChatClient client = ChatClient.builder(chatModel)
.observationRegistry(registry)
.advisorObservationConvention(new CustomAdvisorConvention())
.build();Custom handlers can process observations for logging or custom metrics.
Logs prompt content when observations complete.
class ChatClientPromptContentObservationHandler
implements ObservationHandler<ChatClientObservationContext> {
void onStop(ChatClientObservationContext context);
boolean supportsContext(Observation.Context context);
}Example:
import org.springframework.ai.chat.client.observation.ChatClientPromptContentObservationHandler;
import io.micrometer.observation.ObservationRegistry;
ObservationRegistry registry = ObservationRegistry.create();
// Register handler
registry.observationConfig()
.observationHandler(
new ChatClientPromptContentObservationHandler()
);
ChatClient client = ChatClient.builder(chatModel)
.observationRegistry(registry)
.build();Logs completion content when observations complete.
class ChatClientCompletionObservationHandler
implements ObservationHandler<ChatClientObservationContext> {
void onStop(ChatClientObservationContext context);
boolean supportsContext(Observation.Context context);
}Example:
import org.springframework.ai.chat.client.observation.ChatClientCompletionObservationHandler;
import io.micrometer.observation.ObservationRegistry;
ObservationRegistry registry = ObservationRegistry.create();
// Register handler
registry.observationConfig()
.observationHandler(
new ChatClientCompletionObservationHandler()
);Create custom handlers for specialized processing.
import org.springframework.ai.chat.client.observation.ChatClientObservationContext;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationHandler;
class MetricsCollectionHandler
implements ObservationHandler<ChatClientObservationContext> {
private final MetricsService metricsService;
public MetricsCollectionHandler(MetricsService metricsService) {
this.metricsService = metricsService;
}
@Override
public void onStart(ChatClientObservationContext context) {
// Record request start
metricsService.recordRequestStart(
context.getRequest(),
context.isStream()
);
}
@Override
public void onStop(ChatClientObservationContext context) {
// Record completion
metricsService.recordRequestComplete(
context.getRequest(),
context.getResponse(),
context.isStream()
);
}
@Override
public void onError(ChatClientObservationContext context) {
// Record error
metricsService.recordError(
context.getRequest(),
context.getError()
);
}
@Override
public boolean supportsContext(Observation.Context context) {
return context instanceof ChatClientObservationContext;
}
}
// Register handler
registry.observationConfig()
.observationHandler(new MetricsCollectionHandler(metricsService));Spring Boot auto-configures observability when Micrometer is on the classpath.
application.properties:
# Enable observations
management.observations.enabled=true
# Enable tracing
management.tracing.enabled=true
management.tracing.sampling.probability=1.0
# Enable metrics
management.metrics.export.prometheus.enabled=trueExample with Spring Boot:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.ChatClientCustomizer;
import org.springframework.ai.chat.model.ChatModel;
import io.micrometer.observation.ObservationRegistry;
@SpringBootApplication
public class ChatApplication {
public static void main(String[] args) {
SpringApplication.run(ChatApplication.class, args);
}
// ObservationRegistry auto-configured by Spring Boot
@Bean
public ChatClient chatClient(
ChatModel chatModel,
ObservationRegistry observationRegistry
) {
return ChatClient.builder(chatModel)
.observationRegistry(observationRegistry)
.build();
}
// Or use customizer
@Bean
public ChatClientCustomizer observabilityCustomizer(
ObservationRegistry observationRegistry
) {
return builder -> builder
.observationRegistry(observationRegistry);
}
}Observations automatically generate metrics and traces when configured.
Example Metrics:
spring.ai.chat.client - Timer metric for chat client calls
spring.ai.kind=chat.client, stream=true/falsespring.ai.advisor - Timer metric for advisor execution
spring.ai.advisor.name=MessageChatMemoryAdvisorExample Traces: Each request creates a trace with spans for:
Viewing with Prometheus:
# Request rate
rate(spring_ai_chat_client_seconds_count[5m])
# Request duration (95th percentile)
histogram_quantile(0.95,
rate(spring_ai_chat_client_seconds_bucket[5m])
)
# Advisor performance
rate(spring_ai_advisor_seconds_count{
advisor_name="MessageChatMemoryAdvisor"
}[5m])import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.observation.ChatClientPromptContentObservationHandler;
import org.springframework.ai.chat.client.observation.ChatClientCompletionObservationHandler;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
// Set up observability
MeterRegistry meterRegistry = new SimpleMeterRegistry();
ObservationRegistry observationRegistry = ObservationRegistry.create();
observationRegistry.observationConfig()
.observationHandler(new ChatClientPromptContentObservationHandler())
.observationHandler(new ChatClientCompletionObservationHandler());
// Create chat client with observability
ChatMemory chatMemory = new InMemoryChatMemory();
ChatClient client = ChatClient.builder(chatModel)
.observationRegistry(observationRegistry)
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory)
.build()
)
.build();
// Use client (automatically observed)
String response = client
.prompt()
.user("What is Spring AI?")
.call()
.content();
// Observations automatically recorded:
// - spring.ai.chat.client (main request)
// - spring.ai.advisor (MessageChatMemoryAdvisor)
// - spring.ai.advisor (ChatModelCallAdvisor)