feat(tracing): propagate environment attributes#1726
Conversation
|
@claude review |
| _OBSERVATION_CLASS_MAP: Dict[str, Type["LangfuseObservationWrapper"]] = {} | ||
|
|
||
|
|
||
| def _get_string_span_attribute( |
There was a problem hiding this comment.
we already have a similar function in _client.py . refactor to shared helper
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a9c8ab5ec0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| self._environment = ( | ||
| existing_environment or environment or self._langfuse_client._environment | ||
| ) |
There was a problem hiding this comment.
Use propagated environment when scoring spans
This captures the propagated environment on the observation wrapper, but span.score()/span.score_trace() still call Langfuse.create_score(), whose ScoreBody.environment is taken from the client default (langfuse/_client/client.py:1918) rather than this wrapper value. In a shared proxy with Langfuse(environment="proxy-prod"), a span started under propagate_attributes(environment="dev") is exported as dev, while a score created from that span is exported as proxy-prod, so scores and observations for the same trace are grouped under different environments.
Useful? React with 👍 / 👎.
|
|
||
|
|
||
| _ENVIRONMENT_VALUE_PATTERN = re.compile(r"^(?!langfuse)[a-z0-9_-]+$") | ||
|
|
There was a problem hiding this comment.
this could be a simple one check for the environment up in _validate_propagated_value in the key === environment condition to not start with "langfuse" and return None early if fails else fall through to the other checks
| @@ -327,6 +351,17 @@ def _get_propagated_attributes_from_context( | |||
| span_key = _get_span_key_from_baggage_key(baggage_key) | |||
|
|
|||
There was a problem hiding this comment.
is this necessary? I think we can drop as we don't revalidate on read, only validate on write to baggage / context
| @@ -341,6 +376,17 @@ def _get_propagated_attributes_from_context( | |||
| if value is None: | |||
| continue | |||
There was a problem hiding this comment.
is this necessary? I think we can drop as we don't revalidate on read, only validate on write to baggage / context
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 47d75ff011
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Summary
environmenttopropagate_attributes(...)as a first-class propagated Langfuse attribute.langfuse.environment, includinglangfuse_environmentbaggage for cross-service propagation.Why
Environment was only sourced from local SDK configuration (
Langfuse(environment=...)/LANGFUSE_TRACING_ENVIRONMENT). In a shared LLM proxy used by dev, staging, qa, and prod callers, that local proxy environment can overwrite the caller's intended environment. This change lets callers explicitly propagate the request environment when needed.Validation
uv run --frozen pytest tests/unit/test_propagate_attributes.py::TestPropagateAttributesEnvironmentuv run --frozen pytest tests/unit/test_propagate_attributes.pyuv run --frozen ruff format langfuse/_client/propagation.py langfuse/_client/span.py tests/unit/test_propagate_attributes.pyuv run --frozen ruff check .uv run --frozen mypy langfuse --no-error-summaryGreptile Summary
This PR promotes
environmentto a first-class propagated attribute in the Langfuse Python SDK, allowing callers to override a shared proxy's localLangfuse(environment=...)default on a per-request basis. Validation (_ENVIRONMENT_VALUE_PATTERN,_validate_environment_value) mirrors the server's accepted format (lowercase alphanumeric with optional hyphens/underscores, nolangfuseprefix, ≤ 200 chars) and is applied both when writing to context/baggage and when reading from external baggage.propagation.py: AddsenvironmenttoPropagatedKeys,propagated_keys,_get_propagated_span_key, andpropagate_attributes(); introduces_validate_environment_valueand applies it on both the in-process context path and the baggage-extraction path for defense-in-depth.span.py:LangfuseObservationWrapper.__init__now reads an already-setlangfuse.environmentfrom the underlying OTel span (put there by the span processor'son_start) before falling back to the constructor argument or the client default, preserving the correct precedence order.tests/unit/test_propagate_attributes.py: AddsTestPropagateAttributesEnvironmentcovering in-process propagation, baggage round-trip, client-default override, cross-process context attach, and invalid-value rejection.Confidence Score: 4/5
The core propagation logic, validation, and precedence chain are implemented correctly and are well-tested. The only outstanding item is two inline imports inside test methods that should be moved to the module top.
The new environment propagation follows the same patterns as existing attributes, validation is applied at both write and read sites, and the precedence chain is verified by the span processor ordering. The style finding in the test file does not affect runtime behavior.
tests/unit/test_propagate_attributes.py — two test methods contain inline imports that should be moved to the top of the module.
Sequence Diagram
%%{init: {'theme': 'neutral'}}%% sequenceDiagram participant Caller participant propagate_attributes participant OtelContext participant SpanProcessor participant LangfuseObservationWrapper Caller->>propagate_attributes: "propagate_attributes(environment="staging")" propagate_attributes->>propagate_attributes: _validate_environment_value("staging") propagate_attributes->>OtelContext: set_value("langfuse.propagated.environment", "staging") Note over propagate_attributes,OtelContext: optionally set langfuse_environment baggage Caller->>SpanProcessor: "start_observation("child-span") -> on_start(span, context)" SpanProcessor->>OtelContext: _get_propagated_attributes_from_context(context) Note over SpanProcessor,OtelContext: validates environment from baggage & context OtelContext-->>SpanProcessor: "{"langfuse.environment": "staging", ...}" SpanProcessor->>SpanProcessor: span.set_attributes(propagated_attributes) SpanProcessor->>LangfuseObservationWrapper: "__init__(otel_span, environment=None)" LangfuseObservationWrapper->>LangfuseObservationWrapper: _get_string_span_attribute(span, "langfuse.environment") Note over LangfuseObservationWrapper: returns "staging" (already set by on_start) LangfuseObservationWrapper->>LangfuseObservationWrapper: "self._environment = "staging" (propagated wins)"%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% sequenceDiagram participant Caller participant propagate_attributes participant OtelContext participant SpanProcessor participant LangfuseObservationWrapper Caller->>propagate_attributes: "propagate_attributes(environment="staging")" propagate_attributes->>propagate_attributes: _validate_environment_value("staging") propagate_attributes->>OtelContext: set_value("langfuse.propagated.environment", "staging") Note over propagate_attributes,OtelContext: optionally set langfuse_environment baggage Caller->>SpanProcessor: "start_observation("child-span") -> on_start(span, context)" SpanProcessor->>OtelContext: _get_propagated_attributes_from_context(context) Note over SpanProcessor,OtelContext: validates environment from baggage & context OtelContext-->>SpanProcessor: "{"langfuse.environment": "staging", ...}" SpanProcessor->>SpanProcessor: span.set_attributes(propagated_attributes) SpanProcessor->>LangfuseObservationWrapper: "__init__(otel_span, environment=None)" LangfuseObservationWrapper->>LangfuseObservationWrapper: _get_string_span_attribute(span, "langfuse.environment") Note over LangfuseObservationWrapper: returns "staging" (already set by on_start) LangfuseObservationWrapper->>LangfuseObservationWrapper: "self._environment = "staging" (propagated wins)"Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "Merge branch 'main' into hassiebbot/prop..." | Re-trigger Greptile
Context used:
Learned From
langfuse/langfuse-python#1387