Observability for Spring AI: Logging, Metrics, and Tracing Every Model Call
▶ Watch on YouTube & subscribe to The Stack Underflow
Your ChatClient call returns. The endpoint works, the demo looks great, and then it ships. Three weeks later someone on the platform team asks a question you can’t answer: which model actually served that request, how many tokens did it burn, how long did it take, and — the one that matters at 2am — what do you even look at when it’s wrong or slow?
If your only answer is “check the application logs and hope,” you didn’t ship an AI feature. You shipped a black box with a REST endpoint bolted onto it. Every other service in your stack gets logs, metrics, and traces as a matter of course. A model call is not exempt just because it’s new and the tooling feels unfamiliar — and the good news is Spring AI already wires the hooks in. Nobody has to hand-roll instrumentation for this.
This tutorial covers the three pillars Spring AI 2.0.0 gives you for free — SimpleLoggerAdvisor for request/response logs, Micrometer meters for latency and token usage, and a real span tree for tracing — plus the one decision that isn’t automatic: whether the actual prompt and response text belongs in that trace at all.
The one-sentence version: Spring AI already instruments every
ChatClientcall for logs, metrics, and traces — you turn metadata tagging on by wiring an advisor and reading a couple of config keys, and you turn content logging on separately, on purpose, because that’s a compliance decision, not a default.
Three pillars, mapped onto Spring AI
You already know this shape from every other production service:
| Pillar | What it answers | Spring AI mechanism |
|---|---|---|
| Logs | What exactly went in and came out of this one call? | SimpleLoggerAdvisor |
| Metrics | How is this behaving in aggregate, over time? | Micrometer meters (ObservationRegistry) |
| Traces | Where did the time actually go, inside one call? | Micrometer Observation spans |
Spring AI doesn’t invent new tooling for any of this. It plugs a model call into the same Micrometer/Actuator stack you already use for every other Spring Boot service. That’s the point — a ChatClient call is not a special case; it’s just another span with a model attached to it.
Setup note before any of this works: Spring AI 2.0.0 GA requires Java 21 to build and runs on Spring Boot 4.0 / Spring Framework 7.0. If your project still targets Java 17 as a build floor, that assumption is stale for 2.0.0.
Logs: wire SimpleLoggerAdvisor
SimpleLoggerAdvisor (package org.springframework.ai.chat.client.advisor) implements both CallAdvisor and StreamAdvisor, so it works whether you call .call() or .stream(). Add it to the advisor chain on your ChatClient.Builder:
@Configuration
public class ChatClientConfig {
@Bean
ChatClient chatClient(ChatModel chatModel) {
return ChatClient.builder(chatModel)
.defaultAdvisors(new SimpleLoggerAdvisor())
.build();
}
}
That alone gets you nothing. SimpleLoggerAdvisor logs through SLF4J at DEBUG, and Spring Boot’s default logging level is INFO. Without the following property, the advisor is wired but silent:
logging.level.org.springframework.ai.chat.client.advisor=DEBUG
With that one line, every call through the advisor chain prints the full request (system prompt, user message) and the full response, including token usage, to your console. It’s the fastest way to answer “what did we actually send the model” during development or while chasing down a production incident. SimpleLoggerAdvisor also has a SimpleLoggerAdvisor(int order) constructor if you need to control where it sits in the advisor chain, a custom-formatter constructor for shaping the log output, and a static Builder builder() entry point.
Metrics: the meters Spring AI emits for free
Once Spring Boot Actuator and Micrometer are on the classpath — the same dependencies any Spring Boot service already carries for metrics — Spring AI auto-wires an ObservationRegistry and starts emitting meters for every model call, with zero instrumentation code from you:
Meter constant (AiObservationMetricNames) | What it tracks |
|---|---|
OPERATION_DURATION | Latency of the model call |
TOKEN_USAGE | Prompt and completion token counts |
Both meters carry the same low-cardinality tags out of the box, defined on ChatModelObservationDocumentation.LowCardinalityKeyNames:
AI_OPERATION_TYPE AI_PROVIDER REQUEST_MODEL RESPONSE_MODEL
That means you can slice latency and token cost by provider and by model with no extra code — filter to AI_PROVIDER=openai and RESPONSE_MODEL=gpt-4o in your metrics backend and you have a cost dashboard. The Java constant names are verified against the 2.0.0 API docs; the exact dotted metric string Micrometer publishes (the AiObservationMetricNames class documents it as based on OpenTelemetry’s Semantic Conventions for AI systems) is worth confirming directly against /actuator/metrics on your own build rather than assuming a literal string, since that surface can shift between minor releases.
Traces: the span tree behind one ChatClient call
This is the part most people don’t expect: one ChatClient.prompt().user(...).call() isn’t a single span. It’s a small tree.
AI_CHAT_CLIENT (ChatClientObservationDocumentation) ← the whole ChatClient call
├── SimpleLoggerAdvisor ← advisor spans, in chain order
├── QuestionAnswerAdvisor (if RAG is on the chain — see ep. 6)
├── CHAT_MODEL_OPERATION (ChatModelObservationDocumentation) ← the model call itself, usually the widest span
│ └── provider HTTP call ← e.g. an Anthropic client instrumented since 2.0.0
The parent span (AI_CHAT_CLIENT) wraps everything your ChatClient does, including every advisor in the chain. Nested inside it, the model call gets its own span (CHAT_MODEL_OPERATION), and as of 2.0.0 some provider clients — Anthropic’s is the illustrative example, via the AnthropicSetup overloads that accept an ObservationRegistry and MeterRegistry — instrument the outbound HTTP call to the provider separately. That’s the level of resolution that turns “the request was slow” into “the request was slow because the model call itself took 1.8s, not because an advisor or the network hop did.”
Each span carries high-cardinality tags too — the kind you wouldn’t want on every low-cardinality metric bucket, but that you absolutely want available when you pull up one trace during an incident. On ChatClientObservationDocumentation.HighCardinalityKeyNames:
| Constant | Tag key |
|---|---|
CHAT_CLIENT_CONVERSATION_ID | chatClientConversationId |
CHAT_CLIENT_TOOL_NAMES | chatClientToolNames |
CHAT_CLIENT_ADVISORS | chatClientAdvisors |
Pull up a trace by conversation ID and you can see exactly which advisors ran, which tools were available, and where every millisecond went — without grepping logs.
The compliance fork: content visibility is opt-in
Everything above is metadata: provider, model, duration, token counts, advisor names. None of it is the actual text of the prompt or the response. That’s deliberate, and Spring AI keeps it that way until you say otherwise.
Two separate property groups control it, and they are not interchangeable — one is advisor-level, one is model-level:
# Advisor-level, ChatClientBuilderProperties.Observations (since 1.1.0, current in 2.0.0)
spring.ai.chat.client.observations.log-prompt=true
spring.ai.chat.client.observations.log-completion=true
# Model-level, ChatObservationProperties, prefix spring.ai.chat.observations
spring.ai.chat.observations.log-prompt=true
spring.ai.chat.observations.log-completion=true
spring.ai.chat.observations.include-error-logging=true
If you trained on the earlier 1.0.3 course material, this is a real delta worth knowing: that baseline only had the blunt logging.level...=DEBUG text logger from the section above. These scoped, Observation-API-integrated toggles are new since 1.1.0 and current in 2.0.0 — they let you decide, independently, whether prompt/completion text rides inside the trace itself, separate from whether the DEBUG logger prints it to your console.
Flip these on and the raw prompt and response text becomes part of whatever tracing backend you ship spans to. That’s not a default anyone should silently enable — it’s a data-residency and compliance decision, made on purpose, by someone who owns that call.
Manage the tail, not the average
A meter that reports one duration per call is not, by itself, an SLA. What you actually manage is a distribution: most calls fast and cheap, a long tail that gets slow or expensive, and an occasional call that fails outright. The average hides all of that — it’s dragged down by the fast majority and tells you nothing about what your worst-off users experienced.
Once OPERATION_DURATION and TOKEN_USAGE are flowing into your metrics backend, compute p50, p95, and p99 on latency the same way you would for any other HTTP endpoint. p50 is the typical call. p95 and p99 are what an SLA actually holds you accountable to — and they’re the numbers that catch the slow provider region, the oversized prompt, or the model that’s quietly degrading before your average even moves.
How to apply this right now
- Add
SimpleLoggerAdvisorto everyChatClientyou build, via.defaultAdvisors(new SimpleLoggerAdvisor()), before you ship anything to production. It costs nothing until you turn logging on. - Set
logging.level.org.springframework.ai.chat.client.advisor=DEBUGin a dev or staging profile — leave it offINFO/production unless you’re actively debugging, since full request/response logging is verbose. - Confirm Actuator and Micrometer are on the classpath so
OPERATION_DURATIONandTOKEN_USAGEstart flowing automatically; check/actuator/metricsto see the exact published names on your build. - Build a token-cost dashboard by tag, not by eyeballing logs — group
TOKEN_USAGEbyAI_PROVIDERandRESPONSE_MODELso you can see cost per model, per provider, over time. - Track p95/p99 latency, not the average, and alert on the tail. That’s the number that matches what an SLA actually promises a user.
- Treat
spring.ai.chat.client.observations.*andspring.ai.chat.observations.*as a compliance switch, not a debug flag. Decide who owns that call before you flip either one totrue, and know it puts prompt/response text into your tracing backend.
Common misconceptions
“SimpleLoggerAdvisor logs automatically once it’s on the advisor chain.” It doesn’t. Wiring the advisor and setting the logging level are two separate steps — without logging.level.org.springframework.ai.chat.client.advisor=DEBUG, the advisor is present but silent.
“If I see tags in my trace, my prompts are being logged.” Tags like AI_PROVIDER, REQUEST_MODEL, and chatClientConversationId are metadata — they’re on by default and safe. The actual prompt and response text is a completely separate opt-in, controlled by the log-prompt/log-completion properties, not by the presence of tags.
“The CallAdvisor/StreamAdvisor split is a new 2.0.0 observability feature.” It isn’t — those interfaces date back to 1.0.0, well before the 2.0.0 baseline this series is pinned to. Don’t confuse it with the genuinely new pieces here, which are the scoped spring.ai.chat.client.observations.* content toggles (since 1.1.0) and provider-level HTTP instrumentation (since 2.0.0).
“Average latency is a good enough number to alert on.” It isn’t. A metric is a shape across many calls, not one number. Averages get dragged down by the fast majority and can look healthy while p95/p99 — the experience your worst-off users actually have — quietly blows past your SLA.
Frequently asked questions
What’s the minimum setup to see full request/response logs for a ChatClient call?
Two things: add new SimpleLoggerAdvisor() to .defaultAdvisors(...) on your ChatClient.Builder, and set logging.level.org.springframework.ai.chat.client.advisor=DEBUG. Both are required — the advisor without the logging level produces nothing.
Does Spring AI log my prompts and completions by default?
No. Metadata tags (provider, model, duration, token counts) are on by default and are safe to leave on. The actual prompt/response text only enters logs or traces if you explicitly enable spring.ai.chat.client.observations.log-prompt/log-completion (advisor-level) or spring.ai.chat.observations.log-prompt/log-completion (model-level).
What Java and Spring Boot version does Spring AI 2.0.0 require? Java 21 to build, running on Spring Boot 4.0 and Spring Framework 7.0. Java 17 is only the Spring Boot 4 runtime floor — it is not sufficient to build against Spring AI 2.0.0.
Where do I find the exact Micrometer metric names Spring AI publishes?
The verified Java constants are AiObservationMetricNames.OPERATION_DURATION and AiObservationMetricNames.TOKEN_USAGE. For the exact dotted string your build publishes (it follows OpenTelemetry’s GenAI semantic conventions), check /actuator/metrics on a running instance rather than assuming a literal string.
What’s the difference between spring.ai.chat.client.observations.* and spring.ai.chat.observations.*?
They’re two distinct configuration classes at two different layers. spring.ai.chat.client.observations.* (ChatClientBuilderProperties.Observations, since 1.1.0) controls content logging at the ChatClient/advisor level. spring.ai.chat.observations.* (ChatObservationProperties) controls it at the underlying chat-model level and additionally exposes include-error-logging. Set both deliberately — they don’t imply each other.
Do I need a separate “observability starter” dependency for any of this?
No dedicated Spring AI observability starter is required. This rides on the same Spring Boot Actuator and Micrometer stack every other Spring Boot service already uses; Spring AI just plugs ChatClient and chat-model calls into it as Observations.
Where this fits in the series
This is Episode 9 of Spring AI for Enterprise Java. The previous episode covers wiring Spring AI into MCP servers and clients (MCP Integration). Now that a call is auditable, the next episode turns to whether the answer was any good: Evaluation, Guardrails & Hallucination Control.
Runnable code for this episode lives in the 09-observability module of the spring-ai-enterprise-examples repo.
The video for this episode is in production — subscribe to @TheStackUnderflow on YouTube so it lands in your feed the moment it’s live.
Found this useful? The deep version lives on YouTube — new breakdowns of how AI dev tools actually work, weekly.
Subscribe on YouTube →