11"""Attribute propagation utilities for Langfuse OpenTelemetry integration.
22
33This 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
89from typing import Any , Dict , Generator , List , Literal , Optional , TypedDict , Union , cast
910
1011from opentelemetry import (
3637 "version" ,
3738 "tags" ,
3839 "trace_name" ,
40+ "environment" ,
3941]
4042
4143InternalPropagatedKeys = Literal [
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(
435481def _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+
476554def _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