Skip to content

Commit 2c83216

Browse files
authored
fix(telemetry): added latest semantic conventions as span attributes for langfuse (#1768)
1 parent 1df2438 commit 2c83216

2 files changed

Lines changed: 274 additions & 133 deletions

File tree

src/strands/telemetry/tracer.py

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,17 @@ def _parse_semconv_opt_in(self) -> set[str]:
110110
opt_in_env = os.getenv("OTEL_SEMCONV_STABILITY_OPT_IN", "")
111111
return {value.strip() for value in opt_in_env.split(",")}
112112

113+
@property
114+
def is_langfuse(self) -> bool:
115+
"""Check if Langfuse is configured as the OTLP endpoint.
116+
117+
Returns:
118+
True if Langfuse is the OTLP endpoint, False otherwise.
119+
"""
120+
return "langfuse" in os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "") or "langfuse" in os.getenv(
121+
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", ""
122+
)
123+
113124
def _start_span(
114125
self,
115126
span_name: str,
@@ -142,23 +153,10 @@ def _start_span(
142153

143154
# Add all provided attributes
144155
if attributes:
145-
self._set_attributes(span, attributes)
156+
span.set_attributes(attributes)
146157

147158
return span
148159

149-
def _set_attributes(self, span: Span, attributes: dict[str, AttributeValue]) -> None:
150-
"""Set attributes on a span, handling different value types appropriately.
151-
152-
Args:
153-
span: The span to set attributes on
154-
attributes: Dictionary of attributes to set
155-
"""
156-
if not span:
157-
return
158-
159-
for key, value in attributes.items():
160-
span.set_attribute(key, value)
161-
162160
def _add_optional_usage_and_metrics_attributes(
163161
self, attributes: dict[str, AttributeValue], usage: Usage, metrics: Metrics
164162
) -> None:
@@ -203,7 +201,7 @@ def _end_span(
203201

204202
# Add any additional attributes
205203
if attributes:
206-
self._set_attributes(span, attributes)
204+
span.set_attributes(attributes)
207205

208206
# Handle error if present
209207
if error:
@@ -236,17 +234,24 @@ def end_span_with_error(self, span: Span, error_message: str, exception: Excepti
236234
error = exception or Exception(error_message)
237235
self._end_span(span, error=error)
238236

239-
def _add_event(self, span: Span | None, event_name: str, event_attributes: Attributes) -> None:
237+
def _add_event(
238+
self, span: Span | None, event_name: str, event_attributes: Attributes, to_span_attributes: bool = False
239+
) -> None:
240240
"""Add an event with attributes to a span.
241241
242242
Args:
243243
span: The span to add the event to
244244
event_name: Name of the event
245245
event_attributes: Dictionary of attributes to set on the event
246+
to_span_attributes: Add the attributes to span attributes
246247
"""
247248
if not span:
248249
return
249250

251+
# Add to span attribute since some backend can't read the events
252+
if to_span_attributes and event_attributes:
253+
span.set_attributes(event_attributes)
254+
250255
span.add_event(event_name, attributes=event_attributes)
251256

252257
def _get_event_name_for_message(self, message: Message) -> str:
@@ -358,6 +363,7 @@ def end_model_invoke_span(
358363
]
359364
),
360365
},
366+
to_span_attributes=self.is_langfuse,
361367
)
362368
else:
363369
self._add_event(
@@ -366,7 +372,7 @@ def end_model_invoke_span(
366372
event_attributes={"finish_reason": str(stop_reason), "message": serialize(message["content"])},
367373
)
368374

369-
self._set_attributes(span, attributes)
375+
span.set_attributes(attributes)
370376

371377
def start_tool_call_span(
372378
self,
@@ -423,6 +429,7 @@ def start_tool_call_span(
423429
]
424430
)
425431
},
432+
to_span_attributes=self.is_langfuse,
426433
)
427434
else:
428435
self._add_event(
@@ -476,6 +483,7 @@ def end_tool_call_span(self, span: Span, tool_result: ToolResult | None, error:
476483
]
477484
)
478485
},
486+
to_span_attributes=self.is_langfuse,
479487
)
480488
else:
481489
self._add_event(
@@ -572,6 +580,7 @@ def end_event_loop_cycle_span(
572580
]
573581
)
574582
},
583+
to_span_attributes=self.is_langfuse,
575584
)
576585
else:
577586
self._add_event(span, "gen_ai.choice", event_attributes=event_attributes)
@@ -666,6 +675,7 @@ def end_agent_span(
666675
]
667676
)
668677
},
678+
to_span_attributes=self.is_langfuse,
669679
)
670680
else:
671681
self._add_event(
@@ -675,9 +685,7 @@ def end_agent_span(
675685
)
676686

677687
if hasattr(response, "metrics") and hasattr(response.metrics, "accumulated_usage"):
678-
if "langfuse" in os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "") or "langfuse" in os.getenv(
679-
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", ""
680-
):
688+
if self.is_langfuse:
681689
attributes.update({"langfuse.observation.type": "span"})
682690
accumulated_usage = response.metrics.accumulated_usage
683691
attributes.update(
@@ -736,6 +744,7 @@ def start_multiagent_span(
736744
span,
737745
"gen_ai.client.inference.operation.details",
738746
{"gen_ai.input.messages": serialize([{"role": "user", "parts": parts}])},
747+
to_span_attributes=self.is_langfuse,
739748
)
740749
else:
741750
self._add_event(
@@ -767,6 +776,7 @@ def end_swarm_span(
767776
]
768777
)
769778
},
779+
to_span_attributes=self.is_langfuse,
770780
)
771781
else:
772782
self._add_event(
@@ -816,7 +826,10 @@ def _add_event_messages(self, span: Span, messages: Messages) -> None:
816826
{"role": message["role"], "parts": self._map_content_blocks_to_otel_parts(message["content"])}
817827
)
818828
self._add_event(
819-
span, "gen_ai.client.inference.operation.details", {"gen_ai.input.messages": serialize(input_messages)}
829+
span,
830+
"gen_ai.client.inference.operation.details",
831+
{"gen_ai.input.messages": serialize(input_messages)},
832+
to_span_attributes=self.is_langfuse,
820833
)
821834
else:
822835
for message in messages:

0 commit comments

Comments
 (0)