Skip to content

Commit 717ca2d

Browse files
committed
add docstrings
1 parent a0243ef commit 717ca2d

4 files changed

Lines changed: 249 additions & 51 deletions

File tree

langfuse/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from ._client.constants import ObservationTypeLiteral
88
from ._client.get_client import get_client
99
from ._client.observe import observe
10+
from ._client.propagation import propagate_attributes
1011
from ._client.span import (
1112
LangfuseAgent,
1213
LangfuseChain,
@@ -26,6 +27,7 @@
2627
"Langfuse",
2728
"get_client",
2829
"observe",
30+
"propagate_attributes",
2931
"ObservationTypeLiteral",
3032
"LangfuseSpan",
3133
"LangfuseGeneration",

langfuse/_client/client.py

Lines changed: 47 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1629,9 +1629,44 @@ def update_current_trace(
16291629
) -> None:
16301630
"""Update the current trace with additional information.
16311631
1632-
This method updates the Langfuse trace that the current span belongs to. It's useful for
1633-
adding trace-level metadata like user ID, session ID, or tags that apply to
1634-
the entire Langfuse trace rather than just a single observation.
1632+
.. deprecated:: 3.9.0
1633+
This method is deprecated and will be removed in a future version.
1634+
Use :func:`langfuse.propagate_attributes` instead.
1635+
1636+
**Current behavior**: This method still works as expected - the Langfuse backend
1637+
handles setting trace-level attributes server-side. However, it will be removed
1638+
in a future version, so please migrate to ``propagate_attributes()``.
1639+
1640+
**Why deprecated**: This method only sets attributes on a single span, which means
1641+
child spans created later won't have these attributes. This causes gaps when
1642+
using Langfuse aggregation queries (e.g., filtering by user_id or calculating
1643+
costs per session_id) because only the span with the attribute is included.
1644+
1645+
**Migration**: Replace with ``propagate_attributes()`` to set attributes on ALL
1646+
child spans created within the context. Call it as early as possible in your trace:
1647+
1648+
.. code-block:: python
1649+
1650+
# OLD (deprecated)
1651+
with langfuse.start_as_current_span(name="handle-request") as span:
1652+
user = authenticate_user(request)
1653+
langfuse.update_current_trace(
1654+
user_id=user.id,
1655+
session_id=request.session_id
1656+
)
1657+
# Child spans created here won't have user_id/session_id
1658+
response = process_request(request)
1659+
1660+
# NEW (recommended)
1661+
with langfuse.start_as_current_span(name="handle-request"):
1662+
user = authenticate_user(request)
1663+
with langfuse.propagate_attributes(
1664+
user_id=user.id,
1665+
session_id=request.session_id,
1666+
metadata={"environment": "production"}
1667+
):
1668+
# All child spans will have these attributes
1669+
response = process_request(request)
16351670
16361671
Args:
16371672
name: Updated name for the Langfuse trace
@@ -1644,28 +1679,17 @@ def update_current_trace(
16441679
tags: List of tags to categorize the Langfuse trace
16451680
public: Whether the Langfuse trace should be publicly accessible
16461681
1647-
Example:
1648-
```python
1649-
with langfuse.start_as_current_span(name="handle-request") as span:
1650-
# Get user information
1651-
user = authenticate_user(request)
1652-
1653-
# Update trace with user context
1654-
langfuse.update_current_trace(
1655-
user_id=user.id,
1656-
session_id=request.session_id,
1657-
tags=["production", "web-app"]
1658-
)
1659-
1660-
# Continue processing
1661-
response = process_request(request)
1662-
1663-
# Update span with results
1664-
span.update(output=response)
1665-
```
1682+
See Also:
1683+
:func:`langfuse.propagate_attributes`: Recommended replacement
16661684
"""
16671685
warnings.warn(
1668-
"update_current_trace is deprecated and will be removed in a future version. Use `with langfuse.propagate_attributes(...)` instead. ",
1686+
"update_current_trace() is deprecated and will be removed in a future version. "
1687+
"While it still works (handled server-side), it only sets attributes on a single span, "
1688+
"causing gaps in aggregation queries. "
1689+
"Migrate to `with langfuse.propagate_attributes(user_id=..., session_id=..., metadata={...})` "
1690+
"to propagate attributes to ALL child spans. Call propagate_attributes() as early "
1691+
"as possible in your trace for complete coverage. "
1692+
"See: https://langfuse.com/docs/sdk/python/decorators#trace-level-attributes",
16691693
DeprecationWarning,
16701694
stacklevel=2,
16711695
)

langfuse/_client/propagation.py

Lines changed: 158 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
"""Attribute propagation utilities for Langfuse OpenTelemetry integration.
2+
3+
This module provides the `propagate_attributes` context manager for setting trace-level
4+
attributes (user_id, session_id, metadata) that automatically propagate to all child spans
5+
within the context.
6+
"""
7+
18
from typing import Any, Dict, Generator, List, Literal, Optional, Union
29

310
from opentelemetry import baggage
@@ -26,10 +33,123 @@ def propagate_attributes(
2633
metadata: Optional[Dict[str, str]] = None,
2734
as_baggage: bool = False,
2835
) -> Generator[Any, Any, Any]:
36+
"""Propagate trace-level attributes to all spans created within this context.
37+
38+
This context manager sets attributes on the currently active span AND automatically
39+
propagates them to all new child spans created within the context. This is the
40+
recommended way to set trace-level attributes like user_id, session_id, and metadata
41+
dimensions that should be consistently applied across all observations in a trace.
42+
43+
**IMPORTANT**: Call this as early as possible within your trace/workflow. Only the
44+
currently active span and spans created after entering this context will have these
45+
attributes. Pre-existing spans will NOT be retroactively updated.
46+
47+
**Why this matters**: Langfuse aggregation queries (e.g., total cost by user_id,
48+
filtering by session_id) only include observations that have the attribute set.
49+
If you call `propagate_attributes` late in your workflow, earlier spans won't be
50+
included in aggregations for that attribute.
51+
52+
Args:
53+
user_id: User identifier to associate with all spans in this context.
54+
Must be US-ASCII string, ≤200 characters. Use this to track which user
55+
generated each trace and enable e.g. per-user cost/performance analysis.
56+
session_id: Session identifier to associate with all spans in this context.
57+
Must be US-ASCII string, ≤200 characters. Use this to group related traces
58+
within a user session (e.g., a conversation thread, multi-turn interaction).
59+
metadata: Additional key-value metadata to propagate to all spans.
60+
- Keys and values must be US-ASCII strings
61+
- All values must be ≤200 characters
62+
- Use for dimensions like internal correlating identifiers
63+
- AVOID: large payloads, sensitive data, non-string values (will be dropped with warning)
64+
as_baggage: If True, propagates attributes using OpenTelemetry baggage for
65+
cross-process/service propagation. **Security warning**: When enabled,
66+
attribute values are added to HTTP headers on ALL outbound requests.
67+
Only enable if values are safe to transmit via HTTP headers and you need
68+
cross-service tracing. Default: False.
69+
70+
Returns:
71+
Context manager that propagates attributes to all child spans.
72+
73+
Example:
74+
Basic usage with user and session tracking:
75+
76+
```python
77+
from langfuse import Langfuse
78+
79+
langfuse = Langfuse()
80+
81+
# Set attributes early in the trace
82+
with langfuse.start_as_current_span(name="user_workflow") as span:
83+
with langfuse.propagate_attributes(
84+
user_id="user_123",
85+
session_id="session_abc",
86+
metadata={"experiment": "variant_a", "environment": "production"}
87+
):
88+
# All spans created here will have user_id, session_id, and metadata
89+
with langfuse.start_span(name="llm_call") as llm_span:
90+
# This span inherits: user_id, session_id, experiment, environment
91+
...
92+
93+
with langfuse.start_generation(name="completion") as gen:
94+
# This span also inherits all attributes
95+
...
96+
```
97+
98+
Late propagation (anti-pattern):
99+
100+
```python
101+
with langfuse.start_as_current_span(name="workflow") as span:
102+
# These spans WON'T have user_id
103+
early_span = langfuse.start_span(name="early_work")
104+
early_span.end()
105+
106+
# Set attributes in the middle
107+
with langfuse.propagate_attributes(user_id="user_123"):
108+
# Only spans created AFTER this point will have user_id
109+
late_span = langfuse.start_span(name="late_work")
110+
late_span.end()
111+
112+
# Result: Aggregations by user_id will miss "early_work" span
113+
```
114+
115+
Cross-service propagation with baggage (advanced):
116+
117+
```python
118+
# Service A - originating service
119+
with langfuse.start_as_current_span(name="api_request"):
120+
with langfuse.propagate_attributes(
121+
user_id="user_123",
122+
session_id="session_abc",
123+
as_baggage=True # Propagate via HTTP headers
124+
):
125+
# Make HTTP request to Service B
126+
response = requests.get("https://service-b.example.com/api")
127+
# user_id and session_id are now in HTTP headers
128+
129+
# Service B - downstream service
130+
# OpenTelemetry will automatically extract baggage from HTTP headers
131+
# and propagate to spans in Service B
132+
```
133+
134+
Note:
135+
- **Nesting**: Nesting `propagate_attributes` contexts is possible but
136+
discouraged. Inner contexts will overwrite outer values for the same keys.
137+
- **Migration**: This replaces the deprecated `update_trace()` and
138+
`update_current_trace()` methods, which only set attributes on a single span
139+
(causing aggregation gaps). Always use `propagate_attributes` for new code.
140+
- **Validation**: All attribute values (user_id, session_id, metadata values)
141+
must be strings ≤200 characters. Invalid values will be dropped with a
142+
warning logged. Ensure values meet constraints before calling.
143+
- **OpenTelemetry**: This uses OpenTelemetry context propagation under the hood,
144+
making it compatible with other OTel-instrumented libraries.
145+
146+
Raises:
147+
No exceptions are raised. Invalid values are logged as warnings and dropped.
148+
"""
29149
context = otel_context_api.get_current()
30150
current_span = otel_trace_api.get_current_span()
31151

32-
if user_id is not None:
152+
if user_id is not None and _validate_propagated_string(user_id, "user_id"):
33153
context = _set_propagated_attribute(
34154
key="user_id",
35155
value=user_id,
@@ -38,7 +158,7 @@ def propagate_attributes(
38158
as_baggage=as_baggage,
39159
)
40160

41-
if session_id is not None:
161+
if session_id is not None and _validate_propagated_string(session_id, "session_id"):
42162
context = _set_propagated_attribute(
43163
key="session_id",
44164
value=session_id,
@@ -48,13 +168,20 @@ def propagate_attributes(
48168
)
49169

50170
if metadata is not None:
51-
context = _set_propagated_attribute(
52-
key="metadata",
53-
value=_validate_propagated_metadata(metadata),
54-
context=context,
55-
span=current_span,
56-
as_baggage=as_baggage,
57-
)
171+
# Filter metadata to only include valid string values
172+
validated_metadata: Dict[str, str] = {}
173+
for key, value in metadata.items():
174+
if _validate_propagated_string(value, f"metadata.{key}"):
175+
validated_metadata[key] = value
176+
177+
if validated_metadata:
178+
context = _set_propagated_attribute(
179+
key="metadata",
180+
value=validated_metadata,
181+
context=context,
182+
span=current_span,
183+
as_baggage=as_baggage,
184+
)
58185

59186
# Activate context, execute, and detach context
60187
token = otel_context_api.attach(context=context)
@@ -87,6 +214,9 @@ def _get_propagated_attributes_from_context(
87214
context_key = _get_propagated_context_key(key)
88215
value = otel_context_api.get_value(key=context_key, context=context)
89216

217+
if value is None:
218+
continue
219+
90220
if isinstance(value, dict):
91221
# Handle metadata
92222
for k, v in value.items():
@@ -153,25 +283,29 @@ def _set_propagated_attribute(
153283
return context
154284

155285

156-
def _validate_propagated_metadata(metadata: Dict[str, str]) -> Dict[str, str]:
157-
validated_metadata: Dict[str, str] = {}
286+
def _validate_propagated_string(value: str, attribute_name: str) -> bool:
287+
"""Validate a propagated attribute string value.
158288
159-
for key, value in metadata.items():
160-
if not isinstance(value, str):
161-
langfuse_logger.warning( # type: ignore
162-
f"Propagated attribute value of '{key}' not a string. Dropping value."
163-
)
164-
continue
289+
Args:
290+
value: The string value to validate
291+
attribute_name: Name of the attribute for error messages
165292
166-
if len(value) > 200:
167-
langfuse_logger.warning(
168-
f"Propagated attribute value of '{key}' is over 200 characters ({len(value)} chars). Dropping value."
169-
)
170-
continue
293+
Returns:
294+
True if valid, False otherwise (with warning logged)
295+
"""
296+
if not isinstance(value, str):
297+
langfuse_logger.warning( # type: ignore
298+
f"Propagated attribute '{attribute_name}' value is not a string. Dropping value."
299+
)
300+
return False
171301

172-
validated_metadata[key] = value
302+
if len(value) > 200:
303+
langfuse_logger.warning(
304+
f"Propagated attribute '{attribute_name}' value is over 200 characters ({len(value)} chars). Dropping value."
305+
)
306+
return False
173307

174-
return validated_metadata
308+
return True
175309

176310

177311
def _get_propagated_context_key(key: PropagatedKeys) -> str:

langfuse/_client/span.py

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -223,9 +223,38 @@ def update_trace(
223223
) -> "LangfuseObservationWrapper":
224224
"""Update the trace that this span belongs to.
225225
226-
This method updates trace-level attributes of the trace that this span
227-
belongs to. This is useful for adding or modifying trace-wide information
228-
like user ID, session ID, or tags.
226+
.. deprecated:: 3.9.0
227+
This method is deprecated and will be removed in a future version.
228+
Use :func:`langfuse.propagate_attributes` instead.
229+
230+
**Current behavior**: This method still works as expected - the Langfuse backend
231+
handles setting trace-level attributes server-side. However, it will be removed
232+
in a future version, so please migrate to ``propagate_attributes()``.
233+
234+
**Why deprecated**: This method only sets attributes on a single span, which means
235+
child spans created later won't have these attributes. This causes gaps when
236+
using Langfuse aggregation queries (e.g., filtering by user_id or calculating
237+
costs per session_id) because only the span with the attribute is included.
238+
239+
**Migration**: Replace with ``propagate_attributes()`` to set attributes on ALL
240+
child spans created within the context. Call it as early as possible in your trace:
241+
242+
.. code-block:: python
243+
244+
# OLD (deprecated)
245+
with langfuse.start_as_current_span(name="workflow") as span:
246+
span.update_trace(user_id="user_123", session_id="session_abc")
247+
# Child spans won't have user_id/session_id
248+
249+
# NEW (recommended)
250+
with langfuse.start_as_current_span(name="workflow"):
251+
with langfuse.propagate_attributes(
252+
user_id="user_123",
253+
session_id="session_abc",
254+
metadata={"experiment": "variant_a"}
255+
):
256+
# All child spans will have these attributes
257+
pass
229258
230259
Args:
231260
name: Updated name for the trace
@@ -237,9 +266,18 @@ def update_trace(
237266
metadata: Additional metadata to associate with the trace
238267
tags: List of tags to categorize the trace
239268
public: Whether the trace should be publicly accessible
269+
270+
See Also:
271+
:func:`langfuse.propagate_attributes`: Recommended replacement
240272
"""
241273
warnings.warn(
242-
"update_trace is deprecated and will be removed in a future version. Use `with langfuse.propagate_attributes(...)` instead. ",
274+
"update_trace() is deprecated and will be removed in a future version. "
275+
"While it still works (handled server-side), it only sets attributes on a single span, "
276+
"causing gaps in aggregation queries. "
277+
"Migrate to `with langfuse.propagate_attributes(user_id=..., session_id=..., metadata={...})` "
278+
"to propagate attributes to ALL child spans. Call propagate_attributes() as early "
279+
"as possible in your trace for complete coverage. "
280+
"See: https://langfuse.com/docs/sdk/python/decorators#trace-level-attributes",
243281
DeprecationWarning,
244282
stacklevel=2,
245283
)

0 commit comments

Comments
 (0)