From bd3a1d678c4806fdc439c64725ca377d27d826d4 Mon Sep 17 00:00:00 2001 From: Bob Weinand Date: Wed, 27 Mar 2024 17:55:37 +0100 Subject: [PATCH] Support the frankenphp SAPI Signed-off-by: Bob Weinand --- Makefile | 5 + bridge/_files_integrations.php | 1 + dockerfiles/ci/buster/Dockerfile | 15 ++ dockerfiles/ci/buster/build-php.sh | 1 + dockerfiles/ci/buster/switch-php | 2 + ext/ddtrace.c | 10 +- ext/ddtrace.h | 3 + ext/ddtrace.stub.php | 7 +- ext/ddtrace_arginfo.h | 2 +- ext/integrations/integrations.c | 9 +- ext/integrations/integrations.h | 1 + ext/serializer.c | 9 +- .../Frankenphp/FrankenphpIntegration.php | 61 +++++++ tests/Common/WebFrameworkTestCase.php | 8 + tests/Frameworks/Frankenphp/index.php | 19 +++ .../Util/CommonScenariosDataProviderTrait.php | 8 +- .../Frankenphp/CommonScenariosTest.php | 73 +++++++++ .../Frankenphp/DistributedTracingTest.php | 79 ++++++++++ tests/Sapi/Frankenphp/Caddyfile | 19 +++ tests/Sapi/Frankenphp/FrankenphpServer.php | 149 ++++++++++++++++++ .../Integrations/IntegrationsLoaderTest.php | 1 + tests/WebServer.php | 15 ++ tests/phpunit.xml | 3 + tooling/bin/install-frankenphp.sh | 25 +++ 24 files changed, 509 insertions(+), 16 deletions(-) create mode 100644 src/Integrations/Integrations/Frankenphp/FrankenphpIntegration.php create mode 100644 tests/Frameworks/Frankenphp/index.php create mode 100644 tests/Integrations/Frankenphp/CommonScenariosTest.php create mode 100644 tests/Integrations/Frankenphp/DistributedTracingTest.php create mode 100644 tests/Sapi/Frankenphp/Caddyfile create mode 100644 tests/Sapi/Frankenphp/FrankenphpServer.php create mode 100755 tooling/bin/install-frankenphp.sh diff --git a/Makefile b/Makefile index 98a588b1e9..27813ffc1d 100644 --- a/Makefile +++ b/Makefile @@ -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 \ @@ -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 \ @@ -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) diff --git a/bridge/_files_integrations.php b/bridge/_files_integrations.php index a7112bbbb3..5b0b59a3f1 100644 --- a/bridge/_files_integrations.php +++ b/bridge/_files_integrations.php @@ -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', diff --git a/dockerfiles/ci/buster/Dockerfile b/dockerfiles/ci/buster/Dockerfile index 983aea90ad..9c70530fa4 100644 --- a/dockerfiles/ci/buster/Dockerfile +++ b/dockerfiles/ci/buster/Dockerfile @@ -14,6 +14,7 @@ ENV DEVLIBS \ libclang-rt-16-dev \ llvm-16-dev \ lld-16 \ + libbrotli-dev \ libcurl4-openssl-dev \ libedit-dev \ libffi-dev \ @@ -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 diff --git a/dockerfiles/ci/buster/build-php.sh b/dockerfiles/ci/buster/build-php.sh index 563353bf54..c2d173572e 100755 --- a/dockerfiles/ci/buster/build-php.sh +++ b/dockerfiles/ci/buster/build-php.sh @@ -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 diff --git a/dockerfiles/ci/buster/switch-php b/dockerfiles/ci/buster/switch-php index 12750f4976..5f99103cb5 100755 --- a/dockerfiles/ci/buster/switch-php +++ b/dockerfiles/ci/buster/switch-php @@ -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 \ No newline at end of file diff --git a/ext/ddtrace.c b/ext/ddtrace.c index 100565dc87..e9a522c335 100644 --- a/ext/ddtrace.c +++ b/ext/ddtrace.c @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include @@ -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; @@ -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; @@ -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); } diff --git a/ext/ddtrace.h b/ext/ddtrace.h index efa257e3b4..c836a70c35 100644 --- a/ext/ddtrace.h +++ b/ext/ddtrace.h @@ -4,6 +4,7 @@ #include #include #include +#include #ifndef _WIN32 #include @@ -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) { diff --git a/ext/ddtrace.stub.php b/ext/ddtrace.stub.php index f1c2e0b0cf..c937593422 100644 --- a/ext/ddtrace.stub.php +++ b/ext/ddtrace.stub.php @@ -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 diff --git a/ext/ddtrace_arginfo.h b/ext/ddtrace_arginfo.h index c440ef9a3c..798b44f8c9 100644 --- a/ext/ddtrace_arginfo.h +++ b/ext/ddtrace_arginfo.h @@ -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) diff --git a/ext/integrations/integrations.c b/ext/integrations/integrations.c index 5206095efc..6610d2b710 100644 --- a/ext/integrations/integrations.c +++ b/ext/integrations/integrations.c @@ -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) { @@ -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, @@ -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"); diff --git a/ext/integrations/integrations.h b/ext/integrations/integrations.h index 4f9afce3db..fda17de724 100644 --- a/ext/integrations/integrations.h +++ b/ext/integrations/integrations.h @@ -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") \ diff --git a/ext/serializer.c b/ext/serializer.c index 250911bfe0..24f536ec2b 100644 --- a/ext/serializer.c +++ b/ext/serializer.c @@ -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"))) && @@ -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) { @@ -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); diff --git a/src/Integrations/Integrations/Frankenphp/FrankenphpIntegration.php b/src/Integrations/Integrations/Frankenphp/FrankenphpIntegration.php new file mode 100644 index 0000000000..790dc3eb5e --- /dev/null +++ b/src/Integrations/Integrations/Frankenphp/FrankenphpIntegration.php @@ -0,0 +1,61 @@ +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; + } +} diff --git a/tests/Common/WebFrameworkTestCase.php b/tests/Common/WebFrameworkTestCase.php index 242b37cbf4..c7746f8e06 100644 --- a/tests/Common/WebFrameworkTestCase.php +++ b/tests/Common/WebFrameworkTestCase.php @@ -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 @@ -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(); } } diff --git a/tests/Frameworks/Frankenphp/index.php b/tests/Frameworks/Frankenphp/index.php new file mode 100644 index 0000000000..7da5eeb0c8 --- /dev/null +++ b/tests/Frameworks/Frankenphp/index.php @@ -0,0 +1,19 @@ +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] = [ diff --git a/tests/Integrations/Frankenphp/CommonScenariosTest.php b/tests/Integrations/Frankenphp/CommonScenariosTest.php new file mode 100644 index 0000000000..64bebda421 --- /dev/null +++ b/tests/Integrations/Frankenphp/CommonScenariosTest.php @@ -0,0 +1,73 @@ +tracesFromWebRequest(function () use ($spec) { + $this->call($spec); + }); + + $this->assertFlameGraph($traces, $spanExpectations); + } + + public function provideSpecs() + { + return $this->buildDataProvider( + [ + 'A simple GET request returning a string' => [ + SpanAssertion::build( + 'web.request', + 'frankenphp', + 'web', + 'GET /simple' + )->withExactTags([ + 'http.method' => 'GET', + 'http.url' => 'http://localhost:' . self::PORT . '/simple?key=value&', + 'http.status_code' => '200', + Tag::SPAN_KIND => 'server', + Tag::COMPONENT => 'frankenphp' + ]), + ], + 'A GET request with an exception' => [ + SpanAssertion::build( + 'web.request', + 'frankenphp', + 'web', + 'GET /error' + )->withExactTags([ + 'http.method' => 'GET', + 'http.url' => 'http://localhost:' . self::PORT . '/error?key=value&', + 'http.status_code' => '500', + Tag::SPAN_KIND => 'server', + Tag::COMPONENT => 'frankenphp' + ])->setError('Exception', 'Uncaught Exception: Error page', true), + ], + ], + true + ); + } +} diff --git a/tests/Integrations/Frankenphp/DistributedTracingTest.php b/tests/Integrations/Frankenphp/DistributedTracingTest.php new file mode 100644 index 0000000000..266d61b945 --- /dev/null +++ b/tests/Integrations/Frankenphp/DistributedTracingTest.php @@ -0,0 +1,79 @@ + "x-header", + 'DD_TRACE_HTTP_POST_DATA_PARAM_ALLOWED' => 'foo.password, bar']); + } + + public function testDistributedTracing() + { + $traces = $this->tracesFromWebRequest(function () use (&$traceId) { + \DDTrace\add_distributed_tag("user_id", 42); + \DDTrace\start_span(); + $traceId = \DDTrace\root_span()->id; + $spec = GetSpec::create('request', '/', [ + "User-Agent: Test", + "x-header: somevalue", + 'x_forwarded_for', '127.12.34.1', + ]); + $this->call($spec); + }); + + $trace = $traces[0][0]; + $this->assertSame($traceId, $trace["trace_id"]); + $this->assertSame("42", $trace["meta"]["_dd.p.user_id"]); + $this->assertSame("Test", $trace["meta"]["http.useragent"]); + $this->assertSame("somevalue", $trace["meta"]["http.request.headers.x-header"]); + $this->assertArrayNotHasKey('http.client_ip', $trace["meta"]); + } + + public function testDistributedTracingPostWithAllowedParams() + { + $traces = $this->tracesFromWebRequest(function () use (&$traceId) { + \DDTrace\add_distributed_tag("user_id", 42); + \DDTrace\start_span(); + $traceId = \DDTrace\root_span()->id; + $spec = PostSpec::create('request', '/', [ + 'User-Agent: Test', + 'x-header: somevalue', + 'x_forwarded_for', '127.12.34.1', + ], + 'pass word=should_redact' + . '&foo[password]=should_not_redact' + . '&bar[key1]=value1&bar[key2][baz]=value2&bar[key2][password]=should_not_redact' + ); + $this->call($spec); + }); + + $trace = $traces[0][0]; + $this->assertSame($traceId, $trace["trace_id"]); + $this->assertSame("42", $trace["meta"]["_dd.p.user_id"]); + $this->assertSame("Test", $trace["meta"]["http.useragent"]); + $this->assertSame("somevalue", $trace["meta"]["http.request.headers.x-header"]); + $this->assertSame("", $trace["meta"]["http.request.post.pass_word"]); + $this->assertSame("should_not_redact", $trace["meta"]["http.request.post.foo.password"]); + $this->assertSame("value1", $trace["meta"]["http.request.post.bar.key1"]); + $this->assertSame("value2", $trace["meta"]["http.request.post.bar.key2.baz"]); + $this->assertSame("should_not_redact", $trace["meta"]["http.request.post.bar.key2.password"]); + $this->assertArrayNotHasKey('http.client_ip', $trace["meta"]); + } +} diff --git a/tests/Sapi/Frankenphp/Caddyfile b/tests/Sapi/Frankenphp/Caddyfile new file mode 100644 index 0000000000..68f94900bd --- /dev/null +++ b/tests/Sapi/Frankenphp/Caddyfile @@ -0,0 +1,19 @@ +{ + # Enable FrankenPHP + frankenphp { + worker { + file {{frankenphp_php}} + } + } + # Configure when the directive must be executed + order php_server before file_server + # HTTPS enabled by default... + auto_https off +} + +http://:{{frankenphp_port}} { + bind {{frankenphp_host}} + # Execute PHP files in the current directory and serve assets + php_server + root * {{frankenphp_dir}} +} diff --git a/tests/Sapi/Frankenphp/FrankenphpServer.php b/tests/Sapi/Frankenphp/FrankenphpServer.php new file mode 100644 index 0000000000..3ba424b75a --- /dev/null +++ b/tests/Sapi/Frankenphp/FrankenphpServer.php @@ -0,0 +1,149 @@ +indexFile = $indexFile; + $this->envs = $envs; + + if (getenv('PHPUNIT_COVERAGE')) { + $inis['auto_prepend_file'] = __DIR__ . '/../../save_code_coverage.php'; + + $xdebugExtension = glob(PHP_EXTENSION_DIR . '/xdebug*.so'); + $xdebugExtension = end($xdebugExtension); + $inis['zend_extension'] = $xdebugExtension; + $inis['xdebug.mode'] = 'coverage'; + } + + $replacements = [ + '{{frankenphp_host}}' => $host, + '{{frankenphp_port}}' => $port, + '{{frankenphp_php}}' => $this->indexFile, + '{{frankenphp_dir}}' => dirname($this->indexFile), + ]; + $configContent = str_replace( + array_keys($replacements), + array_values($replacements), + file_get_contents(__DIR__ . '/Caddyfile') + ); + + $this->configDir = sys_get_temp_dir() . uniqid('/frankenphp-', true); + $this->configFile = $this->configDir . "/Caddyfile"; + mkdir($this->configDir); + + $iniString = ""; + foreach ($inis as $ini => $val) { + $iniString .= "$ini = $val\n"; + } + file_put_contents($this->configDir . "/php.ini", $iniString); + + $this->logFile = fopen(dirname($indexFile) . '/' . self::ERROR_LOG, "a+"); + + if (false === file_put_contents($this->configFile, $configContent)) { + throw new \Exception('Error creating temp frankenphp config file: ' . $this->configFile); + } + } + + private static function installFrankenphp() + { + exec(__DIR__ . "/../../../tooling/bin/install-frankenphp.sh"); + } + + public function start() + { + if (!file_exists(readlink("/usr/local/bin/frankenphp"))) { + self::installFrankenphp(); + } + + $cmd = sprintf( + "frankenphp run --config '%s'", + $this->configFile + ); + $envString = "PHPRC=" . $this->configDir; + foreach ($this->envs as $env => $val) { + $envString .= " $env=\"$val\""; + } + $processCmd = "$envString exec $cmd"; + + + // See phpunit_error.log in CircleCI artifacts + error_log("[frankenphp] Starting: '$envString $cmd'"); + if (isset($this->inis['frankenphp'])) { + error_log("[frankenphp] Error log: '" . realpath($this->inis['error_log']) . "'"); + } + + $this->process = new Process($processCmd); + $this->process->start(); + } + + public function stop() + { + error_log("[frankenphp] Stopping..."); + $this->process->stop(0); + } + + public function isFastCgi() + { + return false; + } + + public function checkErrors() + { + $newLogs = stream_get_contents($this->logFile); + if (preg_match("(=== Total [0-9]+ memory leaks detected ===)", $newLogs)) { + return $newLogs; + } + + $newLogs = $this->process->getIncrementalErrorOutput(); + if (!$this->process->isRunning()) { + return "$newLogs\n"; + } + + return null; + } +} diff --git a/tests/Unit/Integrations/IntegrationsLoaderTest.php b/tests/Unit/Integrations/IntegrationsLoaderTest.php index a4a3cfa524..8402cc6bb0 100644 --- a/tests/Unit/Integrations/IntegrationsLoaderTest.php +++ b/tests/Unit/Integrations/IntegrationsLoaderTest.php @@ -168,6 +168,7 @@ public function testWeDidNotForgetToRegisterALibraryForAutoLoading() $excluded[] = 'elasticsearch'; $excluded[] = 'eloquent'; $excluded[] = 'exec'; + $excluded[] = 'frankenphp'; $excluded[] = 'guzzle'; $excluded[] = 'laminas'; $excluded[] = 'laravel'; diff --git a/tests/WebServer.php b/tests/WebServer.php index 17059b24a6..ae2609b479 100644 --- a/tests/WebServer.php +++ b/tests/WebServer.php @@ -4,6 +4,7 @@ use DDTrace\Tests\Nginx\NginxServer; use DDTrace\Tests\Sapi\CliServer\CliServer; +use DDTrace\Tests\Sapi\Frankenphp\FrankenphpServer; use DDTrace\Tests\Sapi\PhpApache\PhpApache; use DDTrace\Tests\Sapi\PhpCgi\PhpCgi; use DDTrace\Tests\Sapi\PhpFpm\PhpFpm; @@ -70,6 +71,7 @@ final class WebServer private $inis = []; private $roadrunnerVersion = null; + private $isFrankenphp = false; private $isSwoole = false; @@ -113,6 +115,11 @@ public function setSwoole() $this->isSwoole = true; } + public function setFrankenphp() + { + $this->isFrankenphp = true; + } + public function start() { $this->errorLogSize = (int)@filesize($this->defaultInis['error_log']); @@ -132,6 +139,14 @@ public function start() $this->envs, $this->inis ); + } elseif ($this->isFrankenphp) { + $this->sapi = new FrankenphpServer( + $this->indexFile, + $this->host, + $this->port, + $this->envs, + $this->inis + ); } else { switch (\getenv('DD_TRACE_TEST_SAPI')) { case 'cgi-fcgi': diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 0cd507c281..c5ee275cdf 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -67,6 +67,9 @@ ./Integrations/Slim/V4 + + ./Integrations/Frankenphp + ./Integrations/Swoole diff --git a/tooling/bin/install-frankenphp.sh b/tooling/bin/install-frankenphp.sh new file mode 100755 index 0000000000..e2e0a3d47b --- /dev/null +++ b/tooling/bin/install-frankenphp.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +set -eux + +frankenphpTarGzUrl=https://github.com/dunglas/frankenphp/archive/refs/tags/v1.1.2.tar.gz +FRANKENPHP_SRC_DIR=/usr/local/src/frankenphp + +mkdir -p $FRANKENPHP_SRC_DIR + +curl -Lo /tmp/frankenphp.tar.gz ${frankenphpTarGzUrl} +tar xf /tmp/frankenphp.tar.gz -C ${FRANKENPHP_SRC_DIR} --strip-components=1 +rm -f /tmp/frankenphp.tar.gz + +cd ${FRANKENPHP_SRC_DIR} +cd caddy/frankenphp + +if ldd $(which php) 2>/dev/null | grep -q libasan; then + ASAN="-fsanitize=address" +else + ASAN="" +fi + +CGO_CFLAGS="$(php-config --includes) $ASAN" CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs) $ASAN" go build + +mv frankenphp $(readlink /usr/local/bin/frankenphp)