uipath-core 0.0.2__py3-none-any.whl
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.
- uipath/core/__init__.py +9 -0
- uipath/core/py.typed +0 -0
- uipath/core/tracing/__init__.py +15 -0
- uipath/core/tracing/_utils.py +122 -0
- uipath/core/tracing/context.py +23 -0
- uipath/core/tracing/decorators.py +282 -0
- uipath/core/tracing/manager.py +314 -0
- uipath_core-0.0.2.dist-info/METADATA +24 -0
- uipath_core-0.0.2.dist-info/RECORD +11 -0
- uipath_core-0.0.2.dist-info/WHEEL +4 -0
- uipath_core-0.0.2.dist-info/licenses/LICENSE +9 -0
uipath/core/__init__.py
ADDED
uipath/core/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""OpenTelemetry tracing module with UiPath integration.
|
|
2
|
+
|
|
3
|
+
This module provides decorators and utilities for instrumenting Python functions
|
|
4
|
+
with OpenTelemetry tracing, including custom processors for UiPath execution tracking.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from uipath.core.tracing.context import UiPathTraceContext
|
|
8
|
+
from uipath.core.tracing.decorators import traced
|
|
9
|
+
from uipath.core.tracing.manager import UiPathTracingManager
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"traced",
|
|
13
|
+
"UiPathTraceContext",
|
|
14
|
+
"UiPathTracingManager",
|
|
15
|
+
]
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Helper utilities for the tracing module."""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import json
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import asdict, is_dataclass
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Any, Dict, Mapping
|
|
10
|
+
from zoneinfo import ZoneInfo
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_supported_params(
|
|
16
|
+
tracer_impl: Callable[..., Any],
|
|
17
|
+
params: Mapping[str, Any],
|
|
18
|
+
) -> Dict[str, Any]:
|
|
19
|
+
"""Extract the parameters supported by the tracer implementation."""
|
|
20
|
+
try:
|
|
21
|
+
sig = inspect.signature(tracer_impl)
|
|
22
|
+
except (TypeError, ValueError):
|
|
23
|
+
# If we can't inspect, pass all parameters and let the function handle it
|
|
24
|
+
return dict(params)
|
|
25
|
+
|
|
26
|
+
supported: Dict[str, Any] = {}
|
|
27
|
+
for name, value in params.items():
|
|
28
|
+
if value is not None and name in sig.parameters:
|
|
29
|
+
supported[name] = value
|
|
30
|
+
return supported
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _simple_serialize_defaults(obj):
|
|
34
|
+
# Handle Pydantic BaseModel instances
|
|
35
|
+
if hasattr(obj, "model_dump") and not isinstance(obj, type):
|
|
36
|
+
return obj.model_dump(exclude_none=True, mode="json")
|
|
37
|
+
|
|
38
|
+
# Handle classes - convert to schema representation
|
|
39
|
+
if isinstance(obj, type) and issubclass(obj, BaseModel):
|
|
40
|
+
return {
|
|
41
|
+
"__class__": obj.__name__,
|
|
42
|
+
"__module__": obj.__module__,
|
|
43
|
+
"schema": obj.model_json_schema(),
|
|
44
|
+
}
|
|
45
|
+
if hasattr(obj, "dict") and not isinstance(obj, type):
|
|
46
|
+
return obj.dict()
|
|
47
|
+
if hasattr(obj, "to_dict") and not isinstance(obj, type):
|
|
48
|
+
return obj.to_dict()
|
|
49
|
+
|
|
50
|
+
# Handle dataclasses
|
|
51
|
+
if is_dataclass(obj) and not isinstance(obj, type):
|
|
52
|
+
return asdict(obj)
|
|
53
|
+
|
|
54
|
+
# Handle enums
|
|
55
|
+
if isinstance(obj, Enum):
|
|
56
|
+
return _simple_serialize_defaults(obj.value)
|
|
57
|
+
|
|
58
|
+
if isinstance(obj, (set, tuple)):
|
|
59
|
+
if hasattr(obj, "_asdict") and callable(obj._asdict):
|
|
60
|
+
return obj._asdict()
|
|
61
|
+
return list(obj)
|
|
62
|
+
|
|
63
|
+
if isinstance(obj, datetime):
|
|
64
|
+
return obj.isoformat()
|
|
65
|
+
|
|
66
|
+
if isinstance(obj, (timezone, ZoneInfo)):
|
|
67
|
+
return obj.tzname(None)
|
|
68
|
+
|
|
69
|
+
# Allow JSON-serializable primitives to pass through unchanged
|
|
70
|
+
if obj is None or isinstance(obj, (bool, int, float, str)):
|
|
71
|
+
return obj
|
|
72
|
+
|
|
73
|
+
return str(obj)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def format_args_for_trace_json(
|
|
77
|
+
signature: inspect.Signature, *args: Any, **kwargs: Any
|
|
78
|
+
) -> str:
|
|
79
|
+
"""Return a JSON string of inputs from the function signature."""
|
|
80
|
+
result = format_args_for_trace(signature, *args, **kwargs)
|
|
81
|
+
return json.dumps(result, default=_simple_serialize_defaults)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def format_object_for_trace_json(
|
|
85
|
+
input_object: Any,
|
|
86
|
+
) -> str:
|
|
87
|
+
"""Return a JSON string of inputs from the function signature."""
|
|
88
|
+
return json.dumps(input_object, default=_simple_serialize_defaults)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def format_args_for_trace(
|
|
92
|
+
signature: inspect.Signature, *args: Any, **kwargs: Any
|
|
93
|
+
) -> Dict[str, Any]:
|
|
94
|
+
try:
|
|
95
|
+
"""Return a dictionary of inputs from the function signature."""
|
|
96
|
+
# Create a parameter mapping by partially binding the arguments
|
|
97
|
+
|
|
98
|
+
parameter_binding = signature.bind_partial(*args, **kwargs)
|
|
99
|
+
|
|
100
|
+
# Fill in default values for any unspecified parameters
|
|
101
|
+
parameter_binding.apply_defaults()
|
|
102
|
+
|
|
103
|
+
# Extract the input parameters, skipping special Python parameters
|
|
104
|
+
result = {}
|
|
105
|
+
for name, value in parameter_binding.arguments.items():
|
|
106
|
+
# Skip class and instance references
|
|
107
|
+
if name in ("self", "cls"):
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
# Handle **kwargs parameters specially
|
|
111
|
+
param_info = signature.parameters.get(name)
|
|
112
|
+
if param_info and param_info.kind == inspect.Parameter.VAR_KEYWORD:
|
|
113
|
+
# Flatten nested kwargs directly into the result
|
|
114
|
+
if isinstance(value, dict):
|
|
115
|
+
result.update(value)
|
|
116
|
+
else:
|
|
117
|
+
# Regular parameter
|
|
118
|
+
result[name] = value
|
|
119
|
+
|
|
120
|
+
return result
|
|
121
|
+
except Exception:
|
|
122
|
+
return {"args": args, "kwargs": kwargs}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Trace context information for tracing and debugging."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Union
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class UiPathTraceContext(BaseModel):
|
|
9
|
+
"""Trace context information for tracing and debugging."""
|
|
10
|
+
|
|
11
|
+
trace_id: Optional[str] = None
|
|
12
|
+
parent_span_id: Optional[str] = None
|
|
13
|
+
root_span_id: Optional[str] = None
|
|
14
|
+
org_id: Optional[str] = None
|
|
15
|
+
tenant_id: Optional[str] = None
|
|
16
|
+
job_id: Optional[str] = None
|
|
17
|
+
folder_key: Optional[str] = None
|
|
18
|
+
process_key: Optional[str] = None
|
|
19
|
+
enabled: Union[bool, str] = False
|
|
20
|
+
reference_id: Optional[str] = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
__all__ = ["UiPathTraceContext"]
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""Tracing decorators for function instrumentation."""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import random
|
|
7
|
+
from functools import wraps
|
|
8
|
+
from typing import Any, Callable, Optional
|
|
9
|
+
|
|
10
|
+
from opentelemetry import trace
|
|
11
|
+
from opentelemetry.trace import NonRecordingSpan, SpanContext, TraceFlags
|
|
12
|
+
from opentelemetry.trace.status import StatusCode
|
|
13
|
+
|
|
14
|
+
from uipath.core.tracing._utils import (
|
|
15
|
+
format_args_for_trace_json,
|
|
16
|
+
format_object_for_trace_json,
|
|
17
|
+
get_supported_params,
|
|
18
|
+
)
|
|
19
|
+
from uipath.core.tracing.manager import UiPathTracingManager
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _opentelemetry_traced(
|
|
25
|
+
name: Optional[str] = None,
|
|
26
|
+
run_type: Optional[str] = None,
|
|
27
|
+
span_type: Optional[str] = None,
|
|
28
|
+
input_processor: Optional[Callable[..., Any]] = None,
|
|
29
|
+
output_processor: Optional[Callable[..., Any]] = None,
|
|
30
|
+
recording: bool = True,
|
|
31
|
+
):
|
|
32
|
+
"""Default tracer implementation using OpenTelemetry."""
|
|
33
|
+
|
|
34
|
+
def decorator(func):
|
|
35
|
+
trace_name = name or func.__name__
|
|
36
|
+
|
|
37
|
+
def get_span():
|
|
38
|
+
if not recording:
|
|
39
|
+
# Create a valid but non-sampled trace context
|
|
40
|
+
# Generate a valid trace ID (not INVALID)
|
|
41
|
+
trace_id = random.getrandbits(128)
|
|
42
|
+
span_id = random.getrandbits(64)
|
|
43
|
+
|
|
44
|
+
non_sampled_context = SpanContext(
|
|
45
|
+
trace_id=trace_id,
|
|
46
|
+
span_id=span_id,
|
|
47
|
+
is_remote=False,
|
|
48
|
+
trace_flags=TraceFlags(0x00), # NOT sampled
|
|
49
|
+
)
|
|
50
|
+
non_recording = NonRecordingSpan(non_sampled_context)
|
|
51
|
+
|
|
52
|
+
# Make it active so children see it
|
|
53
|
+
span_cm = trace.use_span(non_recording)
|
|
54
|
+
span_cm.__enter__()
|
|
55
|
+
return span_cm, non_recording
|
|
56
|
+
|
|
57
|
+
# Normal recording span
|
|
58
|
+
ctx = UiPathTracingManager.get_parent_context()
|
|
59
|
+
span_cm = trace.get_tracer(__name__).start_as_current_span(
|
|
60
|
+
trace_name, context=ctx
|
|
61
|
+
)
|
|
62
|
+
span = span_cm.__enter__()
|
|
63
|
+
return span_cm, span
|
|
64
|
+
|
|
65
|
+
# --------- Sync wrapper ---------
|
|
66
|
+
@wraps(func)
|
|
67
|
+
def sync_wrapper(*args, **kwargs):
|
|
68
|
+
span_cm, span = get_span()
|
|
69
|
+
try:
|
|
70
|
+
span.set_attribute("span_type", span_type or "function_call_sync")
|
|
71
|
+
if run_type is not None:
|
|
72
|
+
span.set_attribute("run_type", run_type)
|
|
73
|
+
|
|
74
|
+
inputs = format_args_for_trace_json(
|
|
75
|
+
inspect.signature(func), *args, **kwargs
|
|
76
|
+
)
|
|
77
|
+
if input_processor:
|
|
78
|
+
processed_inputs = input_processor(json.loads(inputs))
|
|
79
|
+
inputs = json.dumps(processed_inputs, default=str)
|
|
80
|
+
|
|
81
|
+
span.set_attribute("input.mime_type", "application/json")
|
|
82
|
+
span.set_attribute("input.value", inputs)
|
|
83
|
+
|
|
84
|
+
result = func(*args, **kwargs)
|
|
85
|
+
output = output_processor(result) if output_processor else result
|
|
86
|
+
|
|
87
|
+
span.set_attribute("output.value", format_object_for_trace_json(output))
|
|
88
|
+
span.set_attribute("output.mime_type", "application/json")
|
|
89
|
+
return result
|
|
90
|
+
except Exception as e:
|
|
91
|
+
span.record_exception(e)
|
|
92
|
+
span.set_status(StatusCode.ERROR, str(e))
|
|
93
|
+
raise
|
|
94
|
+
finally:
|
|
95
|
+
if span_cm:
|
|
96
|
+
span_cm.__exit__(None, None, None)
|
|
97
|
+
|
|
98
|
+
# --------- Async wrapper ---------
|
|
99
|
+
@wraps(func)
|
|
100
|
+
async def async_wrapper(*args, **kwargs):
|
|
101
|
+
span_cm, span = get_span()
|
|
102
|
+
try:
|
|
103
|
+
span.set_attribute("span_type", span_type or "function_call_async")
|
|
104
|
+
if run_type is not None:
|
|
105
|
+
span.set_attribute("run_type", run_type)
|
|
106
|
+
|
|
107
|
+
inputs = format_args_for_trace_json(
|
|
108
|
+
inspect.signature(func), *args, **kwargs
|
|
109
|
+
)
|
|
110
|
+
if input_processor:
|
|
111
|
+
processed_inputs = input_processor(json.loads(inputs))
|
|
112
|
+
inputs = json.dumps(processed_inputs, default=str)
|
|
113
|
+
|
|
114
|
+
span.set_attribute("input.mime_type", "application/json")
|
|
115
|
+
span.set_attribute("input.value", inputs)
|
|
116
|
+
|
|
117
|
+
result = await func(*args, **kwargs)
|
|
118
|
+
output = output_processor(result) if output_processor else result
|
|
119
|
+
|
|
120
|
+
span.set_attribute("output.value", format_object_for_trace_json(output))
|
|
121
|
+
span.set_attribute("output.mime_type", "application/json")
|
|
122
|
+
return result
|
|
123
|
+
except Exception as e:
|
|
124
|
+
span.record_exception(e)
|
|
125
|
+
span.set_status(StatusCode.ERROR, str(e))
|
|
126
|
+
raise
|
|
127
|
+
finally:
|
|
128
|
+
if span_cm:
|
|
129
|
+
span_cm.__exit__(None, None, None)
|
|
130
|
+
|
|
131
|
+
# --------- Generator wrapper ---------
|
|
132
|
+
@wraps(func)
|
|
133
|
+
def generator_wrapper(*args, **kwargs):
|
|
134
|
+
span_cm, span = get_span()
|
|
135
|
+
try:
|
|
136
|
+
span.set_attribute(
|
|
137
|
+
"span_type", span_type or "function_call_generator_sync"
|
|
138
|
+
)
|
|
139
|
+
if run_type is not None:
|
|
140
|
+
span.set_attribute("run_type", run_type)
|
|
141
|
+
|
|
142
|
+
inputs = format_args_for_trace_json(
|
|
143
|
+
inspect.signature(func), *args, **kwargs
|
|
144
|
+
)
|
|
145
|
+
if input_processor:
|
|
146
|
+
processed_inputs = input_processor(json.loads(inputs))
|
|
147
|
+
inputs = json.dumps(processed_inputs, default=str)
|
|
148
|
+
|
|
149
|
+
span.set_attribute("input.mime_type", "application/json")
|
|
150
|
+
span.set_attribute("input.value", inputs)
|
|
151
|
+
|
|
152
|
+
outputs = []
|
|
153
|
+
for item in func(*args, **kwargs):
|
|
154
|
+
outputs.append(item)
|
|
155
|
+
span.add_event(f"Yielded: {item}")
|
|
156
|
+
yield item
|
|
157
|
+
|
|
158
|
+
output = output_processor(outputs) if output_processor else outputs
|
|
159
|
+
span.set_attribute("output.value", format_object_for_trace_json(output))
|
|
160
|
+
span.set_attribute("output.mime_type", "application/json")
|
|
161
|
+
except Exception as e:
|
|
162
|
+
span.record_exception(e)
|
|
163
|
+
span.set_status(StatusCode.ERROR, str(e))
|
|
164
|
+
raise
|
|
165
|
+
finally:
|
|
166
|
+
if span_cm:
|
|
167
|
+
span_cm.__exit__(None, None, None)
|
|
168
|
+
|
|
169
|
+
# --------- Async generator wrapper ---------
|
|
170
|
+
@wraps(func)
|
|
171
|
+
async def async_generator_wrapper(*args, **kwargs):
|
|
172
|
+
span_cm, span = get_span()
|
|
173
|
+
try:
|
|
174
|
+
span.set_attribute(
|
|
175
|
+
"span_type", span_type or "function_call_generator_async"
|
|
176
|
+
)
|
|
177
|
+
if run_type is not None:
|
|
178
|
+
span.set_attribute("run_type", run_type)
|
|
179
|
+
|
|
180
|
+
inputs = format_args_for_trace_json(
|
|
181
|
+
inspect.signature(func), *args, **kwargs
|
|
182
|
+
)
|
|
183
|
+
if input_processor:
|
|
184
|
+
processed_inputs = input_processor(json.loads(inputs))
|
|
185
|
+
inputs = json.dumps(processed_inputs, default=str)
|
|
186
|
+
|
|
187
|
+
span.set_attribute("input.mime_type", "application/json")
|
|
188
|
+
span.set_attribute("input.value", inputs)
|
|
189
|
+
|
|
190
|
+
outputs = []
|
|
191
|
+
async for item in func(*args, **kwargs):
|
|
192
|
+
outputs.append(item)
|
|
193
|
+
span.add_event(f"Yielded: {item}")
|
|
194
|
+
yield item
|
|
195
|
+
|
|
196
|
+
output = output_processor(outputs) if output_processor else outputs
|
|
197
|
+
span.set_attribute("output.value", format_object_for_trace_json(output))
|
|
198
|
+
span.set_attribute("output.mime_type", "application/json")
|
|
199
|
+
except Exception as e:
|
|
200
|
+
span.record_exception(e)
|
|
201
|
+
span.set_status(StatusCode.ERROR, str(e))
|
|
202
|
+
raise
|
|
203
|
+
finally:
|
|
204
|
+
if span_cm:
|
|
205
|
+
span_cm.__exit__(None, None, None)
|
|
206
|
+
|
|
207
|
+
if inspect.iscoroutinefunction(func):
|
|
208
|
+
return async_wrapper
|
|
209
|
+
elif inspect.isgeneratorfunction(func):
|
|
210
|
+
return generator_wrapper
|
|
211
|
+
elif inspect.isasyncgenfunction(func):
|
|
212
|
+
return async_generator_wrapper
|
|
213
|
+
else:
|
|
214
|
+
return sync_wrapper
|
|
215
|
+
|
|
216
|
+
return decorator
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def traced(
|
|
220
|
+
name: Optional[str] = None,
|
|
221
|
+
run_type: Optional[str] = None,
|
|
222
|
+
span_type: Optional[str] = None,
|
|
223
|
+
input_processor: Optional[Callable[..., Any]] = None,
|
|
224
|
+
output_processor: Optional[Callable[..., Any]] = None,
|
|
225
|
+
hide_input: bool = False,
|
|
226
|
+
hide_output: bool = False,
|
|
227
|
+
recording: bool = True,
|
|
228
|
+
):
|
|
229
|
+
"""Decorator that will trace function invocations.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
name: Optional custom name for the span
|
|
233
|
+
run_type: Optional string to categorize the run type
|
|
234
|
+
span_type: Optional string to categorize the span type
|
|
235
|
+
input_processor: Optional function to process function inputs before recording
|
|
236
|
+
Should accept a dictionary of inputs and return a processed dictionary
|
|
237
|
+
output_processor: Optional function to process function outputs before recording
|
|
238
|
+
Should accept the function output and return a processed value
|
|
239
|
+
hide_input: If True, don't log any input data
|
|
240
|
+
hide_output: If True, don't log any output data
|
|
241
|
+
recording: If False, current span and all child spans are not captured
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
# Apply default processors selectively based on hide flags
|
|
245
|
+
def _default_input_processor(inputs):
|
|
246
|
+
"""Default input processor that doesn't log any actual input data."""
|
|
247
|
+
return {"redacted": "Input data not logged for privacy/security"}
|
|
248
|
+
|
|
249
|
+
def _default_output_processor(outputs):
|
|
250
|
+
"""Default output processor that doesn't log any actual output data."""
|
|
251
|
+
return {"redacted": "Output data not logged for privacy/security"}
|
|
252
|
+
|
|
253
|
+
if hide_input:
|
|
254
|
+
input_processor = _default_input_processor
|
|
255
|
+
if hide_output:
|
|
256
|
+
output_processor = _default_output_processor
|
|
257
|
+
|
|
258
|
+
# Store the parameters for later reapplication
|
|
259
|
+
params = {
|
|
260
|
+
"name": name,
|
|
261
|
+
"run_type": run_type,
|
|
262
|
+
"span_type": span_type,
|
|
263
|
+
"input_processor": input_processor,
|
|
264
|
+
"output_processor": output_processor,
|
|
265
|
+
"recording": recording,
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
tracer_impl = _opentelemetry_traced
|
|
269
|
+
|
|
270
|
+
def decorator(func):
|
|
271
|
+
# Check which parameters are supported by the tracer_impl
|
|
272
|
+
supported_params = get_supported_params(tracer_impl, params)
|
|
273
|
+
|
|
274
|
+
# Decorate the function with only supported parameters
|
|
275
|
+
decorated_func = tracer_impl(**supported_params)(func)
|
|
276
|
+
|
|
277
|
+
return decorated_func
|
|
278
|
+
|
|
279
|
+
return decorator
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
__all__ = ["traced"]
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""Tracing manager for handling tracer implementations and function registry."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Callable, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
from opentelemetry import context, trace
|
|
7
|
+
from opentelemetry.trace import Span, set_span_in_context
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SpanRegistry:
|
|
13
|
+
"""Registry to track all spans and their parent relationships."""
|
|
14
|
+
|
|
15
|
+
MAX_HIERARCHY_DEPTH = 1000 # Hard limit for hierarchy traversal
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
self._spans: Dict[int, Span] = {} # span_id -> span
|
|
19
|
+
self._parent_map: Dict[int, Optional[int]] = {} # span_id -> parent_id
|
|
20
|
+
|
|
21
|
+
def register_span(self, span: Span) -> None:
|
|
22
|
+
"""Register a span and its parent relationship."""
|
|
23
|
+
span_id = span.get_span_context().span_id
|
|
24
|
+
|
|
25
|
+
parent_id: Optional[int] = None
|
|
26
|
+
|
|
27
|
+
if hasattr(span, "parent") and span.parent is not None:
|
|
28
|
+
parent_id = getattr(span.parent, "span_id", None)
|
|
29
|
+
|
|
30
|
+
self._spans[span_id] = span
|
|
31
|
+
self._parent_map[span_id] = parent_id
|
|
32
|
+
|
|
33
|
+
parent_str = "{:016x}".format(parent_id) if parent_id is not None else "None"
|
|
34
|
+
|
|
35
|
+
logger.debug(
|
|
36
|
+
"SpanRegistry: registered span: %s (id: %016x, parent: %s)",
|
|
37
|
+
getattr(span, "name", "unknown"),
|
|
38
|
+
span_id,
|
|
39
|
+
parent_str,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def get_span(self, span_id: int) -> Optional[Span]:
|
|
43
|
+
"""Get a span by ID."""
|
|
44
|
+
return self._spans.get(span_id)
|
|
45
|
+
|
|
46
|
+
def get_parent_id(self, span_id: int) -> Optional[int]:
|
|
47
|
+
"""Get the parent ID of a span."""
|
|
48
|
+
return self._parent_map.get(span_id)
|
|
49
|
+
|
|
50
|
+
def calculate_depth(self, span_id: int) -> int:
|
|
51
|
+
"""Calculate the depth of a span in the hierarchy.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
The depth of the span, capped at MAX_HIERARCHY_DEPTH.
|
|
55
|
+
"""
|
|
56
|
+
depth = 0
|
|
57
|
+
current_id = span_id
|
|
58
|
+
visited = set()
|
|
59
|
+
|
|
60
|
+
while current_id is not None and current_id not in visited:
|
|
61
|
+
visited.add(current_id)
|
|
62
|
+
parent_id = self._parent_map.get(current_id)
|
|
63
|
+
if parent_id is None:
|
|
64
|
+
break
|
|
65
|
+
depth += 1
|
|
66
|
+
if depth >= self.MAX_HIERARCHY_DEPTH:
|
|
67
|
+
logger.warning(
|
|
68
|
+
"Hit MAX_HIERARCHY_DEPTH (%d) while calculating depth for span %016x",
|
|
69
|
+
self.MAX_HIERARCHY_DEPTH,
|
|
70
|
+
span_id,
|
|
71
|
+
)
|
|
72
|
+
break
|
|
73
|
+
current_id = parent_id
|
|
74
|
+
|
|
75
|
+
return depth
|
|
76
|
+
|
|
77
|
+
def is_ancestor(self, ancestor_id: int, descendant_id: int) -> bool:
|
|
78
|
+
"""Check if ancestor_id is an ancestor of descendant_id.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
True if ancestor_id is an ancestor of descendant_id, False otherwise.
|
|
82
|
+
If MAX_HIERARCHY_DEPTH is reached, returns False.
|
|
83
|
+
"""
|
|
84
|
+
current_id: Optional[int] = descendant_id
|
|
85
|
+
visited = set()
|
|
86
|
+
steps = 0
|
|
87
|
+
|
|
88
|
+
while current_id is not None and current_id not in visited:
|
|
89
|
+
if current_id == ancestor_id:
|
|
90
|
+
return True
|
|
91
|
+
visited.add(current_id)
|
|
92
|
+
current_id = self._parent_map.get(current_id)
|
|
93
|
+
steps += 1
|
|
94
|
+
if steps >= self.MAX_HIERARCHY_DEPTH:
|
|
95
|
+
logger.warning(
|
|
96
|
+
"Hit MAX_HIERARCHY_DEPTH (%d) while checking ancestry between %016x and %016x",
|
|
97
|
+
self.MAX_HIERARCHY_DEPTH,
|
|
98
|
+
ancestor_id,
|
|
99
|
+
descendant_id,
|
|
100
|
+
)
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
def clear(self) -> None:
|
|
106
|
+
"""Clear all registered spans."""
|
|
107
|
+
self._spans.clear()
|
|
108
|
+
self._parent_map.clear()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# Global span registry instance
|
|
112
|
+
_span_registry = SpanRegistry()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class UiPathTracingManager:
|
|
116
|
+
"""Static utility class to manage tracing implementations and decorated functions."""
|
|
117
|
+
|
|
118
|
+
_current_span_provider: Optional[Callable[[], Optional[Span]]] = None
|
|
119
|
+
_current_span_ancestors_provider: Optional[Callable[[], List[Span]]] = None
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def register_current_span_provider(
|
|
123
|
+
current_span_provider: Optional[Callable[[], Optional[Span]]],
|
|
124
|
+
):
|
|
125
|
+
"""Register a custom current span provider function.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
current_span_provider: A function that returns the current span from an external
|
|
129
|
+
tracing framework. If None, no custom span parenting will be used.
|
|
130
|
+
"""
|
|
131
|
+
UiPathTracingManager._current_span_provider = current_span_provider
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def get_parent_context() -> context.Context:
|
|
135
|
+
"""Get the parent context for span creation.
|
|
136
|
+
|
|
137
|
+
This method determines the correct parent context when creating a new traced span.
|
|
138
|
+
It handles scenarios where spans may exist in both OpenTelemetry's context (current_span)
|
|
139
|
+
and in an external tracing system (external_span), such as LangGraph.
|
|
140
|
+
|
|
141
|
+
The algorithm follows this priority:
|
|
142
|
+
|
|
143
|
+
1. **No spans available**: Returns the current OpenTelemetry context (empty context)
|
|
144
|
+
|
|
145
|
+
2. **Only current_span exists**: Returns a context with current_span set as parent
|
|
146
|
+
- This is the standard OpenTelemetry behavior for nested traced functions
|
|
147
|
+
|
|
148
|
+
3. **Only external_span exists**: Returns a context with external_span set as parent
|
|
149
|
+
- This occurs when an external tracing system (like LangGraph) has an active span
|
|
150
|
+
but there's no OTel span in the current call stack
|
|
151
|
+
|
|
152
|
+
4. **Both spans exist**: Calls `_get_bottom_most_span()` to determine which is deeper
|
|
153
|
+
- Uses the SpanRegistry to build parent-child relationships
|
|
154
|
+
- Returns the span that is closer to the "bottom" (leaf) of the trace tree
|
|
155
|
+
- This ensures new spans are always attached to the deepest/most specific parent
|
|
156
|
+
|
|
157
|
+
Context Sources:
|
|
158
|
+
- **current_span**: Retrieved from OpenTelemetry's `trace.get_current_span()`
|
|
159
|
+
- Represents the active OTel span in the current execution context
|
|
160
|
+
- Created by `@traced` decorators or manual span creation
|
|
161
|
+
|
|
162
|
+
- **external_span**: Retrieved from the registered custom span provider
|
|
163
|
+
- Set via `register_current_span_provider()`
|
|
164
|
+
- Typically provided by external frameworks (LangGraph, LangChain, etc.)
|
|
165
|
+
- Allows integration with tracing systems outside of OpenTelemetry
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
context.Context: An OpenTelemetry context containing the appropriate parent span,
|
|
169
|
+
or the current empty context if no spans are available
|
|
170
|
+
|
|
171
|
+
Example:
|
|
172
|
+
```python
|
|
173
|
+
# Called by the @traced decorator when creating a new span:
|
|
174
|
+
ctx = UiPathTracingManager.get_parent_context()
|
|
175
|
+
with tracer.start_as_current_span("my_span", context=ctx) as span:
|
|
176
|
+
# New span will have the correct parent based on the logic above
|
|
177
|
+
pass
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
See Also:
|
|
181
|
+
- `_get_bottom_most_span()`: Logic for choosing between two available spans
|
|
182
|
+
- `register_current_span_provider()`: Register external span provider
|
|
183
|
+
- `get_external_current_span()`: Retrieve span from external provider
|
|
184
|
+
"""
|
|
185
|
+
current_span = trace.get_current_span()
|
|
186
|
+
has_current_span = (
|
|
187
|
+
current_span is not None and current_span.get_span_context().is_valid
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
external_span = UiPathTracingManager.get_external_current_span()
|
|
191
|
+
|
|
192
|
+
# Only one or no spans available
|
|
193
|
+
if not has_current_span:
|
|
194
|
+
return (
|
|
195
|
+
set_span_in_context(external_span)
|
|
196
|
+
if external_span is not None
|
|
197
|
+
else context.get_current()
|
|
198
|
+
)
|
|
199
|
+
if external_span is None:
|
|
200
|
+
return set_span_in_context(current_span)
|
|
201
|
+
|
|
202
|
+
# Both spans exist - find the bottom-most one
|
|
203
|
+
bottom_span = UiPathTracingManager._get_bottom_most_span(
|
|
204
|
+
current_span, external_span
|
|
205
|
+
)
|
|
206
|
+
return set_span_in_context(bottom_span)
|
|
207
|
+
|
|
208
|
+
@staticmethod
|
|
209
|
+
def _get_bottom_most_span(
|
|
210
|
+
current_span: Span,
|
|
211
|
+
external_span: Span,
|
|
212
|
+
) -> Span:
|
|
213
|
+
"""Determine which span is deeper in the ancestor tree.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
current_span: The OTel current span
|
|
217
|
+
external_span: The external span from the provider
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
The span that is deeper (closer to the bottom) in the call hierarchy
|
|
221
|
+
"""
|
|
222
|
+
# Register both spans in the registry
|
|
223
|
+
_span_registry.register_span(current_span)
|
|
224
|
+
_span_registry.register_span(external_span)
|
|
225
|
+
|
|
226
|
+
# Also register external ancestors
|
|
227
|
+
external_ancestors = UiPathTracingManager.get_ancestor_spans() or []
|
|
228
|
+
for ancestor in external_ancestors:
|
|
229
|
+
_span_registry.register_span(ancestor)
|
|
230
|
+
|
|
231
|
+
current_span_id = current_span.get_span_context().span_id
|
|
232
|
+
external_span_id = external_span.get_span_context().span_id
|
|
233
|
+
|
|
234
|
+
# Check if one span is an ancestor of the other
|
|
235
|
+
if _span_registry.is_ancestor(external_span_id, current_span_id):
|
|
236
|
+
logger.debug(
|
|
237
|
+
"Traced Context: current_span is a descendant of external_span -> returning current_span (deeper)"
|
|
238
|
+
)
|
|
239
|
+
return current_span
|
|
240
|
+
elif _span_registry.is_ancestor(current_span_id, external_span_id):
|
|
241
|
+
logger.debug(
|
|
242
|
+
"Traced Context: external_span is a descendant of current_span -> returning external_span (deeper)"
|
|
243
|
+
)
|
|
244
|
+
return external_span
|
|
245
|
+
|
|
246
|
+
# Neither is an ancestor of the other - they're in different branches
|
|
247
|
+
# Use depth as tiebreaker
|
|
248
|
+
current_depth = _span_registry.calculate_depth(current_span_id)
|
|
249
|
+
external_depth = _span_registry.calculate_depth(external_span_id)
|
|
250
|
+
|
|
251
|
+
if current_depth > external_depth:
|
|
252
|
+
logger.debug(
|
|
253
|
+
"Traced Context: Different branches, current_span is deeper (depth %d > %d) -> returning current_span",
|
|
254
|
+
current_depth,
|
|
255
|
+
external_depth,
|
|
256
|
+
)
|
|
257
|
+
return current_span
|
|
258
|
+
elif external_depth > current_depth:
|
|
259
|
+
logger.debug(
|
|
260
|
+
"Traced Context: Different branches, external_span is deeper (depth %d > %d) -> returning external_span",
|
|
261
|
+
external_depth,
|
|
262
|
+
current_depth,
|
|
263
|
+
)
|
|
264
|
+
return external_span
|
|
265
|
+
else:
|
|
266
|
+
# Same depth, different branches - default to external
|
|
267
|
+
logger.debug(
|
|
268
|
+
"Traced Context: Same depth (%d), different branches -> defaulting to external_span",
|
|
269
|
+
current_depth,
|
|
270
|
+
)
|
|
271
|
+
return external_span
|
|
272
|
+
|
|
273
|
+
@staticmethod
|
|
274
|
+
def get_external_current_span() -> Optional[Span]:
|
|
275
|
+
"""Get the current span from the external provider, if any."""
|
|
276
|
+
if UiPathTracingManager._current_span_provider is not None:
|
|
277
|
+
try:
|
|
278
|
+
return UiPathTracingManager._current_span_provider()
|
|
279
|
+
except Exception as e:
|
|
280
|
+
logger.warning("Error getting current span from provider: %s", e)
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
@staticmethod
|
|
284
|
+
def get_ancestor_spans() -> List[Span]:
|
|
285
|
+
"""Get the ancestor spans from the registered provider, if any."""
|
|
286
|
+
if UiPathTracingManager._current_span_ancestors_provider is not None:
|
|
287
|
+
try:
|
|
288
|
+
return UiPathTracingManager._current_span_ancestors_provider()
|
|
289
|
+
except Exception as e:
|
|
290
|
+
logger.warning("Error getting ancestor spans from provider: %s", e)
|
|
291
|
+
return []
|
|
292
|
+
|
|
293
|
+
@staticmethod
|
|
294
|
+
def register_current_span_ancestors_provider(
|
|
295
|
+
current_span_ancestors_provider: Optional[Callable[[], List[Span]]],
|
|
296
|
+
):
|
|
297
|
+
"""Register a custom current span ancestors provider function.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
current_span_ancestors_provider: A function that returns a list of ancestor spans
|
|
301
|
+
from an external tracing framework. If None, no custom
|
|
302
|
+
span ancestor information will be used.
|
|
303
|
+
"""
|
|
304
|
+
UiPathTracingManager._current_span_ancestors_provider = (
|
|
305
|
+
current_span_ancestors_provider
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
@staticmethod
|
|
309
|
+
def get_current_span_ancestors_provider():
|
|
310
|
+
"""Get the currently set custom span ancestors provider."""
|
|
311
|
+
return UiPathTracingManager._current_span_ancestors_provider
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
__all__ = ["UiPathTracingManager"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: uipath-core
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: UiPath Core abstractions
|
|
5
|
+
Project-URL: Homepage, https://uipath.com
|
|
6
|
+
Project-URL: Repository, https://github.com/UiPath/uipath-runtime-python
|
|
7
|
+
Project-URL: Documentation, https://uipath.github.io/uipath-python/
|
|
8
|
+
Maintainer-email: Marius Cosareanu <marius.cosareanu@uipath.com>, Cristian Pufu <cristian.pufu@uipath.com>
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Requires-Dist: opentelemetry-instrumentation>=0.59b0
|
|
18
|
+
Requires-Dist: opentelemetry-sdk>=1.38.0
|
|
19
|
+
Requires-Dist: pydantic>=2.12.3
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# UiPath Core
|
|
23
|
+
|
|
24
|
+
Core abstractions and contracts for the UiPath Python SDK.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
uipath/core/__init__.py,sha256=Gvk2xmI4JTOOHhQYtb0jJhSK2xmlrMUVTj7BL9lZQ88,196
|
|
2
|
+
uipath/core/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
uipath/core/tracing/__init__.py,sha256=bY1t3KHAkyPZ1lYFigDFIvUdw4WCNEaackA0NyZr4IM,484
|
|
4
|
+
uipath/core/tracing/_utils.py,sha256=HNI07gEGjdxbSkfO9654JXszsIlN-iGRrSKdxfbQfAw,3994
|
|
5
|
+
uipath/core/tracing/context.py,sha256=jLUNGIRAydtXoi0W5Fs51SHEHSPHLkixXiupzz7zN88,634
|
|
6
|
+
uipath/core/tracing/decorators.py,sha256=bot1vlGcsaMre63TXLWCTrlnNitb9XE8Ynxz0w0z0mk,10721
|
|
7
|
+
uipath/core/tracing/manager.py,sha256=izxopU8Ar3VQCvBmaW_SD2d1VhUjLjID9YhVDT4Qbis,12367
|
|
8
|
+
uipath_core-0.0.2.dist-info/METADATA,sha256=B39O-uWDPumdj0uAiR_BLltGcshkJWu8jp6QQ-lOIlg,971
|
|
9
|
+
uipath_core-0.0.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
10
|
+
uipath_core-0.0.2.dist-info/licenses/LICENSE,sha256=-KBavWXepyDjimmzH5fVAsi-6jNVpIKFc2kZs0Ri4ng,1058
|
|
11
|
+
uipath_core-0.0.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright 2025 UiPath
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|