Skip to content

Commit 023940a

Browse files
authored
feat(tracing): propagate environment attributes (#1726)
1 parent 58aee87 commit 023940a

5 files changed

Lines changed: 365 additions & 18 deletions

File tree

langfuse/_client/client.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,11 @@
8585
LangfuseSpan,
8686
LangfuseTool,
8787
)
88-
from langfuse._client.utils import get_sha256_hash_hex, run_async_safely
88+
from langfuse._client.utils import (
89+
get_sha256_hash_hex,
90+
get_string_span_attribute,
91+
run_async_safely,
92+
)
8993
from langfuse._utils import _get_timestamp, json_path
9094
from langfuse._utils.environment import get_common_release_envs
9195
from langfuse._utils.parse_error import handle_fern_exception
@@ -1820,6 +1824,7 @@ def create_score(
18201824
config_id: Optional[str] = None,
18211825
metadata: Optional[Any] = None,
18221826
timestamp: Optional[datetime] = None,
1827+
environment: Optional[str] = None,
18231828
) -> None: ...
18241829

18251830
@overload
@@ -1840,6 +1845,7 @@ def create_score(
18401845
config_id: Optional[str] = None,
18411846
metadata: Optional[Any] = None,
18421847
timestamp: Optional[datetime] = None,
1848+
environment: Optional[str] = None,
18431849
) -> None: ...
18441850

18451851
def create_score(
@@ -1857,6 +1863,7 @@ def create_score(
18571863
config_id: Optional[str] = None,
18581864
metadata: Optional[Any] = None,
18591865
timestamp: Optional[datetime] = None,
1866+
environment: Optional[str] = None,
18601867
) -> None:
18611868
"""Create a score for a specific trace or observation.
18621869
@@ -1876,6 +1883,14 @@ def create_score(
18761883
config_id: Optional ID of a score config defined in Langfuse
18771884
metadata: Optional metadata to be attached to the score
18781885
timestamp: Optional timestamp for the score (defaults to current UTC time)
1886+
environment: Optional environment override for this score. If omitted,
1887+
the score uses the client-level environment from
1888+
`Langfuse(environment=...)` or `LANGFUSE_TRACING_ENVIRONMENT`.
1889+
Langfuse observation wrapper methods pass their resolved span
1890+
environment here so scores created via `span.score()` or
1891+
`span.score_trace()` stay grouped with the scored observation or
1892+
trace, including request-scoped environments propagated with
1893+
`propagate_attributes(environment=...)`.
18791894
18801895
Example:
18811896
```python
@@ -1915,7 +1930,7 @@ def create_score(
19151930
dataType=data_type, # type: ignore
19161931
comment=comment,
19171932
configId=config_id,
1918-
environment=self._environment,
1933+
environment=environment or self._environment,
19191934
metadata=metadata,
19201935
)
19211936

@@ -2018,6 +2033,9 @@ def score_current_span(
20182033
20192034
This method scores the currently active span in the context. It's a convenient
20202035
way to score the current operation without needing to know its trace and span IDs.
2036+
If the active span has a `langfuse.environment` attribute, including one
2037+
set by `propagate_attributes(environment=...)`, the score uses that
2038+
environment. Otherwise it uses the client-level environment.
20212039
20222040
Args:
20232041
name: Name of the score (e.g., "relevance", "accuracy")
@@ -2065,6 +2083,9 @@ def score_current_span(
20652083
comment=comment,
20662084
config_id=config_id,
20672085
metadata=metadata,
2086+
environment=get_string_span_attribute(
2087+
current_span, LangfuseOtelSpanAttributes.ENVIRONMENT
2088+
),
20682089
)
20692090

20702091
@overload
@@ -2111,6 +2132,9 @@ def score_current_trace(
21112132
This method scores the trace of the currently active span. Unlike score_current_span,
21122133
this method associates the score with the entire trace rather than a specific span.
21132134
It's useful for scoring overall performance or quality of the entire operation.
2135+
If the active span has a `langfuse.environment` attribute, including one
2136+
set by `propagate_attributes(environment=...)`, the score uses that
2137+
environment. Otherwise it uses the client-level environment.
21142138
21152139
Args:
21162140
name: Name of the score (e.g., "user_satisfaction", "overall_quality")
@@ -2156,6 +2180,9 @@ def score_current_trace(
21562180
comment=comment,
21572181
config_id=config_id,
21582182
metadata=metadata,
2183+
environment=get_string_span_attribute(
2184+
current_span, LangfuseOtelSpanAttributes.ENVIRONMENT
2185+
),
21592186
)
21602187

21612188
def flush(self) -> None:

langfuse/_client/propagation.py

Lines changed: 91 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
"""Attribute propagation utilities for Langfuse OpenTelemetry integration.
22
33
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.
4+
attributes (user_id, session_id, metadata, environment, etc.) that automatically
5+
propagate to all child spans within the context.
66
"""
77

8+
import re
89
from typing import Any, Dict, Generator, List, Literal, Optional, TypedDict, Union, cast
910

1011
from opentelemetry import (
@@ -36,6 +37,7 @@
3637
"version",
3738
"tags",
3839
"trace_name",
40+
"environment",
3941
]
4042

4143
InternalPropagatedKeys = Literal[
@@ -55,6 +57,7 @@
5557
"version",
5658
"tags",
5759
"trace_name",
60+
"environment",
5861
"experiment_id",
5962
"experiment_name",
6063
"experiment_metadata",
@@ -99,14 +102,16 @@ def propagate_attributes(
99102
version: Optional[str] = None,
100103
tags: Optional[List[str]] = None,
101104
trace_name: Optional[str] = None,
105+
environment: Optional[str] = None,
102106
as_baggage: bool = False,
103107
) -> _AgnosticContextManager[Any]:
104108
"""Propagate trace-level attributes to all spans created within this context.
105109
106110
This context manager sets attributes on the currently active span AND automatically
107111
propagates them to all new child spans created within the context. This is the
108-
recommended way to set trace-level attributes like user_id, session_id, and metadata
109-
dimensions that should be consistently applied across all observations in a trace.
112+
recommended way to set trace-level attributes like user_id, session_id,
113+
environment, and metadata dimensions that should be consistently applied across
114+
all observations in a trace.
110115
111116
**IMPORTANT**: Call this as early as possible within your trace/workflow. Only the
112117
currently active span and spans created after entering this context will have these
@@ -134,9 +139,19 @@ def propagate_attributes(
134139
tags: List of tags to categorize the group of observations
135140
trace_name: Name to assign to the trace. Must be US-ASCII string, ≤200 characters.
136141
Use this to set a consistent trace name for all spans created within this context.
142+
environment: Langfuse environment to assign to spans created in this context.
143+
Must be a lowercase alphanumeric string with optional hyphens or underscores,
144+
must be ≤40 characters, and must not start with "langfuse". This maps to
145+
the first-class `langfuse.environment` attribute, not to trace metadata.
146+
Use it for request-scoped environments, for example when one shared proxy
147+
handles calls from dev, staging, qa, and prod. A propagated environment
148+
takes precedence over the local client default configured via
149+
`Langfuse(environment=...)` or `LANGFUSE_TRACING_ENVIRONMENT` for spans
150+
created while this propagation context is active.
137151
as_baggage: If True, propagates attributes using OpenTelemetry baggage for
138152
cross-process/service propagation. **Security warning**: When enabled,
139153
attribute values are added to HTTP headers on ALL outbound requests.
154+
This includes `environment` as the `langfuse_environment` baggage entry.
140155
Only enable if values are safe to transmit via HTTP headers and you need
141156
cross-service tracing. Default: False.
142157
@@ -156,11 +171,12 @@ def propagate_attributes(
156171
with langfuse.propagate_attributes(
157172
user_id="user_123",
158173
session_id="session_abc",
159-
metadata={"experiment": "variant_a", "environment": "production"}
174+
environment="production",
175+
metadata={"experiment": "variant_a"}
160176
):
161-
# All spans created here will have user_id, session_id, and metadata
177+
# All spans created here will have user_id, session_id, environment, and metadata
162178
with langfuse.start_observation(name="llm_call") as llm_span:
163-
# This span inherits: user_id, session_id, experiment, environment
179+
# This span inherits user_id, session_id, environment, and experiment metadata
164180
...
165181
166182
with langfuse.start_generation(name="completion") as gen:
@@ -193,22 +209,27 @@ def propagate_attributes(
193209
with langfuse.propagate_attributes(
194210
user_id="user_123",
195211
session_id="session_abc",
212+
environment="staging",
196213
as_baggage=True # Propagate via HTTP headers
197214
):
198215
# Make HTTP request to Service B
199216
response = requests.get("https://service-b.example.com/api")
200-
# user_id and session_id are now in HTTP headers
217+
# user_id, session_id, and environment are now in HTTP headers
201218
202219
# Service B - downstream service
203220
# OpenTelemetry will automatically extract baggage from HTTP headers
204-
# and propagate to spans in Service B
221+
# and propagate attributes to spans in Service B. If Service B has a local
222+
# Langfuse environment configured, the propagated environment wins for
223+
# spans created within this context.
205224
```
206225
207226
Note:
208227
- **Validation**: Attribute values (user_id, session_id, version, tags,
209-
trace_name) must be strings ≤200 characters. Metadata values are
210-
coerced to strings before the 200 character limit is applied. Invalid
211-
values will be dropped with a warning logged.
228+
trace_name) must be strings ≤200 characters. Environment must also match
229+
Langfuse's environment format: lowercase alphanumeric with optional
230+
hyphens or underscores, must be ≤40 characters, and it must not start with "langfuse". Metadata
231+
values are coerced to strings before the 200 character limit is applied.
232+
Invalid values will be dropped with a warning logged.
212233
- **OpenTelemetry**: This uses OpenTelemetry context propagation under the hood,
213234
making it compatible with other OTel-instrumented libraries.
214235
@@ -222,6 +243,7 @@ def propagate_attributes(
222243
version=version,
223244
tags=tags,
224245
trace_name=trace_name,
246+
environment=environment,
225247
as_baggage=as_baggage,
226248
)
227249

@@ -235,6 +257,7 @@ def _propagate_attributes(
235257
version: Optional[str] = None,
236258
tags: Optional[List[str]] = None,
237259
trace_name: Optional[str] = None,
260+
environment: Optional[str] = None,
238261
as_baggage: bool = False,
239262
experiment: Optional[PropagatedExperimentAttributes] = None,
240263
) -> Generator[Any, Any, Any]:
@@ -247,6 +270,7 @@ def _propagate_attributes(
247270
"version": version,
248271
"tags": tags,
249272
"trace_name": trace_name,
273+
"environment": environment,
250274
}
251275

252276
propagated_metadata_attributes: Dict[str, Optional[Dict[str, Any]]] = {
@@ -327,6 +351,17 @@ def _get_propagated_attributes_from_context(
327351
span_key = _get_span_key_from_baggage_key(baggage_key)
328352

329353
if span_key:
354+
if span_key == LangfuseOtelSpanAttributes.ENVIRONMENT:
355+
validated_environment = _validate_environment_value(
356+
value=baggage_value
357+
)
358+
359+
if validated_environment is None:
360+
continue
361+
362+
propagated_attributes[span_key] = validated_environment
363+
continue
364+
330365
propagated_attributes[span_key] = (
331366
baggage_value
332367
if isinstance(baggage_value, (str, list))
@@ -341,6 +376,17 @@ def _get_propagated_attributes_from_context(
341376
if value is None:
342377
continue
343378

379+
if key == "environment":
380+
validated_environment = _validate_environment_value(value=value)
381+
382+
if validated_environment is None:
383+
continue
384+
385+
propagated_attributes[LangfuseOtelSpanAttributes.ENVIRONMENT] = (
386+
validated_environment
387+
)
388+
continue
389+
344390
if isinstance(value, dict):
345391
# Handle metadata
346392
span_key = _get_propagated_span_key(key)
@@ -435,6 +481,9 @@ def _set_propagated_attribute(
435481
def _validate_propagated_value(
436482
*, value: Any, key: str
437483
) -> Optional[Union[str, List[str]]]:
484+
if key == "environment":
485+
return _validate_environment_value(value=value)
486+
438487
if isinstance(value, list):
439488
validated_values = [
440489
v for v in value if _validate_string_value(key=key, value=v)
@@ -473,6 +522,35 @@ def _validate_string_value(*, value: str, key: str) -> bool:
473522
return True
474523

475524

525+
_ENVIRONMENT_VALUE_PATTERN = re.compile(r"^(?!langfuse)[a-z0-9_-]+$")
526+
527+
528+
def _validate_environment_value(*, value: Any) -> Optional[str]:
529+
key = "environment"
530+
531+
if not isinstance(value, str):
532+
langfuse_logger.warning( # type: ignore
533+
f"Propagated attribute '{key}' value is not a string. Dropping value."
534+
)
535+
return None
536+
537+
if len(value) > 40:
538+
langfuse_logger.warning(
539+
f"Propagated attribute '{key}' value is over 40 characters ({len(value)} chars). Dropping value."
540+
)
541+
return None
542+
543+
if not _ENVIRONMENT_VALUE_PATTERN.fullmatch(value):
544+
langfuse_logger.warning(
545+
"Propagated attribute 'environment' must be a lowercase alphanumeric "
546+
"string with optional hyphens or underscores and must not start with "
547+
"'langfuse'. Dropping value."
548+
)
549+
return None
550+
551+
return value
552+
553+
476554
def _get_propagated_context_key(key: str) -> str:
477555
return f"langfuse.propagated.{key}"
478556

@@ -542,6 +620,7 @@ def _get_propagated_span_key(key: str) -> str:
542620
"version": LangfuseOtelSpanAttributes.VERSION,
543621
"tags": LangfuseOtelSpanAttributes.TRACE_TAGS,
544622
"trace_name": LangfuseOtelSpanAttributes.TRACE_NAME,
623+
"environment": LangfuseOtelSpanAttributes.ENVIRONMENT,
545624
"metadata": LangfuseOtelSpanAttributes.TRACE_METADATA,
546625
"experiment_id": LangfuseOtelSpanAttributes.EXPERIMENT_ID,
547626
"experiment_name": LangfuseOtelSpanAttributes.EXPERIMENT_NAME,

0 commit comments

Comments
 (0)