Skip to content

Commit f0b883d

Browse files
📝 Add docstrings to shivampatel/fix-key-issue
Docstrings generation was requested by @Shivamp629. * #3403 (comment) The following files were modified: * `packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/__init__.py` * `packages/opentelemetry-instrumentation-langchain/tests/test_external_run_id.py`
1 parent e66894f commit f0b883d

File tree

2 files changed

+178
-11
lines changed

2 files changed

+178
-11
lines changed

packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/__init__.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -226,19 +226,32 @@ def __call__(
226226
args,
227227
kwargs,
228228
) -> None:
229+
"""
230+
Inject tracing headers for the current run into OpenAI request kwargs and suppress language-model instrumentation while calling the wrapped function.
231+
232+
If kwargs contains a `run_manager`, this looks up a span for `run_manager.run_id` from the callback manager; if a span is found, tracing headers are injected into `kwargs["extra_headers"]`. If no span is found, a debug message is logged and no headers are injected. The function also sets the context key SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY to True as a best-effort fallback; failures to set the context are ignored. Finally, the original wrapped callable is invoked with the (possibly modified) args and kwargs.
233+
234+
Parameters:
235+
kwargs (dict): May contain `run_manager` (used to find a span via `run_manager.run_id`) and `extra_headers` (a dict that will be updated with injected tracing headers if a span is present).
236+
237+
Returns:
238+
The value returned by calling the original `wrapped` callable with the provided args and kwargs.
239+
"""
229240
run_manager = kwargs.get("run_manager")
230241
if run_manager:
231242
run_id = run_manager.run_id
232-
span_holder = self._callback_manager.spans[run_id]
233-
234-
extra_headers = kwargs.get("extra_headers", {})
235-
236-
# Inject tracing context into the extra headers
237-
ctx = set_span_in_context(span_holder.span)
238-
TraceContextTextMapPropagator().inject(extra_headers, context=ctx)
239-
240-
# Update kwargs to include the modified headers
241-
kwargs["extra_headers"] = extra_headers
243+
span_holder = self._callback_manager.spans.get(run_id)
244+
245+
if span_holder:
246+
extra_headers = kwargs.get("extra_headers", {})
247+
ctx = set_span_in_context(span_holder.span)
248+
TraceContextTextMapPropagator().inject(extra_headers, context=ctx)
249+
kwargs["extra_headers"] = extra_headers
250+
else:
251+
logger.debug(
252+
"No span found for run_id %s, skipping header injection",
253+
run_id
254+
)
242255

243256
# In legacy chains like LLMChain, suppressing model instrumentations
244257
# within create_llm_span doesn't work, so this should helps as a fallback
@@ -251,4 +264,4 @@ def __call__(
251264
# This is not critical for core functionality
252265
pass
253266

254-
return wrapped(*args, **kwargs)
267+
return wrapped(*args, **kwargs)
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
"""Tests for handling external run_ids from systems like LangSmith."""
2+
3+
import logging
4+
from unittest.mock import MagicMock, Mock, patch
5+
from uuid import uuid4
6+
7+
import pytest
8+
from opentelemetry.instrumentation.langchain.callback_handler import (
9+
TraceloopCallbackHandler,
10+
)
11+
12+
13+
@pytest.fixture
14+
def callback_handler(tracer_provider):
15+
"""
16+
Create a TraceloopCallbackHandler bound to the provided tracer provider.
17+
18+
Parameters:
19+
tracer_provider: The OpenTelemetry TracerProvider used to initialize the callback handler.
20+
21+
Returns:
22+
TraceloopCallbackHandler: An initialized callback handler associated with the given tracer_provider.
23+
"""
24+
return TraceloopCallbackHandler(tracer_provider=tracer_provider)
25+
26+
27+
@pytest.fixture
28+
def mock_run_manager():
29+
"""
30+
Create a mock run manager with a UUID `run_id` attribute.
31+
32+
Returns:
33+
manager (Mock): A unittest.mock.Mock instance with a `run_id` attribute set to a generated UUID.
34+
"""
35+
manager = Mock()
36+
manager.run_id = uuid4()
37+
return manager
38+
39+
40+
def test_external_run_id_no_keyerror(
41+
callback_handler, mock_run_manager, instrument_legacy, caplog
42+
):
43+
"""Test that external run_ids (e.g., from LangSmith) don't cause KeyError."""
44+
from opentelemetry.instrumentation.langchain import _OpenAITracingWrapper
45+
46+
# Create the wrapper
47+
wrapper = _OpenAITracingWrapper(callback_handler)
48+
49+
# Mock the wrapped function
50+
mock_wrapped = Mock(return_value="test_result")
51+
52+
# Ensure the run_id is NOT in the spans dictionary
53+
# (simulating an external system like LangSmith creating the run_id)
54+
assert mock_run_manager.run_id not in callback_handler.spans
55+
56+
# Create kwargs with the external run_manager
57+
kwargs = {
58+
"run_manager": mock_run_manager,
59+
"extra_headers": {},
60+
}
61+
62+
# Capture debug logs
63+
with caplog.at_level(logging.DEBUG):
64+
# Call the wrapper - should NOT raise KeyError
65+
result = wrapper(mock_wrapped, None, [], kwargs)
66+
67+
# Verify the function was called successfully
68+
assert result == "test_result"
69+
mock_wrapped.assert_called_once()
70+
71+
# Verify debug log was generated
72+
assert any(
73+
"No span found for run_id" in record.message
74+
and "skipping header injection" in record.message
75+
for record in caplog.records
76+
)
77+
78+
# Verify extra_headers were not modified since there's no span
79+
# (the original empty dict should still be there)
80+
assert "traceparent" not in kwargs["extra_headers"]
81+
82+
83+
def test_internal_run_id_injects_headers(
84+
callback_handler, mock_run_manager, instrument_legacy
85+
):
86+
"""Test that internal run_ids (created by OTEL) get headers injected."""
87+
from opentelemetry.instrumentation.langchain import _OpenAITracingWrapper
88+
from opentelemetry.sdk.trace import TracerProvider
89+
from opentelemetry.trace import SpanKind
90+
91+
# Create a real span and add it to the callback handler's spans
92+
tracer = TracerProvider().get_tracer(__name__)
93+
span = tracer.start_span("test_span", kind=SpanKind.CLIENT)
94+
95+
# Create a span holder (mimicking what the callback handler does)
96+
span_holder = Mock()
97+
span_holder.span = span
98+
99+
# Add the span to the callback handler's spans dictionary
100+
callback_handler.spans[mock_run_manager.run_id] = span_holder
101+
102+
# Create the wrapper
103+
wrapper = _OpenAITracingWrapper(callback_handler)
104+
105+
# Mock the wrapped function
106+
mock_wrapped = Mock(return_value="test_result")
107+
108+
# Create kwargs with the internal run_manager
109+
kwargs = {
110+
"run_manager": mock_run_manager,
111+
"extra_headers": {},
112+
}
113+
114+
# Call the wrapper
115+
result = wrapper(mock_wrapped, None, [], kwargs)
116+
117+
# Verify the function was called successfully
118+
assert result == "test_result"
119+
mock_wrapped.assert_called_once()
120+
121+
# Verify headers were injected (traceparent should exist)
122+
assert "traceparent" in kwargs["extra_headers"]
123+
assert kwargs["extra_headers"]["traceparent"] # Should have a value
124+
125+
# Clean up
126+
span.end()
127+
128+
129+
def test_no_run_manager_continues_normally(callback_handler, instrument_legacy):
130+
"""
131+
Ensure the tracing wrapper executes the wrapped function and does not inject trace headers when no `run_manager` is provided in `kwargs`.
132+
133+
Verifies that the wrapped callable is invoked and that `extra_headers` remains without a `traceparent` entry when `kwargs` does not contain a `run_manager`.
134+
"""
135+
from opentelemetry.instrumentation.langchain import _OpenAITracingWrapper
136+
137+
# Create the wrapper
138+
wrapper = _OpenAITracingWrapper(callback_handler)
139+
140+
# Mock the wrapped function
141+
mock_wrapped = Mock(return_value="test_result")
142+
143+
# Create kwargs without run_manager
144+
kwargs = {"extra_headers": {}}
145+
146+
# Call the wrapper - should work fine
147+
result = wrapper(mock_wrapped, None, [], kwargs)
148+
149+
# Verify the function was called successfully
150+
assert result == "test_result"
151+
mock_wrapped.assert_called_once()
152+
153+
# Verify no headers were injected
154+
assert "traceparent" not in kwargs["extra_headers"]

0 commit comments

Comments
 (0)