Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,14 @@ def _create_llm_span(
# If context setting fails, continue without suppression token
token = None

# Detach orphaned context tokens before overwriting (see #3957).
existing = self.spans.get(run_id)
if existing is not None:
if existing.token:
self._safe_detach_context(existing.token)
if getattr(existing, "association_properties_token", None):
self._safe_detach_context(existing.association_properties_token)

Comment on lines +475 to +482
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Detach order is wrong; reused run_id tokens can still leak.

At Line 476, existing = self.spans.get(run_id) happens after _create_span() (Line 441) has already replaced self.spans[run_id]. This detaches the current holder’s token, not the prior holder’s tokens, so the reuse path can still leave stale context attached.

Proposed fix
 def _create_llm_span(
     self,
     run_id: UUID,
@@
 ) -> Span:
+    # Detach previous holder tokens before _create_span overwrites run_id.
+    previous = self.spans.get(run_id)
+    if previous is not None:
+        if previous.token:
+            self._safe_detach_context(previous.token)
+        if getattr(previous, "association_properties_token", None):
+            self._safe_detach_context(previous.association_properties_token)
+
     workflow_name = self.get_workflow_name(parent_run_id)
     entity_path = self.get_entity_path(parent_run_id)
@@
-    existing = self.spans.get(run_id)
-    if existing is not None:
-        if existing.token:
-            self._safe_detach_context(existing.token)
-        if getattr(existing, "association_properties_token", None):
-            self._safe_detach_context(existing.association_properties_token)
+    # _create_span stored current span-context token; detach before replacing holder.
+    current = self.spans.get(run_id)
+    if current is not None and current.token:
+        self._safe_detach_context(current.token)
+    if current is not None and getattr(current, "association_properties_token", None):
+        self._safe_detach_context(current.association_properties_token)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/callback_handler.py`
around lines 475 - 482, The current code detaches tokens from self.spans[run_id]
after _create_span() has already overwritten that entry, which can detach the
new span's token instead of the prior holder's; modify the logic in the method
where _create_span(run_id, ...) is called so that you first retrieve existing =
self.spans.get(run_id) and call _safe_detach_context(existing.token) and
_safe_detach_context(existing.association_properties_token) (guarded by getattr)
before invoking _create_span and assigning to self.spans[run_id]; ensure you
keep the existing variable names (existing, run_id) and the helper
_safe_detach_context to locate and fix the spot.

self.spans[run_id] = SpanHolder(
span, token, None, [], workflow_name, None, entity_path
)
Expand Down
Loading