uipath 2.0.9__tar.gz → 2.0.10__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of uipath might be problematic. Click here for more details.
- {uipath-2.0.9 → uipath-2.0.10}/.vscode/settings.json +1 -1
- {uipath-2.0.9 → uipath-2.0.10}/PKG-INFO +2 -1
- {uipath-2.0.9 → uipath-2.0.10}/pyproject.toml +2 -1
- uipath-2.0.10/src/uipath/tracing/__init__.py +3 -0
- uipath-2.0.10/src/uipath/tracing/_otel_exporters.py +67 -0
- uipath-2.0.10/src/uipath/tracing/_traced.py +165 -0
- uipath-2.0.10/src/uipath/tracing/_utils.py +267 -0
- uipath-2.0.10/tests/tracing/test_span_utils.py +204 -0
- uipath-2.0.10/tests/tracing/test_traced.py +145 -0
- {uipath-2.0.9 → uipath-2.0.10}/uv.lock +140 -1
- {uipath-2.0.9 → uipath-2.0.10}/.cursorrules +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/.editorconfig +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/.gitattributes +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/.github/workflows/build.yml +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/.github/workflows/cd.yml +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/.github/workflows/ci.yml +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/.github/workflows/commitlint.yml +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/.github/workflows/lint.yml +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/.github/workflows/test.yml +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/.gitignore +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/.pre-commit-config.yaml +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/.python-version +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/.vscode/extensions.json +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/CONTRIBUTING.md +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/LICENSE +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/README.md +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/docs/CONTRIBUTING.md +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/docs/actions.md +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/docs/assets/uipath-logo.svg +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/docs/assets.md +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/docs/buckets.md +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/docs/connections.md +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/docs/context_grounding.md +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/docs/getting_started_agent.md +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/docs/getting_started_cli.md +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/docs/getting_started_cloud.md +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/docs/getting_started_sdk.md +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/docs/index.md +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/docs/jobs.md +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/docs/processes.md +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/docs/queues.md +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/docs/sdk.md +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/justfile +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/mkdocs.yml +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/py.typed +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/__init__.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_cli/README.md +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_cli/__init__.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_cli/_auth/_auth_server.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_cli/_auth/_models.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_cli/_auth/_oidc_utils.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_cli/_auth/_portal_service.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_cli/_auth/_utils.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_cli/_auth/auth_config.json +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_cli/_auth/index.html +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_cli/_auth/localhost.crt +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_cli/_auth/localhost.key +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_cli/_runtime/_contracts.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_cli/_runtime/_logging.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_cli/_runtime/_runtime.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_cli/_templates/.psmdcp.template +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_cli/_templates/.rels.template +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_cli/_templates/[Content_Types].xml.template +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_cli/_templates/main.py.template +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_cli/_templates/package.nuspec.template +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_cli/_utils/_common.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_cli/_utils/_input_args.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_cli/_utils/_parse_ast.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_cli/cli_auth.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_cli/cli_deploy.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_cli/cli_init.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_cli/cli_new.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_cli/cli_pack.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_cli/cli_publish.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_cli/cli_run.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_cli/middlewares.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_config.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_execution_context.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_folder_context.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_services/__init__.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_services/_base_service.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_services/actions_service.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_services/api_client.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_services/assets_service.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_services/buckets_service.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_services/connections_service.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_services/connections_service.pyi +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_services/context_grounding_service.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_services/folder_service.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_services/jobs_service.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_services/llm_gateway_service.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_services/processes_service.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_services/queues_service.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_uipath.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_utils/__init__.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_utils/_endpoint.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_utils/_infer_bindings.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_utils/_logs.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_utils/_request_override.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_utils/_request_spec.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_utils/_user_agent.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/_utils/constants.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/models/__init__.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/models/action_schema.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/models/actions.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/models/assets.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/models/connections.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/models/context_grounding.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/models/context_grounding_index.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/models/exceptions.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/models/interrupt_models.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/models/job.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/models/llm_gateway.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/models/processes.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/models/queues.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/src/uipath/py.typed +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/tests/__init__.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/tests/cli/test_init.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/tests/conftest.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/tests/sdk/services/test_llm_integration.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/tests/sdk/services/test_llm_service.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/tests/sdk/services/test_uipath_llm_integration.py +0 -0
- {uipath-2.0.9 → uipath-2.0.10}/tests/sdk/test_config.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: uipath
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.10
|
|
4
4
|
Summary: Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools.
|
|
5
5
|
Project-URL: Homepage, https://uipath.com
|
|
6
6
|
Project-URL: Repository, https://github.com/UiPath/uipath-python
|
|
@@ -15,6 +15,7 @@ Classifier: Topic :: Software Development :: Build Tools
|
|
|
15
15
|
Requires-Python: >=3.10
|
|
16
16
|
Requires-Dist: click>=8.1.8
|
|
17
17
|
Requires-Dist: httpx>=0.28.1
|
|
18
|
+
Requires-Dist: opentelemetry-sdk>=1.32.1
|
|
18
19
|
Requires-Dist: pydantic>=2.11.1
|
|
19
20
|
Requires-Dist: pytest-asyncio>=0.25.3
|
|
20
21
|
Requires-Dist: python-dotenv>=1.0.1
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "uipath"
|
|
3
|
-
version = "2.0.
|
|
3
|
+
version = "2.0.10"
|
|
4
4
|
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
|
|
5
5
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
6
6
|
requires-python = ">=3.10"
|
|
7
7
|
dependencies = [
|
|
8
8
|
"click>=8.1.8",
|
|
9
9
|
"httpx>=0.28.1",
|
|
10
|
+
"opentelemetry-sdk>=1.32.1",
|
|
10
11
|
"pydantic>=2.11.1",
|
|
11
12
|
"pytest-asyncio>=0.25.3",
|
|
12
13
|
"python-dotenv>=1.0.1",
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from typing import Sequence
|
|
5
|
+
|
|
6
|
+
from httpx import Client
|
|
7
|
+
from opentelemetry.sdk.trace import ReadableSpan
|
|
8
|
+
from opentelemetry.sdk.trace.export import (
|
|
9
|
+
SpanExporter,
|
|
10
|
+
SpanExportResult,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from ._utils import _SpanUtils
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LlmOpsHttpExporter(SpanExporter):
|
|
19
|
+
"""An OpenTelemetry span exporter that sends spans to UiPath LLM Ops."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, **kwargs):
|
|
22
|
+
"""Initialize the exporter with the base URL and authentication token."""
|
|
23
|
+
super().__init__(**kwargs)
|
|
24
|
+
self.base_url = self._get_base_url()
|
|
25
|
+
self.auth_token = os.environ.get("UIPATH_ACCESS_TOKEN")
|
|
26
|
+
self.headers = {
|
|
27
|
+
"Content-Type": "application/json",
|
|
28
|
+
"Authorization": f"Bearer {self.auth_token}",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
self.http_client = Client(headers=self.headers)
|
|
32
|
+
|
|
33
|
+
def export(self, spans: Sequence[ReadableSpan]):
|
|
34
|
+
"""Export spans to UiPath LLM Ops."""
|
|
35
|
+
logger.debug(
|
|
36
|
+
f"Exporting {len(spans)} spans to {self.base_url}/llmopstenant_/api/Traces/spans"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
span_list = [
|
|
40
|
+
_SpanUtils.otel_span_to_uipath_span(span).to_dict() for span in spans
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
trace_id = str(span_list[0]["TraceId"])
|
|
44
|
+
url = f"{self.base_url}/llmopstenant_/api/Traces/spans?traceId={trace_id}&source=Robots"
|
|
45
|
+
|
|
46
|
+
logger.debug("payload: ", json.dumps(span_list))
|
|
47
|
+
|
|
48
|
+
res = self.http_client.post(url, json=span_list)
|
|
49
|
+
|
|
50
|
+
if res.status_code == 200:
|
|
51
|
+
return SpanExportResult.SUCCESS
|
|
52
|
+
else:
|
|
53
|
+
return SpanExportResult.FAILURE
|
|
54
|
+
|
|
55
|
+
def force_flush(self, timeout_millis: int = 30000) -> bool:
|
|
56
|
+
"""Force flush the exporter."""
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
def _get_base_url(self) -> str:
|
|
60
|
+
uipath_url = (
|
|
61
|
+
os.environ.get("UIPATH_URL")
|
|
62
|
+
or "https://cloud.uipath.com/dummyOrg/dummyTennant/"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
uipath_url = uipath_url.rstrip("/")
|
|
66
|
+
|
|
67
|
+
return uipath_url
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from opentelemetry import trace
|
|
8
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
9
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
10
|
+
|
|
11
|
+
from ._otel_exporters import LlmOpsHttpExporter
|
|
12
|
+
from ._utils import _SpanUtils
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
trace.set_tracer_provider(TracerProvider())
|
|
17
|
+
trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(LlmOpsHttpExporter())) # type: ignore
|
|
18
|
+
tracer = trace.get_tracer(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def wait_for_tracers():
|
|
22
|
+
"""Wait for all tracers to finish."""
|
|
23
|
+
trace.get_tracer_provider().shutdown() # type: ignore
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TracedDecoratorRegistry:
|
|
27
|
+
"""Registry for tracing decorators."""
|
|
28
|
+
|
|
29
|
+
_decorators: dict[str, Any] = {}
|
|
30
|
+
_active_decorator = "opentelemetry"
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def register_decorator(cls, name, decorator_factory):
|
|
34
|
+
"""Register a decorator factory function with a name."""
|
|
35
|
+
cls._decorators[name] = decorator_factory
|
|
36
|
+
cls._active_decorator = name
|
|
37
|
+
return cls
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def get_decorator(cls):
|
|
41
|
+
"""Get the currently active decorator factory."""
|
|
42
|
+
return cls._decorators.get(cls._active_decorator)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _opentelemetry_traced():
|
|
46
|
+
def decorator(func):
|
|
47
|
+
@wraps(func)
|
|
48
|
+
def sync_wrapper(*args, **kwargs):
|
|
49
|
+
with tracer.start_as_current_span(func.__name__) as span:
|
|
50
|
+
span.set_attribute("span_type", "function_call_sync")
|
|
51
|
+
span.set_attribute(
|
|
52
|
+
"inputs",
|
|
53
|
+
_SpanUtils.format_args_for_trace_json(
|
|
54
|
+
inspect.signature(func), *args, **kwargs
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
try:
|
|
58
|
+
result = func(*args, **kwargs)
|
|
59
|
+
span.set_attribute(
|
|
60
|
+
"output", json.dumps(result, default=str)
|
|
61
|
+
) # Record output
|
|
62
|
+
return result
|
|
63
|
+
except Exception as e:
|
|
64
|
+
span.record_exception(e)
|
|
65
|
+
span.set_status(
|
|
66
|
+
trace.status.Status(trace.status.StatusCode.ERROR, str(e))
|
|
67
|
+
)
|
|
68
|
+
raise
|
|
69
|
+
|
|
70
|
+
@wraps(func)
|
|
71
|
+
async def async_wrapper(*args, **kwargs):
|
|
72
|
+
with tracer.start_as_current_span(func.__name__) as span:
|
|
73
|
+
span.set_attribute("span_type", "function_call_async")
|
|
74
|
+
span.set_attribute(
|
|
75
|
+
"inputs",
|
|
76
|
+
_SpanUtils.format_args_for_trace_json(
|
|
77
|
+
inspect.signature(func), *args, **kwargs
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
try:
|
|
81
|
+
result = await func(*args, **kwargs)
|
|
82
|
+
span.set_attribute(
|
|
83
|
+
"output", json.dumps(result, default=str)
|
|
84
|
+
) # Record output
|
|
85
|
+
return result
|
|
86
|
+
except Exception as e:
|
|
87
|
+
span.record_exception(e)
|
|
88
|
+
span.set_status(
|
|
89
|
+
trace.status.Status(trace.status.StatusCode.ERROR, str(e))
|
|
90
|
+
)
|
|
91
|
+
raise
|
|
92
|
+
|
|
93
|
+
@wraps(func)
|
|
94
|
+
def generator_wrapper(*args, **kwargs):
|
|
95
|
+
with tracer.start_as_current_span(func.__name__) as span:
|
|
96
|
+
span.set_attribute("span_type", "function_call_generator_sync")
|
|
97
|
+
span.set_attribute(
|
|
98
|
+
"inputs",
|
|
99
|
+
_SpanUtils.format_args_for_trace_json(
|
|
100
|
+
inspect.signature(func), *args, **kwargs
|
|
101
|
+
),
|
|
102
|
+
)
|
|
103
|
+
outputs = []
|
|
104
|
+
try:
|
|
105
|
+
for item in func(*args, **kwargs):
|
|
106
|
+
outputs.append(item)
|
|
107
|
+
span.add_event(f"Yielded: {item}") # Add event for each yield
|
|
108
|
+
yield item
|
|
109
|
+
span.set_attribute(
|
|
110
|
+
"output", json.dumps(outputs, default=str)
|
|
111
|
+
) # Record aggregated outputs
|
|
112
|
+
except Exception as e:
|
|
113
|
+
span.record_exception(e)
|
|
114
|
+
span.set_status(
|
|
115
|
+
trace.status.Status(trace.status.StatusCode.ERROR, str(e))
|
|
116
|
+
)
|
|
117
|
+
raise
|
|
118
|
+
|
|
119
|
+
@wraps(func)
|
|
120
|
+
async def async_generator_wrapper(*args, **kwargs):
|
|
121
|
+
with tracer.start_as_current_span(func.__name__) as span:
|
|
122
|
+
span.set_attribute("span_type", "function_call_generator_async")
|
|
123
|
+
span.set_attribute(
|
|
124
|
+
"inputs",
|
|
125
|
+
_SpanUtils.format_args_for_trace_json(
|
|
126
|
+
inspect.signature(func), *args, **kwargs
|
|
127
|
+
),
|
|
128
|
+
)
|
|
129
|
+
outputs = []
|
|
130
|
+
try:
|
|
131
|
+
async for item in func(*args, **kwargs):
|
|
132
|
+
outputs.append(item)
|
|
133
|
+
span.add_event(f"Yielded: {item}") # Add event for each yield
|
|
134
|
+
yield item
|
|
135
|
+
span.set_attribute(
|
|
136
|
+
"output", json.dumps(outputs, default=str)
|
|
137
|
+
) # Record aggregated outputs
|
|
138
|
+
except Exception as e:
|
|
139
|
+
span.record_exception(e)
|
|
140
|
+
span.set_status(
|
|
141
|
+
trace.status.Status(trace.status.StatusCode.ERROR, str(e))
|
|
142
|
+
)
|
|
143
|
+
raise
|
|
144
|
+
|
|
145
|
+
if inspect.iscoroutinefunction(func):
|
|
146
|
+
return async_wrapper
|
|
147
|
+
elif inspect.isgeneratorfunction(func):
|
|
148
|
+
return generator_wrapper
|
|
149
|
+
elif inspect.isasyncgenfunction(func):
|
|
150
|
+
return async_generator_wrapper
|
|
151
|
+
else:
|
|
152
|
+
return sync_wrapper
|
|
153
|
+
|
|
154
|
+
return decorator
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def traced():
|
|
158
|
+
"""Decorator that will trace function invocations."""
|
|
159
|
+
decorator_factory = TracedDecoratorRegistry.get_decorator()
|
|
160
|
+
|
|
161
|
+
if decorator_factory:
|
|
162
|
+
return decorator_factory()
|
|
163
|
+
else:
|
|
164
|
+
# Fallback to original implementation if no active decorator
|
|
165
|
+
return _opentelemetry_traced()
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import random
|
|
6
|
+
import uuid
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from os import environ as env
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
from opentelemetry.sdk.trace import ReadableSpan
|
|
13
|
+
from opentelemetry.trace import StatusCode
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class UiPathSpan:
|
|
20
|
+
"""Represents a span in the UiPath tracing system."""
|
|
21
|
+
|
|
22
|
+
id: uuid.UUID
|
|
23
|
+
trace_id: uuid.UUID
|
|
24
|
+
name: str
|
|
25
|
+
attributes: str
|
|
26
|
+
parent_id: Optional[uuid.UUID] = None
|
|
27
|
+
start_time: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
28
|
+
end_time: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
29
|
+
status: int = 1
|
|
30
|
+
created_at: str = field(default_factory=lambda: datetime.now().isoformat() + "Z")
|
|
31
|
+
updated_at: str = field(default_factory=lambda: datetime.now().isoformat() + "Z")
|
|
32
|
+
organization_id: Optional[str] = field(
|
|
33
|
+
default_factory=lambda: env.get("UIPATH_ORGANIZATION_ID", "")
|
|
34
|
+
)
|
|
35
|
+
tenant_id: Optional[str] = field(
|
|
36
|
+
default_factory=lambda: env.get("UIPATH_TENANT_ID", "")
|
|
37
|
+
)
|
|
38
|
+
expiry_time_utc: Optional[str] = None
|
|
39
|
+
folder_key: Optional[str] = field(
|
|
40
|
+
default_factory=lambda: env.get("UIPATH_FOLDER_KEY_XYZ", "")
|
|
41
|
+
)
|
|
42
|
+
source: Optional[str] = None
|
|
43
|
+
span_type: str = "Coded Agents"
|
|
44
|
+
process_key: Optional[str] = field(
|
|
45
|
+
default_factory=lambda: env.get("UIPATH_PROCESS_UUID")
|
|
46
|
+
)
|
|
47
|
+
job_key: Optional[str] = field(default_factory=lambda: env.get("UIPATH_JOB_KEY"))
|
|
48
|
+
|
|
49
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
50
|
+
"""Convert the Span to a dictionary suitable for JSON serialization."""
|
|
51
|
+
return {
|
|
52
|
+
"Id": str(self.id),
|
|
53
|
+
"TraceId": str(self.trace_id),
|
|
54
|
+
"ParentId": str(self.parent_id) if self.parent_id else None,
|
|
55
|
+
"Name": self.name,
|
|
56
|
+
"StartTime": self.start_time,
|
|
57
|
+
"EndTime": self.end_time,
|
|
58
|
+
"Attributes": self.attributes,
|
|
59
|
+
"Status": self.status,
|
|
60
|
+
"CreatedAt": self.created_at,
|
|
61
|
+
"UpdatedAt": self.updated_at,
|
|
62
|
+
"OrganizationId": self.organization_id,
|
|
63
|
+
"TenantId": self.tenant_id,
|
|
64
|
+
"ExpiryTimeUtc": self.expiry_time_utc,
|
|
65
|
+
"FolderKey": self.folder_key,
|
|
66
|
+
"Source": self.source,
|
|
67
|
+
"SpanType": self.span_type,
|
|
68
|
+
"ProcessKey": self.process_key,
|
|
69
|
+
"JobKey": self.job_key,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class _SpanUtils:
|
|
74
|
+
@staticmethod
|
|
75
|
+
def span_id_to_uuid4(span_id: int) -> uuid.UUID:
|
|
76
|
+
"""Convert a 64-bit span ID to a valid UUID4 format.
|
|
77
|
+
|
|
78
|
+
Creates a UUID where:
|
|
79
|
+
- The 64 least significant bits contain the span ID
|
|
80
|
+
- The UUID version (bits 48-51) is set to 4
|
|
81
|
+
- The UUID variant (bits 64-65) is set to binary 10
|
|
82
|
+
"""
|
|
83
|
+
# Generate deterministic high bits using the span_id as seed
|
|
84
|
+
temp_random = random.Random(span_id)
|
|
85
|
+
high_bits = temp_random.getrandbits(64)
|
|
86
|
+
|
|
87
|
+
# Combine high bits and span ID into a 128-bit integer
|
|
88
|
+
combined = (high_bits << 64) | span_id
|
|
89
|
+
|
|
90
|
+
# Set version to 4 (UUID4)
|
|
91
|
+
combined = (combined & ~(0xF << 76)) | (0x4 << 76)
|
|
92
|
+
|
|
93
|
+
# Set variant to binary 10
|
|
94
|
+
combined = (combined & ~(0x3 << 62)) | (2 << 62)
|
|
95
|
+
|
|
96
|
+
# Convert to hex string in UUID format
|
|
97
|
+
hex_str = format(combined, "032x")
|
|
98
|
+
return uuid.UUID(hex_str)
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def trace_id_to_uuid4(trace_id: int) -> uuid.UUID:
|
|
102
|
+
"""Convert a 128-bit trace ID to a valid UUID4 format.
|
|
103
|
+
|
|
104
|
+
Modifies the trace ID to conform to UUID4 requirements:
|
|
105
|
+
- The UUID version (bits 48-51) is set to 4
|
|
106
|
+
- The UUID variant (bits 64-65) is set to binary 10
|
|
107
|
+
"""
|
|
108
|
+
# Set version to 4 (UUID4)
|
|
109
|
+
uuid_int = (trace_id & ~(0xF << 76)) | (0x4 << 76)
|
|
110
|
+
|
|
111
|
+
# Set variant to binary 10
|
|
112
|
+
uuid_int = (uuid_int & ~(0x3 << 62)) | (2 << 62)
|
|
113
|
+
|
|
114
|
+
# Convert to hex string in UUID format
|
|
115
|
+
hex_str = format(uuid_int, "032x")
|
|
116
|
+
return uuid.UUID(hex_str)
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def otel_span_to_uipath_span(otel_span: ReadableSpan) -> UiPathSpan:
|
|
120
|
+
"""Convert an OpenTelemetry span to a UiPathSpan."""
|
|
121
|
+
# Extract the context information from the OTel span
|
|
122
|
+
span_context = otel_span.get_span_context()
|
|
123
|
+
|
|
124
|
+
# OTel uses hexadecimal strings, we need to convert to UUID
|
|
125
|
+
trace_id = _SpanUtils.trace_id_to_uuid4(span_context.trace_id)
|
|
126
|
+
span_id = _SpanUtils.span_id_to_uuid4(span_context.span_id)
|
|
127
|
+
|
|
128
|
+
trace_id_str = os.environ.get("UIPATH_TRACE_ID")
|
|
129
|
+
if trace_id_str:
|
|
130
|
+
trace_id = uuid.UUID(trace_id_str)
|
|
131
|
+
|
|
132
|
+
# Get parent span ID if it exists
|
|
133
|
+
parent_id = None
|
|
134
|
+
if otel_span.parent is not None:
|
|
135
|
+
parent_id = _SpanUtils.span_id_to_uuid4(otel_span.parent.span_id)
|
|
136
|
+
|
|
137
|
+
# Map status
|
|
138
|
+
status = 1 # Default to OK
|
|
139
|
+
if otel_span.status.status_code == StatusCode.ERROR:
|
|
140
|
+
status = 2 # Error
|
|
141
|
+
|
|
142
|
+
# Convert attributes to a format compatible with UiPathSpan
|
|
143
|
+
attributes_dict: dict[str, Any] = (
|
|
144
|
+
dict(otel_span.attributes) if otel_span.attributes else {}
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
original_inputs = attributes_dict.get("inputs", None)
|
|
148
|
+
original_outputs = attributes_dict.get("outputs", None)
|
|
149
|
+
|
|
150
|
+
if original_inputs:
|
|
151
|
+
try:
|
|
152
|
+
if isinstance(original_inputs, str):
|
|
153
|
+
json_inputs = json.loads(original_inputs)
|
|
154
|
+
attributes_dict["inputs"] = json_inputs
|
|
155
|
+
else:
|
|
156
|
+
attributes_dict["inputs"] = original_inputs
|
|
157
|
+
except Exception as e:
|
|
158
|
+
print(f"Error parsing inputs: {e}")
|
|
159
|
+
attributes_dict["inputs"] = str(original_inputs)
|
|
160
|
+
|
|
161
|
+
if original_outputs:
|
|
162
|
+
try:
|
|
163
|
+
if isinstance(original_outputs, str):
|
|
164
|
+
json_outputs = json.loads(original_outputs)
|
|
165
|
+
attributes_dict["outputs"] = json_outputs
|
|
166
|
+
else:
|
|
167
|
+
attributes_dict["outputs"] = original_outputs
|
|
168
|
+
except Exception as e:
|
|
169
|
+
print(f"Error parsing outputs: {e}")
|
|
170
|
+
attributes_dict["outputs"] = str(original_outputs)
|
|
171
|
+
|
|
172
|
+
# Add events as additional attributes if they exist
|
|
173
|
+
if otel_span.events:
|
|
174
|
+
events_list = [
|
|
175
|
+
{
|
|
176
|
+
"name": event.name,
|
|
177
|
+
"timestamp": event.timestamp,
|
|
178
|
+
"attributes": dict(event.attributes) if event.attributes else {},
|
|
179
|
+
}
|
|
180
|
+
for event in otel_span.events
|
|
181
|
+
]
|
|
182
|
+
attributes_dict["events"] = events_list
|
|
183
|
+
|
|
184
|
+
# Add links as additional attributes if they exist
|
|
185
|
+
if hasattr(otel_span, "links") and otel_span.links:
|
|
186
|
+
links_list = [
|
|
187
|
+
{
|
|
188
|
+
"trace_id": link.context.trace_id,
|
|
189
|
+
"span_id": link.context.span_id,
|
|
190
|
+
"attributes": dict(link.attributes) if link.attributes else {},
|
|
191
|
+
}
|
|
192
|
+
for link in otel_span.links
|
|
193
|
+
]
|
|
194
|
+
attributes_dict["links"] = links_list
|
|
195
|
+
|
|
196
|
+
span_type_value = attributes_dict.get("span_type", "OpenTelemetry")
|
|
197
|
+
span_type = str(span_type_value)
|
|
198
|
+
|
|
199
|
+
# Create UiPathSpan from OpenTelemetry span
|
|
200
|
+
start_time = datetime.fromtimestamp(
|
|
201
|
+
(otel_span.start_time or 0) / 1e9
|
|
202
|
+
).isoformat()
|
|
203
|
+
|
|
204
|
+
end_time_str = None
|
|
205
|
+
if otel_span.end_time is not None:
|
|
206
|
+
end_time_str = datetime.fromtimestamp(
|
|
207
|
+
(otel_span.end_time or 0) / 1e9
|
|
208
|
+
).isoformat()
|
|
209
|
+
else:
|
|
210
|
+
end_time_str = datetime.now().isoformat()
|
|
211
|
+
|
|
212
|
+
return UiPathSpan(
|
|
213
|
+
id=span_id,
|
|
214
|
+
trace_id=trace_id,
|
|
215
|
+
parent_id=parent_id,
|
|
216
|
+
name=otel_span.name,
|
|
217
|
+
attributes=json.dumps(attributes_dict),
|
|
218
|
+
start_time=start_time,
|
|
219
|
+
end_time=end_time_str,
|
|
220
|
+
status=status,
|
|
221
|
+
span_type=span_type,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
@staticmethod
|
|
225
|
+
def format_args_for_trace_json(
|
|
226
|
+
signature: inspect.Signature, *args: Any, **kwargs: Any
|
|
227
|
+
) -> str:
|
|
228
|
+
"""Return a JSON string of inputs from the function signature."""
|
|
229
|
+
result = _SpanUtils.format_args_for_trace(signature, *args, **kwargs)
|
|
230
|
+
return json.dumps(result, default=str)
|
|
231
|
+
|
|
232
|
+
@staticmethod
|
|
233
|
+
def format_args_for_trace(
|
|
234
|
+
signature: inspect.Signature, *args: Any, **kwargs: Any
|
|
235
|
+
) -> Dict[str, Any]:
|
|
236
|
+
try:
|
|
237
|
+
"""Return a dictionary of inputs from the function signature."""
|
|
238
|
+
# Create a parameter mapping by partially binding the arguments
|
|
239
|
+
|
|
240
|
+
parameter_binding = signature.bind_partial(*args, **kwargs)
|
|
241
|
+
|
|
242
|
+
# Fill in default values for any unspecified parameters
|
|
243
|
+
parameter_binding.apply_defaults()
|
|
244
|
+
|
|
245
|
+
# Extract the input parameters, skipping special Python parameters
|
|
246
|
+
result = {}
|
|
247
|
+
for name, value in parameter_binding.arguments.items():
|
|
248
|
+
# Skip class and instance references
|
|
249
|
+
if name in ("self", "cls"):
|
|
250
|
+
continue
|
|
251
|
+
|
|
252
|
+
# Handle **kwargs parameters specially
|
|
253
|
+
param_info = signature.parameters.get(name)
|
|
254
|
+
if param_info and param_info.kind == inspect.Parameter.VAR_KEYWORD:
|
|
255
|
+
# Flatten nested kwargs directly into the result
|
|
256
|
+
if isinstance(value, dict):
|
|
257
|
+
result.update(value)
|
|
258
|
+
else:
|
|
259
|
+
# Regular parameter
|
|
260
|
+
result[name] = value
|
|
261
|
+
|
|
262
|
+
return result
|
|
263
|
+
except Exception as e:
|
|
264
|
+
logger.warning(
|
|
265
|
+
f"Error formatting arguments for trace: {e}. Using args and kwargs directly."
|
|
266
|
+
)
|
|
267
|
+
return {"args": args, "kwargs": kwargs}
|