Skip to content

tracer: keep the trace of a request blocked during startup - #4445

Draft
cataphract wants to merge 1 commit into
masterfrom
glopes/appsec-startup-block-autoflush
Draft

cataphract wants to merge 1 commit into
masterfrom
glopes/appsec-startup-block-autoflush

Conversation

@cataphract

@cataphract cataphract commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Note: I'm looking for another strategy for the fix.

Description

When AppSec blocks a request during request startup (RINIT), it calls ddtrace_close_all_spans_and_flush(). That function closes all open spans, then force-flushes. The flush has to be forced because ddtrace_flush_tracer() otherwise drops traces while PG(during_request_startup) is set.

With DD_TRACE_AUTO_FLUSH_ENABLED=1 on a web SAPI (php-fpm, CGI, Apache, ...), closing the root span already triggers the regular auto-flush. That flush is not forced, so it serializes the closed spans, removes them, and drops them because the request is still starting up. The forced flush that comes next has nothing left to send. The trace of the blocked request, which carries the AppSec event, never reaches the agent. Auto-flush is on by default only for the CLI SAPI, so only users who turn it on explicitly for a web SAPI are affected.

The fix: ddtrace_close_all_spans_and_flush() turns off the root-span auto-flush while it closes the spans (through a new DDTRACE_G(suppress_auto_flush) flag), so only its own forced flush sends them. The flag is restored if a bailout happens. Outside request startup, what gets sent stays the same: before, the auto-flush sent the trace and the forced flush found an empty buffer; now the forced flush sends it.

Alternatives considered:

  • Passing a force flag down to the auto-flush. This means threading it through the whole span-closing call chain, for a single caller.
  • Checking PG(during_request_startup) before serializing in ddtrace_flush_tracer(). Closed spans would then stay alive instead of being dropped, which changes behaviour for every request that ends without running PHP code (php-fpm status page, missing script).

Tests

New appsec/tests/extension/req_abort_in_req_init_auto_flush.phpt. It runs a php-cgi child that AppSec blocks in request init (datadog.appsec.testing_abort_rinit), with a mock agent in the parent. It checks that the trace arrives with auto-flush both off and on. Without the fix, it fails only when auto-flush is on:

     auto-flush off: blocked: yes, trace sent: yes
002+ auto-flush on: blocked: yes, trace sent: no
002- auto-flush on: blocked: yes, trace sent: yes

Runs with the fix:

  • Full appsec extension suite, PHP 8.3 debug: 374 tests, 372 passed, 2 skipped (the same 2 that are skipped on master), 0 failed.
  • req_abort_* appsec tests, PHP 7.4 debug: 38 tests, all passed.
  • 208 tracer tests/ext tests that use DD_TRACE_AUTO_FLUSH_ENABLED or dd_trace_close_all_spans_and_flush, PHP 8.3 debug, with test-agent and request-replayer: 184 passed, 24 skipped, 0 failed.

🤖 Generated with Claude Code

When AppSec blocks a request while PHP is still in request startup, it
calls ddtrace_close_all_spans_and_flush(), which closes every open span
and then force-flushes the tracer. The flush has to be forced because
ddtrace_flush_tracer() otherwise discards traces while
PG(during_request_startup) is set, to avoid reporting requests that
never ran any PHP code.

With DD_TRACE_AUTO_FLUSH_ENABLED=1 on a web SAPI, closing the root span
in the first step already triggers the regular auto-flush. That flush
is not forced: it serializes the closed spans, removes them from the
tracer and then drops them because the request is still starting up.
The forced flush that follows finds nothing left to send, so the trace
of the blocked request, which carries the AppSec event, never reaches
the agent. Auto-flush is only on by default for the CLI SAPI, where
AppSec does not block requests, so this only affects users who enable
it explicitly for php-fpm, CGI, Apache and the like.

ddtrace_close_all_spans_and_flush() now suppresses the root-span
auto-flush while it closes the spans, so that only its own forced flush
sends them. Outside request startup this does not change what is sent:
before, the auto-flush sent the trace and the forced flush found an
empty buffer; now the forced flush sends it. The flag is restored on
bailout so that a fatal error in a span close hook cannot leave
auto-flush disabled for the rest of the request.

Two alternatives were rejected. Passing the force flag into the
auto-flush would require threading it through the span closing call
chain for a single caller. Checking PG(during_request_startup) before
serializing in ddtrace_flush_tracer() would leave the closed spans
alive instead of dropping them, which changes behaviour for every
request that ends without running PHP code, such as php-fpm status
pages or missing scripts.

The new test runs a php-cgi child that AppSec blocks in request init,
with a mock agent in the parent, and checks that the trace arrives
with auto-flush both off and on. Without the fix it fails only in the
auto-flush case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@datadog-datadog-prod-us1

datadog-datadog-prod-us1 Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Pipelines  Tests

✨ Unblock PR with BitsAI

❌ Errors

Your PR has failed checks. Please review the issues below and take necessary action before merging.

🚦 10 Pipeline jobs failed

DDCI - DataDog/dd-trace-php | test appsec extension: [7.0, amd64, debug-zts] — 🔧 Needs a code fix, caused by this PR

View more details · View in DDCI

DDCI - DataDog/dd-trace-php | test appsec extension: [7.0, amd64, debug] — 🔧 Needs a code fix, caused by this PR

View more details · View in DDCI

DDCI - DataDog/dd-trace-php | test appsec extension: [7.0, arm64, debug-zts] — 🔧 Needs a code fix, caused by this PR

View more details · View in DDCI

View all 10 failed jobs.

ℹ️ Info

No other issues found (see more)

🧪 All tests passed
❄️ No new flaky tests detected

🎯 Code Coverage (details)
• Patch Coverage: 100.00%
• Overall Coverage: 79.96% (-0.04%)

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 1bf0e24 | Docs | View more details | Give us feedback!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant