Skip to content

fix: add span attributes for input messages in ChatModelPromptContentObservationHandler#6057

Open
EvanYao826 wants to merge 4 commits into
spring-projects:mainfrom
EvanYao826:fix/chat-model-prompt-content-span-tags
Open

fix: add span attributes for input messages in ChatModelPromptContentObservationHandler#6057
EvanYao826 wants to merge 4 commits into
spring-projects:mainfrom
EvanYao826:fix/chat-model-prompt-content-span-tags

Conversation

@EvanYao826
Copy link
Copy Markdown

Fixes #6051


Problem

ChatModelPromptContentObservationHandler.onStop() emits prompt content to logs but does not set span attributes. Tracing systems have no visibility into the actual prompt content sent to the chat model.

Fix

Add gen_ai.input.messages span tag in onStop() following OpenTelemetry GenAI semantic conventions. The tag is only set when:

  • A TracingContext with a non-null Span is present
  • The prompt contains at least one message

Changes

  • ChatModelPromptContentObservationHandler.java: Added sampleSpanTag() method that serializes messages as JSON array and sets span tag
  • ChatModelPromptContentObservationHandlerTests.java: Added 5 tests covering: span tag set with tracing context, no tag without tracing context, no tag with empty prompt, no tag with null span, single message tag

AI-agent involvement: This contribution was assisted by an AI coding agent.

@jewoodev
Copy link
Copy Markdown
Contributor

@EvanYao826 Thanks for the PR. Looks like we may have crossed paths — I posted an analysis comment on the issue itself around the same time (my earlier issue comment: #6051 (comment)), so it seems we were working in parallel without seeing each other's work. Looking at that analysis side by side with this PR's diff, I'd like to share four things — three are concerns I think should be addressed before merge, and the last one is a scope question for the maintainers.

1. JSON escaping defect

In serializeMessages(), message text is concatenated directly into the JSON string without escaping:

joiner.add("{\"role\":\"" + role + "\",\"content\":\"" + content + "\"}");

When user input contains ", \, newlines, or control characters, an invalid JSON string is emitted as the span attribute. For example, if a user types Say "hi", the result is:

{"role":"user","content":"Say "hi""}

OTel-native backends could fail to parse this attribute as valid JSON. Since user input is essentially arbitrary text, this isn't a rare edge case but a predictable input pattern.

spring-ai-model already declares tools.jackson.core:jackson-databind (Jackson 3) as a compile dependency, so using Jackson's JsonMapper/ObjectMapper or an existing shared serializer in Spring AI would avoid the hand-rolled escaping here.

2. Optional micrometer-tracing classpath compatibility

spring-ai-model/pom.xml declares micrometer-tracing with <optional>true</optional> (L61-65), so it does not transitively reach consumers of spring-ai-model.

This PR adds direct imports of io.micrometer.tracing.Span and io.micrometer.tracing.handler.TracingObservationHandler to ChatModelPromptContentObservationHandler. However, ChatObservationAutoConfiguration has a fallback path — TracerNotPresentObservationConfiguration (L122-133) — that instantiates this handler directly even when tracing is missing from the classpath:

@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingClass("io.micrometer.tracing.Tracer")
static class TracerNotPresentObservationConfiguration {

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnProperty(prefix = ChatObservationProperties.CONFIG_PREFIX, name = "log-prompt",
            havingValue = "true")
    ChatModelPromptContentObservationHandler chatModelPromptContentObservationHandler() {
        logPromptContentWarning();
        return new ChatModelPromptContentObservationHandler();
    }
}

For consumers without micrometer-tracing on the classpath, loading this handler class during ApplicationContext refresh could throw NoClassDefFoundError and fail startup.

One direction consistent with the existing structure would be to move the span tagging into a separate handler (e.g., ChatModelPromptSpanContentObservationHandler) guarded by @ConditionalOnClass(Tracer.class) + @ConditionalOnBean(Tracer.class), keeping the existing logging handler free of tracing imports. That mirrors the current TracerPresentObservationConfiguration vs TracerNotPresentObservationConfiguration split.

3. OTel spec's content-capture opt-in guidance

The OTel GenAI semantic conventions mark gen_ai.input.messages / gen_ai.output.messages as opt-in and state:

"SHOULD NOT capture by default, but SHOULD provide an option for users to opt in."

gen-ai-spans

Currently, when a TracingContext/Span is present, this PR tags gen_ai.input.messages without a separate content-capture opt-in. Spring AI already has the spring.ai.chat.observations.log-prompt flag — reusing it, or introducing a dedicated flag such as include-content, so that the span tag is set only when the user has explicitly opted in, would be consistent with this guidance. Exposing prompt content on spans by default without an explicit content-capture opt-in is inconsistent with the spec's SHOULD-NOT guidance.

4. Scope question for maintainers

Two items from the issue body and my earlier analysis are not covered by this PR. I'd appreciate maintainer input on whether they belong in the same PR or in a follow-up:

  • Add INPUT_MESSAGES/OUTPUT_MESSAGES constants to AiObservationAttributes — the PR currently defines these keys as package-private constants inside the handler, so other conventions/handlers cannot reuse them.
  • Counterpart for ChatModelCompletionObservationHandler — the output-messages side is missing. Given that the issue covers both input and output messages, it would feel natural to pair them.

@EvanYao826
Copy link
Copy Markdown
Author

@jewoodev Thanks for the detailed analysis! You're right on all four points. Here's what I've changed:

1. JSON escaping — Replaced manual string concatenation with Jackson ObjectMapper (already a compile dependency). The serializer properly handles quotes, backslashes, and control characters.

2. micrometer-tracing classpath — Moved span tagging into a new ChatModelPromptSpanContentObservationHandler guarded by @ConditionalOnClass(Tracer.class). The existing logging handler remains free of tracing imports, so TracerNotPresentObservationConfiguration continues to work without micrometer-tracing on the classpath.

3. OTel content-capture opt-in — The span tagging now only activates when spring.ai.chat.observations.log-prompt=true, consistent with the existing opt-in flag and the OTel spec's SHOULD-NOT default guidance.

4. Constants — Moved INPUT_MESSAGES and OUTPUT_MESSAGES to AiObservationAttributes so other conventions/handlers can reuse them.

Regarding scope: I agree OUTPUT_MESSAGES for the completion handler should be a follow-up PR. Happy to work on that next.

Pushed the updated commits. Looking forward to your feedback!

@jewoodev
Copy link
Copy Markdown
Contributor

@EvanYao826 Thanks for the update — the four fixes all look right from the commit message.

Heads up that the new commit isn't reflected on this PR yet. The PR's head ref fix/chat-model-prompt-content-span-tags is still at the original 4a6368e, while the updated commit 8fadd0e ("address review feedback on span attributes for input messages") landed on a separate branch fix/span-attributes-input-messages in your fork. Looks like a branch mix-up.

If you push (or cherry-pick) 8fadd0e onto fix/chat-model-prompt-content-span-tags, the PR will pick it up automatically. Happy to re-review once it's in.

Copy link
Copy Markdown
Contributor

@jewoodev jewoodev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was the output constant added intentionally for something here? I ask because I just opened #6193, the output-side counterpart, which actually consumes gen_ai.output.messages. If it indeed isn't used here, would you mind dropping OUTPUT_MESSAGES so it lives in the PR that consumes it? That keeps each constant added by the PR that uses it and avoids both PRs adding the same enum member.

* The output messages received from the model, serialized as a JSON array following
* the OpenTelemetry GenAI semantic conventions.
*/
OUTPUT_MESSAGES("gen_ai.output.messages"),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adds OUTPUT_MESSAGES to AiObservationAttributes but only uses INPUT_MESSAGES.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — I've removed the unused OUTPUT_MESSAGES constant. Only INPUT_MESSAGES is referenced in this handler. Updated the PR accordingly.

EvanYao826 and others added 3 commits May 28, 2026 22:12
…ObservationHandler

The onStop() method only emitted prompt content to logs but did not set
span attributes. This meant tracing systems had no visibility into the
actual prompt content sent to the chat model.

Add gen_ai.input.messages span tag in onStop() following OpenTelemetry
GenAI semantic conventions. The tag is only set when a TracingContext
with a non-null Span is present and the prompt is non-empty.

Fixes spring-projects#6051

---
*AI-agent involvement: This contribution was assisted by an AI coding agent.*

Signed-off-by: EvanYao826 <2869018789@qq.com>
Signed-off-by: EvanYao826 <155432245+EvanYao826@users.noreply.github.com>
- Use Jackson ObjectMapper for JSON serialization instead of manual string concat
- Move span tagging to separate ChatModelPromptSpanContentObservationHandler
  guarded by @ConditionalOnClass(Tracer.class) to avoid NoClassDefFoundError
- Only capture gen_ai.input.messages when content-capture is explicitly opted in
- Move INPUT_MESSAGES/OUTPUT_MESSAGES constants to AiObservationAttributes

Signed-off-by: EvanYao826 <155432245+EvanYao826@users.noreply.github.com>
OUTPUT_MESSAGES was defined but never used in the codebase.
Remove it to keep the API clean.

Signed-off-by: EvanYao826 <155432245+EvanYao826@users.noreply.github.com>
@EvanYao826 EvanYao826 force-pushed the fix/chat-model-prompt-content-span-tags branch from dec47d3 to 838ea32 Compare May 28, 2026 14:14
Remove accidentally committed local Maven repository path.
Refs: spring-projects#6057
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ChatModelPromptContentObservationHandler should tag span attributes for LLM Observability, not only log to SLF4J

2 participants