Skip to content

feat(php): Add /ffe endpoint and OTel SDK to PHP weblog#6475

Draft
leoromanovsky wants to merge 1 commit intomainfrom
leo.romanovsky/php-ffe-weblog
Draft

feat(php): Add /ffe endpoint and OTel SDK to PHP weblog#6475
leoromanovsky wants to merge 1 commit intomainfrom
leo.romanovsky/php-ffe-weblog

Conversation

@leoromanovsky
Copy link
Contributor

Motivation

The PHP weblog needs a /ffe endpoint to participate in FFE system tests (feature flag evaluation + OTel metrics). This PR adds the endpoint handler, wires up Apache routing, installs the OTel PHP SDK, and configures PHP-FPM so the FFE scenario works end-to-end.

Companion to DataDog/dd-trace-php#3630 (FFE + FlagEvalMetrics implementation) and DataDog/system-tests#6410 (test enablement).

Changes

  • utils/build/docker/php/common/ffe.php: New PHP handler for /ffe. Evaluates feature flags via DDTrace\FeatureFlags\Provider, includes a retry loop (usleep(100ms) × 5) for PHP-FPM first-request timing where FFE_STATE.config is None until SIGVTALRM fires, and calls forceFlush() on the OTel meter provider so metrics are exported to the agent synchronously within the request.
  • utils/build/docker/php/parametric/server.php: Add /ffe/start and /ffe/evaluate endpoints for parametric tests.
  • utils/build/docker/php/apache-mod/php.conf: RewriteRule ^/ffe$ /ffe.php [L] so Apache routes /ffe to the handler.
  • utils/build/docker/php/common/composer.json and composer.gte8.2.json: Add open-telemetry/sdk so the OTel PHP SDK is available when DD_METRICS_OTEL_ENABLED=true.
  • utils/build/docker/php/common/install_ddtrace.sh: Updated to include FFE-related install steps.
  • utils/build/docker/php/php-fpm/php-fpm.conf: php-fpm tuning for FFE scenario.
  • utils/_context/_scenarios/__init__.py: Add OTEL_PHP_AUTOLOAD_ENABLED=true and OTEL_METRICS_EXPORTER=otlp to the FEATURE_FLAGGING_AND_EXPERIMENTATION scenario env so the PHP OTel SDK autoloads and routes metrics to the OTLP exporter.

Decisions

usleep() retry for PHP-FPM first-request timing. On the very first request to a PHP-FPM worker, FFE_STATE.config is None because the ddog_process_remote_configs call that loads the flag config only fires via a SIGVTALRM VM interrupt. That interrupt is delivered at PHP opcode boundaries — but only while PHP is executing, not while FPM is idle waiting for a request. Calling usleep() inside the handler yields CPU and makes PHP sleep at an opcode boundary; when the VM wakes up, the pending interrupt runs ddog_process_remote_configs → store_config(). The loop retries up to 5× (500ms total), which is sufficient for the default 200ms RC poll interval in tests.

forceFlush() after evaluation. The OTel SDK batches metrics on a 10s export interval. Without an explicit flush the metrics test would time out waiting for data to appear in interfaces.agent.get_metrics(). Flushing inline after Provider::flush() ensures both the exposure event and the OTel metric reach the agent in the same request lifecycle.

OTel SDK via Composer, not bundled. The open-telemetry/sdk package is added to the PHP weblog's composer.json rather than bundled in the tracer, keeping the dependency separate and consistent with how other weblogs consume OTel.

Companion PRs

- Add /ffe handler (ffe.php): evaluates feature flags via DDTrace\FeatureFlags\Provider,
  includes retry loop for PHP-FPM first-request timing (SIGVTALRM delay), and flushes
  OTel meter provider after each evaluation so metrics reach the agent synchronously
- Add /ffe/start + /ffe/evaluate to parametric server.php
- Add Apache RewriteRule for /ffe -> /ffe.php in apache-mod config
- Add open-telemetry/sdk + exporter-otlp to composer.json (both PHP variants) so the
  OTel counter in FlagEvalMetrics can export when DD_METRICS_OTEL_ENABLED=true
- Update install_ddtrace.sh and php-fpm.conf to support the FFE scenario
- Add OTEL_PHP_AUTOLOAD_ENABLED=true and OTEL_METRICS_EXPORTER=otlp to the FFE
  scenario env so the OTel PHP SDK autoloads and routes metrics to the OTLP exporter
@github-actions
Copy link
Contributor

CODEOWNERS have been resolved as:

utils/build/docker/php/common/ffe.php                                   @DataDog/apm-php @DataDog/system-tests-core
utils/_context/_scenarios/__init__.py                                   @DataDog/system-tests-core
utils/build/docker/php/apache-mod/php.conf                              @DataDog/apm-php @DataDog/system-tests-core
utils/build/docker/php/common/composer.gte8.2.json                      @DataDog/apm-php @DataDog/system-tests-core
utils/build/docker/php/common/composer.json                             @DataDog/apm-php @DataDog/system-tests-core
utils/build/docker/php/common/install_ddtrace.sh                        @DataDog/apm-php @DataDog/system-tests-core
utils/build/docker/php/parametric/server.php                            @DataDog/apm-php @DataDog/system-tests-core
utils/build/docker/php/php-fpm/php-fpm.conf                             @DataDog/apm-php @DataDog/system-tests-core

@datadog-datadog-prod-us1
Copy link

datadog-datadog-prod-us1 bot commented Mar 11, 2026

⚠️ Tests

Fix all issues with BitsAI or with Cursor

⚠️ Warnings

🧪 5 Tests failed

tests.debugger.test_debugger_probe_snapshot.Test_Debugger_Method_Probe_Snaphots.test_mix_snapshot[uds] from system_tests_suite (Datadog) (Fix with Cursor)
AssertionError: assert 'Snapshot was not received' is None
 +  where 'Snapshot was not received' = <built-in method join of str object at 0x7f671622aa60>(['Snapshot was not received'])
 +    where <built-in method join of str object at 0x7f671622aa60> = '\n'.join
 +    and   ['Snapshot was not received'] = <tests.debugger.test_debugger_probe_snapshot.Test_Debugger_Method_Probe_Snaphots object at 0x7f670c195220>.setup_failures

self = <tests.debugger.test_debugger_probe_snapshot.Test_Debugger_Method_Probe_Snaphots object at 0x7f670c195220>

    @slow
    def test_mix_snapshot(self):
>       self._assert()
...
tests.debugger.test_debugger_symdb.Test_Debugger_SymDb.test_symdb_upload[echo] from system_tests_suite (Datadog) (Fix with Cursor)
ValueError: No scope containing debugger controller with scope_type CLASS or MODULE was found in the symbols

self = <tests.debugger.test_debugger_symdb.Test_Debugger_SymDb object at 0x7f35bd5507d0>

    def test_symdb_upload(self):
>       self._assert()

tests/debugger/test_debugger_symdb.py:90: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
tests/debugger/test_debugger_symdb.py:24: in _assert
...
tests.debugger.test_debugger_telemetry.Test_Debugger_Telemetry.test_telemetry_co[poc] from system_tests_suite (Datadog) (Fix with Cursor)
AssertionError: Telemetry was not received
assert None
 +  where None = <tests.debugger.test_debugger_telemetry.Test_Debugger_Telemetry object at 0x7f4510b6dd90>.telemetry

self = <tests.debugger.test_debugger_telemetry.Test_Debugger_Telemetry object at 0x7f4510b6dd90>

    @slow
    def test_telemetry_co(self):
>       self._assert(required_telemetry=["code_origin_for_spans_enabled"])

...
View all

ℹ️ Info

❄️ No new flaky tests detected

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 71a5dfa | Docs | Datadog PR Page | Was this helpful? React with 👍/👎 or give us feedback!

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