Skip to content

feat(span-processor): mark app root spans - #1651

Merged
hassiebp merged 14 commits into
mainfrom
add-app-root-span-detection
May 19, 2026
Merged

feat(span-processor): mark app root spans#1651
hassiebp merged 14 commits into
mainfrom
add-app-root-span-detection

Conversation

@hassiebp

@hassiebphassiebp commented May 12, 2026

Copy link
Copy Markdown
Collaborator

What

Adds SDK-side app-root marking for exported spans so Langfuse V4 can default observation tables to the top-most exported observations without relying on the OpenTelemetry root span being exported.

Why

Default and custom span filtering can remove infrastructure or instrumentation spans before export. When that happens, the backend may never receive the original OTEL root span, so filtering the UI by parent_observation_id IS NULL can miss the useful root row. The SDK now marks the best-effort app root on the span before export.

Changes

  • Add langfuse.internal.is_app_root as the internal app-root marker attribute.
  • Track per-trace, processor-local parent export state in LangfuseSpanProcessor.
  • Mark an exported span as an app root when its immediate parent is not expected to export.
  • Use langfuse_trace_id baggage for best-effort distributed suppression after active Langfuse spans start.
  • Avoid self-suppression by reading baggage from the span's parent context and writing baggage only into the child context used for downstream work.
  • Keep internal langfuse_trace_id baggage out of propagated user metadata.
  • Add focused unit coverage for filtered parents, sibling roots, baggage suppression, local parent precedence, and start-time false positive behavior.

Limitations

  • The decision is best-effort and made at span start using currently available filter information.
  • If should_export_span changes its decision by span end, the SDK does not repair already-started descendants.
  • Distributed suppression depends on normal OTEL context and baggage propagation.
  • Processor-local state is cleaned when spans end; never-ended spans can retain state for that trace.

Verification

  • uv run --frozen ruff check langfuse/_client/span_processor.py langfuse/_client/client.py langfuse/_client/propagation.py langfuse/_client/attributes.py tests/unit/test_app_root_detection.py tests/unit/test_propagate_attributes.py
  • uv run --frozen ruff format --check langfuse/_client/span_processor.py langfuse/_client/client.py langfuse/_client/propagation.py langfuse/_client/attributes.py tests/unit/test_app_root_detection.py tests/unit/test_propagate_attributes.py
  • uv run --frozen mypy langfuse --no-error-summary
  • uv run --frozen pytest tests/unit/test_app_root_detection.py tests/unit/test_propagate_attributes.py tests/unit/test_otel.py -q

Greptile Summary

This PR adds best-effort "app root" span detection to the Langfuse SDK so that Langfuse V4 can identify the topmost exported observation per trace even when OTEL infrastructure spans are filtered out before export.

  • LangfuseSpanProcessor now tracks per-trace, per-span export-expectation state in a thread-safe dict, marking a span as langfuse.internal.is_app_root at on_start time when its immediate parent is not expected to export. Cleanup runs in on_end's finally block to ensure the state is always released.
  • client.py injects a langfuse_trace_id baggage entry into the child context after each start_as_current_observation span starts, enabling distributed suppression for downstream services while avoiding self-suppression.
  • The internal baggage key is explicitly excluded from _get_propagated_attributes_from_context to prevent it from surfacing as user metadata.

Confidence Score: 3/5

The feature is logically sound and the threading model is correct, but _get_readable_span relies on a private OTel SDK method that, if ever removed or renamed, will silently disable app-root marking across the entire SDK with only a DEBUG-level log.

The core detection and cleanup logic is well-designed and the new test coverage is thorough. The risk is concentrated in _get_readable_span, which bridges from the mutable Span seen at on_start to the ReadableSpan interface expected by _should_export_span. Using span._readable_span() achieves this, but if the OTel SDK drops the private method the feature degrades invisibly with no warning and no fallback.

langfuse/_client/span_processor.py, specifically the _get_readable_span static method around line 311.

Sequence Diagram

sequenceDiagram
participant C as Client (start_as_current_observation)
participant SP as LangfuseSpanProcessor
participant BS as BatchSpanProcessor
C->>SP: on_start(span, parent_context)
SP->>SP: _mark_app_root_candidate(span, parent_context)
note over SP: Reads langfuse_trace_id from parent_context baggage
SP->>BS: super().on_start(span, parent_context)
SP-->>C: returns
C->>C: attach baggage token (langfuse_trace_id)
C->>C: yield Langfuse span wrapper
C->>C: finally: detach baggage token
C->>SP: on_end(span)
SP->>SP: project/scope/filter checks
alt should export
SP->>BS: super().on_end(span)
end
SP->>SP: finally: _cleanup_app_root_state(span)
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---### Issue 1 of 2
langfuse/_client/span_processor.py:311-317
**Private OTel SDK API creates silent feature degradation path**`_readable_span` is a private method on `opentelemetry.sdk.trace.Span`. If the OTel SDK ever renames or removes it, `getattr` returns `None`, the callable check raises `TypeError`, which is caught by the `try/except` in `on_start` at DEBUG level — and the entire `IS_APP_ROOT` feature silently stops working with no user-visible error. Since `opentelemetry.sdk.trace.Span` already implements `ReadableSpan` (it inherits from it), you can replace the private accessor with a direct cast: `cast(ReadableSpan, span)`. This removes the fragile dependency without changing runtime behavior.
### Issue 2 of 2
tests/unit/test_propagate_attributes.py:1638-1641
Import added inside a test method — the team's style rule requires imports to live at the top of the module, not nested inside functions or methods. The pre-existing `from opentelemetry import baggage` and `from opentelemetry import context as otel_context` in this same method also violate the rule, but this PR adds a third one. All three should be hoisted to the module level.
```suggestion from opentelemetry import baggage from opentelemetry import context as otel_context```

Reviews (1): Last reviewed commit: "Merge branch 'main' into add-app-root-sp..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

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 May 12, 2026 12:54
@github-actions

Copy link
Copy Markdown

@claude review

@hassiebphassiebp changed the title feat: mark app root spans in SDKfeat(span-processor): mark app root spans in SDKMay 12, 2026
@hassiebphassiebp changed the title feat(span-processor): mark app root spans in SDKfeat(span-processor): mark app root spansMay 12, 2026
Comment threadlangfuse/_client/span_processor.py Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot 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:fffa395845

ℹ️ 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 threadlangfuse/_client/client.py
Comment threadlangfuse/_client/span_processor.py

@chatgpt-codex-connectorchatgpt-codex-connectorBot 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:ba553a13db

ℹ️ 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 threadlangfuse/_client/span_processor.py Outdated
Comment threadlangfuse/_client/client.py
wochingeand others added 3 commits May 19, 2026 14:03
chore: add codeowners for github config
Co-authored-by: Codex Opus 4.6 (1M context) <noreply@anthropic.com>
…updates (#1645)
* chore(deps): bump the github-actions group across 1 directory with 5 updates
Bumps the github-actions group with 5 updates in the / directory:
| Package | From | To |
| --- | --- | --- |
| [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) | `8.0.0` | `8.1.0` |
| [actions/cache](https://github.com/actions/cache) | `5.0.4` | `5.0.5` |
| [github/codeql-action](https://github.com/github/codeql-action) | `4.35.1` | `4.35.4` |
| [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) | `3.0.0` | `3.1.0` |
| [slackapi/slack-github-action](https://github.com/slackapi/slack-github-action) | `3.0.1` | `3.0.3` |
Updates `astral-sh/setup-uv` from 8.0.0 to 8.1.0
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](astral-sh/setup-uv@cec2083...0880764)
Updates `actions/cache` from 5.0.4 to 5.0.5
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](actions/cache@6682284...27d5ce7)
Updates `github/codeql-action` from 4.35.1 to 4.35.4
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@c10b806...68bde55)
Updates `dependabot/fetch-metadata` from 3.0.0 to 3.1.0
- [Release notes](https://github.com/dependabot/fetch-metadata/releases)
- [Commits](dependabot/fetch-metadata@ffa630c...25dd0e3)
Updates `slackapi/slack-github-action` from 3.0.1 to 3.0.3
- [Release notes](https://github.com/slackapi/slack-github-action/releases)
- [Changelog](https://github.com/slackapi/slack-github-action/blob/main/CHANGELOG.md)
- [Commits](slackapi/slack-github-action@af78098...45a88b9)
---
updated-dependencies:
- dependency-name: actions/cache
dependency-version: 5.0.5
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: github-actions
- dependency-name: astral-sh/setup-uv
dependency-version: 8.1.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: github-actions
- dependency-name: dependabot/fetch-metadata
dependency-version: 3.1.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: github-actions
- dependency-name: github/codeql-action
dependency-version: 4.35.2
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: github-actions
- dependency-name: slackapi/slack-github-action
dependency-version: 3.0.2
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: github-actions
...
Signed-off-by: dependabot[bot] <support@github.com>
* ci: remove stale cache action version comment
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tobias Wochinger <tobias.wochinger@clickhouse.com>
Bumps [langsmith](https://github.com/langchain-ai/langsmith-sdk) from 0.7.22 to 0.8.0.
- [Release notes](https://github.com/langchain-ai/langsmith-sdk/releases)
- [Commits](langchain-ai/langsmith-sdk@v0.7.22...v0.8.0)
---
updated-dependencies:
- dependency-name: langsmith
dependency-version: 0.8.0
dependency-type: indirect
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
@hassiebp
hassiebp requested a review from a team as a code ownerMay 19, 2026 12:03
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Comment threadlangfuse/_client/span_processor.py Outdated
Comment threadlangfuse/_client/span_processor.py
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Comment threadlangfuse/_client/span_processor.py
@blacksmith-sh

This comment has been minimized.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Comment threadlangfuse/_client/span_processor.py
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@hassiebp
hassiebp merged commit 246bef7 into mainMay 19, 2026
18 checks passed
@hassiebp
hassiebp deleted the add-app-root-span-detection branch May 19, 2026 13:52
Sign up for freeto 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.

2 participants

@hassiebp@wochinge