Skip to content
This repository was archived by the owner on Mar 4, 2026. It is now read-only.

Commit 4e0036b

Browse files
committed
feat(tracing): add current span ancestors provider
1 parent 6a36690 commit 4e0036b

2 files changed

Lines changed: 551 additions & 18 deletions

File tree

src/uipath/core/tracing/manager.py

Lines changed: 256 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,297 @@
11
"""Tracing manager for handling tracer implementations and function registry."""
22

33
import logging
4-
from typing import Any, Callable, Optional
4+
from typing import Any, Callable, Dict, List, Optional
55

66
from opentelemetry import context, trace
7+
from opentelemetry.sdk.trace import ReadableSpan
78
from opentelemetry.trace import set_span_in_context
89

910
logger = logging.getLogger(__name__)
1011

1112

13+
class SpanRegistry:
14+
"""Registry to track all spans and their parent relationships."""
15+
16+
MAX_HIERARCHY_DEPTH = 1000 # Hard limit for hierarchy traversal
17+
18+
def __init__(self):
19+
self._spans: Dict[int, ReadableSpan] = {} # span_id -> span
20+
self._parent_map: Dict[int, Optional[int]] = {} # span_id -> parent_id
21+
22+
def register_span(self, span: ReadableSpan) -> None:
23+
"""Register a span and its parent relationship."""
24+
span_id = span.get_span_context().span_id
25+
parent_id = span.parent.span_id if span.parent else None
26+
27+
self._spans[span_id] = span
28+
self._parent_map[span_id] = parent_id
29+
30+
parent_str = f"{parent_id:016x}" if parent_id is not None else "None"
31+
logger.debug(
32+
f"SpanRegistry: registered span: {span.name} (id: {span_id:016x}, parent: {parent_str})"
33+
)
34+
35+
def get_span(self, span_id: int) -> Optional[ReadableSpan]:
36+
"""Get a span by ID."""
37+
return self._spans.get(span_id)
38+
39+
def get_parent_id(self, span_id: int) -> Optional[int]:
40+
"""Get the parent ID of a span."""
41+
return self._parent_map.get(span_id)
42+
43+
def calculate_depth(self, span_id: int) -> int:
44+
"""Calculate the depth of a span in the hierarchy.
45+
46+
Returns:
47+
The depth of the span, capped at MAX_HIERARCHY_DEPTH.
48+
"""
49+
depth = 0
50+
current_id = span_id
51+
visited = set()
52+
53+
while current_id is not None and current_id not in visited:
54+
visited.add(current_id)
55+
parent_id = self._parent_map.get(current_id)
56+
if parent_id is None:
57+
break
58+
depth += 1
59+
if depth >= self.MAX_HIERARCHY_DEPTH:
60+
logger.warning(
61+
f"Hit MAX_HIERARCHY_DEPTH ({self.MAX_HIERARCHY_DEPTH}) while calculating depth for span {span_id:016x}"
62+
)
63+
break
64+
current_id = parent_id
65+
66+
return depth
67+
68+
def is_ancestor(self, ancestor_id: int, descendant_id: int) -> bool:
69+
"""Check if ancestor_id is an ancestor of descendant_id.
70+
71+
Returns:
72+
True if ancestor_id is an ancestor of descendant_id, False otherwise.
73+
If MAX_HIERARCHY_DEPTH is reached, returns False.
74+
"""
75+
current_id = descendant_id
76+
visited = set()
77+
steps = 0
78+
79+
while current_id is not None and current_id not in visited:
80+
if current_id == ancestor_id:
81+
return True
82+
visited.add(current_id)
83+
current_id = self._parent_map.get(current_id)
84+
steps += 1
85+
if steps >= self.MAX_HIERARCHY_DEPTH:
86+
logger.warning(
87+
f"Hit MAX_HIERARCHY_DEPTH ({self.MAX_HIERARCHY_DEPTH}) while checking ancestry between {ancestor_id:016x} and {descendant_id:016x}"
88+
)
89+
return False
90+
91+
return False
92+
93+
def clear(self) -> None:
94+
"""Clear all registered spans."""
95+
self._spans.clear()
96+
self._parent_map.clear()
97+
98+
99+
# Global span registry instance
100+
_span_registry = SpanRegistry()
101+
102+
12103
class UiPathTracingManager:
13104
"""Static utility class to manage tracing implementations and decorated functions."""
14105

15106
_current_span_provider: Optional[Callable[[], Any]] = None
107+
_current_span_ancestors_provider: Optional[Callable[[], List[Any]]] = None
16108

17-
@classmethod
109+
@staticmethod
18110
def register_current_span_provider(
19-
cls, current_span_provider: Optional[Callable[[], Any]]
111+
current_span_provider: Optional[Callable[[], Any]],
20112
):
21113
"""Register a custom current span provider function.
22114
23115
Args:
24116
current_span_provider: A function that returns the current span from an external
25117
tracing framework. If None, no custom span parenting will be used.
26118
"""
27-
cls._current_span_provider = current_span_provider
119+
UiPathTracingManager._current_span_provider = current_span_provider
28120

29121
@staticmethod
30-
def get_parent_context():
122+
def get_parent_context() -> context.Context:
31123
"""Get the parent context for span creation.
32124
33-
Prioritizes:
34-
1. Currently active OTel span (for recursion/children)
35-
2. External span provider (for top-level calls)
36-
3. Current context as fallback
125+
This method determines the correct parent context when creating a new traced span.
126+
It handles scenarios where spans may exist in both OpenTelemetry's context (current_span)
127+
and in an external tracing system (external_span), such as LangGraph.
128+
129+
The algorithm follows this priority:
130+
131+
1. **No spans available**: Returns the current OpenTelemetry context (empty context)
132+
133+
2. **Only current_span exists**: Returns a context with current_span set as parent
134+
- This is the standard OpenTelemetry behavior for nested traced functions
135+
136+
3. **Only external_span exists**: Returns a context with external_span set as parent
137+
- This occurs when an external tracing system (like LangGraph) has an active span
138+
but there's no OTel span in the current call stack
139+
140+
4. **Both spans exist**: Calls `_get_bottom_most_span()` to determine which is deeper
141+
- Uses the SpanRegistry to build parent-child relationships
142+
- Returns the span that is closer to the "bottom" (leaf) of the trace tree
143+
- This ensures new spans are always attached to the deepest/most specific parent
144+
145+
Context Sources:
146+
- **current_span**: Retrieved from OpenTelemetry's `trace.get_current_span()`
147+
- Represents the active OTel span in the current execution context
148+
- Created by `@traced` decorators or manual span creation
149+
150+
- **external_span**: Retrieved from the registered custom span provider
151+
- Set via `register_current_span_provider()`
152+
- Typically provided by external frameworks (LangGraph, LangChain, etc.)
153+
- Allows integration with tracing systems outside of OpenTelemetry
154+
155+
Returns:
156+
context.Context: An OpenTelemetry context containing the appropriate parent span,
157+
or the current empty context if no spans are available
158+
159+
Example:
160+
```python
161+
# Called by the @traced decorator when creating a new span:
162+
ctx = UiPathTracingManager.get_parent_context()
163+
with tracer.start_as_current_span("my_span", context=ctx) as span:
164+
# New span will have the correct parent based on the logic above
165+
pass
166+
```
167+
168+
See Also:
169+
- `_get_bottom_most_span()`: Logic for choosing between two available spans
170+
- `register_current_span_provider()`: Register external span provider
171+
- `get_external_current_span()`: Retrieve span from external provider
37172
"""
38-
# Always use the currently active OTel span if valid (recursion / children)
39173
current_span = trace.get_current_span()
174+
has_current_span = (
175+
current_span is not None and current_span.get_span_context().is_valid
176+
)
40177

41-
if current_span is not None and current_span.get_span_context().is_valid:
178+
external_span = UiPathTracingManager.get_external_current_span()
179+
has_external_span = external_span is not None
180+
181+
# Only one or no spans available
182+
if not has_current_span:
183+
return (
184+
set_span_in_context(external_span)
185+
if has_external_span
186+
else context.get_current()
187+
)
188+
if not has_external_span:
42189
return set_span_in_context(current_span)
43190

44-
# Only for the very top-level call, fallback to external span provider
191+
# Both spans exist - find the bottom-most one
192+
bottom_span = UiPathTracingManager._get_bottom_most_span(
193+
current_span, external_span
194+
)
195+
return set_span_in_context(bottom_span)
196+
197+
@staticmethod
198+
def _get_bottom_most_span(
199+
current_span: ReadableSpan, external_span: ReadableSpan
200+
) -> ReadableSpan:
201+
"""Determine which span is deeper in the ancestor tree.
202+
203+
Args:
204+
current_span: The OTel current span
205+
external_span: The external span from the provider
206+
207+
Returns:
208+
The span that is deeper (closer to the bottom) in the call hierarchy
209+
"""
210+
# Register both spans in the registry
211+
_span_registry.register_span(current_span)
212+
_span_registry.register_span(external_span)
213+
214+
# Also register external ancestors
215+
external_ancestors = UiPathTracingManager.get_ancestor_spans() or []
216+
for ancestor in external_ancestors:
217+
_span_registry.register_span(ancestor)
218+
219+
current_span_id = current_span.get_span_context().span_id
220+
external_span_id = external_span.get_span_context().span_id
221+
222+
# Check if one span is an ancestor of the other
223+
if _span_registry.is_ancestor(external_span_id, current_span_id):
224+
logger.debug(
225+
"Traced Context: current_span is a descendant of external_span -> returning current_span (deeper)"
226+
)
227+
return current_span
228+
elif _span_registry.is_ancestor(current_span_id, external_span_id):
229+
logger.debug(
230+
"Traced Context: external_span is a descendant of current_span -> returning external_span (deeper)"
231+
)
232+
return external_span
233+
234+
# Neither is an ancestor of the other - they're in different branches
235+
# Use depth as tiebreaker
236+
current_depth = _span_registry.calculate_depth(current_span_id)
237+
external_depth = _span_registry.calculate_depth(external_span_id)
238+
239+
if current_depth > external_depth:
240+
logger.debug(
241+
f"Traced Context: Different branches, current_span is deeper (depth {current_depth} > {external_depth}) -> returning current_span"
242+
)
243+
return current_span
244+
elif external_depth > current_depth:
245+
logger.debug(
246+
f"Traced Context: Different branches, external_span is deeper (depth {external_depth} > {current_depth}) -> returning external_span"
247+
)
248+
return external_span
249+
else:
250+
# Same depth, different branches - default to external
251+
logger.debug(
252+
f"Traced Context: Same depth ({current_depth}), different branches -> defaulting to external_span"
253+
)
254+
return external_span
255+
256+
@staticmethod
257+
def get_external_current_span():
258+
"""Get the current span from the external provider, if any."""
45259
if UiPathTracingManager._current_span_provider is not None:
46260
try:
47-
external_span = UiPathTracingManager._current_span_provider()
48-
if external_span is not None:
49-
return set_span_in_context(external_span)
261+
return UiPathTracingManager._current_span_provider()
50262
except Exception as e:
51263
logger.warning(f"Error getting current span from provider: {e}")
264+
return None
265+
266+
@staticmethod
267+
def get_ancestor_spans() -> List[Any]:
268+
"""Get the ancestor spans from the registered provider, if any."""
269+
if UiPathTracingManager._current_span_ancestors_provider is not None:
270+
try:
271+
return UiPathTracingManager._current_span_ancestors_provider()
272+
except Exception as e:
273+
logger.warning(f"Error getting ancestor spans from provider: {e}")
274+
return []
52275

53-
# Last fallback
54-
ctx = context.get_current()
276+
@staticmethod
277+
def register_current_span_ancestors_provider(
278+
current_span_ancestors_provider: Optional[Callable[[], List[Any]]],
279+
):
280+
"""Register a custom current span ancestors provider function.
55281
56-
return ctx
282+
Args:
283+
current_span_ancestors_provider: A function that returns a list of ancestor spans
284+
from an external tracing framework. If None, no custom
285+
span ancestor information will be used.
286+
"""
287+
UiPathTracingManager._current_span_ancestors_provider = (
288+
current_span_ancestors_provider
289+
)
290+
291+
@staticmethod
292+
def get_current_span_ancestors_provider():
293+
"""Get the currently set custom span ancestors provider."""
294+
return UiPathTracingManager._current_span_ancestors_provider
57295

58296

59297
__all__ = ["UiPathTracingManager"]

0 commit comments

Comments
 (0)