diff --git a/src/sap_cloud_sdk/core/telemetry/tracer.py b/src/sap_cloud_sdk/core/telemetry/tracer.py index 5cca857..9f211c3 100644 --- a/src/sap_cloud_sdk/core/telemetry/tracer.py +++ b/src/sap_cloud_sdk/core/telemetry/tracer.py @@ -9,7 +9,7 @@ from contextlib import contextmanager, nullcontext from typing import Optional, Dict, Any -from opentelemetry import trace +from opentelemetry import trace, baggage from opentelemetry.trace import Status, StatusCode, Span from sap_cloud_sdk.core.telemetry.genai_operation import GenAIOperation @@ -34,6 +34,28 @@ _ATTR_SERVER_ADDRESS = "server.address" +def _resolve_conversation_id(conversation_id: Optional[str]) -> Optional[str]: + """ + Resolve conversation ID with priority: explicit param > baggage > None. + + Priority order: + 1. Explicit conversation_id parameter (highest priority) + 2. W3C baggage header value (gen_ai.conversation.id) + 3. None (no conversation ID) + + This ensures consistent conversation ID across all spans in a trace, + even when traceloop or other instrumentation sets a different value. + """ + if conversation_id is not None: + return conversation_id + + baggage_conversation_id = baggage.get_baggage(_ATTR_GEN_AI_CONVERSATION_ID) + if baggage_conversation_id: + return baggage_conversation_id + + return None + + @contextmanager def _propagate_attributes(attrs: Dict[str, Any]): """Push attrs onto the propagation stack for the duration of the context.""" @@ -149,8 +171,9 @@ def chat_span( _ATTR_GEN_AI_PROVIDER_NAME: provider, _ATTR_GEN_AI_REQUEST_MODEL: model, } - if conversation_id is not None: - base_attrs[_ATTR_GEN_AI_CONVERSATION_ID] = conversation_id + resolved_conversation_id = _resolve_conversation_id(conversation_id) + if resolved_conversation_id is not None: + base_attrs[_ATTR_GEN_AI_CONVERSATION_ID] = resolved_conversation_id if server_address is not None: base_attrs[_ATTR_SERVER_ADDRESS] = server_address # Add tenant_id if set @@ -312,8 +335,9 @@ def invoke_agent_span( base_attrs[_ATTR_GEN_AI_AGENT_ID] = agent_id if agent_description is not None: base_attrs[_ATTR_GEN_AI_AGENT_DESCRIPTION] = agent_description - if conversation_id is not None: - base_attrs[_ATTR_GEN_AI_CONVERSATION_ID] = conversation_id + resolved_conversation_id = _resolve_conversation_id(conversation_id) + if resolved_conversation_id is not None: + base_attrs[_ATTR_GEN_AI_CONVERSATION_ID] = resolved_conversation_id if server_address is not None: base_attrs[_ATTR_SERVER_ADDRESS] = server_address # Add tenant_id if set diff --git a/tests/core/unit/telemetry/test_tracer.py b/tests/core/unit/telemetry/test_tracer.py index e3fa2fe..6d8bf06 100644 --- a/tests/core/unit/telemetry/test_tracer.py +++ b/tests/core/unit/telemetry/test_tracer.py @@ -981,3 +981,39 @@ def test_propagate_default_is_false(self): child_attrs = captures[1]["attributes"] assert "custom" not in child_attrs + + +class TestResolveConversationId: + """Test suite for _resolve_conversation_id function.""" + + def test_resolve_conversation_id_explicit_param(self): + """Test that explicit conversation_id parameter takes highest priority.""" + from sap_cloud_sdk.core.telemetry.tracer import _resolve_conversation_id + + with patch('opentelemetry.baggage.get_baggage', return_value='baggage_id'): + result = _resolve_conversation_id('explicit_id') + assert result == 'explicit_id' + + def test_resolve_conversation_id_from_baggage(self): + """Test that baggage value is used when no explicit param provided.""" + from sap_cloud_sdk.core.telemetry.tracer import _resolve_conversation_id + + with patch('opentelemetry.baggage.get_baggage', return_value='baggage_id'): + result = _resolve_conversation_id(None) + assert result == 'baggage_id' + + def test_resolve_conversation_id_no_baggage(self): + """Test that None is returned when no explicit param and no baggage.""" + from sap_cloud_sdk.core.telemetry.tracer import _resolve_conversation_id + + with patch('opentelemetry.baggage.get_baggage', return_value=None): + result = _resolve_conversation_id(None) + assert result is None + + def test_resolve_conversation_id_empty_baggage(self): + """Test that empty baggage string is treated as no value.""" + from sap_cloud_sdk.core.telemetry.tracer import _resolve_conversation_id + + with patch('opentelemetry.baggage.get_baggage', return_value=''): + result = _resolve_conversation_id(None) + assert result is None