diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index cad224be8..9879982c3 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.52" +version = "0.1.53" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" 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 9cb49826b..120c39b68 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_span_utils.py +++ b/packages/uipath-platform/src/uipath/platform/common/_span_utils.py @@ -29,6 +29,16 @@ class AttachmentDirection(IntEnum): OUT = 2 +class VerbosityLevel(IntEnum): + VERBOSE = 0 + TRACE = 1 + INFORMATION = 2 + WARNING = 3 + ERROR = 4 + CRITICAL = 5 + OFF = 6 + + class SpanAttachment(BaseModel): """Represents an attachment in the UiPath tracing system.""" @@ -87,6 +97,7 @@ class UiPathSpan: # Top-level fields for internal tracing schema execution_type: Optional[int] = None agent_version: Optional[str] = None + verbosity_level: Optional[int] = None attachments: Optional[List[SpanAttachment]] = None def to_dict(self, serialize_attributes: bool = True) -> Dict[str, Any]: @@ -114,7 +125,7 @@ def to_dict(self, serialize_attributes: bool = True) -> Dict[str, Any]: for att in self.attachments ] - return { + result: Dict[str, Any] = { "Id": self.id, "TraceId": self.trace_id, "ParentId": self.parent_id, @@ -138,6 +149,9 @@ def to_dict(self, serialize_attributes: bool = True) -> Dict[str, Any]: "AgentVersion": self.agent_version, "Attachments": attachments_out, } + if self.verbosity_level is not None: + result["VerbosityLevel"] = self.verbosity_level + return result class _SpanUtils: @@ -284,6 +298,7 @@ def otel_span_to_uipath_span( execution_type = attributes_dict.get("executionType") agent_version = attributes_dict.get("agentVersion") reference_id = env.get("UIPATH_AGENT_ID") or attributes_dict.get("agentId") + verbosity_level = attributes_dict.get("verbosityLevel") # Source: override via uipath.source attribute, else DEFAULT_SOURCE uipath_source = attributes_dict.get("uipath.source") @@ -334,6 +349,7 @@ def otel_span_to_uipath_span( span_type=span_type, execution_type=execution_type, agent_version=agent_version, + verbosity_level=verbosity_level, reference_id=reference_id, source=source, attachments=attachments, diff --git a/packages/uipath-platform/tests/services/test_span_utils.py b/packages/uipath-platform/tests/services/test_span_utils.py index 268ffc34c..c10729d31 100644 --- a/packages/uipath-platform/tests/services/test_span_utils.py +++ b/packages/uipath-platform/tests/services/test_span_utils.py @@ -10,6 +10,85 @@ from uipath.platform.common import UiPathSpan, _SpanUtils +class TestOTelToUiPathSpan: + """OTEL attribute -> top-level UiPathSpan field mapping. + + `_SpanUtils.otel_span_to_uipath_span` lifts a small set of OTEL + span attributes onto dedicated `UiPathSpan` fields surfaced under + `to_dict()`. This test documents that mapping — adding a new row + means the attribute is newly mapped, removing one breaks + downstream consumers. + """ + + ATTRIBUTE_FIELD_MAP = [ + ("executionType", "execution_type", "ExecutionType", 1), + ("agentVersion", "agent_version", "AgentVersion", "1.2.3"), + ("agentId", "reference_id", "ReferenceId", "ref-abc"), + ("verbosityLevel", "verbosity_level", "VerbosityLevel", 6), + ] + + @patch.dict(os.environ, {"UIPATH_ORGANIZATION_ID": "test-org"}) + def test_attributes_map_to_top_level_fields(self) -> None: + attrs = { + otel_attr: value for otel_attr, _, _, value in self.ATTRIBUTE_FIELD_MAP + } + + mock_span = Mock(spec=OTelSpan) + mock_context = SpanContext( + trace_id=0x123456789ABCDEF0123456789ABCDEF0, + span_id=0x0123456789ABCDEF, + 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 = attrs + mock_span.events = [] + mock_span.links = [] + now_ns = int(datetime.now().timestamp() * 1e9) + mock_span.start_time = now_ns + mock_span.end_time = now_ns + 1_000_000 + + uipath_span = _SpanUtils.otel_span_to_uipath_span(mock_span) + span_dict = uipath_span.to_dict() + + for _, span_field, top_level_key, value in self.ATTRIBUTE_FIELD_MAP: + assert getattr(uipath_span, span_field) == value, span_field + assert span_dict[top_level_key] == value, top_level_key + + @patch.dict(os.environ, {"UIPATH_ORGANIZATION_ID": "test-org"}) + def test_verbosity_level_omitted_when_unset(self) -> None: + """Spans that don't set verbosityLevel must not carry the key on the wire. + + Backwards compat: pre-existing spans never emitted VerbosityLevel; the + LLMOps backend applies its own default. Adding `"VerbosityLevel": null` + unconditionally would change the wire format for every existing span. + """ + mock_span = Mock(spec=OTelSpan) + mock_context = SpanContext( + trace_id=0x123456789ABCDEF0123456789ABCDEF0, + span_id=0x0123456789ABCDEF, + is_remote=False, + ) + mock_span.get_span_context.return_value = mock_context + mock_span.name = "legacy-span" + mock_span.parent = None + mock_span.status.status_code = StatusCode.OK + mock_span.attributes = {"someOtherAttr": "value"} + mock_span.events = [] + mock_span.links = [] + now_ns = int(datetime.now().timestamp() * 1e9) + mock_span.start_time = now_ns + mock_span.end_time = now_ns + 1_000_000 + + uipath_span = _SpanUtils.otel_span_to_uipath_span(mock_span) + span_dict = uipath_span.to_dict() + + assert uipath_span.verbosity_level is None + assert "VerbosityLevel" not in span_dict + + class TestNormalizeIds: """Tests for OTEL ID normalization functions.""" diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 72baba331..4293ee066 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.52" +version = "0.1.53" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 12777e925..2d50973d8 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,13 +1,13 @@ [project] name = "uipath" -version = "2.10.66" +version = "2.10.67" description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools." readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" dependencies = [ "uipath-core>=0.5.8, <0.6.0", "uipath-runtime>=0.10.1, <0.11.0", - "uipath-platform>=0.1.47, <0.2.0", + "uipath-platform>=0.1.53, <0.2.0", "click>=8.3.1", "httpx>=0.28.1", "pyjwt>=2.10.1", diff --git a/packages/uipath/src/uipath/tracing/__init__.py b/packages/uipath/src/uipath/tracing/__init__.py index aaef6328c..e6c37bc99 100644 --- a/packages/uipath/src/uipath/tracing/__init__.py +++ b/packages/uipath/src/uipath/tracing/__init__.py @@ -5,6 +5,7 @@ AttachmentDirection, AttachmentProvider, SpanAttachment, + VerbosityLevel, ) from ._live_tracking_processor import LiveTrackingSpanProcessor @@ -23,4 +24,5 @@ "AttachmentDirection", "AttachmentProvider", "SpanAttachment", + "VerbosityLevel", ] diff --git a/packages/uipath/tests/tracing/test_otel_exporters.py b/packages/uipath/tests/tracing/test_otel_exporters.py index 648dd9190..fc5a370c0 100644 --- a/packages/uipath/tests/tracing/test_otel_exporters.py +++ b/packages/uipath/tests/tracing/test_otel_exporters.py @@ -810,5 +810,18 @@ def test_none_stays_none(self, mock_env_vars, mock_span): assert payload["ProcessKey"] is None +class TestVerbosityLevelReexport: + """VerbosityLevel from uipath-platform is re-exported via uipath.tracing.""" + + def test_uipath_tracing_reexports_verbosity_level(self) -> None: + from uipath.platform.common._span_utils import ( + VerbosityLevel as _CommonVerbosity, + ) + from uipath.tracing import VerbosityLevel as _TracingVerbosity + + assert _TracingVerbosity is _CommonVerbosity + assert _TracingVerbosity.OFF == 6 + + if __name__ == "__main__": unittest.main() diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 3cc0f8f89..c97b619da 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.66" +version = "2.10.67" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, @@ -2691,7 +2691,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.52" +version = "0.1.53" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" },