Skip to content

Commit 1e70201

Browse files
committed
feat: propagate trace attributes onto all child spans on update
1 parent 140d192 commit 1e70201

4 files changed

Lines changed: 755 additions & 41 deletions

File tree

langfuse/_client/client.py

Lines changed: 200 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""
55

66
import asyncio
7+
import json
78
import logging
89
import os
910
import re
@@ -27,8 +28,11 @@
2728

2829
import backoff
2930
import httpx
30-
from opentelemetry import trace
31-
from opentelemetry import trace as otel_trace_api
31+
from opentelemetry import (
32+
trace as otel_trace_api,
33+
baggage as otel_baggage_api,
34+
context as otel_context_api,
35+
)
3236
from opentelemetry.sdk.trace import TracerProvider
3337
from opentelemetry.sdk.trace.id_generator import RandomIdGenerator
3438
from opentelemetry.util._decorator import (
@@ -111,6 +115,11 @@
111115
)
112116
from langfuse.types import MaskFunction, ScoreDataType, SpanLevel, TraceContext
113117

118+
# Context key constants for Langfuse context propagation
119+
LANGFUSE_CTX_USER_ID = "langfuse.ctx.user.id"
120+
LANGFUSE_CTX_SESSION_ID = "langfuse.ctx.session.id"
121+
LANGFUSE_CTX_METADATA = "langfuse.ctx.metadata"
122+
114123

115124
class Langfuse:
116125
"""Main client for Langfuse tracing and platform features.
@@ -1667,6 +1676,11 @@ def update_current_trace(
16671676
span.update(output=response)
16681677
```
16691678
"""
1679+
warnings.warn(
1680+
"update_current_trace is deprecated and will be removed in a future version. ",
1681+
DeprecationWarning,
1682+
stacklevel=2,
1683+
)
16701684
if not self._tracing_enabled:
16711685
langfuse_logger.debug(
16721686
"Operation skipped: update_current_trace - Tracing is disabled or client is in no-op mode."
@@ -1811,7 +1825,7 @@ def _create_remote_parent_span(
18111825
is_remote=False,
18121826
)
18131827

1814-
return trace.NonRecordingSpan(span_context)
1828+
return otel_trace_api.NonRecordingSpan(span_context)
18151829

18161830
def _is_valid_trace_id(self, trace_id: str) -> bool:
18171831
pattern = r"^[0-9a-f]{32}$"
@@ -3450,3 +3464,186 @@ def clear_prompt_cache(self) -> None:
34503464
"""
34513465
if self._resources is not None:
34523466
self._resources.prompt_cache.clear()
3467+
3468+
@_agnosticcontextmanager
3469+
def session(self, id: str, *, as_baggage: bool = False) -> _AgnosticContextManager:
3470+
"""Create a session context manager that propagates session_id to all child spans.
3471+
3472+
Args:
3473+
id (str): The session identifier to propagate to child spans.
3474+
as_baggage (bool, optional): If True, stores the session_id in OpenTelemetry baggage
3475+
for cross-service propagation. If False, stores only in local context for
3476+
current-service propagation. Defaults to False.
3477+
3478+
Returns:
3479+
Context manager that sets session_id on all spans created within its scope.
3480+
3481+
Warning:
3482+
When as_baggage=True, the session_id will be included in HTTP headers of any
3483+
outbound requests made within this context. Only use this for non-sensitive
3484+
identifiers that are safe to transmit across service boundaries.
3485+
3486+
Example:
3487+
```python
3488+
# Local context only (default)
3489+
with langfuse.session(id="session_123"):
3490+
with langfuse.start_as_current_span(name="process-request") as span:
3491+
# This span and all its children will have session_id="session_123"
3492+
child_span = langfuse.start_span(name="child-operation")
3493+
3494+
# Cross-service propagation (use with caution)
3495+
with langfuse.session(id="session_123", as_baggage=True):
3496+
# session_id will be propagated to external service calls
3497+
response = requests.get("https://api.example.com/data")
3498+
```
3499+
"""
3500+
# Set context variable
3501+
new_context = otel_context_api.set_value(LANGFUSE_CTX_SESSION_ID, id)
3502+
token = otel_context_api.attach(new_context)
3503+
3504+
# Set baggage if requested
3505+
baggage_token = None
3506+
if as_baggage:
3507+
new_baggage = otel_baggage_api.set_baggage("session.id", id)
3508+
baggage_token = otel_context_api.attach(new_baggage)
3509+
3510+
try:
3511+
yield
3512+
finally:
3513+
# Always detach context token
3514+
otel_context_api.detach(token)
3515+
3516+
# Detach baggage token if it was set
3517+
if baggage_token is not None:
3518+
otel_context_api.detach(baggage_token)
3519+
3520+
@_agnosticcontextmanager
3521+
def user(self, id: str, *, as_baggage: bool = False) -> _AgnosticContextManager:
3522+
"""Create a user context manager that propagates user_id to all child spans.
3523+
3524+
Args:
3525+
id (str): The user identifier to propagate to child spans.
3526+
as_baggage (bool, optional): If True, stores the user_id in OpenTelemetry baggage
3527+
for cross-service propagation. If False, stores only in local context for
3528+
current-service propagation. Defaults to False.
3529+
3530+
Returns:
3531+
Context manager that sets user_id on all spans created within its scope.
3532+
3533+
Warning:
3534+
When as_baggage=True, the user_id will be included in HTTP headers of any
3535+
outbound requests made within this context. This may leak sensitive user
3536+
information to external services. Use with extreme caution.
3537+
3538+
Example:
3539+
```python
3540+
# Local context only (default, recommended for user IDs)
3541+
with langfuse.user(id="user_456"):
3542+
with langfuse.start_as_current_span(name="user-action") as span:
3543+
# This span and all its children will have user_id="user_456"
3544+
pass
3545+
3546+
# Cross-service propagation (NOT recommended for sensitive user IDs)
3547+
with langfuse.user(id="public_user_456", as_baggage=True):
3548+
# user_id will be propagated to external service calls
3549+
response = requests.get("https://api.example.com/data")
3550+
```
3551+
"""
3552+
# Set context variable
3553+
new_context = otel_context_api.set_value(LANGFUSE_CTX_USER_ID, id)
3554+
token = otel_context_api.attach(new_context)
3555+
3556+
# Set baggage if requested
3557+
baggage_token = None
3558+
if as_baggage:
3559+
new_baggage = otel_baggage_api.set_baggage("user.id", id)
3560+
baggage_token = otel_context_api.attach(new_baggage)
3561+
3562+
try:
3563+
yield
3564+
finally:
3565+
# Always detach context token
3566+
otel_context_api.detach(token)
3567+
3568+
# Detach baggage token if it was set
3569+
if baggage_token is not None:
3570+
otel_context_api.detach(baggage_token)
3571+
3572+
@_agnosticcontextmanager
3573+
def metadata(
3574+
self, *, as_baggage: bool = False, **kwargs
3575+
) -> _AgnosticContextManager:
3576+
"""Create a metadata context manager that propagates metadata to all child spans.
3577+
3578+
Args:
3579+
as_baggage (bool, optional): If True, stores the metadata in OpenTelemetry baggage
3580+
for cross-service propagation. If False, stores only in local context for
3581+
current-service propagation. Defaults to False.
3582+
**kwargs: Metadata key-value pairs. Values should not exceed 200 characters.
3583+
3584+
Returns:
3585+
Context manager that sets metadata on all spans created within its scope.
3586+
3587+
Warning:
3588+
When as_baggage=True, all metadata key-value pairs will be included in HTTP
3589+
headers of any outbound requests made within this context. Ensure no sensitive
3590+
information is included in the metadata when using cross-service propagation.
3591+
3592+
Example:
3593+
```python
3594+
# Local context only (default)
3595+
with langfuse.metadata(experiment="A/B", version="1.2.3"):
3596+
with langfuse.start_as_current_span(name="experiment-run") as span:
3597+
# This span and all its children will have the metadata
3598+
pass
3599+
3600+
# Cross-service propagation (use with caution)
3601+
with langfuse.metadata(as_baggage=True, experiment="A/B", service="api"):
3602+
# metadata will be propagated to external service calls
3603+
response = requests.get("https://api.example.com/data")
3604+
```
3605+
"""
3606+
if not kwargs:
3607+
# No metadata to set, just yield
3608+
yield
3609+
return
3610+
3611+
# Convert metadata dict to JSON string for context storage
3612+
metadata_json = json.dumps(kwargs)
3613+
3614+
# Set context variable
3615+
new_context = otel_context_api.set_value(LANGFUSE_CTX_METADATA, metadata_json)
3616+
token = otel_context_api.attach(new_context)
3617+
3618+
# Set baggage if requested
3619+
baggage_tokens = []
3620+
if as_baggage:
3621+
current_baggage = otel_baggage_api.get_all()
3622+
new_baggage = current_baggage
3623+
3624+
# Add each metadata key-value pair to baggage
3625+
for key, value in kwargs.items():
3626+
# Convert value to string and truncate if needed for baggage
3627+
str_value = str(value)
3628+
if len(str_value) > 200:
3629+
str_value = str_value[:200]
3630+
3631+
baggage_key = f"metadata.{key}"
3632+
new_baggage = otel_baggage_api.set_baggage(
3633+
baggage_key, str_value, new_baggage
3634+
)
3635+
3636+
# Attach the new baggage context
3637+
if new_baggage != current_baggage:
3638+
baggage_token = otel_context_api.attach(new_baggage)
3639+
baggage_tokens.append(baggage_token)
3640+
3641+
try:
3642+
yield
3643+
finally:
3644+
# Always detach context token
3645+
otel_context_api.detach(token)
3646+
3647+
# Detach all baggage tokens if they were set
3648+
for baggage_token in baggage_tokens:
3649+
otel_context_api.detach(baggage_token)

0 commit comments

Comments
 (0)