Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 29 additions & 5 deletions src/sap_cloud_sdk/core/telemetry/tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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."""
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions tests/core/unit/telemetry/test_tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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