Skip to content

Commit f4c9954

Browse files
ianchinirgaclaude
authored
fix(langchain): allow configuration of metadata key prefix (#3367)
Co-authored-by: Nir Gazit <[email protected]> Co-authored-by: Claude <[email protected]>
1 parent 589e104 commit f4c9954

File tree

9 files changed

+146
-130
lines changed

9 files changed

+146
-130
lines changed

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from opentelemetry.instrumentation.langchain.version import __version__
1717
from opentelemetry.instrumentation.utils import unwrap
1818
from opentelemetry.metrics import get_meter
19-
from opentelemetry.semconv_ai import Meters, SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY
19+
from opentelemetry.semconv_ai import Meters, SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY, SpanAttributes
2020
from opentelemetry.trace import get_tracer
2121
from opentelemetry.trace.propagation import set_span_in_context
2222
from opentelemetry.trace.propagation.tracecontext import (
@@ -37,10 +37,23 @@ def __init__(
3737
exception_logger=None,
3838
disable_trace_context_propagation=False,
3939
use_legacy_attributes: bool = True,
40+
metadata_key_prefix: str = SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES
4041
):
42+
"""Create a Langchain instrumentor instance.
43+
44+
Args:
45+
exception_logger: A callable that takes an Exception as input. This will be
46+
used to log exceptions that occur during instrumentation. If None, exceptions will not be logged.
47+
disable_trace_context_propagation: If True, disables trace context propagation to LLM providers.
48+
use_legacy_attributes: If True, uses span attributes for Inputs/Outputs instead of events.
49+
metadata_key_prefix: Prefix for metadata keys added to spans. Defaults to
50+
`SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES`.
51+
Useful for using with other backends.
52+
"""
4153
super().__init__()
4254
Config.exception_logger = exception_logger
4355
Config.use_legacy_attributes = use_legacy_attributes
56+
Config.metadata_key_prefix = metadata_key_prefix
4457
self.disable_trace_context_propagation = disable_trace_context_propagation
4558

4659
def instrumentation_dependencies(self) -> Collection[str]:

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
LLMResult,
2828
)
2929
from opentelemetry import context as context_api
30+
from opentelemetry.instrumentation.langchain.config import Config
3031
from opentelemetry.instrumentation.langchain.event_emitter import emit_event
3132
from opentelemetry.instrumentation.langchain.event_models import (
3233
ChoiceEvent,
@@ -290,7 +291,7 @@ def _create_span(
290291
for key, value in sanitized_metadata.items():
291292
_set_span_attribute(
292293
span,
293-
f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.{key}",
294+
f"{Config.metadata_key_prefix}.{key}",
294295
value,
295296
)
296297

@@ -752,7 +753,7 @@ def _handle_error(
752753
return
753754

754755
span = self._get_span(run_id)
755-
span.set_status(Status(StatusCode.ERROR))
756+
span.set_status(Status(StatusCode.ERROR), str(error))
756757
span.record_exception(error)
757758
self._end_span(span, run_id)
758759

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from typing import Optional
22

33
from opentelemetry._logs import Logger
4+
from opentelemetry.semconv_ai import SpanAttributes
45

56

67
class Config:
78
exception_logger = None
89
use_legacy_attributes = True
910
event_logger: Optional[Logger] = None
11+
metadata_key_prefix: str = SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ def set_chat_response_usage(
346346
)
347347
_set_span_attribute(
348348
span,
349-
SpanAttributes.GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS,
349+
SpanAttributes.LLM_USAGE_CACHE_READ_INPUT_TOKENS,
350350
cache_read_tokens,
351351
)
352352
if record_token_usage:

packages/opentelemetry-instrumentation-langchain/tests/test_chains.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -757,7 +757,7 @@ async def test_astream_with_events_with_content(
757757
assert len(chunks) == 144
758758

759759
logs = log_exporter.get_finished_logs()
760-
assert len(logs) == 1
760+
assert len(logs) == 2
761761

762762
# Validate user message Event
763763
assert_message_in_logs(
@@ -802,7 +802,7 @@ async def test_astream_with_events_with_no_content(
802802
assert len(chunks) == 144
803803

804804
logs = log_exporter.get_finished_logs()
805-
assert len(logs) == 1
805+
assert len(logs) == 2
806806

807807
# Validate user message Event
808808
assert_message_in_logs(logs[0], "gen_ai.user.message", {})

packages/opentelemetry-instrumentation-langchain/tests/test_documents_chains.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def test_sequential_chain_with_events_with_content(
9393
] == [span.name for span in spans]
9494

9595
logs = log_exporter.get_finished_logs()
96-
assert len(logs) == 1
96+
assert len(logs) == 2
9797

9898
# Validate user message Event
9999
assert_message_in_logs(
@@ -107,12 +107,12 @@ def test_sequential_chain_with_events_with_content(
107107
)
108108

109109
# Validate AI choice Event
110-
# _choice_event = {
111-
# "index": 0,
112-
# "finish_reason": "unknown",
113-
# "message": {"content": response["output_text"]},
114-
# }
115-
# assert_message_in_logs(logs[1], "gen_ai.choice", _choice_event)
110+
_choice_event = {
111+
"index": 0,
112+
"finish_reason": "unknown",
113+
"message": {"content": response["output_text"]},
114+
}
115+
assert_message_in_logs(logs[1], "gen_ai.choice", _choice_event)
116116

117117

118118
@pytest.mark.vcr
@@ -139,14 +139,14 @@ def test_sequential_chain_with_events_with_no_content(
139139
] == [span.name for span in spans]
140140

141141
logs = log_exporter.get_finished_logs()
142-
assert len(logs) == 1
142+
assert len(logs) == 2
143143

144144
# Validate user message Event
145145
assert_message_in_logs(logs[0], "gen_ai.user.message", {})
146146

147147
# Validate AI choice Event
148-
# choice_event = {"index": 0, "finish_reason": "unknown", "message": {}}
149-
# assert_message_in_logs(logs[1], "gen_ai.choice", choice_event)
148+
choice_event = {"index": 0, "finish_reason": "unknown", "message": {}}
149+
assert_message_in_logs(logs[1], "gen_ai.choice", choice_event)
150150

151151

152152
def assert_message_in_logs(log: LogData, event_name: str, expected_content: dict):

packages/opentelemetry-instrumentation-langchain/tests/test_lcel.py

Lines changed: 66 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ class Joke(BaseModel):
185185
assert output_parser_task_span.parent.span_id == workflow_span.context.span_id
186186

187187
logs = log_exporter.get_finished_logs()
188-
assert len(logs) == 2
188+
assert len(logs) == 3
189189

190190
# Validate system message Event
191191
assert_message_in_logs(
@@ -198,23 +198,23 @@ class Joke(BaseModel):
198198
)
199199

200200
# Validate AI choice Event
201-
# _choice_event = {
202-
# "index": 0,
203-
# "finish_reason": "function_call",
204-
# "message": {"content": ""},
205-
# "tool_calls": [
206-
# {
207-
# "id": "",
208-
# "function": {
209-
# "name": "Joke",
210-
# "arguments": '{"setup":"Why couldn\'t the bicycle stand up by itself?","punchline":"It was two '
211-
# 'tired!"}',
212-
# },
213-
# "type": "function",
214-
# }
215-
# ],
216-
# }
217-
# assert_message_in_logs(logs[2], "gen_ai.choice", _choice_event)
201+
_choice_event = {
202+
"index": 0,
203+
"finish_reason": "function_call",
204+
"message": {"content": ""},
205+
"tool_calls": [
206+
{
207+
"id": "",
208+
"function": {
209+
"name": "Joke",
210+
"arguments": '{"setup":"Why couldn\'t the bicycle stand up by itself?","punchline":"It was two '
211+
'tired!"}',
212+
},
213+
"type": "function",
214+
}
215+
],
216+
}
217+
assert_message_in_logs(logs[2], "gen_ai.choice", _choice_event)
218218

219219

220220
@pytest.mark.vcr
@@ -269,7 +269,7 @@ class Joke(BaseModel):
269269
assert output_parser_task_span.parent.span_id == workflow_span.context.span_id
270270

271271
logs = log_exporter.get_finished_logs()
272-
assert len(logs) == 2
272+
assert len(logs) == 3
273273

274274
# Validate system message Event
275275
assert_message_in_logs(logs[0], "gen_ai.system.message", {})
@@ -278,13 +278,13 @@ class Joke(BaseModel):
278278
assert_message_in_logs(logs[1], "gen_ai.user.message", {})
279279

280280
# Validate AI choice Event
281-
# _choice_event = {
282-
# "index": 0,
283-
# "finish_reason": "function_call",
284-
# "message": {},
285-
# "tool_calls": [{"function": {"name": "Joke"}, "id": "", "type": "function"}],
286-
# }
287-
# assert_message_in_logs(logs[2], "gen_ai.choice", _choice_event)
281+
_choice_event = {
282+
"index": 0,
283+
"finish_reason": "function_call",
284+
"message": {},
285+
"tool_calls": [{"function": {"name": "Joke"}, "id": "", "type": "function"}],
286+
}
287+
assert_message_in_logs(logs[2], "gen_ai.choice", _choice_event)
288288

289289

290290
@pytest.mark.vcr
@@ -382,7 +382,7 @@ async def test_async_lcel_with_events_with_content(
382382
assert output_parser_task_span.parent.span_id == workflow_span.context.span_id
383383

384384
logs = log_exporter.get_finished_logs()
385-
assert len(logs) == 1
385+
assert len(logs) == 2
386386

387387
# Validate user message Event
388388
assert_message_in_logs(
@@ -394,12 +394,12 @@ async def test_async_lcel_with_events_with_content(
394394
assert response != ""
395395

396396
# Validate AI choice Event
397-
# _choice_event = {
398-
# "index": 0,
399-
# "finish_reason": "stop",
400-
# "message": {"content": response},
401-
# }
402-
# assert_message_in_logs(logs[1], "gen_ai.choice", _choice_event)
397+
_choice_event = {
398+
"index": 0,
399+
"finish_reason": "stop",
400+
"message": {"content": response},
401+
}
402+
assert_message_in_logs(logs[1], "gen_ai.choice", _choice_event)
403403

404404

405405
@pytest.mark.vcr
@@ -441,18 +441,18 @@ async def test_async_lcel_with_events_with_no_content(
441441
assert output_parser_task_span.parent.span_id == workflow_span.context.span_id
442442

443443
logs = log_exporter.get_finished_logs()
444-
assert len(logs) == 1
444+
assert len(logs) == 2
445445

446446
# Validate user message Event
447447
assert_message_in_logs(logs[0], "gen_ai.user.message", {})
448448

449449
# Validate AI choice Event
450-
# _choice_event = {
451-
# "index": 0,
452-
# "finish_reason": "stop",
453-
# "message": {},
454-
# }
455-
# assert_message_in_logs(logs[1], "gen_ai.choice", _choice_event)
450+
_choice_event = {
451+
"index": 0,
452+
"finish_reason": "stop",
453+
"message": {},
454+
}
455+
assert_message_in_logs(logs[1], "gen_ai.choice", _choice_event)
456456

457457

458458
@pytest.mark.vcr
@@ -907,7 +907,7 @@ class Joke(BaseModel):
907907
) == set([span.name for span in spans])
908908

909909
logs = log_exporter.get_finished_logs()
910-
assert len(logs) == 2
910+
assert len(logs) == 3
911911

912912
# Validate system message Event
913913
assert_message_in_logs(
@@ -920,23 +920,23 @@ class Joke(BaseModel):
920920
)
921921

922922
# Validate AI choice Event
923-
# _choice_event = {
924-
# "index": 0,
925-
# "finish_reason": "function_call",
926-
# "message": {"content": ""},
927-
# "tool_calls": [
928-
# {
929-
# "id": "",
930-
# "function": {
931-
# "name": "Joke",
932-
# "arguments": '{"setup":"Why couldn\'t the bicycle stand up by '
933-
# 'itself?","punchline":"Because it was two tired!"}',
934-
# },
935-
# "type": "function",
936-
# }
937-
# ],
938-
# }
939-
# assert_message_in_logs(logs[2], "gen_ai.choice", _choice_event)
923+
_choice_event = {
924+
"index": 0,
925+
"finish_reason": "function_call",
926+
"message": {"content": ""},
927+
"tool_calls": [
928+
{
929+
"id": "",
930+
"function": {
931+
"name": "Joke",
932+
"arguments": '{"setup":"Why couldn\'t the bicycle stand up by '
933+
'itself?","punchline":"Because it was two tired!"}',
934+
},
935+
"type": "function",
936+
}
937+
],
938+
}
939+
assert_message_in_logs(logs[2], "gen_ai.choice", _choice_event)
940940

941941

942942
@pytest.mark.vcr
@@ -983,7 +983,7 @@ class Joke(BaseModel):
983983
) == set([span.name for span in spans])
984984

985985
logs = log_exporter.get_finished_logs()
986-
assert len(logs) == 2
986+
assert len(logs) == 3
987987

988988
# Validate system message Event
989989
assert_message_in_logs(logs[0], "gen_ai.system.message", {})
@@ -992,13 +992,13 @@ class Joke(BaseModel):
992992
assert_message_in_logs(logs[1], "gen_ai.user.message", {})
993993

994994
# Validate AI choice Event
995-
# _choice_event = {
996-
# "index": 0,
997-
# "finish_reason": "function_call",
998-
# "message": {},
999-
# "tool_calls": [{"function": {"name": "Joke"}, "id": "", "type": "function"}],
1000-
# }
1001-
# assert_message_in_logs(logs[2], "gen_ai.choice", _choice_event)
995+
_choice_event = {
996+
"index": 0,
997+
"finish_reason": "function_call",
998+
"message": {},
999+
"tool_calls": [{"function": {"name": "Joke"}, "id": "", "type": "function"}],
1000+
}
1001+
assert_message_in_logs(logs[2], "gen_ai.choice", _choice_event)
10021002

10031003

10041004
def assert_message_in_logs(log: LogData, event_name: str, expected_content: dict):

0 commit comments

Comments
 (0)