Skip to content

Commit

Permalink
Support the frankenphp SAPI
Browse files Browse the repository at this point in the history
Signed-off-by: Bob Weinand <bob.weinand@datadoghq.com>
  • Loading branch information
bwoebi committed Mar 28, 2024
1 parent 7b4a640 commit bd3a1d6
Show file tree
Hide file tree
Showing 24 changed files with 509 additions and 16 deletions.
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,7 @@ TEST_INTEGRATIONS_82 := \
test_integrations_elasticsearch7 \
test_integrations_elasticsearch8 \
test_integrations_predis1 \
test_integrations_frankenphp \
test_integrations_roadrunner \
test_integrations_sqlsrv \
test_integrations_swoole_5 \
Expand Down Expand Up @@ -943,6 +944,7 @@ TEST_INTEGRATIONS_83 := \
test_integrations_elasticsearch7 \
test_integrations_elasticsearch8 \
test_integrations_predis1 \
test_integrations_frankenphp \
test_integrations_roadrunner \
test_integrations_sqlsrv \
test_integrations_swoole_5 \
Expand Down Expand Up @@ -1214,6 +1216,9 @@ test_integrations_phpredis5: global_test_run_dependencies
test_integrations_predis1: global_test_run_dependencies
$(MAKE) test_scenario_predis1
$(call run_tests_debug,tests/Integrations/Predis)
test_integrations_frankenphp: global_test_run_dependencies
$(MAKE) test_scenario_default
$(call run_tests_debug,--testsuite=frankenphp-test)
test_integrations_roadrunner: global_test_run_dependencies
$(call run_composer_with_retry,tests/Frameworks/Roadrunner/Version_2,)
$(call run_tests_debug,tests/Integrations/Roadrunner/V2)
Expand Down
1 change: 1 addition & 0 deletions bridge/_files_integrations.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
__DIR__ . '/../src/Integrations/Integrations/Mongo/MongoIntegration.php',
__DIR__ . '/../src/Integrations/Integrations/MongoDB/MongoDBIntegration.php',
__DIR__ . '/../src/Integrations/Integrations/Slim/SlimIntegration.php',
__DIR__ . '/../src/Integrations/Integrations/Frankenphp/FrankenphpIntegration.php',
__DIR__ . '/../src/Integrations/Integrations/Swoole/SwooleIntegration.php',
__DIR__ . '/../src/Integrations/Integrations/SQLSRV/SQLSRVIntegration.php',
__DIR__ . '/../src/Integrations/Integrations/Symfony/SymfonyIntegration.php',
Expand Down
15 changes: 15 additions & 0 deletions dockerfiles/ci/buster/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ ENV DEVLIBS \
libclang-rt-16-dev \
llvm-16-dev \
lld-16 \
libbrotli-dev \
libcurl4-openssl-dev \
libedit-dev \
libffi-dev \
Expand Down Expand Up @@ -282,6 +283,20 @@ RUN mkdir -p -v "${CARGO_HOME}" "${RUSTUP_HOME}" \

ENV PATH="/rust/cargo/bin:${PATH}"

ARG GO_VERSION="1.22.1"
ARG GO_SHA256_ARM="e56685a245b6a0c592fc4a55f0b7803af5b3f827aaa29feab1f40e491acf35b8"
ARG GO_SHA256_X86="aab8e15785c997ae20f9c88422ee35d962c4562212bb0f879d052a35c8307c7f"
ENV GOROOT=/go
RUN MARCH=$(uname -m) \
&& GO_SHA256=$(if [ "$MARCH" = "x86_64" ]; then echo ${GO_SHA256_X86}; elif [ "$MARCH" = "aarch64" ]; then echo ${GO_SHA256_ARM}; fi) \
&& FILENAME=go${GO_VERSION}.linux-$(if [ "$MARCH" = "x86_64" ]; then echo amd64; elif [ "$MARCH" = "aarch64" ]; then echo arm64; fi).tar.gz \
&& curl -L --write-out '%{http_code}' -O https://go.dev/dl/${FILENAME} \
&& printf '%s %s' "$GO_SHA256" "$FILENAME" | sha256sum --check --status \
&& tar -xf "$FILENAME" -C / \
&& rm "${FILENAME}"

ENV PATH="/go/bin:${PATH}"

# Add the wait script to the image: note SHA 672a28f0509433e3b4b9bcd4d9cd7668cea7e31a has been reviewed and should not
# be changed without an appropriate code review.
ADD https://raw.githubusercontent.com/eficode/wait-for/672a28f0509433e3b4b9bcd4d9cd7668cea7e31a/wait-for /usr/bin/wait-for
Expand Down
1 change: 1 addition & 0 deletions dockerfiles/ci/buster/build-php.sh
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ ${PHP_SRC_DIR}/configure \
$(if [[ $INSTALL_VERSION == *zts* ]]; then echo --enable$(if grep -q 'maintainer-zts' ${PHP_SRC_DIR}/configure; then echo "-maintainer"; fi)-zts; fi) \
`# https://externals.io/message/118859` \
$(if [[ $INSTALL_VERSION == *zts* ]]; then echo --disable-zend-signals; fi) \
$(if [[ $INSTALL_VERSION == *zts* && ${PHP_VERSION_ID} -ge 82 ]]; then echo --enable-zend-max-execution-timers; fi) \
--prefix=${INSTALL_DIR} \
--with-config-file-path=${INSTALL_DIR} \
--with-config-file-scan-dir=${INSTALL_DIR}/conf.d
Expand Down
2 changes: 2 additions & 0 deletions dockerfiles/ci/buster/switch-php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ sudo ln -sf /opt/php/${phpVersion}/bin/php-config /usr/local/bin/php-config
sudo ln -sf /opt/php/${phpVersion}/bin/phpdbg /usr/local/bin/phpdbg
sudo ln -sf /opt/php/${phpVersion}/bin/phpize /usr/local/bin/phpize
sudo ln -sf /opt/php/${phpVersion}/sbin/php-fpm /usr/local/bin/php-fpm
sudo ln -sf /opt/php/${phpVersion}/lib/libphp.so /usr/lib/libphp.so
sudo ln -sf /opt/php/${phpVersion}/bin/frankenphp /usr/local/bin/frankenphp
if [ "${phpVersion}" != *asan* ]; then
sudo ln -sf /opt/php/${phpVersion}/lib/apache2handler-libphp.so /usr/lib/apache2/modules/libphp.so
fi
10 changes: 7 additions & 3 deletions ext/ddtrace.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
#include <Zend/zend_exceptions.h>
#include <Zend/zend_extensions.h>
#include <Zend/zend_smart_str.h>
#include <components/sapi/sapi.h>
#include <headers/headers.h>
#include <hook/hook.h>
#include <json/json.h>
Expand Down Expand Up @@ -118,7 +117,7 @@ static bool dd_has_other_observers;
static int dd_observer_extension_backup = -1;
#endif

static datadog_php_sapi ddtrace_active_sapi = DATADOG_PHP_SAPI_UNKNOWN;
datadog_php_sapi ddtrace_active_sapi = DATADOG_PHP_SAPI_UNKNOWN;

_Atomic(int64_t) ddtrace_warn_legacy_api;

Expand Down Expand Up @@ -2633,11 +2632,14 @@ static ddtrace_distributed_tracing_result dd_parse_distributed_tracing_headers_f
UNUSED(return_value);

dd_fci_fcc_pair func;
bool use_server_headers = false;
zend_array *array = NULL;

ZEND_PARSE_PARAMETERS_START(1, 1)
DD_PARAM_PROLOGUE(0, 0);
if (UNEXPECTED(!zend_parse_arg_func(_arg, &func.fci, &func.fcc, false, &_error, true))) {
if (Z_TYPE_P(_arg) == IS_NULL) {
use_server_headers = true;
} else if (UNEXPECTED(!zend_parse_arg_func(_arg, &func.fci, &func.fcc, false, &_error, true))) {
if (!_error) {
zend_argument_type_error(1, "must be a valid callback or of type array, %s given", zend_zval_value_name(_arg));
_error_code = ZPP_ERROR_FAILURE;
Expand Down Expand Up @@ -2669,6 +2671,8 @@ static ddtrace_distributed_tracing_result dd_parse_distributed_tracing_headers_f

if (array) {
return ddtrace_read_distributed_tracing_ids(dd_read_array_header, array);
} else if (use_server_headers) {
return ddtrace_read_distributed_tracing_ids(ddtrace_read_zai_header, &func);
} else {
return ddtrace_read_distributed_tracing_ids(dd_read_userspace_header, &func);
}
Expand Down
3 changes: 3 additions & 0 deletions ext/ddtrace.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <stdbool.h>
#include <stdint.h>
#include <components-rs/ddtrace.h>
#include <components/sapi/sapi.h>

#ifndef _WIN32
#include <dogstatsd_client/client.h>
Expand All @@ -25,6 +26,8 @@ typedef struct ddtrace_root_span_data ddtrace_root_span_data;
typedef struct ddtrace_span_stack ddtrace_span_stack;
typedef struct ddtrace_span_link ddtrace_span_link;

extern datadog_php_sapi ddtrace_active_sapi;

static inline zend_array *ddtrace_property_array(zval *zv) {
ZVAL_DEREF(zv);
if (Z_TYPE_P(zv) != IS_ARRAY) {
Expand Down
7 changes: 4 additions & 3 deletions ext/ddtrace.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -486,10 +486,11 @@ function get_sanitized_exception_trace(\Exception|\Throwable $exception, int $sk
* Update datadog headers for distributed tracing for new spans. Also applies this information to the current trace,
* if there is one, as well as the future ones if it isn't overwritten
*
* @param array|callable(string):mixed $headersOrCallback Either an array with a lowercase header to value mapping,
* or a callback, which given a header name for distributed tracing, returns the value it should be updated to.
* @param null|array|callable(string):mixed $headersOrCallback Either an array with a lowercase header to value mapping,
* or a callback, which given a header name for distributed tracing, returns the value it should be updated to. If null,
* this reads the headers directly from the $_SERVER superglobal.
*/
function consume_distributed_tracing_headers(array|callable $headersOrCallback): void {}
function consume_distributed_tracing_headers(null|array|callable $headersOrCallback): void {}

/**
* Get information on the key-value pairs of the datadog headers for distributed tracing
Expand Down
2 changes: 1 addition & 1 deletion ext/ddtrace_arginfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_get_sanitized_exception_
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_consume_distributed_tracing_headers, 0, 1, IS_VOID, 0)
ZEND_ARG_TYPE_MASK(0, headersOrCallback, MAY_BE_ARRAY|MAY_BE_CALLABLE, NULL)
ZEND_ARG_TYPE_MASK(0, headersOrCallback, MAY_BE_NULL|MAY_BE_ARRAY|MAY_BE_CALLABLE, NULL)
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_generate_distributed_tracing_headers, 0, 0, IS_ARRAY, 0)
Expand Down
9 changes: 5 additions & 4 deletions ext/integrations/integrations.c
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,7 @@ typedef struct {
} dd_integration_aux;

void dd_integration_aux_free(void *auxiliary) {
dd_integration_aux *aux = auxiliary;
zend_string_release(aux->classname);
free(aux);
free(auxiliary);
}

static void dd_invoke_integration_loader_and_unhook_posthook(zend_ulong invocation, zend_execute_data *execute_data, zval *retval, void *auxiliary, void *dynamic) {
Expand Down Expand Up @@ -104,7 +102,7 @@ static bool dd_invoke_integration_loader_and_unhook_prehook(zend_ulong invocatio
static void dd_hook_method_and_unhook_on_first_call(zai_str Class, zai_str method, zai_str callback, ddtrace_integration_name name, bool posthook) {
dd_integration_aux *aux = malloc(sizeof(*aux));
aux->name = name;
aux->classname = zend_string_init(callback.ptr, callback.len, 1);
aux->classname = zend_string_init_interned(callback.ptr, callback.len, 1);
aux->id = zai_hook_install(Class, method,
posthook ? NULL : dd_invoke_integration_loader_and_unhook_prehook,
posthook ? dd_invoke_integration_loader_and_unhook_posthook : NULL,
Expand Down Expand Up @@ -194,6 +192,9 @@ void ddtrace_integrations_minit(void) {
DD_SET_UP_DEFERRED_LOADING_BY_METHOD(DDTRACE_INTEGRATION_ELOQUENT, "Illuminate\\Database\\Eloquent\\Model", "destroy",
"DDTrace\\Integrations\\Eloquent\\EloquentIntegration");

DD_SET_UP_DEFERRED_LOADING_BY_FUNCTION(DDTRACE_INTEGRATION_FRANKENPHP, "frankenphp_handle_request",
"DDTrace\\Integrations\\Frankenphp\\FrankenphpIntegration");

DD_SET_UP_DEFERRED_LOADING_BY_METHOD(DDTRACE_INTEGRATION_GUZZLE, "GuzzleHttp\\Client", "__construct",
"DDTrace\\Integrations\\Guzzle\\GuzzleIntegration");

Expand Down
1 change: 1 addition & 0 deletions ext/integrations/integrations.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
INTEGRATION(DRUPAL, "drupal") \
INTEGRATION(ELASTICSEARCH, "elasticsearch") \
INTEGRATION(ELOQUENT, "eloquent") \
INTEGRATION(FRANKENPHP, "frankenphp") \
INTEGRATION(GUZZLE, "guzzle") \
INTEGRATION(LAMINAS, "laminas") \
INTEGRATION(LARAVEL, "laravel") \
Expand Down
9 changes: 7 additions & 2 deletions ext/serializer.c
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,8 @@ static zend_string *dd_build_req_url(zend_array *_server) {
return ZSTR_EMPTY_ALLOC();
}

zend_bool is_https = zend_hash_str_exists(_server, ZEND_STRL("HTTPS"));
zval *https = zend_hash_str_find(_server, ZEND_STRL("HTTPS"));
bool is_https = https && i_zend_is_true(https);

zval *host_zv;
if ((!(host_zv = zend_hash_str_find(_server, ZEND_STRL("HTTP_HOST"))) &&
Expand Down Expand Up @@ -1166,7 +1167,8 @@ static void _serialize_meta(zval *el, ddtrace_span_data *span) {
meta = &meta_zv;

zval *exception_zv = &span->property_exception;
if (Z_TYPE_P(exception_zv) == IS_OBJECT && instanceof_function(Z_OBJCE_P(exception_zv), zend_ce_throwable)) {
bool has_exception = Z_TYPE_P(exception_zv) == IS_OBJECT && instanceof_function(Z_OBJCE_P(exception_zv), zend_ce_throwable);
if (has_exception) {
ignore_error = false;
enum dd_exception exception_type = DD_EXCEPTION_THROWN;
if (is_root_span) {
Expand Down Expand Up @@ -1224,6 +1226,9 @@ static void _serialize_meta(zval *el, ddtrace_span_data *span) {

if (ddtrace_span_is_entrypoint_root(span)) {
int status = SG(sapi_headers).http_response_code;
if (ddtrace_active_sapi == DATADOG_PHP_SAPI_FRANKENPHP && !status) {
status = has_exception ? 500 : 200;
}
struct iter *headers = dd_iterate_sapi_headers();
dd_set_entrypoint_root_span_props_end(Z_ARR_P(meta), status, headers, ignore_error);
efree(headers);
Expand Down
61 changes: 61 additions & 0 deletions src/Integrations/Integrations/Frankenphp/FrankenphpIntegration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

namespace DDTrace\Integrations\Frankenphp;

use DDTrace\HookData;
use DDTrace\Integrations\Integration;
use DDTrace\SpanStack;
use DDTrace\Tag;
use DDTrace\Type;

use function DDTrace\consume_distributed_tracing_headers;

class FrankenphpIntegration extends Integration
{
const NAME = 'frankenphp';

public function getName()
{
return self::NAME;
}

/**
* {@inheritdoc}
*/
public function requiresExplicitTraceAnalyticsEnabling()
{
return false;
}

public function init()
{
$integration = $this;

ini_set("datadog.trace.auto_flush_enabled", 1);
ini_set("datadog.trace.generate_root_span", 0);

\DDTrace\install_hook('frankenphp_handle_request', function (HookData $hook) use ($integration) {
$hook->data = \DDTrace\install_hook(
$hook->args[0],
function (HookData $hook) use ($integration) {
$rootSpan = $hook->span(new SpanStack());
$rootSpan->name = "web.request";
$rootSpan->service = \ddtrace_config_app_name('frankenphp');
$rootSpan->type = Type::WEB_SERVLET;
$rootSpan->meta[Tag::COMPONENT] = FrankenphpIntegration::NAME;
$rootSpan->meta[Tag::SPAN_KIND] = Tag::SPAN_KIND_VALUE_SERVER;
unset($rootSpan->meta["closure.declaration"]);
$integration->addTraceAnalyticsIfEnabled($rootSpan);

consume_distributed_tracing_headers(null);
}, function (HookData $hook) {
var_dump($hook->span());
}
);
}, function (HookData $hook) {
\DDTrace\remove_hook($hook->data);
});

return Integration::LOADED;
}
}
8 changes: 8 additions & 0 deletions tests/Common/WebFrameworkTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ protected static function isSwoole()
return false;
}

protected static function isFrankenphp()
{
return false;
}

/**
* Get additional envs to be set in the web server.
* @return array
Expand Down Expand Up @@ -138,6 +143,9 @@ protected static function setUpWebServer(array $additionalEnvs = [], array $addi
if ($version = static::isSwoole()) {
self::$appServer->setSwoole($version);
}
if ($version = static::isFrankenphp()) {
self::$appServer->setFrankenphp();
}
self::$appServer->start();
}
}
Expand Down
19 changes: 19 additions & 0 deletions tests/Frameworks/Frankenphp/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

// Prevent worker script termination when a client connection is interrupted
ignore_user_abort(true);

$handler = static function () {
switch (explode("?", $_SERVER["REQUEST_URI"])[0]) {
case "/error":
throw new \Exception("Error page");
default:
echo "Hello FrankenPHP!";
}
error_log(var_export($_SERVER, true));
};

for ($running = true; $running;) {
$running = \frankenphp_handle_request($handler);
// error_log(var_export(\dd_trace_serialize_closed_spans(), true));
}
8 changes: 5 additions & 3 deletions tests/Frameworks/Util/CommonScenariosDataProviderTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ trait CommonScenariosDataProviderTrait
* @param array $definedExpectations
* @return array
*/
public function buildDataProvider($definedExpectations)
public function buildDataProvider($definedExpectations, $skipExpectations = false)
{
$scenarios = TestScenarios::all();

Expand Down Expand Up @@ -51,7 +51,7 @@ public function buildDataProvider($definedExpectations)
if ($i !== false) {
unset($unexpectedRequest[$i]);
}
if ($unexpectedRequest) {
if ($unexpectedRequest && !$skipExpectations) {
throw new \Exception(
'Found the following scenarios not having any expectation defined: '
. implode(', ', $unexpectedRequest)
Expand All @@ -62,7 +62,9 @@ public function buildDataProvider($definedExpectations)
foreach ($scenarios as $request) {
$key = $request->getName();
if (!isset($definedExpectations[$key])) {
error_log('This framework is missing the following scenario test: ' . $key);
if (!$skipExpectations) {
error_log('This framework is missing the following scenario test: ' . $key);
}
continue;
}
$dataProvider[$key] = [
Expand Down
Loading

0 comments on commit bd3a1d6

Please sign in to comment.