diff --git a/utils/_context/_scenarios/__init__.py b/utils/_context/_scenarios/__init__.py index 048384bf5d1..d28009cfea2 100644 --- a/utils/_context/_scenarios/__init__.py +++ b/utils/_context/_scenarios/__init__.py @@ -542,6 +542,8 @@ class _Scenarios: weblog_env={ "DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED": "true", "DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS": "0.2", + "OTEL_PHP_AUTOLOAD_ENABLED": "true", + "OTEL_METRICS_EXPORTER": "otlp", }, doc="", scenario_groups=[scenario_groups.ffe], diff --git a/utils/build/docker/php/apache-mod/php.conf b/utils/build/docker/php/apache-mod/php.conf index aaf88b2f4e9..217e90e3fdf 100644 --- a/utils/build/docker/php/apache-mod/php.conf +++ b/utils/build/docker/php/apache-mod/php.conf @@ -28,6 +28,7 @@ RewriteRule "^/signup$" "/signup/" RewriteRule "^/shell_execution$" "/shell_execution/" RewriteRule "^/llm$" "/llm/" + RewriteRule "^/ffe$" "/ffe.php" [L] RewriteCond /var/www/html/%{REQUEST_URI} !-f RewriteRule "^/rasp/(.*)" "/rasp/$1.php" [L] RewriteRule "^/api_security.sampling/.*" "/api_security_sampling.php$0" [L] diff --git a/utils/build/docker/php/common/composer.gte8.2.json b/utils/build/docker/php/common/composer.gte8.2.json index b82c054e130..1972aeb2174 100644 --- a/utils/build/docker/php/common/composer.gte8.2.json +++ b/utils/build/docker/php/common/composer.gte8.2.json @@ -5,7 +5,14 @@ "weblog/acme": "*", "monolog/monolog": "*", "openai-php/client": "*", - "guzzlehttp/guzzle": "*" + "guzzlehttp/guzzle": "*", + "open-telemetry/sdk": "^1.0.0", + "open-telemetry/exporter-otlp": "^1.0.0" + }, + "config": { + "allow-plugins": { + "php-http/discovery": true + } }, "repositories": [ { diff --git a/utils/build/docker/php/common/composer.json b/utils/build/docker/php/common/composer.json index 4abe446e7d4..0cc6dc30451 100644 --- a/utils/build/docker/php/common/composer.json +++ b/utils/build/docker/php/common/composer.json @@ -3,7 +3,15 @@ "type": "project", "require": { "weblog/acme": "*", - "monolog/monolog": "*" + "monolog/monolog": "*", + "open-telemetry/sdk": "^1.0.0", + "open-telemetry/exporter-otlp": "^1.0.0", + "guzzlehttp/guzzle": "^7.0" + }, + "config": { + "allow-plugins": { + "php-http/discovery": true + } }, "repositories": [ { diff --git a/utils/build/docker/php/common/ffe.php b/utils/build/docker/php/common/ffe.php new file mode 100644 index 00000000000..d63975aa0d3 --- /dev/null +++ b/utils/build/docker/php/common/ffe.php @@ -0,0 +1,57 @@ + 'Invalid JSON body']); + exit; +} + +$flag = isset($input['flag']) ? $input['flag'] : null; +$variationType = isset($input['variationType']) ? $input['variationType'] : null; +$defaultValue = isset($input['defaultValue']) ? $input['defaultValue'] : null; +$targetingKey = array_key_exists('targetingKey', $input) ? $input['targetingKey'] : ''; +$attributes = isset($input['attributes']) ? $input['attributes'] : []; + +try { + $provider = \DDTrace\FeatureFlags\Provider::getInstance(); + $provider->start(); + + // On the first request to a PHP-FPM worker, the RC config may not yet be + // loaded into FFE_STATE (the VM interrupt that calls ddog_process_remote_configs + // fires at opcode boundaries, but hasn't run before start() is called). + // Each usleep() allows the pending SIGVTALRM interrupt to be processed. + if (!$provider->isReady()) { + for ($i = 0; $i < 5 && !$provider->isReady(); $i++) { + usleep(100000); // 100ms — allow VM interrupt to process RC update + $provider->start(); // re-check after interrupt may have fired + } + } + + $result = $provider->evaluate($flag, $variationType, $defaultValue, $targetingKey, $attributes); + + // Flush exposure events immediately for system test observability + $provider->flush(); + + // Flush OTel metrics immediately so the agent receives them before the test reads + $meterProvider = \OpenTelemetry\API\Globals::meterProvider(); + if (method_exists($meterProvider, 'forceFlush')) { + $meterProvider->forceFlush(); + } + + echo json_encode($result); +} catch (\Throwable $e) { + echo json_encode(['value' => $defaultValue, 'reason' => 'ERROR', 'error' => $e->getMessage()]); +} diff --git a/utils/build/docker/php/common/install_ddtrace.sh b/utils/build/docker/php/common/install_ddtrace.sh index 6ecad2800ce..de1974c127e 100755 --- a/utils/build/docker/php/common/install_ddtrace.sh +++ b/utils/build/docker/php/common/install_ddtrace.sh @@ -42,7 +42,10 @@ fi EXTRA_ARGS="" PHP_VERSION=$(php -r "echo PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION;") if [ "$(printf '%s\n' "7.1" "$PHP_VERSION" | sort -V | head -n1)" = "7.1" ]; then - EXTRA_ARGS="--enable-profiling" + # Only enable profiling if using default release download or if local package contains profiling + if [ -z "${PKG:-}" ] || tar -tzf "$PKG" 2>/dev/null | grep -q "datadog-profiling"; then + EXTRA_ARGS="--enable-profiling" + fi fi INI_FILE=/etc/php/php.ini diff --git a/utils/build/docker/php/parametric/server.php b/utils/build/docker/php/parametric/server.php index 5d87f161db5..c2817a5629b 100644 --- a/utils/build/docker/php/parametric/server.php +++ b/utils/build/docker/php/parametric/server.php @@ -546,6 +546,41 @@ function remappedSpanKind($spanKind) { return jsonResponse([]); })); +// FFE (Feature Flags & Experimentation) endpoints +$ffeProvider = null; + +$router->addRoute('POST', '/ffe/start', new ClosureRequestHandler(function (Request $req) use (&$ffeProvider) { + try { + if ($ffeProvider === null) { + $ffeProvider = \DDTrace\FeatureFlags\Provider::getInstance(); + } + $ffeProvider->start(); + return jsonResponse([]); + } catch (\Throwable $e) { + return new Response(status: 500, body: json_encode(['error' => $e->getMessage()])); + } +})); + +$router->addRoute('POST', '/ffe/evaluate', new ClosureRequestHandler(function (Request $req) use (&$ffeProvider) { + try { + if ($ffeProvider === null) { + $ffeProvider = \DDTrace\FeatureFlags\Provider::getInstance(); + $ffeProvider->start(); + } + + $flag = arg($req, 'flag'); + $variationType = arg($req, 'variationType'); + $defaultValue = arg($req, 'defaultValue'); + $targetingKey = arg($req, 'targetingKey'); + $attributes = arg($req, 'attributes') ?? []; + + $result = $ffeProvider->evaluate($flag, $variationType, $defaultValue, $targetingKey, $attributes); + return jsonResponse($result); + } catch (\Throwable $e) { + return new Response(status: 500, body: json_encode(['error' => $e->getMessage()])); + } +})); + $middleware = new class implements Middleware { public function handleRequest(Request $request, RequestHandler $next): Response { $response = $next->handleRequest($request); diff --git a/utils/build/docker/php/php-fpm/php-fpm.conf b/utils/build/docker/php/php-fpm/php-fpm.conf index f827a0bb7d6..0270674f8f7 100644 --- a/utils/build/docker/php/php-fpm/php-fpm.conf +++ b/utils/build/docker/php/php-fpm/php-fpm.conf @@ -44,6 +44,7 @@ RewriteRule "^/signup$" "/signup/" RewriteRule "^/shell_execution$" "/shell_execution/" RewriteRule "^/rasp/(.*)" "/rasp/$1.php" [L] + RewriteRule "^/ffe$" "/ffe.php" RewriteRule "^/debugger$" "/debugger/" RewriteCond /var/www/html/%{REQUEST_URI} !-f RewriteRule "^/api_security.sampling/.*" "/api_security_sampling.php/$0" [L]