Skip to content

feat: database spans, and pin that sampling stays env-driven - #14420

Open
ogabrielluiz wants to merge 2 commits into
release-1.12.0from
feat/dependency-spans
Open

feat: database spans, and pin that sampling stays env-driven#14420
ogabrielluiz wants to merge 2 commits into
release-1.12.0from
feat/dependency-spans

Conversation

@ogabrielluiz

@ogabrielluiz ogabrielluiz commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Updated after verifying against a live OTLP backend: the DB instrumentation in the first version of this PR produced connection-pool noise and no query spans. It now instruments the engine where it is built. See the comment thread.

Stacked on #14422 and merges after it. The URL redaction that was originally in this PR moved there, so it can land at security speed; the diff below is against that branch.

Database spans

SQLAlchemy is instrumented with an explicit tracer_provider, so a slow request can be attributed to the queries it made.

Allowlisted only after checking what those spans actually carry. db.statement keeps bound parameters as placeholders:

INSERT INTO messagetable (text) VALUES (?)

so chat message text stays in the database. Verified by probe rather than assumed, because assuming is how the outbound-HTTP hole got opened the first time.

What this deliberately does not do

The ticket also asks for httpx spans. This does not add them, and does not allowlist httpx, requests or urllib3.

Those are the transports the OpenAI and Anthropic SDKs ride on, and the vendor SDKs instrument them globally against whichever provider is global — ours. Admitting them puts one span per outbound provider call into the operator's APM. Four existing tests encode that decision explicitly, and outbound provider health already has a leak-safe metrics path built for exactly this reason. Reversing it is a security-posture call that belongs with the people who made it, not a side effect of this ticket.

Redacting URLs would remove the credential from those spans, but not the underlying question of whether LLM vendor traffic belongs in the operator's APM at all. That question is still open.

All four guard tests still pass unchanged.

Sampling

No change was needed. The provider takes no explicit sampler, so OTEL_TRACES_SAMPLER already applies. Pinned with an end-to-end determinism check against a loopback collector: ratio 0.0 exports 0 spans, 1.0 exports all 20. The test exists so that adding an explicit sampler later cannot silently take env control away.

Verification

Mutation Result
Remove status from the sampler probe n/a — sampling is SDK behaviour; the test guards against a future explicit sampler
  • 6 new tests, subprocess-isolated because the tracer provider is process-global.
  • The nesting criterion is partially covered: the dependency-to-flow-span edge is asserted, and the server-to-flow edge already had coverage. A single traced request asserting the whole tree end to end is not automated here.
  • 477 telemetry and tracing tests pass, including the 17 in test_application_span_filter.py that guard the boundary.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 55e08689-4378-48ca-838f-2d21bad1313b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Changes

The change adds SQLAlchemy OpenTelemetry instrumentation, excludes outbound HTTP instrumentation, redacts credentials from exported URL attributes, wires instrumentation into application startup, and adds subprocess telemetry tests.

Dependency telemetry

Layer / File(s) Summary
Observability sanitization
src/lfx/src/lfx/observability.py
Allowlisted spans now include SQLAlchemy spans. URL query strings and userinfo are removed before export.
Dependency instrumentation wiring
src/lfx/src/lfx/observability.py, src/backend/base/langflow/main.py, src/lfx/src/lfx/cli/serve_app.py, src/backend/base/pyproject.toml, src/lfx/pyproject.toml
SQLAlchemy instrumentation is configured during application startup. Async engines are supported, and instrumentation failures are logged without stopping startup.
Telemetry validation and supporting updates
src/backend/tests/unit/services/telemetry/test_dependency_span_redaction.py, .secrets.baseline
Tests validate URL redaction, allowlists, collector export, and sampler ratios. The secrets baseline records updated metadata.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CreateApp
  participant FastAPIInstrumentation
  participant DependencyInstrumentation
  participant SQLAlchemy
  CreateApp->>FastAPIInstrumentation: instrument FastAPI application
  CreateApp->>DependencyInstrumentation: instrument dependencies
  DependencyInstrumentation->>SQLAlchemy: configure instrumentation
Loading

Possibly related PRs

Suggested reviewers: erichare


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Test Coverage For New Implementations ❌ Error The added test file covers URL redaction, scope membership, and sampling, but no test invokes instrument_dependencies or executes SQLAlchemy to verify DB spans and placeholder redaction. Add a subprocess or integration test that instruments a real SQLAlchemy engine, executes a parameterized statement, and asserts the exported span and db.statement contain no bound values.
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (7 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Test Quality And Coverage ✅ Passed The PR includes 6 comprehensive tests covering all main functionality: URL redaction security (API key leak prevention), SQLAlchemy instrumentation allowlisting, deliberate exclusion of outbound HT...
Test File Naming And Structure ✅ Passed The added backend file matches test_*.py, uses pytest fixtures and teardown, has descriptive test names, and covers positive/negative boundary cases; its unit placement matches nearby telemetry sub...
Excessive Mock Usage Warning ✅ Passed The new test file uses real implementations, not excessive mocks. Tests employ subprocess isolation for process-global tracer state management and real OpenTelemetry SDK components (InMemorySpanExp...
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: SQLAlchemy database spans and environment-driven sampling behavior.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/dependency-spans

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the bug Something isn't working label Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

✅ Test Coverage Advisor

No source changes detected without accompanying tests. Thanks for keeping coverage up! 🎉

Advisory check only — never blocks merge.

@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 44.44444% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 62.88%. Comparing base (8238c69) to head (842d6ea).
⚠️ Report is 2 commits behind head on release-1.12.0.

Files with missing lines Patch % Lines
src/lfx/src/lfx/observability.py 16.66% 10 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##           release-1.12.0   #14420      +/-   ##
==================================================
+ Coverage           61.51%   62.88%   +1.37%     
==================================================
  Files                2408     2376      -32     
  Lines              240471   240606     +135     
  Branches            36217    37390    +1173     
==================================================
+ Hits               147914   151309    +3395     
+ Misses              90617    87357    -3260     
  Partials             1940     1940              
Flag Coverage Δ
frontend 62.01% <ø> (+2.14%) ⬆️
lfx 61.40% <16.66%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/backend/base/langflow/main.py 63.29% <100.00%> (ø)
...backend/base/langflow/services/database/service.py 69.02% <100.00%> (ø)
src/lfx/src/lfx/observability.py 51.83% <16.66%> (-1.63%) ⬇️

... and 527 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions github-actions Bot added bug Something isn't working and removed bug Something isn't working labels Aug 5, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/backend/tests/unit/services/telemetry/test_dependency_span_redaction.py (1)

105-106: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Run both probes through uv run.

  • src/backend/tests/unit/services/telemetry/test_dependency_span_redaction.py#L105-L106: invoke the file probe through uv run python.
  • src/backend/tests/unit/services/telemetry/test_dependency_span_redaction.py#L175-L176: invoke the inline sampler probe through uv run python.

As per coding guidelines, “Backend code must use uv run when running Python commands to ensure correct environment setup.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/tests/unit/services/telemetry/test_dependency_span_redaction.py`
around lines 105 - 106, Update both subprocess probes in
test_dependency_span_redaction.py: the file probe at lines 105-106 and the
inline sampler probe at lines 175-176 must invoke Python through “uv run python”
instead of calling sys.executable directly. Apply the same command-prefix change
at both sites.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/backend/tests/unit/services/telemetry/test_dependency_span_redaction.py`:
- Around line 147-149: Replace the allowlist-only check in
test_database_spans_are_allowlisted with a subprocess-based probe that calls
instrument_dependencies(), executes a parameterized SQLite query using a unique
sentinel, and inspects the exported SQLAlchemy span. Assert instrumentation
succeeds and the span contains SQL parameter placeholders without exposing the
sentinel value.
- Around line 118-124: Extend the telemetry redaction tests around
SERVER_SPAN_PROBE with an allowed-span probe containing query parameters and URL
userinfo, then assert the exported url.full and http.url values retain only the
scheme, host, port, and path, with credentials and query data removed. Keep the
existing serve API key regression intact and verify the new pytest coverage
exercises both URL attributes.

---

Nitpick comments:
In `@src/backend/tests/unit/services/telemetry/test_dependency_span_redaction.py`:
- Around line 105-106: Update both subprocess probes in
test_dependency_span_redaction.py: the file probe at lines 105-106 and the
inline sampler probe at lines 175-176 must invoke Python through “uv run python”
instead of calling sys.executable directly. Apply the same command-prefix change
at both sites.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 381dd170-ff07-42e5-84e5-f755665dc6bb

📥 Commits

Reviewing files that changed from the base of the PR and between e2d0dc4 and 6c32b6a.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • .secrets.baseline
  • src/backend/base/langflow/main.py
  • src/backend/base/pyproject.toml
  • src/backend/tests/unit/services/telemetry/test_dependency_span_redaction.py
  • src/lfx/pyproject.toml
  • src/lfx/src/lfx/cli/serve_app.py
  • src/lfx/src/lfx/observability.py

Comment on lines +118 to +124
def test_the_serve_api_key_never_reaches_the_apm():
"""Regression for a live leak: the key is accepted as a query param and the span kept it."""
result = run_probe(SERVER_SPAN_PROBE)

assert result["spans"], "expected a server span carrying url.path"
blob = json.dumps(result["spans"])
assert SECRET not in blob, f"serve API key reached the APM: {blob}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Cover URL userinfo redaction.

This probe tests only url.query. It does not prove that url.full and http.url remove user:password@host.

Add an allowed-span probe with query and userinfo attributes. Assert that the exported attributes retain the scheme, host, port, and path only.

As per coding guidelines, “For new backend implementations or bug fixes, ensure corresponding pytest test files are included ... and verify the tests actually cover the new or changed behavior.”

🧰 Tools
🪛 ast-grep (0.45.0)

[info] 122-122: use jsonify instead of json.dumps for JSON output
Context: json.dumps(result["spans"])
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/tests/unit/services/telemetry/test_dependency_span_redaction.py`
around lines 118 - 124, Extend the telemetry redaction tests around
SERVER_SPAN_PROBE with an allowed-span probe containing query parameters and URL
userinfo, then assert the exported url.full and http.url values retain only the
scheme, host, port, and path, with credentials and query data removed. Keep the
existing serve API key regression intact and verify the new pytest coverage
exercises both URL attributes.

Source: Coding guidelines

Comment on lines +147 to +149
def test_database_spans_are_allowlisted():
"""Verified separately to carry bound-parameter placeholders, never row values."""
assert "opentelemetry.instrumentation.sqlalchemy" in APPLICATION_INSTRUMENTATION_SCOPES

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Exercise SQLAlchemy instrumentation with a real query.

This assertion checks only a string in the allowlist. It passes if _instrument_sqlalchemy() returns on ImportError, catches an instrumentation error, or exports bound values instead of SQL placeholders.

Add a subprocess probe that invokes instrument_dependencies(), runs a parameterized SQLite query with a sentinel value, and asserts that an exported SQLAlchemy span omits that sentinel.

As per coding guidelines, “For new backend implementations or bug fixes, ensure corresponding pytest test files are included ... and verify the tests actually cover the new or changed behavior.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/tests/unit/services/telemetry/test_dependency_span_redaction.py`
around lines 147 - 149, Replace the allowlist-only check in
test_database_spans_are_allowlisted with a subprocess-based probe that calls
instrument_dependencies(), executes a parameterized SQLite query using a unique
sentinel, and inspects the exported SQLAlchemy span. Assert instrumentation
succeeds and the span contains SQL parameter placeholders without exposing the
sentinel value.

Source: Coding guidelines

@ogabrielluiz ogabrielluiz changed the title fix: keep the serve API key out of the operator's APM feat: database spans, and pin that sampling stays env-driven Aug 5, 2026
@ogabrielluiz
ogabrielluiz force-pushed the feat/dependency-spans branch from 6c32b6a to edff026 Compare August 5, 2026 14:46
@ogabrielluiz
ogabrielluiz changed the base branch from release-1.12.0 to fix/redact-url-query-in-spans August 5, 2026 14:46
@github-actions github-actions Bot added enhancement New feature or request and removed bug Something isn't working enhancement New feature or request labels Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Frontend Unit Test Coverage Report

Coverage Summary

Lines Statements Branches Functions
Coverage: 50%
50.55% (72899/144200) 70.32% (10233/14550) 47.19% (1677/3553)

Unit Test Results

Tests Skipped Failures Errors Time
5558 0 💤 0 ❌ 0 🔥 20m 25s ⏱️

@ogabrielluiz
ogabrielluiz force-pushed the fix/redact-url-query-in-spans branch from ec94200 to c028037 Compare August 5, 2026 15:25
@ogabrielluiz
ogabrielluiz force-pushed the feat/dependency-spans branch from edff026 to efe096b Compare August 5, 2026 15:26
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Aug 5, 2026
@ogabrielluiz

Copy link
Copy Markdown
Contributor Author

Self-review after running this against a real APM (grafana/otel-lgtm, local): the DB spans in this PR do not deliver what the description claims, and I would not merge it as is.

One POST /api/v1/run produced 20 spans, 13 of them sqlalchemy/connect, and zero query spans. No db.statement anywhere in the trace. So the net effect today is connection-pool noise rather than "a slow request can be attributed to the queries it made".

Cause: langflow builds an AsyncEngine (services/database/service.py:24, create_async_engine), and instrument_dependencies() is called from the app factory with no engine, because the engine does not exist yet at that point. SQLAlchemyInstrumentor then instruments globally, which picks up pool events but not async query execution. The engine=... path that handles this is already in the function (getattr(engine, "sync_engine", engine)), it is just never called with one.

Fix is to call it once the database service has built its engine, passing that engine, rather than from the app factory. I will push that rather than leave the claim standing.

Worth noting for whoever reviews: the unit tests passed throughout, because they assert the scope is allowlisted and that the instrumentor is wired, not that a query span ever reaches an exporter. The gap only showed up against a real backend.

@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Migration Validation Passed

All migrations follow the Expand-Contract pattern correctly.

@ogabrielluiz

Copy link
Copy Markdown
Contributor Author

Heads up @erichare @Cristhianzl @Adam-Aghili, this changed materially after I asked you to look, so please review from the current head rather than what you were pinged on.

What moved: instrument_dependencies() is now instrument_database(engine), the engine argument is required rather than optional, and the call moved out of the app factory into DatabaseService where the engine is built. So it is a different mechanism and a renamed public function, not a tidy-up.

Why: the DB spans did not work. Verified against a live OTLP backend, one API request produced 13 sqlalchemy connect spans and zero query spans, no db.statement anywhere. Langflow builds an AsyncEngine and the instrumentor patches the sync engine underneath it, so instrumenting globally from the app factory (where no engine exists yet) attached to pool events only. Same request after the change: 28 query spans with db.statement, bound parameters still placeholders, and the flow's input text absent from the trace.

The part worth a reviewer's attention: my unit tests passed before and after. They assert the scope is allowlisted and the instrumentor is wired, not that a query span ever reaches an exporter, so nothing in CI would have caught this. If you have a view on how to test that without standing up a collector in CI, I would take it.

The two CI reds here are a docker job whose build step was cancelled mid-build and a Playwright shard; both look like flakes and I have re-run them. I will not call them clean until they come back green.

@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Aug 6, 2026
Base automatically changed from fix/redact-url-query-in-spans to release-1.12.0 August 7, 2026 14:39
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Aug 7, 2026
SQLAlchemy is instrumented with an explicit tracer_provider, so a slow
request can be attributed to the queries it made. Allowlisted only after
probing what those spans carry: db.statement keeps bound parameters as
placeholders, so chat message text stays in the database.

The outbound HTTP scopes (httpx, requests, urllib3) are deliberately still
absent. They are the transports the LLM vendor SDKs ride on, and those SDKs
instrument them globally against whichever provider is global, so admitting
them would put one span per outbound provider call into the operator's APM.
Outbound provider health has a leak-safe metrics path instead. Left as its
own decision rather than a side effect of this ticket.

Sampling needed no change: the provider takes no explicit sampler, so
OTEL_TRACES_SAMPLER already applies. Pinned end to end against a loopback
collector, 0 spans at ratio 0.0 and all 20 at 1.0, so that adding a sampler
later cannot silently take env control away.
The DB spans this PR added did not deliver what it claimed. Verified against
a live backend: one API request produced 13 sqlalchemy connect spans and zero
query spans, with no db.statement anywhere. Connection-pool noise dressed up
as database visibility.

Langflow builds an AsyncEngine, and the instrumentor patches the sync engine
underneath it. instrument_dependencies() was called from the app factory with
no engine, because the engine does not exist that early, so SQLAlchemy
instrumented globally and attached to pool events only.

Instrument where the engine is built instead, passing it. Renamed to
instrument_database, and the engine argument is required rather than optional:
the engine-less call is exactly the broken configuration, and a global
instrument() would also win the race and make a later engine-specific call a
silent no-op.

Same request after the change: 28 query spans carrying db.statement, bound
parameters still placeholders, and the flow's input text absent from the trace.

The unit tests passed throughout. They assert the scope is allowlisted and the
instrumentor is wired, not that a query span reaches an exporter, so this was
only visible against a real backend.
@ogabrielluiz
ogabrielluiz force-pushed the feat/dependency-spans branch from 31d5a2e to 842d6ea Compare August 7, 2026 15:28
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Aug 7, 2026

@Cristhianzl Cristhianzl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

@github-actions github-actions Bot added the lgtm This PR has been approved by a maintainer label Aug 7, 2026
@ogabrielluiz
ogabrielluiz enabled auto-merge August 7, 2026 17:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request lgtm This PR has been approved by a maintainer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants