diff --git a/packages/uipath-platform/src/uipath/platform/common/_span_utils.py b/packages/uipath-platform/src/uipath/platform/common/_span_utils.py index 448648067..782fa02db 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_span_utils.py +++ b/packages/uipath-platform/src/uipath/platform/common/_span_utils.py @@ -267,6 +267,26 @@ def otel_span_to_uipath_span( ] attributes_dict["links"] = links_list + # Add process context attributes from environment variables + for env_key, attr_key in ( + ("UIPATH_PROCESS_UUID", "codedAgentId"), + ("UIPATH_PROCESS_KEY", "codedAgentName"), + ("UIPATH_PROCESS_VERSION", "codedAgentVersion"), + ): + value = env.get(env_key) + if value: + attributes_dict[attr_key] = value + + # Add custom trace attributes from UIPATH_TRACE_ATTR__ prefixed env vars + # Follows ASP.NET Core convention: env vars override config using __ + _TRACE_ATTR_PREFIX = "UIPATH_TRACE_ATTR__" + _prefix_len = len(_TRACE_ATTR_PREFIX) + for key, value in env.items(): + if key.startswith(_TRACE_ATTR_PREFIX): + attr_name = key[_prefix_len:] + if attr_name: + attributes_dict[attr_name] = value + span_type_value = attributes_dict.get("span_type", "OpenTelemetry") span_type = str(span_type_value) diff --git a/packages/uipath-platform/tests/services/test_span_utils.py b/packages/uipath-platform/tests/services/test_span_utils.py index 80cd0d2db..dfc914790 100644 --- a/packages/uipath-platform/tests/services/test_span_utils.py +++ b/packages/uipath-platform/tests/services/test_span_utils.py @@ -67,7 +67,9 @@ class TestSpanUtils: "UIPATH_ORGANIZATION_ID": "test-org", "UIPATH_TENANT_ID": "test-tenant", "UIPATH_FOLDER_KEY": "test-folder", - "UIPATH_PROCESS_UUID": "test-process", + "UIPATH_PROCESS_UUID": "test-process-uuid", + "UIPATH_PROCESS_KEY": "test-process-key", + "UIPATH_PROCESS_VERSION": "1.2.3", "UIPATH_JOB_KEY": "test-job", }, ) @@ -122,6 +124,11 @@ def test_otel_span_to_uipath_span(self): assert attributes["key1"] == "value1" assert attributes["key2"] == 123 + # Verify coded agent attributes from env vars + assert attributes["codedAgentId"] == "test-process-uuid" + assert attributes["codedAgentName"] == "test-process-key" + assert attributes["codedAgentVersion"] == "1.2.3" + # Test with error status mock_span.status.description = "Test error description" mock_span.status.status_code = StatusCode.ERROR @@ -427,3 +434,75 @@ def test_uipath_span_source_override_with_uipath_source(self): # String source still in Attributes JSON attrs = json.loads(span_dict["Attributes"]) assert attrs["source"] == "runtime" + + @patch.dict( + os.environ, + { + "UIPATH_ORGANIZATION_ID": "test-org", + "UIPATH_TRACE_ATTR__CUSTOMERID": "cust-123", + "UIPATH_TRACE_ATTR__ENVIRONMENT": "staging", + "UIPATH_TRACE_ATTR__TEAM": "platform", + }, + ) + def test_custom_trace_attributes_from_env(self): + """Test that UIPATH_TRACE_ATTR__ prefixed env vars are added to attributes.""" + mock_span = Mock(spec=OTelSpan) + + trace_id = 0x123456789ABCDEF0123456789ABCDEF0 + span_id = 0x0123456789ABCDEF + mock_context = SpanContext(trace_id=trace_id, span_id=span_id, is_remote=False) + mock_span.get_span_context.return_value = mock_context + + mock_span.name = "test-span" + mock_span.parent = None + mock_span.status.status_code = StatusCode.OK + mock_span.attributes = {"existing": "value"} + mock_span.events = [] + mock_span.links = [] + + current_time_ns = int(datetime.now().timestamp() * 1e9) + mock_span.start_time = current_time_ns + mock_span.end_time = current_time_ns + 1000000 + + uipath_span = _SpanUtils.otel_span_to_uipath_span(mock_span) + attrs = json.loads(uipath_span.attributes) + + # On Windows, env var keys are uppercased; on Linux they preserve case. + # Use case-insensitive lookup to make the test cross-platform. + attrs_lower = {k.lower(): v for k, v in attrs.items()} + assert attrs_lower["customerid"] == "cust-123" + assert attrs_lower["environment"] == "staging" + assert attrs_lower["team"] == "platform" + assert attrs["existing"] == "value" + + @patch.dict( + os.environ, + { + "UIPATH_ORGANIZATION_ID": "test-org", + "UIPATH_TRACE_ATTR__": "empty-key-ignored", + }, + ) + def test_custom_trace_attributes_ignores_empty_key(self): + """Test that UIPATH_TRACE_ATTR__ with no suffix is ignored.""" + mock_span = Mock(spec=OTelSpan) + + trace_id = 0x123456789ABCDEF0123456789ABCDEF0 + span_id = 0x0123456789ABCDEF + mock_context = SpanContext(trace_id=trace_id, span_id=span_id, is_remote=False) + mock_span.get_span_context.return_value = mock_context + + mock_span.name = "test-span" + mock_span.parent = None + mock_span.status.status_code = StatusCode.OK + mock_span.attributes = {} + mock_span.events = [] + mock_span.links = [] + + current_time_ns = int(datetime.now().timestamp() * 1e9) + mock_span.start_time = current_time_ns + mock_span.end_time = current_time_ns + 1000000 + + uipath_span = _SpanUtils.otel_span_to_uipath_span(mock_span) + attrs = json.loads(uipath_span.attributes) + + assert "" not in attrs