1313class 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