Skip to content

Commit 02d3dea

Browse files
committed
internal: showcase context propagation using baggage
1 parent 621b989 commit 02d3dea

6 files changed

Lines changed: 89 additions & 21 deletions

File tree

langfuse/_client/attributes.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
from langfuse.model import PromptClient
2424
from langfuse.types import MapValue, SpanLevel
2525

26+
from opentelemetry import baggage
27+
import opentelemetry.context as otel_context
28+
2629

2730
class LangfuseOtelSpanAttributes:
2831
# Langfuse-Trace attributes
@@ -61,6 +64,16 @@ class LangfuseOtelSpanAttributes:
6164
AS_ROOT = "langfuse.internal.as_root"
6265

6366

67+
def propagate_attributes(
68+
*,
69+
current_ctx: otel_context.Context,
70+
dict_to_propagate: Dict[str, Any],
71+
) -> otel_context.Context:
72+
for key, value in dict_to_propagate.items():
73+
current_ctx = baggage.set_baggage(key, str(value), context=current_ctx)
74+
return current_ctx
75+
76+
6477
def create_trace_attributes(
6578
*,
6679
name: Optional[str] = None,

langfuse/_client/span.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
TYPE_CHECKING,
2121
Any,
2222
Dict,
23+
Iterator,
2324
List,
2425
Literal,
2526
Optional,
@@ -29,7 +30,9 @@
2930
overload,
3031
)
3132

33+
from contextlib import contextmanager
3234
from opentelemetry import trace as otel_trace_api
35+
from opentelemetry import context
3336
from opentelemetry.util._decorator import _AgnosticContextManager
3437

3538
from langfuse.model import PromptClient
@@ -42,6 +45,7 @@
4245
create_generation_attributes,
4346
create_span_attributes,
4447
create_trace_attributes,
48+
propagate_attributes,
4549
)
4650
from langfuse._client.constants import (
4751
ObservationTypeLiteral,
@@ -203,6 +207,7 @@ def end(self, *, end_time: Optional[int] = None) -> "LangfuseObservationWrapper"
203207

204208
return self
205209

210+
@contextmanager
206211
def update_trace(
207212
self,
208213
*,
@@ -215,7 +220,7 @@ def update_trace(
215220
metadata: Optional[Any] = None,
216221
tags: Optional[List[str]] = None,
217222
public: Optional[bool] = None,
218-
) -> "LangfuseObservationWrapper":
223+
) -> Iterator[None]:
219224
"""Update the trace that this span belongs to.
220225
221226
This method updates trace-level attributes of the trace that this span
@@ -234,7 +239,7 @@ def update_trace(
234239
public: Whether the trace should be publicly accessible
235240
"""
236241
if not self._otel_span.is_recording():
237-
return self
242+
yield
238243

239244
media_processed_input = self._process_media_and_apply_mask(
240245
data=input, field="input", span=self._otel_span
@@ -258,9 +263,19 @@ def update_trace(
258263
public=public,
259264
)
260265

266+
ctx = otel_trace_api.set_span_in_context(self._otel_span)
267+
ctx = propagate_attributes(
268+
current_ctx=ctx,
269+
dict_to_propagate=attributes,
270+
)
271+
272+
token = context.attach(ctx)
261273
self._otel_span.set_attributes(attributes)
262274

263-
return self
275+
try:
276+
yield
277+
finally:
278+
context.detach(token)
264279

265280
@overload
266281
def score(

langfuse/_client/span_processor.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@
1515
import os
1616
from typing import Dict, List, Optional
1717

18+
from opentelemetry.context import Context
1819
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
19-
from opentelemetry.sdk.trace import ReadableSpan
20+
from opentelemetry.sdk.trace import ReadableSpan, Span
2021
from opentelemetry.sdk.trace.export import BatchSpanProcessor
22+
from opentelemetry.processor.baggage import BaggageSpanProcessor, ALLOW_ALL_BAGGAGE_KEYS
2123

2224
from langfuse._client.constants import LANGFUSE_TRACER_NAME
2325
from langfuse._client.environment_variables import (
@@ -65,6 +67,10 @@ def __init__(
6567
else []
6668
)
6769

70+
# Initialize a BaggageSpanProcessor so baggage keys are attached to spans.
71+
# Allow all baggage keys by default (same behaviour as before).
72+
self._baggage_processor = BaggageSpanProcessor(ALLOW_ALL_BAGGAGE_KEYS)
73+
6874
env_flush_at = os.environ.get(LANGFUSE_FLUSH_AT, None)
6975
flush_at = flush_at or int(env_flush_at) if env_flush_at is not None else None
7076

@@ -105,6 +111,16 @@ def __init__(
105111
else None,
106112
)
107113

114+
def on_start(self, span: Span, parent_context: Optional[Context] = None) -> None:
115+
# Forward to baggage processor first so baggage can be attached early.
116+
self._baggage_processor.on_start(span, parent_context)
117+
118+
# Call parent on_start if it exists (no-op for BatchSpanProcessor but safe).
119+
try:
120+
super().on_start(span, parent_context)
121+
except TypeError:
122+
super().on_start(span)
123+
108124
def on_end(self, span: ReadableSpan) -> None:
109125
# Only export spans that belong to the scoped project
110126
# This is important to not send spans to wrong project in multi-project setups

poetry.lock

Lines changed: 19 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ requests = "^2"
2020
opentelemetry-api = "^1.33.1"
2121
opentelemetry-sdk = "^1.33.1"
2222
opentelemetry-exporter-otlp-proto-http = "^1.33.1"
23+
opentelemetry-processor-baggage = "^0.58b0"
2324

2425
[tool.poetry.group.dev.dependencies]
2526
pytest = ">=7.4,<9.0"

tests/test_core_sdk.py

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -606,24 +606,24 @@ def test_score_trace_nested_observation():
606606

607607
# Create a parent span and set trace name
608608
with langfuse.start_as_current_span(name="parent-span") as parent_span:
609-
parent_span.update_trace(name=trace_name)
610-
611-
# Create a child span
612-
child_span = langfuse.start_span(name="span")
609+
# Set trace name and trace metadata as attributes on parent-span _and_ span using context manager.
610+
with parent_span.update_trace(name=trace_name, metadata={"foo": "bar"}):
611+
# Create a child span
612+
child_span = langfuse.start_span(name="span")
613613

614-
# Score the child span
615-
child_span.score(
616-
name="valuation",
617-
value=0.5,
618-
comment="This is a comment",
619-
)
614+
# Score the child span
615+
child_span.score(
616+
name="valuation",
617+
value=0.5,
618+
comment="This is a comment",
619+
)
620620

621-
# Get IDs for verification
622-
child_span_id = child_span.id
623-
trace_id = parent_span.trace_id
621+
# Get IDs for verification
622+
child_span_id = child_span.id
623+
trace_id = parent_span.trace_id
624624

625-
# End the child span
626-
child_span.end()
625+
# End the child span
626+
child_span.end()
627627

628628
# Ensure data is sent
629629
langfuse.flush()
@@ -635,6 +635,12 @@ def test_score_trace_nested_observation():
635635
assert trace.name == trace_name
636636
assert len(trace.scores) == 1
637637

638+
observations = trace.observations
639+
assert len(observations) == 2 # Parent span and child span
640+
# Do not verify the attributes, since we actually strip them on the server for Langfuse SDK spans.
641+
# for obs in observations:
642+
# assert obs.metadata["attributes"]["langfuse.trace.metadata.foo"] == "bar"
643+
638644
score = trace.scores[0]
639645

640646
assert score.name == "valuation"

0 commit comments

Comments
 (0)