Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions utils/_context/_scenarios/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
1 change: 1 addition & 0 deletions utils/build/docker/php/apache-mod/php.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
9 changes: 8 additions & 1 deletion utils/build/docker/php/common/composer.gte8.2.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
{
Expand Down
10 changes: 9 additions & 1 deletion utils/build/docker/php/common/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
{
Expand Down
57 changes: 57 additions & 0 deletions utils/build/docker/php/common/ffe.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

header('Content-Type: application/json');

// Load Composer autoload so that OTel SDK classes are available for metrics
if (file_exists(__DIR__ . '/vendor/autoload.php')) {
require_once __DIR__ . '/vendor/autoload.php';
// Initialize OTel SDK so metrics can be exported (requires OTEL_PHP_AUTOLOAD_ENABLED=true)
if (class_exists('\OpenTelemetry\SDK\SdkAutoloader')) {
\OpenTelemetry\SDK\SdkAutoloader::autoload();
}
}

$input = json_decode(file_get_contents('php://input'), true);

if (!is_array($input)) {
http_response_code(400);
echo json_encode(['error' => '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()]);
}
5 changes: 4 additions & 1 deletion utils/build/docker/php/common/install_ddtrace.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 35 additions & 0 deletions utils/build/docker/php/parametric/server.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions utils/build/docker/php/php-fpm/php-fpm.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Loading