@@ -561,3 +561,91 @@ def test_emit_llo_attributes_openlit_style_events(self):
561561 self .assertEqual (len (output_messages ), 1 )
562562 self .assertEqual (output_messages [0 ]["content" ], "Quantum computing is..." )
563563 self .assertEqual (output_messages [0 ]["role" ], "assistant" )
564+
565+ def test_emit_llo_attributes_with_session_id (self ):
566+ """
567+ Verify session.id attribute from span is copied to event attributes when present.
568+ """
569+ attributes = {
570+ "session.id" : "test-session-123" ,
571+ "gen_ai.prompt" : "Hello, AI" ,
572+ "gen_ai.completion" : "Hello! How can I help you?" ,
573+ }
574+
575+ span = self ._create_mock_span (attributes )
576+ span .end_time = 1234567899
577+ span .instrumentation_scope = MagicMock ()
578+ span .instrumentation_scope .name = "test.scope"
579+
580+ self .llo_handler ._emit_llo_attributes (span , attributes )
581+
582+ self .event_logger_mock .emit .assert_called_once ()
583+ emitted_event = self .event_logger_mock .emit .call_args [0 ][0 ]
584+
585+ # Verify session.id was copied to event attributes
586+ self .assertIsNotNone (emitted_event .attributes )
587+ self .assertEqual (emitted_event .attributes .get ("session.id" ), "test-session-123" )
588+ # Event class always adds event.name
589+ self .assertIn ("event.name" , emitted_event .attributes )
590+
591+ # Verify event body still contains LLO data
592+ event_body = emitted_event .body
593+ self .assertIn ("input" , event_body )
594+ self .assertIn ("output" , event_body )
595+
596+ def test_emit_llo_attributes_without_session_id (self ):
597+ """
598+ Verify event attributes do not contain session.id when not present in span attributes.
599+ """
600+ attributes = {
601+ "gen_ai.prompt" : "Hello, AI" ,
602+ "gen_ai.completion" : "Hello! How can I help you?" ,
603+ }
604+
605+ span = self ._create_mock_span (attributes )
606+ span .end_time = 1234567899
607+ span .instrumentation_scope = MagicMock ()
608+ span .instrumentation_scope .name = "test.scope"
609+
610+ self .llo_handler ._emit_llo_attributes (span , attributes )
611+
612+ self .event_logger_mock .emit .assert_called_once ()
613+ emitted_event = self .event_logger_mock .emit .call_args [0 ][0 ]
614+
615+ # Verify session.id is not in event attributes
616+ self .assertIsNotNone (emitted_event .attributes )
617+ self .assertNotIn ("session.id" , emitted_event .attributes )
618+ # Event class always adds event.name
619+ self .assertIn ("event.name" , emitted_event .attributes )
620+
621+ def test_emit_llo_attributes_with_session_id_and_other_attributes (self ):
622+ """
623+ Verify only session.id is copied from span attributes when mixed with other attributes.
624+ """
625+ attributes = {
626+ "session.id" : "session-456" ,
627+ "user.id" : "user-789" ,
628+ "gen_ai.prompt" : "What's the weather?" ,
629+ "gen_ai.completion" : "I can't check the weather." ,
630+ "other.attribute" : "some-value" ,
631+ }
632+
633+ span = self ._create_mock_span (attributes )
634+ span .end_time = 1234567899
635+ span .instrumentation_scope = MagicMock ()
636+ span .instrumentation_scope .name = "test.scope"
637+
638+ self .llo_handler ._emit_llo_attributes (span , attributes )
639+
640+ self .event_logger_mock .emit .assert_called_once ()
641+ emitted_event = self .event_logger_mock .emit .call_args [0 ][0 ]
642+
643+ # Verify only session.id was copied to event attributes (plus event.name from Event class)
644+ self .assertIsNotNone (emitted_event .attributes )
645+ self .assertEqual (emitted_event .attributes .get ("session.id" ), "session-456" )
646+ self .assertIn ("event.name" , emitted_event .attributes )
647+ # Verify other span attributes were not copied
648+ self .assertNotIn ("user.id" , emitted_event .attributes )
649+ self .assertNotIn ("other.attribute" , emitted_event .attributes )
650+ self .assertNotIn ("gen_ai.prompt" , emitted_event .attributes )
651+ self .assertNotIn ("gen_ai.completion" , emitted_event .attributes )
0 commit comments