Skip to content

Commit b1a8780

Browse files
committed
Fix langchain integration
* Add optional `parent_span` argument to POTelSpan constructor and fix `start_child` * `run_id` is reused for the top level pipeline in langchain, so make sure to close that span or else we get orphans * Dont't use context manager enter/exit since we're doing manual span management * Set correct statuses while finishing the spans
1 parent 2d71a85 commit b1a8780

File tree

4 files changed

+50
-29
lines changed

4 files changed

+50
-29
lines changed

sentry_sdk/integrations/langchain.py

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import sentry_sdk
55
from sentry_sdk.ai.monitoring import set_ai_pipeline_name, record_token_usage
6-
from sentry_sdk.consts import OP, SPANDATA
6+
from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS
77
from sentry_sdk.ai.utils import set_data_normalized
88
from sentry_sdk.scope import should_send_default_pii
99
from sentry_sdk.tracing import POTelSpan as Span
@@ -122,8 +122,9 @@ def _handle_error(self, run_id, error):
122122
span_data = self.span_map[run_id]
123123
if not span_data:
124124
return
125-
sentry_sdk.capture_exception(error, span_data.span.scope)
126-
span_data.span.__exit__(None, None, None)
125+
sentry_sdk.capture_exception(error)
126+
span_data.span.set_status(SPANSTATUS.INTERNAL_ERROR)
127+
span_data.span.finish()
127128
del self.span_map[run_id]
128129

129130
def _normalize_langchain_message(self, message):
@@ -135,23 +136,27 @@ def _normalize_langchain_message(self, message):
135136
def _create_span(self, run_id, parent_id, **kwargs):
136137
# type: (SentryLangchainCallback, UUID, Optional[Any], Any) -> WatchedSpan
137138

138-
watched_span = None # type: Optional[WatchedSpan]
139-
if parent_id:
140-
parent_span = self.span_map.get(parent_id) # type: Optional[WatchedSpan]
141-
if parent_span:
142-
watched_span = WatchedSpan(parent_span.span.start_child(**kwargs))
143-
parent_span.children.append(watched_span)
144-
if watched_span is None:
145-
watched_span = WatchedSpan(
146-
sentry_sdk.start_span(only_if_parent=True, **kwargs)
147-
)
139+
parent_watched_span = self.span_map.get(parent_id) if parent_id else None
140+
sentry_span = sentry_sdk.start_span(
141+
parent_span=parent_watched_span.span if parent_watched_span else None,
142+
only_if_parent=True,
143+
**kwargs,
144+
)
145+
watched_span = WatchedSpan(sentry_span)
146+
if parent_watched_span:
147+
parent_watched_span.children.append(watched_span)
148148

149149
if kwargs.get("op", "").startswith("ai.pipeline."):
150150
if kwargs.get("name"):
151151
set_ai_pipeline_name(kwargs.get("name"))
152152
watched_span.is_pipeline = True
153153

154-
watched_span.span.__enter__()
154+
# the same run_id is reused for the pipeline it seems
155+
# so we need to end the older span to avoid orphan spans
156+
existing_span_data = self.span_map.get(run_id)
157+
if existing_span_data is not None:
158+
self._exit_span(existing_span_data, run_id)
159+
155160
self.span_map[run_id] = watched_span
156161
self.gc_span_map()
157162
return watched_span
@@ -162,7 +167,8 @@ def _exit_span(self, span_data, run_id):
162167
if span_data.is_pipeline:
163168
set_ai_pipeline_name(None)
164169

165-
span_data.span.__exit__(None, None, None)
170+
span_data.span.set_status(SPANSTATUS.OK)
171+
span_data.span.finish()
166172
del self.span_map[run_id]
167173

168174
def on_llm_start(

sentry_sdk/integrations/opentelemetry/span_processor.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,3 +291,17 @@ def _common_span_transaction_attributes_as_json(self, span):
291291
common_json["tags"] = tags
292292

293293
return common_json
294+
295+
def _log_debug_info(self):
296+
# type: () -> None
297+
import pprint
298+
299+
pprint.pprint(
300+
{
301+
format_span_id(span_id): [
302+
(format_span_id(child.context.span_id), child.name)
303+
for child in children
304+
]
305+
for span_id, children in self._children_spans.items()
306+
}
307+
)

sentry_sdk/tracing.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1213,6 +1213,7 @@ def __init__(
12131213
source=TRANSACTION_SOURCE_CUSTOM, # type: str
12141214
attributes=None, # type: OTelSpanAttributes
12151215
only_if_parent=False, # type: bool
1216+
parent_span=None, # type: Optional[POTelSpan]
12161217
otel_span=None, # type: Optional[OtelSpan]
12171218
**_, # type: dict[str, object]
12181219
):
@@ -1231,7 +1232,7 @@ def __init__(
12311232
self._otel_span = otel_span
12321233
else:
12331234
skip_span = False
1234-
if only_if_parent:
1235+
if only_if_parent and parent_span is None:
12351236
parent_span_context = get_current_span().get_span_context()
12361237
skip_span = (
12371238
not parent_span_context.is_valid or parent_span_context.is_remote
@@ -1262,8 +1263,17 @@ def __init__(
12621263
if sampled is not None:
12631264
attributes[SentrySpanAttribute.CUSTOM_SAMPLED] = sampled
12641265

1266+
parent_context = None
1267+
if parent_span is not None:
1268+
parent_context = otel_trace.set_span_in_context(
1269+
parent_span._otel_span
1270+
)
1271+
12651272
self._otel_span = tracer.start_span(
1266-
span_name, start_time=start_timestamp, attributes=attributes
1273+
span_name,
1274+
context=parent_context,
1275+
start_time=start_timestamp,
1276+
attributes=attributes,
12671277
)
12681278

12691279
self.origin = origin or DEFAULT_SPAN_ORIGIN
@@ -1506,10 +1516,7 @@ def timestamp(self):
15061516

15071517
def start_child(self, **kwargs):
15081518
# type: (**Any) -> POTelSpan
1509-
kwargs.setdefault("sampled", self.sampled)
1510-
1511-
span = POTelSpan(only_if_parent=True, **kwargs)
1512-
return span
1519+
return POTelSpan(sampled=self.sampled, parent_span=self, **kwargs)
15131520

15141521
def iter_headers(self):
15151522
# type: () -> Iterator[Tuple[str, str]]

tests/integrations/langchain/test_langchain.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -187,17 +187,11 @@ def test_langchain_agent(
187187
assert "measurements" not in chat_spans[0]
188188

189189
if send_default_pii and include_prompts:
190-
assert (
191-
"You are very powerful"
192-
in chat_spans[0]["data"]["ai.input_messages"][0]["content"]
193-
)
190+
assert "You are very powerful" in chat_spans[0]["data"]["ai.input_messages"]
194191
assert "5" in chat_spans[0]["data"]["ai.responses"]
195192
assert "word" in tool_exec_span["data"]["ai.input_messages"]
196193
assert 5 == int(tool_exec_span["data"]["ai.responses"])
197-
assert (
198-
"You are very powerful"
199-
in chat_spans[1]["data"]["ai.input_messages"][0]["content"]
200-
)
194+
assert "You are very powerful" in chat_spans[1]["data"]["ai.input_messages"]
201195
assert "5" in chat_spans[1]["data"]["ai.responses"]
202196
else:
203197
assert "ai.input_messages" not in chat_spans[0].get("data", {})

0 commit comments

Comments
 (0)