diff --git a/packages/traceloop-sdk/tests/test_metrics_common_attributes.py b/packages/traceloop-sdk/tests/test_metrics_common_attributes.py new file mode 100644 index 0000000000..34a6677636 --- /dev/null +++ b/packages/traceloop-sdk/tests/test_metrics_common_attributes.py @@ -0,0 +1,54 @@ +import json + +from opentelemetry.context import attach, set_value +from opentelemetry.semconv_ai import SpanAttributes + +from traceloop.sdk.tracing.tracing import metrics_common_attributes + + +def test_scalar_association_properties_pass_through(): + attach(set_value("association_properties", {"user_id": 1, "user_name": "John"})) + + attributes = metrics_common_attributes() + + assert attributes[f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.user_id"] == 1 + assert ( + attributes[f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.user_name"] + == "John" + ) + + +def test_list_valued_association_property_is_json_encoded(): + """LangGraph tags spans with list-valued baggage (e.g. langgraph_triggers). + Metric attributes must be hashable/scalar, so a raw list here previously + crashed the OTel metrics SDK's aggregation and silently dropped every + response-side span attribute set after it (gen_ai.output.messages, etc.).""" + attach( + set_value( + "association_properties", + {"langgraph_triggers": ["branch:to:resolve_naics"]}, + ) + ) + + attributes = metrics_common_attributes() + + key = f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.langgraph_triggers" + assert attributes[key] == json.dumps(["branch:to:resolve_naics"]) + # must be hashable, matching what the OTel metrics SDK requires for its + # aggregation key: frozenset(attributes.items()) + hash(attributes[key]) + + +def test_dict_valued_association_property_is_json_encoded(): + attach( + set_value( + "association_properties", + {"metadata": {"nested": "value"}}, + ) + ) + + attributes = metrics_common_attributes() + + key = f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.metadata" + assert attributes[key] == json.dumps({"nested": "value"}) + hash(attributes[key]) diff --git a/packages/traceloop-sdk/traceloop/sdk/tracing/tracing.py b/packages/traceloop-sdk/traceloop/sdk/tracing/tracing.py index 8808f85eab..a43fa6ee85 100644 --- a/packages/traceloop-sdk/traceloop/sdk/tracing/tracing.py +++ b/packages/traceloop-sdk/traceloop/sdk/tracing/tracing.py @@ -1,4 +1,5 @@ import atexit +import json import logging import os from urllib.parse import urlparse @@ -1221,6 +1222,9 @@ def metrics_common_attributes(): association_properties = get_value("association_properties") if association_properties is not None: for key, value in association_properties.items(): + if isinstance(value, (list, dict)): + # OTel metric attributes must be scalar/hashable + value = json.dumps(value) common_attributes[ f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.{key}" ] = value