Skip to content

feat(tracing): propagate environment attributes#1726

Merged
hassiebp merged 4 commits into
mainfrom
hassiebbot/propagate-environment
Jun 25, 2026
Merged

feat(tracing): propagate environment attributes#1726
hassiebp merged 4 commits into
mainfrom
hassiebbot/propagate-environment

Conversation

@hassiebp

@hassiebp hassiebp commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add environment to propagate_attributes(...) as a first-class propagated Langfuse attribute.
  • Map propagated environments to langfuse.environment, including langfuse_environment baggage for cross-service propagation.
  • Preserve propagated environments over local client defaults so shared proxies can attribute spans to the caller environment.
  • Cover in-process propagation, baggage extraction, client-default precedence, and invalid environment values with unit tests.

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::TestPropagateAttributesEnvironment
  • uv run --frozen pytest tests/unit/test_propagate_attributes.py
  • uv run --frozen ruff format langfuse/_client/propagation.py langfuse/_client/span.py tests/unit/test_propagate_attributes.py
  • uv run --frozen ruff check .
  • uv run --frozen mypy langfuse --no-error-summary

Greptile Summary

This PR promotes environment to a first-class propagated attribute in the Langfuse Python SDK, allowing callers to override a shared proxy's local Langfuse(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, no langfuse prefix, ≤ 200 chars) and is applied both when writing to context/baggage and when reading from external baggage.

  • propagation.py: Adds environment to PropagatedKeys, propagated_keys, _get_propagated_span_key, and propagate_attributes(); introduces _validate_environment_value and 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-set langfuse.environment from the underlying OTel span (put there by the span processor's on_start) before falling back to the constructor argument or the client default, preserving the correct precedence order.
  • tests/unit/test_propagate_attributes.py: Adds TestPropagateAttributesEnvironment covering 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)"
Loading
%%{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)"
Loading
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
tests/unit/test_propagate_attributes.py:1878-1879
**Imports inside test methods**

`from opentelemetry import baggage` and `from opentelemetry import context as otel_context` are placed inside the test method bodies, and a third occurrence of `from opentelemetry import context as otel_context` appears inside `test_environment_baggage_overrides_client_default_after_context_attach`. Both imports should be moved to the top of the module where all other `opentelemetry` imports live.

Reviews (1): Last reviewed commit: "Merge branch 'main' into hassiebbot/prop..." | Re-trigger Greptile

Context used:

  • Rule used - Move imports to the top of the module instead of p... (source)

Learned From
langfuse/langfuse-python#1387

@hassiebp hassiebp marked this pull request as ready for review June 25, 2026 09:38
@github-actions

Copy link
Copy Markdown

@claude review

Comment thread langfuse/_client/span.py Outdated
_OBSERVATION_CLASS_MAP: Dict[str, Type["LangfuseObservationWrapper"]] = {}


def _get_string_span_attribute(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we already have a similar function in _client.py . refactor to shared helper

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread langfuse/_client/span.py
Comment on lines +151 to +153
self._environment = (
existing_environment or environment or self._langfuse_client._environment
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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_-]+$")

@hassiebp hassiebp Jun 25, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this necessary? I think we can drop as we don't revalidate on read, only validate on write to baggage / context

Comment thread langfuse/_client/propagation.py
Comment thread langfuse/_client/propagation.py

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread langfuse/_client/span.py
@hassiebp hassiebp merged commit 023940a into main Jun 25, 2026
18 checks passed
@hassiebp hassiebp deleted the hassiebbot/propagate-environment branch June 25, 2026 11:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant