@@ -36,6 +36,7 @@ class LLMObsSpanMapperTest extends DDCoreSpecification {
3636 .withTag(" _ml_obs_metric.input_tokens" , 50 )
3737 .withTag(" _ml_obs_metric.output_tokens" , 25 )
3838 .withTag(" _ml_obs_metric.total_tokens" , 75 )
39+ .withTag(" _ml_obs_tag.session_id" , " abc-123-session" )
3940 .start()
4041
4142 llmSpan. setSpanType(InternalSpanTypes . LLMOBS )
@@ -132,6 +133,10 @@ class LLMObsSpanMapperTest extends DDCoreSpecification {
132133 spanData[" _dd" ][" trace_id" ] == spanData[" trace_id" ]
133134 spanData[" _dd" ][" apm_trace_id" ] == spanData[" trace_id" ]
134135
136+ // Top-level session_id field — what the LLM Trace Explorer's Sessions filter queries.
137+ spanData. containsKey(" session_id" )
138+ spanData[" session_id" ] == " abc-123-session"
139+
135140 spanData. containsKey(" meta" )
136141 spanData[" meta" ][" span.kind" ] == " llm"
137142 spanData[" meta" ]. containsKey(" error" )
@@ -176,6 +181,7 @@ class LLMObsSpanMapperTest extends DDCoreSpecification {
176181
177182 spanData. containsKey(" tags" )
178183 spanData[" tags" ]. contains(" language:jvm" )
184+ spanData[" tags" ]. contains(" session_id:abc-123-session" )
179185 }
180186
181187 def " test LLMObsSpanMapper writes no spans when none are LLMObs spans" () {
@@ -297,69 +303,6 @@ class LLMObsSpanMapperTest extends DDCoreSpecification {
297303 spanNames. contains(" chat-completion-3" )
298304 }
299305
300- def " test LLMObsSpanMapper writes top-level session_id when set" () {
301- setup :
302- def mapper = new LLMObsSpanMapper ()
303- def tracer = tracerBuilder(). writer(new ListWriter ()). build()
304-
305- def sessionId = " abc-123-session"
306-
307- def llmSpan = tracer. buildSpan(" datadog" , " openai.request" )
308- .withResourceName(" createCompletion" )
309- .withTag(" _ml_obs_tag.span.kind" , Tags . LLMOBS_LLM_SPAN_KIND )
310- .withTag(" _ml_obs_tag.model_name" , " gpt-4" )
311- .withTag(" _ml_obs_tag.model_provider" , " openai" )
312- .withTag(" _ml_obs_tag.session_id" , sessionId)
313- .start()
314- llmSpan. setSpanType(InternalSpanTypes . LLMOBS )
315- llmSpan. finish()
316-
317- def trace = [llmSpan]
318- CapturingByteBufferConsumer sink = new CapturingByteBufferConsumer ()
319- MsgPackWriter packer = new MsgPackWriter (new FlushingBuffer (16 * 1024 , sink))
320-
321- when :
322- packer. format(trace, mapper)
323- packer. flush()
324-
325- then :
326- sink. captured != null
327- def payload = mapper. newPayload()
328- payload. withBody(1 , sink. captured)
329-
330- def channel = new ByteArrayOutputStream ()
331- payload. writeTo(new WritableByteChannel () {
332- @Override
333- int write (ByteBuffer src ) throws IOException {
334- def bytes = new byte [src. remaining()]
335- src. get(bytes)
336- channel. write(bytes)
337- return bytes. length
338- }
339-
340- @Override
341- boolean isOpen () {
342- return true
343- }
344-
345- @Override
346- void close () throws IOException { }
347- })
348-
349- def result = objectMapper. readValue(channel. toByteArray(), Map )
350- def spanData = result[" spans" ][0 ]
351-
352- then :
353- // Top-level session_id field is present with the right value — this is what
354- // the LLM Trace Explorer's Sessions filter queries.
355- spanData. containsKey(" session_id" )
356- spanData[" session_id" ] == sessionId
357-
358- // The session_id:<value> entry is ALSO present in the tags[] array, matching
359- // dd-trace-py and dd-trace-js wire-format behavior.
360- spanData[" tags" ]. contains(" session_id:${ sessionId} " . toString())
361- }
362-
363306 def " test LLMObsSpanMapper omits top-level session_id when not set" () {
364307 setup :
365308 def mapper = new LLMObsSpanMapper ()
0 commit comments