Skip to content

Commit df6b82d

Browse files
authored
feat: propagate trace attributes onto all child spans on update (#1396)
1 parent d210b49 commit df6b82d

File tree

7 files changed

+120
-164
lines changed

7 files changed

+120
-164
lines changed

langfuse/_client/attributes.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
ObservationTypeGenerationLike,
1919
ObservationTypeSpanLike,
2020
)
21-
2221
from langfuse._utils.serializer import EventSerializer
2322
from langfuse.model import PromptClient
2423
from langfuse.types import MapValue, SpanLevel

langfuse/_client/client.py

Lines changed: 46 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,27 @@
1616
Any,
1717
Callable,
1818
Dict,
19+
Generator,
1920
List,
2021
Literal,
2122
Optional,
2223
Type,
2324
Union,
2425
cast,
2526
overload,
26-
Generator,
2727
)
2828

2929
import backoff
3030
import httpx
3131
from opentelemetry import (
3232
baggage as otel_baggage_api,
33-
trace as otel_trace_api,
33+
)
34+
from opentelemetry import (
3435
context as otel_context_api,
3536
)
37+
from opentelemetry import (
38+
trace as otel_trace_api,
39+
)
3640
from opentelemetry.sdk.trace import TracerProvider
3741
from opentelemetry.sdk.trace.id_generator import RandomIdGenerator
3842
from opentelemetry.util._decorator import (
@@ -43,14 +47,12 @@
4347

4448
from langfuse._client.attributes import LangfuseOtelSpanAttributes
4549
from langfuse._client.constants import (
50+
LANGFUSE_CORRELATION_CONTEXT_KEY,
4651
ObservationTypeGenerationLike,
4752
ObservationTypeLiteral,
4853
ObservationTypeLiteralNoEvent,
4954
ObservationTypeSpanLike,
5055
get_observation_types_list,
51-
LANGFUSE_CTX_USER_ID,
52-
LANGFUSE_CTX_SESSION_ID,
53-
LANGFUSE_CTX_METADATA,
5456
)
5557
from langfuse._client.datasets import DatasetClient, DatasetItemClient
5658
from langfuse._client.environment_variables import (
@@ -76,7 +78,10 @@
7678
LangfuseSpan,
7779
LangfuseTool,
7880
)
79-
from langfuse._client.utils import run_async_safely
81+
from langfuse._client.utils import (
82+
get_attribute_key_from_correlation_context,
83+
run_async_safely,
84+
)
8085
from langfuse._utils import _get_timestamp
8186
from langfuse._utils.parse_error import handle_fern_exception
8287
from langfuse._utils.prompt_cache import PromptCache
@@ -219,10 +224,8 @@ def __init__(
219224
additional_headers: Optional[Dict[str, str]] = None,
220225
tracer_provider: Optional[TracerProvider] = None,
221226
):
222-
self._host = (
223-
host
224-
if host is not None
225-
else os.environ.get(LANGFUSE_HOST, "https://cloud.langfuse.com")
227+
self._host = host or cast(
228+
str, os.environ.get(LANGFUSE_HOST, "https://cloud.langfuse.com")
226229
)
227230
self._environment = environment or cast(
228231
str, os.environ.get(LANGFUSE_TRACING_ENVIRONMENT)
@@ -361,19 +364,18 @@ def start_span(
361364
)
362365

363366
@_agnosticcontextmanager
364-
def with_attributes(
367+
def correlation_context(
365368
self,
366-
session_id: Optional[str] = None,
367-
user_id: Optional[str] = None,
368-
metadata: Optional[dict[str, str]] = None,
369+
correlation_context: Dict[str, str],
370+
*,
369371
as_baggage: bool = False,
370372
) -> Generator[None, None, None]:
371-
"""Creates a context manager that propagates the given attributes to all spans created within the context.
373+
"""Create a context manager that propagates the given correlation_context to all spans within the context manager's scope.
372374
373375
Args:
374-
session_id (str): Session identifier.
375-
user_id (str): User identifier.
376-
metadata (dict): Additional metadata to associate with all spans in the context. Values must be strings and are truncated to 200 characters.
376+
correlation_context (Dict[str, str]): Dictionary containing key-value pairs to be propagated
377+
to all spans within the context manager's scope. Common keys include user_id, session_id,
378+
and custom metadata. All values must be strings below 200 characters.
377379
as_baggage (bool, optional): If True, stores the values in OpenTelemetry baggage
378380
for cross-service propagation. If False, stores only in local context for
379381
current-service propagation. Defaults to False.
@@ -386,79 +388,55 @@ def with_attributes(
386388
outbound requests made within this context. Only use this for non-sensitive
387389
identifiers that are safe to transmit across service boundaries.
388390
389-
Example:
391+
Examples:
390392
```python
391-
# Local context only (default)
392-
with langfuse.with_attributes(session_id="session_123"):
393+
# Local context only (default) - pass context as dictionary
394+
with langfuse.correlation_context({"session_id": "session_123"}):
393395
with langfuse.start_as_current_span(name="process-request") as span:
394396
# This span and all its children will have session_id="session_123"
395397
child_span = langfuse.start_span(name="child-operation")
396398
399+
# Multiple values in context dictionary
400+
with langfuse.correlation_context({"user_id": "user_456", "experiment": "A"}):
401+
# All spans will have both user_id and experiment attributes
402+
span = langfuse.start_span(name="experiment-operation")
403+
397404
# Cross-service propagation (use with caution)
398-
with langfuse.with_attributes(session_id="session_123", as_baggage=True):
405+
with langfuse.correlation_context({"session_id": "session_123"}, as_baggage=True):
399406
# session_id will be propagated to external service calls
400407
response = requests.get("https://api.example.com/data")
401408
```
402409
"""
403410
current_context = otel_context_api.get_current()
404411
current_span = otel_trace_api.get_current_span()
405412

406-
# Process session_id
407-
if session_id is not None:
408-
current_context = otel_context_api.set_value(
409-
LANGFUSE_CTX_SESSION_ID, session_id, current_context
410-
)
411-
if current_span is not None and current_span.is_recording():
412-
current_span.set_attribute("session.id", session_id)
413-
if as_baggage:
414-
current_context = otel_baggage_api.set_baggage(
415-
"session.id", session_id, current_context
416-
)
413+
current_context = otel_context_api.set_value(
414+
LANGFUSE_CORRELATION_CONTEXT_KEY, correlation_context, current_context
415+
)
417416

418-
# Process user_id
419-
if user_id is not None:
420-
current_context = otel_context_api.set_value(
421-
LANGFUSE_CTX_USER_ID, user_id, current_context
422-
)
423-
if current_span is not None and current_span.is_recording():
424-
current_span.set_attribute("user.id", user_id)
425-
if as_baggage:
426-
current_context = otel_baggage_api.set_baggage(
427-
"user.id", user_id, current_context
417+
for key, value in correlation_context.items():
418+
if len(value) > 200:
419+
langfuse_logger.warning(
420+
f"Correlation context key '{key}' is over 200 characters ({len(value)} chars). Dropping value."
428421
)
422+
continue
429423

430-
# Process metadata
431-
if metadata is not None:
432-
# Truncate values with size > 200 to 200 characters and emit warning including the ky
433-
for k, v in metadata.items():
434-
if not isinstance(v, str):
435-
# Ignore unreachable mypy warning as this runtime guard should make sense either way
436-
warnings.warn( # type: ignore[unreachable]
437-
f"Metadata values must be strings, got {type(v)} for key '{k}'"
438-
)
439-
del metadata[k]
440-
if len(v) > 200:
441-
warnings.warn(
442-
f"Metadata value for key '{k}' exceeds 200 characters and will be truncated."
443-
)
444-
metadata[k] = v[:200]
424+
attribute_key = get_attribute_key_from_correlation_context(key)
445425

446-
current_context = otel_context_api.set_value(
447-
LANGFUSE_CTX_METADATA, metadata, current_context
448-
)
449426
if current_span is not None and current_span.is_recording():
450-
for k, v in metadata.items():
451-
current_span.set_attribute(f"langfuse.metadata.{k}", v)
427+
current_span.set_attribute(attribute_key, value)
428+
452429
if as_baggage:
453-
for k, v in metadata.items():
454-
current_context = otel_baggage_api.set_baggage(
455-
f"langfuse.metadata.{k}", str(v), current_context
456-
)
430+
current_context = otel_baggage_api.set_baggage(
431+
key, value, current_context
432+
)
457433

458434
# Activate context, execute, and detach context
459435
token = otel_context_api.attach(current_context)
436+
460437
try:
461438
yield
439+
462440
finally:
463441
otel_context_api.detach(token)
464442

@@ -1780,7 +1758,7 @@ def update_current_trace(
17801758
```
17811759
"""
17821760
warnings.warn(
1783-
"update_current_trace is deprecated and will be removed in a future version. Use `with langfuse.with_attributes(...)` instead. ",
1761+
"update_current_trace is deprecated and will be removed in a future version. Use `with langfuse.correlation_context(...)` instead. ",
17841762
DeprecationWarning,
17851763
stacklevel=2,
17861764
)

langfuse/_client/constants.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,13 @@
33
This module defines constants used throughout the Langfuse OpenTelemetry integration.
44
"""
55

6-
from typing import Literal, List, get_args, Union, Any
6+
from typing import Any, List, Literal, Union, get_args
7+
78
from typing_extensions import TypeAlias
89

910
LANGFUSE_TRACER_NAME = "langfuse-sdk"
1011

11-
# Context key constants for Langfuse context propagation
12-
LANGFUSE_CTX_USER_ID = "langfuse.ctx.user.id"
13-
LANGFUSE_CTX_SESSION_ID = "langfuse.ctx.session.id"
14-
LANGFUSE_CTX_METADATA = "langfuse.ctx.metadata"
12+
LANGFUSE_CORRELATION_CONTEXT_KEY = "langfuse.ctx.correlation"
1513

1614

1715
"""Note: this type is used with .__args__ / get_args in some cases and therefore must remain flat"""

langfuse/_client/span.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
and scoring integration specific to Langfuse's observability platform.
1414
"""
1515

16+
import warnings
1617
from datetime import datetime
1718
from time import time_ns
18-
import warnings
1919
from typing import (
2020
TYPE_CHECKING,
2121
Any,
@@ -44,10 +44,10 @@
4444
create_trace_attributes,
4545
)
4646
from langfuse._client.constants import (
47-
ObservationTypeLiteral,
4847
ObservationTypeGenerationLike,
49-
ObservationTypeSpanLike,
48+
ObservationTypeLiteral,
5049
ObservationTypeLiteralNoEvent,
50+
ObservationTypeSpanLike,
5151
get_observation_types_list,
5252
)
5353
from langfuse.logger import langfuse_logger
@@ -234,7 +234,7 @@ def update_trace(
234234
public: Whether the trace should be publicly accessible
235235
"""
236236
warnings.warn(
237-
"update_trace is deprecated and will be removed in a future version. Use `with langfuse.with_attributes(...)` instead. ",
237+
"update_trace is deprecated and will be removed in a future version. Use `with langfuse.correlation_context(...)` instead. ",
238238
DeprecationWarning,
239239
stacklevel=2,
240240
)

0 commit comments

Comments
 (0)