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

Commit d210a60

Browse files
committed
final
1 parent 0743802 commit d210a60

2 files changed

Lines changed: 395 additions & 45 deletions

File tree

src/uipath/core/tracing/manager.py

Lines changed: 100 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
class SpanRegistry:
1414
"""Registry to track all spans and their parent relationships."""
1515

16+
MAX_HIERARCHY_DEPTH = 1000 # Hard limit for hierarchy traversal
17+
1618
def __init__(self):
1719
self._spans: Dict[int, ReadableSpan] = {} # span_id -> span
1820
self._parent_map: Dict[int, Optional[int]] = {} # span_id -> parent_id
@@ -26,7 +28,9 @@ def register_span(self, span: ReadableSpan) -> None:
2628
self._parent_map[span_id] = parent_id
2729

2830
parent_str = f"{parent_id:016x}" if parent_id is not None else "None"
29-
logger.info(f"Registered span: {span.name} (id: {span_id:016x}, parent: {parent_str})")
31+
logger.info(
32+
f"Registered span: {span.name} (id: {span_id:016x}, parent: {parent_str})"
33+
)
3034

3135
def get_span(self, span_id: int) -> Optional[ReadableSpan]:
3236
"""Get a span by ID."""
@@ -37,7 +41,11 @@ def get_parent_id(self, span_id: int) -> Optional[int]:
3741
return self._parent_map.get(span_id)
3842

3943
def calculate_depth(self, span_id: int) -> int:
40-
"""Calculate the depth of a span in the hierarchy."""
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+
"""
4149
depth = 0
4250
current_id = span_id
4351
visited = set()
@@ -48,32 +56,58 @@ def calculate_depth(self, span_id: int) -> int:
4856
if parent_id is None:
4957
break
5058
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
5164
current_id = parent_id
5265

5366
return depth
5467

5568
def is_ancestor(self, ancestor_id: int, descendant_id: int) -> bool:
56-
"""Check if ancestor_id is an ancestor of descendant_id."""
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+
"""
5775
current_id = descendant_id
5876
visited = set()
77+
steps = 0
5978

6079
while current_id is not None and current_id not in visited:
6180
if current_id == ancestor_id:
6281
return True
6382
visited.add(current_id)
6483
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
6590

6691
return False
6792

6893
def get_chain(self, span_id: int) -> List[int]:
69-
"""Get the complete ancestor chain for a span (bottom to top)."""
94+
"""Get the complete ancestor chain for a span (bottom to top).
95+
96+
Returns:
97+
List of span IDs from the given span to the root, capped at MAX_HIERARCHY_DEPTH.
98+
"""
7099
chain = []
71100
current_id = span_id
72101
visited = set()
73102

74103
while current_id is not None and current_id not in visited:
75104
chain.append(current_id)
76105
visited.add(current_id)
106+
if len(chain) >= self.MAX_HIERARCHY_DEPTH:
107+
logger.warning(
108+
f"Hit MAX_HIERARCHY_DEPTH ({self.MAX_HIERARCHY_DEPTH}) while building chain for span {span_id:016x}"
109+
)
110+
break
77111
current_id = self._parent_map.get(current_id)
78112

79113
return chain
@@ -110,10 +144,53 @@ def register_current_span_provider(
110144
def get_parent_context() -> context.Context:
111145
"""Get the parent context for span creation.
112146
113-
Prioritizes:
114-
- Bottom-most span when both current_span and external_span exist
115-
- Single available span (current_span or external_span)
116-
- Current context as fallback
147+
This method determines the correct parent context when creating a new traced span.
148+
It handles scenarios where spans may exist in both OpenTelemetry's context (current_span)
149+
and in an external tracing system (external_span), such as LangGraph.
150+
151+
The algorithm follows this priority:
152+
153+
1. **No spans available**: Returns the current OpenTelemetry context (empty context)
154+
155+
2. **Only current_span exists**: Returns a context with current_span set as parent
156+
- This is the standard OpenTelemetry behavior for nested traced functions
157+
158+
3. **Only external_span exists**: Returns a context with external_span set as parent
159+
- This occurs when an external tracing system (like LangGraph) has an active span
160+
but there's no OTel span in the current call stack
161+
162+
4. **Both spans exist**: Calls `_get_bottom_most_span()` to determine which is deeper
163+
- Uses the SpanRegistry to build parent-child relationships
164+
- Returns the span that is closer to the "bottom" (leaf) of the trace tree
165+
- This ensures new spans are always attached to the deepest/most specific parent
166+
167+
Context Sources:
168+
- **current_span**: Retrieved from OpenTelemetry's `trace.get_current_span()`
169+
- Represents the active OTel span in the current execution context
170+
- Created by `@traced` decorators or manual span creation
171+
172+
- **external_span**: Retrieved from the registered custom span provider
173+
- Set via `register_current_span_provider()`
174+
- Typically provided by external frameworks (LangGraph, LangChain, etc.)
175+
- Allows integration with tracing systems outside of OpenTelemetry
176+
177+
Returns:
178+
context.Context: An OpenTelemetry context containing the appropriate parent span,
179+
or the current empty context if no spans are available
180+
181+
Example:
182+
```python
183+
# Called by the @traced decorator when creating a new span:
184+
ctx = UiPathTracingManager.get_parent_context()
185+
with tracer.start_as_current_span("my_span", context=ctx) as span:
186+
# New span will have the correct parent based on the logic above
187+
pass
188+
```
189+
190+
See Also:
191+
- `_get_bottom_most_span()`: Logic for choosing between two available spans
192+
- `register_current_span_provider()`: Register external span provider
193+
- `get_external_current_span()`: Retrieve span from external provider
117194
"""
118195
current_span = trace.get_current_span()
119196
has_current_span = (
@@ -152,9 +229,6 @@ def _get_bottom_most_span(
152229
Returns:
153230
The span that is deeper (closer to the bottom) in the call hierarchy
154231
"""
155-
logger.info("=" * 80)
156-
logger.info("Determining bottom-most span...")
157-
158232
# Register both spans in the registry
159233
_span_registry.register_span(current_span)
160234
_span_registry.register_span(external_span)
@@ -167,38 +241,16 @@ def _get_bottom_most_span(
167241
current_span_id = current_span.get_span_context().span_id
168242
external_span_id = external_span.get_span_context().span_id
169243

170-
# Get chains from registry
171-
current_chain = _span_registry.get_chain(current_span_id)
172-
external_chain = _span_registry.get_chain(external_span_id)
173-
174-
logger.info(f"current_span: '{current_span.name}' depth: {_span_registry.calculate_depth(current_span_id)}")
175-
logger.info(f"external_span: '{external_span.name}' depth: {_span_registry.calculate_depth(external_span_id)}")
176-
177-
# Log the chains
178-
logger.info("current_span chain (bottom to top):")
179-
for i, span_id in enumerate(current_chain):
180-
span = _span_registry.get_span(span_id)
181-
if span:
182-
parent_id = _span_registry.get_parent_id(span_id)
183-
parent_str = f"{parent_id:016x}" if parent_id is not None else "None"
184-
logger.info(f" [{i}] {span.name} (id: {span_id:016x}, parent: {parent_str})")
185-
186-
logger.info("external_span chain (bottom to top):")
187-
for i, span_id in enumerate(external_chain):
188-
span = _span_registry.get_span(span_id)
189-
if span:
190-
parent_id = _span_registry.get_parent_id(span_id)
191-
parent_str = f"{parent_id:016x}" if parent_id is not None else "None"
192-
logger.info(f" [{i}] {span.name} (id: {span_id:016x}, parent: {parent_str})")
193-
194244
# Check if one span is an ancestor of the other
195245
if _span_registry.is_ancestor(external_span_id, current_span_id):
196-
logger.info("RESULT: current_span is a descendant of external_span -> returning current_span (deeper)")
197-
logger.info("=" * 80)
246+
logger.debug(
247+
"Traced Context: current_span is a descendant of external_span -> returning current_span (deeper)"
248+
)
198249
return current_span
199250
elif _span_registry.is_ancestor(current_span_id, external_span_id):
200-
logger.info("RESULT: external_span is a descendant of current_span -> returning external_span (deeper)")
201-
logger.info("=" * 80)
251+
logger.debug(
252+
"Traced Context: external_span is a descendant of current_span -> returning external_span (deeper)"
253+
)
202254
return external_span
203255

204256
# Neither is an ancestor of the other - they're in different branches
@@ -207,17 +259,20 @@ def _get_bottom_most_span(
207259
external_depth = _span_registry.calculate_depth(external_span_id)
208260

209261
if current_depth > external_depth:
210-
logger.info(f"RESULT: Different branches, current_span is deeper (depth {current_depth} > {external_depth}) -> returning current_span")
211-
logger.info("=" * 80)
262+
logger.debug(
263+
f"Traced Context: Different branches, current_span is deeper (depth {current_depth} > {external_depth}) -> returning current_span"
264+
)
212265
return current_span
213266
elif external_depth > current_depth:
214-
logger.info(f"RESULT: Different branches, external_span is deeper (depth {external_depth} > {current_depth}) -> returning external_span")
215-
logger.info("=" * 80)
267+
logger.debug(
268+
f"Traced Context: Different branches, external_span is deeper (depth {external_depth} > {current_depth}) -> returning external_span"
269+
)
216270
return external_span
217271
else:
218272
# Same depth, different branches - default to external
219-
logger.info(f"RESULT: Same depth ({current_depth}), different branches -> defaulting to external_span")
220-
logger.info("=" * 80)
273+
logger.debug(
274+
f"Traced Context: Same depth ({current_depth}), different branches -> defaulting to external_span"
275+
)
221276
return external_span
222277

223278
@staticmethod

0 commit comments

Comments
 (0)