From f40c169bc7a42aafea0fea351d70a4c333aed912 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Sun, 30 Aug 2026 00:21:31 -0600 Subject: [PATCH 01/47] feat: combine tracing and profiling in ddtrace.so --- .claude/ci/appsec-native-tests.md | 20 +- .claude/ci/benchmarks.md | 8 +- .claude/ci/building-locally.md | 40 +- .claude/ci/compile-artifacts.md | 51 +- .claude/ci/diagnosing-failures.md | 2 +- .claude/ci/github-actions-profiler.md | 123 ++-- .claude/ci/index.md | 22 +- .claude/ci/meta-improv-instr.md | 9 +- .claude/ci/meta-job-group-doc.md | 6 +- .claude/ci/package-dind-verification.md | 2 +- .claude/ci/package-native-verification.md | 6 +- .claude/ci/packaging-oci.md | 10 +- .claude/ci/shared-zai-tea-tests.md | 34 +- .claude/ci/tracer-integration-tests.md | 8 +- .claude/ci/tracer-unit-tests.md | 44 +- .claude/ci/tracer-web-tests.md | 6 +- .claude/project/profiling.md | 79 ++- .github/workflows/prof_asan.yml | 14 +- .github/workflows/prof_correctness.yml | 103 ++- .gitlab/build-profiler.sh | 19 +- .gitlab/generate-package.php | 28 +- .gitlab/generate-profiler.php | 6 +- Makefile | 3 + benchmark/otel-profiler-context/README.md | 6 +- benchmark/otel-profiler-context/run.sh | 6 +- components-rs/build.rs | 11 +- components-rs/config.rs | 9 + components-rs/config_codegen.rs | 115 ++++ components-rs/lib.rs | 23 +- config.m4 | 47 +- config.w32 | 6 +- datadog-setup.php | 56 +- .../test_install_custom_ext_dir_and_ini.sh | 1 + ...test_install_custom_extension_directory.sh | 1 + ...tall_uninstall_install_tracer_profiling.sh | 10 +- .../test_profiler_install_disabled.sh | 5 +- .../test_profiler_install_enabled.sh | 5 +- .../verify_packages/installer/utils.sh | 23 +- ext/configuration.c | 10 +- ext/configuration.h | 35 +- ext/configuration_shared.h | 69 ++ ext/datadog.c | 262 +++++-- ext/datadog.h | 6 +- ext/handlers_pcntl.c | 4 +- ext/phpinfo.c | 2 +- ext/remote_config.c | 4 +- ext/sidecar.c | 6 +- ext/signals.c | 2 +- ext/startup_logging.c | 14 +- ext/telemetry.c | 4 +- loader/bin/test_apache_reload.sh | 4 +- loader/dd_library_loader.c | 59 +- loader/tests/functional/test_php_output.php | 6 - loader/tests/functional/test_phpinfo.php | 8 +- package/ddtrace.ini.example | 1 + profiling/README.md | 40 +- profiling/build.rs | 50 +- profiling/configuration.h | 53 ++ profiling/src/bindings/mod.rs | 7 + profiling/src/capi.rs | 2 +- profiling/src/config.rs | 638 ++---------------- profiling/src/configuration.c | 80 +++ profiling/src/lib.rs | 167 ++++- profiling/src/lifecycle.h | 25 + profiling/src/module_globals.rs | 46 +- profiling/src/php_ffi.c | 75 +- profiling/src/php_ffi.h | 19 +- profiling/tests/correctness/timeline.php | 4 +- profiling/tests/phpt/agent_host_port_01.phpt | 23 + .../tests/phpt/allocation_bind_static_01.phpt | 2 +- .../tests/phpt/allocation_func_get_args.phpt | 2 +- .../tests/phpt/allocation_generator_01.phpt | 4 +- profiling/tests/phpt/allocation_jit_01.phpt | 2 +- .../phpt/allocation_sampling_distance.phpt | 2 +- .../tests/phpt/combined_default_disabled.phpt | 15 + profiling/tests/phpt/combined_identity.phpt | 23 + profiling/tests/phpt/disabled_hooks_01.phpt | 2 +- profiling/tests/phpt/exceptions_01.phpt | 4 +- .../tests/phpt/exceptions_fibers_throw.phpt | 2 +- .../phpt/exceptions_generator_throw.phpt | 2 +- profiling/tests/phpt/exceptions_zts_01.phpt | 8 +- profiling/tests/phpt/fibers_01.phpt | 2 +- profiling/tests/phpt/fork_01.phpt | 7 +- .../tests/phpt/gc_collect_cycles_01.phpt | 2 +- profiling/tests/phpt/gc_mem_caches_01.phpt | 2 +- profiling/tests/phpt/gc_mem_caches_02.phpt | 2 +- profiling/tests/phpt/heap_live_01.phpt | 2 +- profiling/tests/phpt/heap_live_02.phpt | 2 +- .../tests/phpt/heap_live_phpinfo_01.phpt | 4 +- .../tests/phpt/heap_live_phpinfo_02.phpt | 4 +- .../tests/phpt/heap_live_phpinfo_03.phpt | 4 +- .../tests/phpt/heap_live_phpinfo_04.phpt | 4 +- profiling/tests/phpt/jit_01.phpt | 2 +- profiling/tests/phpt/jit_02.phpt | 2 +- profiling/tests/phpt/jit_03.phpt | 2 +- .../tests/phpt/memory_get_peak_usage_01.phpt | 2 +- .../tests/phpt/native_thread_alloc_01.phpt | 2 +- .../pcntl_async_signal_helper_thread.phpt | 7 +- profiling/tests/phpt/phpinfo_01.phpt | 4 +- profiling/tests/phpt/phpinfo_02.phpt | 4 +- profiling/tests/phpt/phpinfo_03.phpt | 4 +- profiling/tests/phpt/phpinfo_04.phpt | 4 +- profiling/tests/phpt/phpinfo_05.phpt | 4 +- profiling/tests/phpt/phpinfo_06.phpt | 4 +- profiling/tests/phpt/phpinfo_07.phpt | 4 +- profiling/tests/phpt/phpinfo_08.phpt | 4 +- profiling/tests/phpt/phpinfo_ini_01.phpt | 4 +- profiling/tests/phpt/phpinfo_ini_02.phpt | 4 +- profiling/tests/phpt/preload_01.phpt | 4 +- profiling/tests/phpt/service_cli_01.phpt | 4 +- profiling/tests/phpt/service_web_01.phpt | 23 +- .../standalone_conflict_ddtrace_first.phpt | 37 + .../standalone_conflict_profiler_first.phpt | 37 + .../timeline_fatal_error_null_filename.phpt | 2 +- profiling/tests/phpt/trampolines_01.phpt | 2 +- profiling/tests/phpt/trampolines_02.phpt | 2 +- profiling/tests/phpt/uds_01.phpt | 4 +- profiling/tests/phpt/uds_02.phpt | 4 +- profiling/tests/phpt/zts_01.phpt | 2 +- tests/ext/profiling/runtime_id_01.phpt | 4 +- tests/ext/profiling/runtime_id_02.phpt | 4 +- tests/randomized/config/inis.php | 1 - tooling/bin/build-debug-artifact | 26 +- tooling/bin/generate-final-artifact.sh | 89 +-- tooling/bin/generate-ssi-package.sh | 47 +- tooling/bin/pecl-build | 3 - tracer/ddtrace.c | 2 +- tracer/engine_hooks.c | 19 + tracer/engine_hooks.h | 3 + tracer/tracer_api.h | 7 +- zend_abstract_interface/config/config.h | 2 +- .../config/config_stable_file.c | 16 +- 132 files changed, 1810 insertions(+), 1386 deletions(-) create mode 100644 components-rs/config.rs create mode 100644 components-rs/config_codegen.rs create mode 100644 ext/configuration_shared.h create mode 100644 profiling/configuration.h create mode 100644 profiling/src/configuration.c create mode 100644 profiling/src/lifecycle.h create mode 100644 profiling/tests/phpt/agent_host_port_01.phpt create mode 100644 profiling/tests/phpt/combined_default_disabled.phpt create mode 100644 profiling/tests/phpt/combined_identity.phpt create mode 100644 profiling/tests/phpt/standalone_conflict_ddtrace_first.phpt create mode 100644 profiling/tests/phpt/standalone_conflict_profiler_first.phpt diff --git a/.claude/ci/appsec-native-tests.md b/.claude/ci/appsec-native-tests.md index 21521e3b249..24b42dc387a 100644 --- a/.claude/ci/appsec-native-tests.md +++ b/.claude/ci/appsec-native-tests.md @@ -8,12 +8,12 @@ file. | CI Job | Image | What it does | |--------|-------|-------------| -| `test appsec extension: [{ver}, {arch}, debug]` | `datadog/dd-trace-ci:php-{ver}_bookworm-6` | Builds appsec PHP extension + runs phpunit `.phpt` tests | +| `test appsec extension: [{ver}, {arch}, debug]` | `datadog/dd-trace-ci:php-{ver}_bookworm-10` | Builds appsec PHP extension + runs phpunit `.phpt` tests | | `test appsec extension: [{ver}, {arch}, debug-zts]` | same | ZTS variant | | `test appsec extension: [{ver}, {arch}, debug-zts-asan]` | same | ASAN variant (PHP 7.4+) | -| `test appsec helper asan` | `datadog/dd-trace-ci:bookworm-6` | Builds C++ helper with ASAN, runs gtest suite | -| `appsec lint` | `datadog/dd-trace-ci:php-8.3_bookworm-6` | clang-format + clang-tidy | -| `appsec code coverage` | `datadog/dd-trace-ci:php-8.3_bookworm-6` | Coverage instrumented build (not needed locally) | +| `test appsec helper asan` | `datadog/dd-trace-ci:bookworm-10` | Builds C++ helper with ASAN, runs gtest suite | +| `appsec lint` | `datadog/dd-trace-ci:php-8.3_bookworm-10` | clang-format + clang-tidy | +| `appsec code coverage` | `datadog/dd-trace-ci:php-8.3_bookworm-10` | Coverage instrumented build (not needed locally) | Runner: `arch:amd64` + `arch:arm64` Matrix: PHP 7.0+ × {debug, debug-zts, debug-zts-asan (7.4+)} @@ -25,7 +25,7 @@ Docker option between the image name and `--`: ```bash .claude/ci/dockerh --cache appsec-ext-8.3-debug-arm64 --overlayfs --php debug \ - datadog/dd-trace-ci:php-8.3_bookworm-6 --platform linux/arm64 -- bash -c '...' + datadog/dd-trace-ci:php-8.3_bookworm-10 --platform linux/arm64 -- bash -c '...' ``` ## Why `--overlayfs` is needed @@ -43,7 +43,7 @@ All commands are run from the repo root. Replace `8.3` with the desired PHP vers ```bash .claude/ci/dockerh --cache appsec-ext-8.3-debug --overlayfs --php debug \ - datadog/dd-trace-ci:php-8.3_bookworm-6 -- bash -c ' + datadog/dd-trace-ci:php-8.3_bookworm-10 -- bash -c ' set -e sudo apt-get update -qq && sudo apt-get install -y -qq \ libc++-17-dev libc++abi-17-dev > /dev/null 2>&1 @@ -90,7 +90,7 @@ to save ~10 seconds: ```bash .claude/ci/dockerh --cache appsec-ext-8.3-debug --overlayfs --php debug \ - datadog/dd-trace-ci:php-8.3_bookworm-6 -- bash -c ' + datadog/dd-trace-ci:php-8.3_bookworm-10 -- bash -c ' sudo apt-get update -qq && sudo apt-get install -y -qq \ libc++-17-dev libc++abi-17-dev > /dev/null 2>&1 cd appsec/build @@ -145,7 +145,7 @@ docker volume rm php-appsec-8.3-debug php-tracer-8.3-debug ## Helper tests (C++ ASAN) -The C++ helper tests use the `bookworm-6` image (no PHP needed). The +The C++ helper tests use the `bookworm-10` image (no PHP needed). The binary is a gtest executable. With `--overlayfs --root`, all writes (including `appsec/build-helper`) persist in the Docker volume automatically — no manual bind mount needed. @@ -154,7 +154,7 @@ automatically — no manual bind mount needed. ```bash .claude/ci/dockerh --cache appsec-helper --overlayfs --root \ - datadog/dd-trace-ci:bookworm-6 -- bash -c ' + datadog/dd-trace-ci:bookworm-10 -- bash -c ' set -e apt-get update -qq && apt-get install -y -qq \ libc++-17-dev libc++abi-17-dev > /dev/null 2>&1 @@ -192,7 +192,7 @@ persisted by `--overlayfs`): ```bash .claude/ci/dockerh --cache appsec-helper --overlayfs --root \ - datadog/dd-trace-ci:bookworm-6 -- bash -c ' + datadog/dd-trace-ci:bookworm-10 -- bash -c ' apt-get update -qq && apt-get install -y -qq libc++1-17 libc++abi1-17 > /dev/null 2>&1 ./appsec/build-helper/tests/helper/ddappsec_helper_test \ --gtest_filter="WafTest.TraceAttributesAreSent" diff --git a/.claude/ci/benchmarks.md b/.claude/ci/benchmarks.md index 25174cd234b..3cf25748109 100644 --- a/.claude/ci/benchmarks.md +++ b/.claude/ci/benchmarks.md @@ -68,7 +68,7 @@ the link, so no special handling is needed. ```bash .claude/ci/dockerh --cache bench-82 --clean-cache --overlayfs --root \ - datadog/dd-trace-ci:php-8.2_bookworm-6 \ + datadog/dd-trace-ci:php-8.2_bookworm-10 \ -e DD_TRACE_AUTOLOAD_NO_COMPILE=true \ -- bash -c ' git config --global --add safe.directory /project/dd-trace-php @@ -93,7 +93,7 @@ Build step (skip if `.so` already in overlay or downloaded from CI): ```bash .claude/ci/dockerh --cache bench-82-split --overlayfs --root \ - datadog/dd-trace-ci:php-8.2_bookworm-6 \ + datadog/dd-trace-ci:php-8.2_bookworm-10 \ -e SHARED=1 \ -e CI_COMMIT_SHA=$(git rev-parse HEAD) \ -e CI_COMMIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) \ @@ -104,7 +104,7 @@ Benchmark step (every run — the ext dir is outside the overlay): ```bash .claude/ci/dockerh --cache bench-82-split --overlayfs --root \ - datadog/dd-trace-ci:php-8.2_bookworm-6 \ + datadog/dd-trace-ci:php-8.2_bookworm-10 \ -e SHARED=1 \ -e DD_TRACE_AUTOLOAD_NO_COMPILE=true \ -- bash -c ' @@ -135,7 +135,7 @@ tooling/bin/download-artifacts \ # Place it in the overlay via bind-mount .claude/ci/dockerh --cache bench-82-dl --overlayfs --root \ - datadog/dd-trace-ci:php-8.2_bookworm-6 \ + datadog/dd-trace-ci:php-8.2_bookworm-10 \ -v /tmp/bench-ext:/tmp/bench-ext:ro \ -- bash -c ' mkdir -p tmp/build_extension/modules diff --git a/.claude/ci/building-locally.md b/.claude/ci/building-locally.md index 9c3f0f08a58..211e53e53e1 100644 --- a/.claude/ci/building-locally.md +++ b/.claude/ci/building-locally.md @@ -92,7 +92,7 @@ Used before running tracer unit tests, .phpt tests, etc.: ```bash .claude/ci/dockerh --cache tracer-8.3-debug --overlayfs --php debug \ - datadog/dd-trace-ci:php-8.3_bookworm-6 -- bash -c ' + datadog/dd-trace-ci:php-8.3_bookworm-10 -- bash -c ' set -e git submodule update --init libdatadog make -j$(nproc) all @@ -138,7 +138,7 @@ Reproduces the `compile extension: debug` CI job exactly: ```bash .claude/ci/dockerh --cache tracer-8.3-debug --overlayfs --root \ - datadog/dd-trace-ci:php-8.3_bookworm-6 \ + datadog/dd-trace-ci:php-8.3_bookworm-10 \ -e CI_COMMIT_SHA=$(git rev-parse HEAD) \ -e CI_COMMIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) \ -e SHARED=1 \ @@ -156,7 +156,7 @@ enables `-fsanitize=address` in the Rust sidecar. ```bash .claude/ci/dockerh --cache tracer-8.3-asan --overlayfs \ --php debug-zts-asan \ - datadog/dd-trace-ci:php-8.3_bookworm-6 -- bash -c ' + datadog/dd-trace-ci:php-8.3_bookworm-10 -- bash -c ' set -e export COMPILE_ASAN=1 make -j$(nproc) all @@ -242,15 +242,19 @@ git submodule update --init \ ### For correctness tests (bookworm) -`CARGO_TARGET_DIR` **must** be set explicitly (see -[github-actions-profiler.md](github-actions-profiler.md) for why): +Build the loadable extension through phpize/configure/Make from the repository +root. Make owns the Cargo feature set, PHP headers, target directory, and final +module name: ```bash dockerh --cache profiler-8.3-nts --php nts \ - datadog/dd-trace-ci:php-8.3_bookworm-6 -- bash -c ' -export CARGO_TARGET_DIR=/project/dd-trace-php/target -cd profiling && cargo rustc --features=trigger_time_sample \ - --profile profiler-release --crate-type=cdylib + datadog/dd-trace-ci:php-8.3_bookworm-10 -- bash -c ' +cd /project/dd-trace-php +phpize +DDTRACE_PROFILING_FEATURES=trigger_time_sample \ + ./configure --disable-ddtrace-tracer --enable-ddtrace-profiling --disable-ddtrace-rust-debug +make -j"$(nproc)" +php -n -d extension=modules/datadog-profiling.so --ri datadog-profiling ' ``` @@ -258,23 +262,23 @@ cd profiling && cargo rustc --features=trigger_time_sample \ Bookworm is too recent for binary compatibility purposes. -`build-profiler.sh` takes two arguments: the output directory prefix -and the thread safety mode (`nts` or `zts`). It calls `switch-php` -internally, so use `--root` (not `--php`). The output prefix must -match the directory layout expected by `generate-final-artifact.sh`: -`datadog-profiling/{triplet}/lib/php/{PHP_API}/`. +`build-profiler.sh` takes an output directory prefix, the thread safety mode +(`nts` or `zts`), and an optional `combined` artifact mode. Package jobs use +combined mode. It calls `switch-php` internally, so use `--root` (not +`--php`). The output prefix must match the directory layout expected by +`generate-final-artifact.sh`: `combined-ddtrace/{triplet}/lib/php/{PHP_API}/`. Build one PHP version at a time (each centos-7 image ships one version). For a single version (e.g. 8.2, ABI `20220829`): ```bash -.claude/ci/dockerh --cache compile-profiler-8.2-gnu --overlayfs \ +.claude/ci/dockerh --cache compile-combined-8.2-gnu --overlayfs \ --root \ datadog/dd-trace-ci:php-8.2_centos-7 \ -e CI_COMMIT_SHA=$(git rev-parse HEAD) \ -e CI_COMMIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) \ -- bash -c 'PHP_VERSION=8.2 bash .gitlab/build-profiler.sh \ - datadog-profiling/x86_64-unknown-linux-gnu/lib/php/20220829 nts' + combined-ddtrace/x86_64-unknown-linux-gnu/lib/php/20220829 nts combined' ``` ## Sidecar (Rust) @@ -330,8 +334,8 @@ compiled `.so` files: - `appsec_$(uname -m)/` — appsec extensions (`ddappsec-{API}[-zts].so`) + helpers (`libddappsec-helper.so` and `libddappsec-helper-rust.so`) + `recommended.json` -- `datadog-profiling/{triplet}/lib/php/{API}/` — profiler - extensions +- `combined-ddtrace/{triplet}/lib/php/{API}/` — combined NTS/ZTS + `ddtrace.so` artifacts used to replace the tracer-only package inputs Missing files cause hard `cp` failures. This means that we need to build (or download from CI) all these individual artifacts. This is rarely desirable when diff --git a/.claude/ci/compile-artifacts.md b/.claude/ci/compile-artifacts.md index e5435e00ca7..284259e4d5b 100644 --- a/.claude/ci/compile-artifacts.md +++ b/.claude/ci/compile-artifacts.md @@ -15,7 +15,7 @@ If you need to run this step outside a build script and your host lacks `binutil ```bash .claude/ci/dockerh --cache tracer-8.3-debug \ - datadog/dd-trace-ci:php-8.3_bookworm-6 -- \ + datadog/dd-trace-ci:php-8.3_bookworm-10 -- \ objcopy --compress-debug-sections /project/dd-trace-php/tmp/build_extension/modules/ddtrace.so ``` @@ -43,8 +43,8 @@ If you need to run this step outside a build script and your host lacks `binutil | CI Job | Image | What it does | |--------|-------|--------------| -| `compile extension: debug` | `dd-trace-ci:php-{ver}_bookworm-6` | Runs `append-build-id.sh` to stamp VERSION; compiles Rust (`compile_rust.sh`, debug profile) and C (`make -j static`) in parallel; `make static` also builds `php_sidecar_mockgen` (a secondary Rust build generating `mock_php.c` stubs); rewrites ldflags via `sed -i`; links `ddtrace.a` + `libdatadog_php.a` → `ddtrace.so` with `-soname ddtrace.so`. Sets `SHARED=1` (adds `--cfg php_shared_build` to `RUSTFLAGS`). | -| `compile extension: debug-zts-asan` | `dd-trace-ci:php-{ver}_bookworm-6` | Same as `compile extension: debug` (inherits `SHARED=1` via `extends:`) but with `WITH_ASAN=1` (sets `ASAN=1`+`COMPILE_ASAN=1`) and `SWITCH_PHP_VERSION=debug-zts-asan`; produces `ddtrace.so` instrumented with AddressSanitizer for ASAN test jobs | +| `compile extension: debug` | `dd-trace-ci:php-{ver}_bookworm-10` | Runs `append-build-id.sh` to stamp VERSION; compiles Rust (`compile_rust.sh`, debug profile) and C (`make -j static`) in parallel; `make static` also builds `php_sidecar_mockgen` (a secondary Rust build generating `mock_php.c` stubs); rewrites ldflags via `sed -i`; links `ddtrace.a` + `libdatadog_php.a` → `ddtrace.so` with `-soname ddtrace.so`. Sets `SHARED=1` (adds `--cfg php_shared_build` to `RUSTFLAGS`). | +| `compile extension: debug-zts-asan` | `dd-trace-ci:php-{ver}_bookworm-10` | Same as `compile extension: debug` (inherits `SHARED=1` via `extends:`) but with `WITH_ASAN=1` (sets `ASAN=1`+`COMPILE_ASAN=1`) and `SWITCH_PHP_VERSION=debug-zts-asan`; produces `ddtrace.so` instrumented with AddressSanitizer for ASAN test jobs | | `Prepare code` | `php:8.2-cli` | Runs `composer update` + `make generate` to produce `src/bridge/_generated_*.php` | Runner: `arch:{amd64,arm64}` @@ -72,19 +72,19 @@ files via `classpreloader`: `_generated_api.php`, `_generated_tracer.php`, and | `compile tracing extension: [{ver}, {arch}, {triplet}]` | `dd-trace-ci:php-{ver}_{platform}` | Builds NTS + debug + ZTS static archives (`.a`) and standalone `.so` via `build-tracing.sh` (debug skipped on alpine); outputs `ddtrace-{PHP_API}{suffix}[-debug\|-zts].{a,so}` under `extensions_{arch}/` and `standalone_{arch}/` | | `compile tracing sidecar: [{arch}, {triplet}]` | `dd-trace-ci:php-8.1_{platform}` | Builds `libdatadog_php.{a,so}` (FFI bridge library; `datadog-php` crate in `components-rs/`) via `build-sidecar.sh` → `compile_rust.sh` → `cargo build`; profile `tracer-release` (LTO, 1 codegen unit, panic=abort); `RUSTFLAGS=--cfg tokio_unstable --cfg php_shared_build`; `SIDECAR_VERSION` embedded from `VERSION` file | | `link tracing extension: [{arch}, {triplet}]` | `dd-trace-ci:php-8.1_{platform}` | Rewrites `-export-symbols` → `-Wl,--retain-symbols-file` in the `.ldflags` file via `sed -i`; links each per-version `.a` in `extensions_$(uname -m)/` against `libdatadog_php_$(uname -m)${suffix}.a` with `-whole-archive` and the rewritten ldflags, setting `-soname ddtrace.so`; all links run in parallel background processes; post-processes each `.so` with `objcopy --compress-debug-sections` | -| `aggregate tracing extension: [{arch}]` | `dd-trace-ci:php-7.4_bookworm-6` | No-op `ls` that aggregates artifacts from all `compile tracing extension` jobs for one arch into a single artifact set | -| `compile tracing extension asan: [{ver}, {arch}, {triplet}]` | `dd-trace-ci:php-{ver}_bookworm-6` | Switches to `debug-zts-asan` PHP; builds `ddtrace.so` directly with `RUST_DEBUG_BUILD=1` (Rust debug profile, no `.a` intermediate); copies to `extensions_$(uname -m)/ddtrace-${ABI_NO}-debug-zts.so`; post-processes with `objcopy --compress-debug-sections` | +| `aggregate tracing extension: [{arch}]` | `dd-trace-ci:php-7.4_bookworm-10` | No-op `ls` that aggregates artifacts from all `compile tracing extension` jobs for one arch into a single artifact set | +| `compile tracing extension asan: [{ver}, {arch}, {triplet}]` | `dd-trace-ci:php-{ver}_bookworm-10` | Switches to `debug-zts-asan` PHP; builds `ddtrace.so` directly with `RUST_DEBUG_BUILD=1` (Rust debug profile, no `.a` intermediate); copies to `extensions_$(uname -m)/ddtrace-${ABI_NO}-debug-zts.so`; post-processes with `objcopy --compress-debug-sections` | | `compile appsec extension: [{ver}, {arch}, {triplet}]` | `dd-trace-ci:php-{ver}_{platform}` | Builds NTS and ZTS appsec extensions sequentially via cmake+make in `appsec/build/` and `appsec/build-zts/`; cmake flags: `-DCMAKE_BUILD_TYPE=RelWithDebInfo -DDD_APPSEC_BUILD_HELPER=OFF -DDD_APPSEC_TESTING=OFF -DDD_APPSEC_EXTENSION_STATIC_LIBSTDCXX=ON`; outputs `appsec_$(uname -m)/ddappsec-$PHP_API${suffix}[-zts].so`; post-processes with `objcopy --compress-debug-sections` | | `compile appsec helper` | `registry.ddbuild.io/images/mirror/b1o7r7e0/nginx_musl_toolchain` (original gone) | Builds `libddappsec-helper.so` via cmake+make with musl toolchain (`-DCMAKE_TOOLCHAIN_FILE=/sysroot/$(arch)-none-linux-musl/Toolchain.cmake`); `DD_APPSEC_ENABLE_PATCHELF_LIBC=ON` strips musl libc dependency via patchelf; runs gtest suite (`make ddappsec_helper_test && ./tests/helper/ddappsec_helper_test`); copies `recommended.json` to `appsec_$(uname -m)/` | | `compile appsec helper rust` | `dd-appsec-php-ci:nginx-fpm-php-8.5-release-musl` | Builds `libddappsec-helper-rust.so` via `cargo +nightly-$RUST_TARGET` with `--release -Zhost-config -Ztarget-applies-to-host --target $(uname -m)-unknown-linux-musl`; removes musl libc dep with `patchelf --remove-needed`; runs `cargo +nightly-$RUST_TARGET test --release` after build; output in `appsec_$(uname -m)/` | -| `compile profiler extension: [{ver}, {arch}, {triplet}]` | `dd-trace-ci:php-{ver}_{platform}` | Builds NTS and ZTS profiler extensions via `cargo build --profile profiler-release` in `profiling/`; for ZTS, `touch build.rs` forces the build script to re-run after `switch-php` to pick up ZTS headers; outputs `datadog-profiling[-zts].so` under a prefix dir; on alpine+aarch64 symlinks clang17 over clang20 to work around a bindgen incompatibility | +| `compile combined extension: [{ver}, {arch}, {triplet}]` | `dd-trace-ci:php-{ver}_{platform}` | Builds PHP-version-specific NTS and ZTS combined `ddtrace.so` artifacts through `.gitlab/build-profiler.sh`, which uses root phpize/configure/Make in isolated source copies; package generation overlays these onto the historical ddtrace artifact paths; on alpine+aarch64 symlinks clang17 over clang20 to work around a bindgen incompatibility | | `compile loader: [{host_os}, {arch}]` | `dd-trace-ci:php-8.3_{platform}` (alpine: `php-compile-extension-alpine-8.3`) | Builds `dd_library_loader-$(uname -m)-${HOST_OS}.so` (SSI loader) via `phpize`+`configure`+`make` in `loader/`; on musl installs build deps via `apk add`; embeds `PHP_DD_LIBRARY_LOADER_VERSION` from `VERSION` file in CFLAGS | | `compile extension windows: [{ver}]` | `dd-trace-ci:php-{ver}_windows` | Runs a long-lived container via `docker run -d` + `docker exec`; builds NTS then ZTS via `phpize.bat` + `configure.bat --enable-debug-pack` + `nmake`; reuses NTS Rust `target/` for ZTS by moving it; outputs `extensions_x86_64/php_ddtrace-${ABI_NO}[-zts].dll` and `.pdb` debug symbols | -| `pecl build` | `dd-trace-ci:php-7.4_bookworm-6` | Runs `tooling/bin/pecl-build` via `make build_pecl_package`; regenerates PHP bridge files via `composer -dtooling/generation`; mutates `package.xml` (version, date, file list) and `Cargo.toml` (strips profiling workspace member) in-place; produces `datadog_trace-*.tgz` via `pear package`; requires a clean tree to re-run | +| `pecl build` | `dd-trace-ci:php-7.4_bookworm-10` | Runs `tooling/bin/pecl-build` via `make build_pecl_package`; regenerates PHP bridge files via `composer -dtooling/generation`; mutates `package.xml` (version, date, file list) and `Cargo.toml` (strips profiling workspace member) in-place; produces `datadog_trace-*.tgz` via `pear package`; requires a clean tree to re-run | Runner: `arch:{amd64,arm64}` (Linux jobs) or `windows-v2:2019` (Windows) Matrix (tracing/appsec extension): PHP 7.0+ x 4 build platforms (x86_64-alpine-linux-musl, aarch64-alpine-linux-musl, x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu) -Matrix (profiler extension): PHP 7.1+ x same 4 platforms +Matrix (combined extension): PHP 7.1+ x same 4 platforms Matrix (ASAN tracing): PHP 7.4+ x {x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu} Matrix (Windows): PHP 7.2+ @@ -134,7 +134,7 @@ prepare code cache cargo deps: [{arch}, {triplet}] | | | | | +----. | | | - | +-- compile profiler extension: [{ver}, {arch}, {triplet}]* + | +-- compile combined extension: [{ver}, {arch}, {triplet}]* | +-- compile tracing extension: [{ver}, {arch}, {triplet}] (prepare code only) | | @@ -181,7 +181,7 @@ prepare code cache cargo deps: [{arch}, {triplet}] for `aws-lc-sys` build failures on musl targets. - The Cargo cache uses the default `pull-push` policy in `cache cargo deps` and - `policy: pull` (read-only) in `compile profiler extension` and `compile tracing + `policy: pull` (read-only) in `compile combined extension` and `compile tracing sidecar`. `compile tracing extension` has no `cache:` block at all (this is expected since it runs `make static`, a pure C/PHP build; the Rust compilation is handled by `compile tracing sidecar`). @@ -196,10 +196,11 @@ prepare code cache cargo deps: [{arch}, {triplet}] a new function that must be callable from appsec, profiler, or the SSI loader, add it to `datadog.sym` or the linker will drop it. -- `CARGO_TARGET_DIR` must not be set explicitly for `compile_rust.sh`. The default - (`target`) is resolved relative to the workspace root by Cargo. An explicit value - becomes CWD-relative; since `compile_rust.sh` `cd`s into `components-rs/`, this - silently breaks the build. +- `CARGO_TARGET_DIR` is owned by the top-level build path. `config.m4` forwards + configured target directories to the root Cargo package for combined and + standalone-profiler Make builds. Scripts that call `compile_rust.sh` directly + must likewise pass a workspace-root-relative or absolute target directory and + consume the matching output. - ASAN artifacts in the package pipeline have no "asan" in their filename: `compile tracing extension asan` outputs `ddtrace-{ABI}-debug-zts.so`, which is @@ -314,7 +315,7 @@ correct version string. | compile tracing sidecar | — | ~3 min | | compile appsec extension (per version) | ~2 min | ~2 min | | compile appsec helper rust | ~3 min | ~3 min | -| compile profiler extension (per version) | — | ~2 min | +| compile combined extension (per version) | — | ~2 min | | compile loader | ~4 sec | ~4 sec | Subsequent runs with cached Rust artifacts: C-only changes rebuild @@ -332,7 +333,7 @@ build dependencies. ```bash # compile extension: debug (tracer pipeline, PHP 8.3) .claude/ci/dockerh --cache tracer-8.3-debug --overlayfs --root \ - datadog/dd-trace-ci:php-8.3_bookworm-6 \ + datadog/dd-trace-ci:php-8.3_bookworm-10 \ -e CI_COMMIT_SHA=$(git rev-parse HEAD) \ -e CI_COMMIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) \ -e SHARED=1 \ @@ -340,7 +341,7 @@ build dependencies. # compile extension: debug-zts-asan (tracer pipeline, PHP 8.3) .claude/ci/dockerh --cache tracer-8.3-debug-zts-asan --overlayfs --root \ - datadog/dd-trace-ci:php-8.3_bookworm-6 \ + datadog/dd-trace-ci:php-8.3_bookworm-10 \ -e CI_COMMIT_SHA=$(git rev-parse HEAD) \ -e CI_COMMIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) \ -e WITH_ASAN=1 \ @@ -373,20 +374,20 @@ build dependencies. -e CI_COMMIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) \ -- bash -c 'PHP_VERSION=8.3 bash .gitlab/build-appsec.sh' -# compile profiler extension (PHP 8.3, linux-gnu) -.claude/ci/dockerh --cache compile-profiler-8.3-gnu --overlayfs --root \ +# compile combined extension (PHP 8.3, linux-gnu) +.claude/ci/dockerh --cache compile-combined-8.3-gnu --overlayfs --root \ datadog/dd-trace-ci:php-8.3_centos-7 \ -e CI_COMMIT_SHA=$(git rev-parse HEAD) \ -e CI_COMMIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) \ - -- bash -c 'PHP_VERSION=8.3 bash .gitlab/build-profiler.sh datadog-profiling/x86_64-unknown-linux-gnu/lib/php/20230831 nts' + -- bash -c 'PHP_VERSION=8.3 bash .gitlab/build-profiler.sh combined-ddtrace/x86_64-unknown-linux-gnu/lib/php/20230831 nts combined' -# compile profiler extension ZTS variant (PHP 8.3, linux-gnu) +# compile combined extension ZTS variant (PHP 8.3, linux-gnu) # Reuse the same cache — build-profiler.sh calls switch-php internally -.claude/ci/dockerh --cache compile-profiler-8.3-gnu --overlayfs --root \ +.claude/ci/dockerh --cache compile-combined-8.3-gnu --overlayfs --root \ datadog/dd-trace-ci:php-8.3_centos-7 \ -e CI_COMMIT_SHA=$(git rev-parse HEAD) \ -e CI_COMMIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) \ - -- bash -c 'PHP_VERSION=8.3 bash .gitlab/build-profiler.sh datadog-profiling/x86_64-unknown-linux-gnu/lib/php/20230831 zts' + -- bash -c 'PHP_VERSION=8.3 bash .gitlab/build-profiler.sh combined-ddtrace/x86_64-unknown-linux-gnu/lib/php/20230831 zts combined' # compile loader (linux-gnu) .claude/ci/dockerh --cache compile-loader-gnu --overlayfs \ @@ -411,14 +412,14 @@ build dependencies. # pecl build .claude/ci/dockerh --cache compile-pecl --overlayfs \ - datadog/dd-trace-ci:php-7.4_bookworm-6 \ + datadog/dd-trace-ci:php-7.4_bookworm-10 \ -e CI_COMMIT_SHA=$(git rev-parse HEAD) \ -e CI_COMMIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) \ -- make build_pecl_package ``` `--overlayfs` is used for package pipeline jobs because their output directories -(`extensions_*/`, `standalone_*/`, `appsec_*/`, `datadog-profiling/`, etc.) are +(`extensions_*/`, `standalone_*/`, `appsec_*/`, `combined-ddtrace/`, etc.) are written to the project root and to files like `VERSION` and `*.ldflags`. The overlayfs mode mounts the checkout read-only as the lower dir and uses a Docker named volume (`dd-ci-{NAME}`) as the upper dir, so all writes go to the volume diff --git a/.claude/ci/diagnosing-failures.md b/.claude/ci/diagnosing-failures.md index 00febd82a60..835bf994667 100644 --- a/.claude/ci/diagnosing-failures.md +++ b/.claude/ci/diagnosing-failures.md @@ -28,7 +28,7 @@ non-fatal issues. If a test flips green→red **deterministically across every PHP version/SAPI** with no corresponding source/commit change, suspect a **mutable CI image tag -being republished** under the same tag (the `bookworm-{N}` images are rebuilt +being republished** under the same tag (the `bookworm-10` images are rebuilt in place with `docker buildx bake --no-cache --pull`), not a code regression. Verify the mechanism empirically before fixing: diff --git a/.claude/ci/github-actions-profiler.md b/.claude/ci/github-actions-profiler.md index d362ef36646..eebd15c8c2f 100644 --- a/.claude/ci/github-actions-profiler.md +++ b/.claude/ci/github-actions-profiler.md @@ -20,11 +20,18 @@ ASAN matrix: PHP 8.3+ × {arm64, amd64}. ## What It Tests -Each job builds the profiler extension with `--features=trigger_time_sample`, then runs -PHP scripts that exercise profiling (allocations, wall/cpu time, exceptions, IO, timeline, -strange frames). The scripts output pprof files (zstd-compressed protobuf). The +Each job builds through phpize/configure/Make with +`DDTRACE_PROFILING_FEATURES=trigger_time_sample`, then runs PHP scripts that exercise +profiling (allocations, wall/cpu time, exceptions, IO, timeline, strange frames). PHP +8.5 correctness cells use combined `ddtrace.so`; older cells retain the standalone +profiler. The scripts output pprof files (zstd-compressed protobuf). The `Datadog/prof-correctness/analyze` GitHub Action then checks each pprof against a JSON -expectations file. +expectations file. The PHP 8.5 NTS combined cell also runs tracer/profiler runtime-ID +integration with profiling both enabled and disabled and checks that closing a web root +span enqueues its endpoint information in the profiler. It then rebuilds a standalone +profiler and verifies that loading it alongside `ddtrace.so` fails in both load orders. +The PHP 8.4 NTS standalone cell also creates an OpenTelemetry SDK tracer and active span +to verify that the userland SDK and standalone profiler coexist. Test cases (NTS): `allocations`, `time`, `strange_frames`, `timeline`, `exceptions`, `io`, `allocation_time_combined`, plus `allocations` re-run with 1-byte sampling distance (with @@ -34,52 +41,48 @@ ZTS adds: `exceptions_zts`. ## Local Reproduction -Use `.claude/ci/dockerh` with the `datadog/dd-trace-ci:php-_bookworm-{N}` image -matching the PHP version under test (see `index.md` for image version and contents). The CI -uses clang-19 on ubuntu-24.04; clang-17 in the image works fine. +Use `.claude/ci/dockerh` with the `datadog/dd-trace-ci:php-_bookworm-10` image +matching the PHP version under test (see `index.md` for image contents). The CI +installs and uses clang-20 on ubuntu-24.04. Actions jobs use `shivammathur/setup-php` instead, but the same `dd-trace-ci` image is a suitable local substitute. -**Image naming:** use `php-8.1_bookworm-N` for PHP 8.1 tests, `php-8.3_bookworm-N` for -8.3, etc. — the image is tagged by PHP version, so the version in the tag must match the -PHP version being tested. +**Image naming:** use `php-8.1_bookworm-10` for PHP 8.1 tests, +`php-8.3_bookworm-10` for 8.3, etc. The version in the tag must match the PHP version +being tested. **Cache naming:** use a separate `--cache` name per `(php-version, phpts)` pair (e.g. `profiler-8.1-zts`) to avoid mixing NTS and ZTS build artifacts. ### Build the profiler extension -`cargo rustc` must be run from the `profiling/` subdirectory (the workspace `profiler-release` -profile is defined in the repo root `Cargo.toml`, but the crate itself lives in `profiling/`). - -**`CARGO_TARGET_DIR` must be set explicitly** to `/project/dd-trace-php/target`. Without -it the `cbindgen` build script inside `libdatadog` calls `cargo locate-project --workspace`, -which resolves to the `libdatadog/` submodule's own workspace (not the repo root), and -tries to create `libdatadog/target/include/datadog/library-config.h`. That path is inside -the read-only source mount with no writable overlay, causing a -`ReadOnlyFilesystem (os error 30)` panic. Pointing `CARGO_TARGET_DIR` at the already- -overlaid `/project/dd-trace-php/target` fixes it. +Loadable artifacts must go through phpize/configure/Make from the repository root. +Do not load a Cargo target-directory cdylib. ```bash -# NTS example (PHP 8.3) -dockerh --cache profiler-8.3-nts --php nts datadog/dd-trace-ci:php-8.3_bookworm-6 -- bash -c ' -export CARGO_TARGET_DIR=/project/dd-trace-php/target -cd profiling && cargo rustc --features=trigger_time_sample --profile profiler-release --crate-type=cdylib +# Standalone NTS example (PHP 8.3) +dockerh --cache profiler-8.3-nts --php nts datadog/dd-trace-ci:php-8.3_bookworm-10 -- bash -c ' +cd /project/dd-trace-php +phpize +DDTRACE_PROFILING_FEATURES=trigger_time_sample \ + ./configure --disable-ddtrace-tracer --enable-ddtrace-profiling --disable-ddtrace-rust-debug +make -j"$(nproc)" ' -# ZTS example (PHP 8.1) — note --php zts, matching image version, and separate cache name -dockerh --cache profiler-8.1-zts --php zts datadog/dd-trace-ci:php-8.1_bookworm-6 -- bash -c ' -export CARGO_TARGET_DIR=/project/dd-trace-php/target -cd profiling && cargo rustc --features=trigger_time_sample --profile profiler-release --crate-type=cdylib +# Combined ZTS example (PHP 8.5) +dockerh --cache profiler-8.5-zts --php zts datadog/dd-trace-ci:php-8.5_bookworm-10 -- bash -c ' +cd /project/dd-trace-php +phpize +DDTRACE_PROFILING_FEATURES=trigger_time_sample \ + ./configure --enable-ddtrace-tracer --enable-ddtrace-profiling --disable-ddtrace-rust-debug +make -j"$(nproc)" ' ``` -Output: `/project/dd-trace-php/target/profiler-release/libdatadog_php_profiling.so` -(persisted in the host cache at `~/.cache/dd-ci//target/`). - -The second run reuses the build cache and completes in seconds. Never run `--clean-cache` -between iterations — the Rust build takes 5–15 minutes from scratch. +The supported outputs are `modules/datadog-profiling.so` and +`modules/ddtrace.so`, respectively. Use separate caches for PHP versions and NTS/ZTS +variants. ### Run a single test case @@ -88,8 +91,7 @@ write pprof output there — no extra mounts needed: ```bash dockerh --cache profiler-8.3-nts --php nts \ - datadog/dd-trace-ci:php-8.3_bookworm-6 -- bash -c ' -export CARGO_TARGET_DIR=/project/dd-trace-php/target + datadog/dd-trace-ci:php-8.3_bookworm-10 -- bash -c ' export DD_PROFILING_LOG_LEVEL=warn # use "trace" only when debugging — trace is verbose and slows execution export DD_PROFILING_EXPERIMENTAL_FEATURES_ENABLED=1 export DD_PROFILING_EXPERIMENTAL_EXCEPTION_SAMPLING_DISTANCE=1 @@ -100,7 +102,7 @@ TEST_CASE=allocations OUT=/project/dd-trace-php/tmp/correctness/$TEST_CASE mkdir -p $OUT DD_PROFILING_OUTPUT_PPROF=$OUT/test.pprof \ - php -d extension=/project/dd-trace-php/target/profiler-release/libdatadog_php_profiling.so \ + php -d extension=/project/dd-trace-php/modules/datadog-profiling.so \ /project/dd-trace-php/profiling/tests/correctness/$TEST_CASE.php ls -la $OUT/ ' @@ -123,18 +125,13 @@ export DD_PROFILING_ENABLED=Off # Verify test.pprof.1.zst does NOT exist ``` -**Note:** the CI script checks for the `.lz4` extension (an older format), but the current -profiler outputs `.zst`. This means the CI "no profile" check always passes regardless of -whether a `.zst` file is produced. Locally, check for `.zst` if you want a meaningful -verification. - ### Inspecting pprof output The pprof files are zstd-compressed protobuf. Use `go tool pprof` (available in the dd-trace-ci image) to inspect them. Pass `--user root` so `apt-get install` works: ```bash -dockerh --cache profiler-8.3-nts --php nts datadog/dd-trace-ci:php-7.3_bookworm-6 --user root -- bash -c ' +dockerh --cache profiler-8.3-nts --php nts datadog/dd-trace-ci:php-7.3_bookworm-10 --user root -- bash -c ' apt-get update -qq > /dev/null 2>&1 && apt-get install -y -qq zstd > /dev/null 2>&1 PPROF_DIR=/project/dd-trace-php/tmp/correctness/allocations @@ -202,14 +199,17 @@ frame name formatting. The implementation is in `profiling/src/capi.rs` and ## Debug Build -For a debug (unoptimized) build: +Select Rust debug mode through configure, then consume the Make output: ```bash -cargo rustc --features=trigger_time_sample --profile dev --crate-type=cdylib +phpize +DDTRACE_PROFILING_FEATURES=trigger_time_sample \ + ./configure --disable-ddtrace-tracer --enable-ddtrace-profiling --enable-ddtrace-rust-debug +make -j"$(nproc)" ``` -Output: `target/debug/libdatadog_php_profiling.so` (~144 MB vs ~20 MB for profiler-release). -Use the same `php -d extension=...` command, just point to the debug path. +The artifact remains `modules/datadog-profiling.so`. Profiling PHPT and correctness +expectations are intended for optimized builds. ## ZTS tests -- parallel PECL extension @@ -225,25 +225,26 @@ installs version `v1.2.7` from GitHub via the `extensions` matrix parameter ## ASAN Build Builds the profiler with AddressSanitizer using a pinned nightly Rust toolchain -and clang-17, then runs the `.phpt` test suite with `--asan`. +and clang-20, then runs the `.phpt` test suite with `--asan`. ### Local reproduction ```bash dockerh --cache profiler-asan-8.3-nts --php nts-asan \ - datadog/dd-trace-ci:php-8.3_bookworm-6 --user root --privileged -- bash -c ' -export CARGO_TARGET_DIR=/project/dd-trace-php/target -export CC=clang-17 -export CFLAGS="-fsanitize=address -fno-omit-frame-pointer" + datadog/dd-trace-ci:php-8.3_bookworm-10 --user root --privileged -- bash -c ' +cd /project/dd-trace-php +export CC=clang-20 +export CFLAGS="-fsanitize=address -fsanitize-address-use-after-scope -fno-omit-frame-pointer" export LDFLAGS="-fsanitize=address -shared-libasan" -export RUSTC_LINKER=lld-17 -RUST_TOOLCHAIN=nightly-2025-06-13 - -cd profiling -triplet=$(uname -m)-unknown-linux-gnu -RUSTFLAGS="-Zsanitizer=address" cargo +${RUST_TOOLCHAIN} build -Zbuild-std=std,panic_abort \ - --target $triplet --profile profiler-release -cp -v "$CARGO_TARGET_DIR/$triplet/profiler-release/libdatadog_php_profiling.so" \ +export RUSTC_LINKER=lld-20 +rustup override set nightly-2025-06-13 +export RUSTFLAGS="-Zsanitizer=address -C force-frame-pointers=yes" +export DDTRACE_PROFILING_TARGET="$(uname -m)-unknown-linux-gnu" +export DDTRACE_PROFILING_CARGO_BUILD_FLAGS="-Zbuild-std=std,panic_abort" +phpize +./configure --disable-ddtrace-tracer --enable-ddtrace-profiling --disable-ddtrace-rust-debug +make -j"$(nproc)" +cp -v modules/datadog-profiling.so \ "$(php-config --extension-dir)/datadog-profiling.so" # run-tests.php writes temp files next to .phpt files, so both must be in a writable dir. @@ -268,10 +269,6 @@ the workflow file for the current pinned version. - **Expected ASAN test counts:** 39 total, ~27 pass, ~12 skip (30%), 0 fail. The skips are normal (platform/env conditions). A non-zero fail count indicates a real problem. -- The `profiler-release` profile is defined in the workspace root `Cargo.toml`, not in - `profiling/Cargo.toml`. It inherits from `release` with `panic = "abort"`. - `dockerh` runs the container as your host UID so cache dirs are writable without any permission tricks. Pass `--user root` after the image name if you need to install packages with `apt-get`. -- CI checks for `.lz4` extension in the "no profile" test, but the current profiler - outputs `.zst` (zstandard). Both are valid pprof compression formats. diff --git a/.claude/ci/index.md b/.claude/ci/index.md index 8840e82ebb2..9d02b8c795b 100644 --- a/.claude/ci/index.md +++ b/.claude/ci/index.md @@ -107,12 +107,10 @@ unit tests and web tests both using PHP 8.3 debug on amd64) **can** share a ## Image versions -CI images are tagged `datadog/dd-trace-ci:php-{version}_bookworm-{N}` where `N` -is an iteration number shared across all GitLab appsec jobs. Find the current -value by searching for `bookworm-` in `.gitlab/generate-appsec.php` -. -The `php-8.3_bookworm-{N}` image contains: Rust (see -`profiling/rust-toolchain.toml` for the pinned version), clang-17, Go, and +The current CI images are tagged +`datadog/dd-trace-ci:php-{version}_bookworm-10`. +The `php-8.3_bookworm-10` image contains Rust (see the repository +`rust-toolchain.toml` for the pinned version), Clang, Go, and multiple PHP builds under `/opt/php/` (nts, zts, debug, etc.). Use `--php nts` (or another variant) with `dockerh` to select the right build — see the `--php` section above. @@ -122,7 +120,7 @@ in CI scripts are mirrors of `datadog/dd-trace-ci:TAG` on Docker Hub. Pull them directly without authentication — no registry login or image export/import needed: ```bash -docker pull datadog/dd-trace-ci:php-8.3_bookworm-6 +docker pull datadog/dd-trace-ci:php-8.3_bookworm-10 ``` (The exception is registry.ddbuild.io/images/mirror/b1o7r7e0/nginx_musl_toolchain, @@ -254,7 +252,7 @@ this file instead of duplicating build commands. ### Group A — Native Linux unit / extension / helper tests Runner: `arch:amd64` + `arch:arm64` -Image: `datadog/dd-trace-ci:php-{version}_bookworm-6` or `datadog/dd-trace-ci:bookworm-6` +Image: `datadog/dd-trace-ci:php-{version}_bookworm-10` or `datadog/dd-trace-ci:bookworm-10` No Docker daemon — tests run directly in the container. → **[appsec-native-tests.md](appsec-native-tests.md)** @@ -274,7 +272,7 @@ Covers: `Unit tests`, `PHP Language Tests`, `test_c`, `ASAN test_c`, `Opcache te ### Group B — Native Linux web framework tests Runner: `arch:amd64` -Image: `datadog/dd-trace-ci:php-{version}_bookworm-6` +Image: `datadog/dd-trace-ci:php-{version}_bookworm-10` GitLab service containers: test-agent, httpbin, request-replayer → **[tracer-web-tests.md](tracer-web-tests.md)** @@ -289,7 +287,7 @@ Covers: `test_web_laravel_*`, `test_web_symfony_*`, `test_web_wordpress_*`, ### Group C — Native Linux service integration tests Runner: `arch:amd64` -Image: `datadog/dd-trace-ci:php-{version}_bookworm-6` +Image: `datadog/dd-trace-ci:php-{version}_bookworm-10` GitLab service containers: MySQL, Redis, Kafka, Elasticsearch, MongoDB, etc. → **[tracer-integration-tests.md](tracer-integration-tests.md)** @@ -308,14 +306,14 @@ Covers: `test_integrations_amqp*`, `test_integrations_curl`, `test_integrations_ ### Group D — Native Linux compile / artifact build Runner: `arch:amd64` + `arch:arm64` -Image: `datadog/dd-trace-ci:php-{version}_bookworm-6` +Image: `datadog/dd-trace-ci:php-{version}_bookworm-10` Produces `.so` artifacts consumed by Groups B, C, H. → **[compile-artifacts.md](compile-artifacts.md)** Covers: `compile extension: debug/release/zts/...` (tracer pipeline), `compile tracing extension / sidecar / loader / asan` (package pipeline), `compile appsec extension`, `compile appsec helper`, `compile appsec helper rust`, -`compile profiler extension`, `compile extension windows`, `link tracing extension`, +`compile combined extension`, `compile extension windows`, `link tracing extension`, `aggregate tracing extension`, `pecl build`, `prepare code`, `cache cargo deps` --- diff --git a/.claude/ci/meta-improv-instr.md b/.claude/ci/meta-improv-instr.md index 1c4f107f783..fa85f5ae2f6 100644 --- a/.claude/ci/meta-improv-instr.md +++ b/.claude/ci/meta-improv-instr.md @@ -58,8 +58,8 @@ Good gaps to capture: - Dependencies (packages, files, env vars) that must be present but are not mentioned - Paths or filenames that differ between the docs and reality -- Steps that are described as necessary but turn out to be no-ops (e.g. CI - checks `.lz4` but profiler emits `.zst` — the check is vacuously true) +- Steps that are described as necessary but turn out to be no-ops or duplicate + work already owned by the generated Makefile - Speed differences large enough to matter (e.g. `EXECUTION_TIME=3` saves 50+ seconds per run; `DD_PROFILING_LOG_LEVEL=warn` vs `trace` measurably affects throughput) @@ -99,8 +99,7 @@ a "Reproducing Locally" example: | `parallel` PECL needs `libpcre2-dev` + GitHub URL for v1.2.7 | `github-actions-profiler.md` | | `parallel.so` not persisted in overlay cache — copy to project dir | `github-actions-profiler.md` | | Image tag must match PHP version under test | `github-actions-profiler.md` | -| `cargo rustc` must run from `profiling/` subdir | `github-actions-profiler.md` | -| Output path in docs differed from CI workflow path | `github-actions-profiler.md` | +| Loadable profiler artifacts must use root phpize/configure/Make | `github-actions-profiler.md` | +| Supported output paths are under `modules/`, not Cargo target directories | `github-actions-profiler.md` | | `EXECUTION_TIME=3` applies to all time-based tests, not just `allocations` | `github-actions-profiler.md` | | `DD_PROFILING_LOG_LEVEL=warn` recommended (trace slows execution) | `github-actions-profiler.md` | -| "No profile" check is vacuously true (`.lz4` vs `.zst`) | `github-actions-profiler.md` | diff --git a/.claude/ci/meta-job-group-doc.md b/.claude/ci/meta-job-group-doc.md index 2749b48afab..ae05ad1c323 100644 --- a/.claude/ci/meta-job-group-doc.md +++ b/.claude/ci/meta-job-group-doc.md @@ -33,9 +33,9 @@ with columns `CI Job`, `Image`, `What it does`: | CI Job | Image | What it does | |--------|-------|--------------| -| `test appsec extension: [{ver}, {arch}, debug]` | `dd-trace-ci:php-{ver}_bookworm-6` | Builds extension + runs .phpt tests | -| `test appsec helper asan` | `dd-trace-ci:bookworm-6` | C++ helper ASAN gtest suite | -| `appsec code coverage` | `dd-trace-ci:php-8.3_bookworm-6` | (not needed locally) | +| `test appsec extension: [{ver}, {arch}, debug]` | `dd-trace-ci:php-{ver}_bookworm-10` | Builds extension + runs .phpt tests | +| `test appsec helper asan` | `dd-trace-ci:bookworm-10` | C++ helper ASAN gtest suite | +| `appsec code coverage` | `dd-trace-ci:php-8.3_bookworm-10` | (not needed locally) | Runner: `arch:amd64` + `arch:arm64` Matrix: PHP 7.0+ × {debug, debug-zts, debug-zts-asan (7.4+)} diff --git a/.claude/ci/package-dind-verification.md b/.claude/ci/package-dind-verification.md index eab84099483..d0faff9553d 100644 --- a/.claude/ci/package-dind-verification.md +++ b/.claude/ci/package-dind-verification.md @@ -257,7 +257,7 @@ TRIPLET=x86_64-alpine-linux-musl \ ``` The script needs compiled extensions in `extensions_x86_64/`, -`datadog-profiling/`, `appsec_x86_64/`, and `src/`. If built via +`combined-ddtrace/`, `appsec_x86_64/`, and `src/`. If built via `dockerh --overlayfs`, extract from volumes first (see [compile-artifacts.md](compile-artifacts.md)). diff --git a/.claude/ci/package-native-verification.md b/.claude/ci/package-native-verification.md index ca523f679a1..c4cb22ff9f8 100644 --- a/.claude/ci/package-native-verification.md +++ b/.claude/ci/package-native-verification.md @@ -28,7 +28,7 @@ in the `verify` stage of the **package-trigger** child pipeline and use native r | `Loader test on {arch} libc: [{ver}, {flavour}]` | `dd-trace-ci:php-{ver}_{suffix}` | Extracts SSI loader package, runs `loader/bin/test.sh` phpt tests; optionally runs `check_glibc_version.sh` | | `Loader test on {arch} alpine` | `alpine:3.20` | Installs PHP 8.3 from apk, extracts SSI loader musl package, runs `loader/bin/test.sh` | | `min install tests` | `dd-trace-ci:php-8.0-shared-ext` | Installs `.deb` package via `dpkg`, runs `make run_tests` + `make test_c` against the installed extension | -| `pecl tests: [{ver}]` | `dd-trace-ci:php-{ver}_bookworm-6` | Installs ddtrace from PECL `.tgz`, runs `pecl run-tests` against the installed extension | +| `pecl tests: [{ver}]` | `dd-trace-ci:php-{ver}_bookworm-10` | Installs ddtrace from PECL `.tgz`, runs `pecl run-tests` against the installed extension | | `test early PHP 8.1` | `ubuntu:jammy` | Installs stock Ubuntu 22.04 PHP 8.1 (no sury), installs ddtrace via `datadog-setup.php`, runs `pecl run-tests` | | `x-profiling phpt tests on Alpine: [{ver}]` | `dd-trace-ci:php-compile-extension-alpine-{ver}` | Installs full package on Alpine via `datadog-setup.php --enable-profiling`, runs profiling phpt tests | @@ -274,7 +274,7 @@ Runs as non-root (no `--root`). ```bash .claude/ci/dockerh --cache pecl-8.3 --overlayfs --php nts \ - datadog/dd-trace-ci:php-8.3_bookworm-6 -- \ + datadog/dd-trace-ci:php-8.3_bookworm-10 -- \ bash -c ' cp /project/dd-trace-php/pecl/datadog_trace-*.tgz ./datadog_trace.tgz pecl install datadog_trace.tgz @@ -293,7 +293,7 @@ first. ```bash .claude/ci/dockerh --cache loader-8.3 --overlayfs --php nts \ - datadog/dd-trace-ci:php-8.3_bookworm-6 \ + datadog/dd-trace-ci:php-8.3_bookworm-10 \ -v /path/to/packages:/artifacts:ro \ -- bash -c ' export XDEBUG_SO_NAME=xdebug-3.3.2.so diff --git a/.claude/ci/packaging-oci.md b/.claude/ci/packaging-oci.md index ce26172cc46..65a4b89b3c0 100644 --- a/.claude/ci/packaging-oci.md +++ b/.claude/ci/packaging-oci.md @@ -15,7 +15,7 @@ All compile, link, and aggregate jobs (`compile tracing extension`, `compile tracing sidecar`, `link tracing extension`, `aggregate tracing extension`, `compile appsec extension`, `compile appsec helper`, `compile appsec helper rust`, -`compile profiler extension`, `compile loader`, `compile extension windows`) are +`compile combined extension`, `compile loader`, `compile extension windows`) are documented in [compile-artifacts.md](compile-artifacts.md). | CI Job | Image | What it does | @@ -60,14 +60,14 @@ compile tracing extension ─┐ compile appsec extension ─┤ compile appsec helper ─┤→ generate-final-artifact.sh → .tar.gz compile appsec helper rust─┤ │ -compile profiler extension─┤ v +compile combined extension─┤ v compile loader ─┘ nfpm → .deb/.rpm/.apk │ prepare-oci-package.sh → OCI image ``` -Intermediate artifacts (`extensions_*/`, `appsec_*/`, -`datadog-profiling/`) feed into `generate-final-artifact.sh`, which +Intermediate artifacts (`extensions_*/`, `appsec_*/`, and +`combined-ddtrace/`) feed into `generate-final-artifact.sh`, which produces per-platform `.tar.gz` tarballs. Those are then packaged into `.deb`/`.rpm`/`.apk` by nfpm, and into OCI images by `prepare-oci-package.sh`. See @@ -132,7 +132,7 @@ shown above. - **`package loader` depends on many upstream compile jobs** — appsec helper (C++ and Rust), loader (glibc and musl), tracing extension aggregates, sidecar, all - appsec and profiler extension versions. A single upstream failure blocks packaging. + AppSec and combined extension versions. A single upstream failure blocks packaging. See [building-locally.md § SSI Loader Package Assembly](building-locally.md#ssi-loader-package-assembly) for local reproduction and important caveats (empty stubs do not work; `standalone_*/` not `extensions_*/`; must run on amd64). diff --git a/.claude/ci/shared-zai-tea-tests.md b/.claude/ci/shared-zai-tea-tests.md index 567d5692d64..2aab8514ffd 100644 --- a/.claude/ci/shared-zai-tea-tests.md +++ b/.claude/ci/shared-zai-tea-tests.md @@ -7,13 +7,13 @@ child pipeline; all job definitions and matrices are inline. | CI Job | Image | What it does | |--------|-------|-------------| -| `Build & Test Tea` | `dd-trace-ci:php-{ver}_bookworm-6` | Builds the TEA (Test Execution Abstraction) library from `tea/`, runs its ctest suite, installs artifacts for downstream jobs | -| `Zend Abstract Interface Tests: [{ver}, {variant}]` | `dd-trace-ci:php-{ver}_bookworm-6` | Builds and tests the ZAI library (`zend_abstract_interface/`) against a specific PHP variant | -| `Extension Tea Tests: [{ver}, {variant}]` | `dd-trace-ci:php-{ver}_bookworm-6` | Builds ddtrace.so via `make install`, then builds and runs the extension-level TEA tests in `tests/tea/` | +| `Build & Test Tea` | `dd-trace-ci:php-{ver}_bookworm-10` | Builds the TEA (Test Execution Abstraction) library from `tea/`, runs its ctest suite, installs artifacts for downstream jobs | +| `Zend Abstract Interface Tests: [{ver}, {variant}]` | `dd-trace-ci:php-{ver}_bookworm-10` | Builds and tests the ZAI library (`zend_abstract_interface/`) against a specific PHP variant | +| `Extension Tea Tests: [{ver}, {variant}]` | `dd-trace-ci:php-{ver}_bookworm-10` | Builds ddtrace.so via `make install`, then builds and runs the extension-level TEA tests in `tests/tea/` | | `ZAI Shared Tests: [{ver}]` | `dd-trace-ci:php-{ver}-shared-ext` | Runs ZAI tests with shared extensions (curl, json) on a special image; only PHP 7.4 and 8.0 | -| `C components ASAN` | `dd-trace-ci:centos-7`, `dd-trace-ci:php-compile-extension-alpine`, `dd-trace-ci:bookworm-6` | Builds C components (`components/`) with ASAN (on Debian) or plain Debug (on CentOS/Alpine), runs ctest | -| `C components UBSAN` | `dd-trace-ci:bookworm-6` | Builds C components with UBSAN, runs ctest with `--repeat until-fail:10` | -| `Configuration Consistency` | `dd-trace-ci:php-{latest}_bookworm-6` | Runs `tooling/generate-supported-configurations.sh` and verifies `metadata/supported-configurations.json` is up-to-date | +| `C components ASAN` | `dd-trace-ci:centos-7`, `dd-trace-ci:php-compile-extension-alpine`, `dd-trace-ci:bookworm-10` | Builds C components (`components/`) with ASAN (on Debian) or plain Debug (on CentOS/Alpine), runs ctest | +| `C components UBSAN` | `dd-trace-ci:bookworm-10` | Builds C components with UBSAN, runs ctest with `--repeat until-fail:10` | +| `Configuration Consistency` | `dd-trace-ci:php-{latest}_bookworm-10` | Runs `tooling/generate-supported-configurations.sh` and verifies `metadata/supported-configurations.json` is up-to-date | Runner: `arch:amd64` (all jobs in this pipeline are amd64-only) @@ -26,9 +26,9 @@ Matrix: - **Extension Tea Tests**: PHP 7.0+ x {debug, debug-zts-asan (7.4+), nts, zts}. Pre-7.4 skips `debug-zts-asan`. - **ZAI Shared Tests**: PHP 7.4, 8.0 only, `nts` variant only. -- **C components ASAN**: three images (centos-7, alpine, bookworm-6); +- **C components ASAN**: three images (centos-7, alpine, bookworm-10); ASAN toolchain only on Debian (bookworm). -- **C components UBSAN**: bookworm-6 only. +- **C components UBSAN**: bookworm-10 only. - **Configuration Consistency**: latest PHP version, single run. ## What It Tests @@ -68,7 +68,7 @@ script produces from current source. Fails if they diverge. ```bash .claude/ci/dockerh --cache tea-8.3-debug --overlayfs --php debug \ - datadog/dd-trace-ci:php-8.3_bookworm-6 -- bash -c ' + datadog/dd-trace-ci:php-8.3_bookworm-10 -- bash -c ' set -e mkdir -p tmp/build-tea-debug cd tmp/build-tea-debug @@ -91,7 +91,7 @@ For ASAN, add `-DCMAKE_TOOLCHAIN_FILE=../../cmake/asan.cmake` and use ```bash .claude/ci/dockerh --cache tea-8.3-debug --overlayfs --php debug \ - datadog/dd-trace-ci:php-8.3_bookworm-6 -- bash -c ' + datadog/dd-trace-ci:php-8.3_bookworm-10 -- bash -c ' cd tmp/build-tea-debug make test ARGS="--output-on-failure -R tea_sapi" ' @@ -115,7 +115,7 @@ Requires TEA artifacts from the previous step to exist at ```bash .claude/ci/dockerh --cache tea-8.3-debug --overlayfs --php debug \ - datadog/dd-trace-ci:php-8.3_bookworm-6 -- bash -c ' + datadog/dd-trace-ci:php-8.3_bookworm-10 -- bash -c ' set -e mkdir -p tmp/build_zai && cd tmp/build_zai CMAKE_PREFIX_PATH=/opt/catch2 Tea_ROOT=../../tmp/tea/debug \ @@ -136,7 +136,7 @@ use `--php debug-zts-asan`, and a separate cache name. ```bash .claude/ci/dockerh --cache tea-8.3-debug --overlayfs --php debug \ - datadog/dd-trace-ci:php-8.3_bookworm-6 -- bash -c ' + datadog/dd-trace-ci:php-8.3_bookworm-10 -- bash -c ' cd tmp/build_zai ctest --output-on-failure -R config ' @@ -150,7 +150,7 @@ Requires TEA artifacts at `tmp/tea/{variant}/`. ```bash .claude/ci/dockerh --cache ext-tea-8.3-debug --overlayfs --php debug \ - datadog/dd-trace-ci:php-8.3_bookworm-6 -- bash -c ' + datadog/dd-trace-ci:php-8.3_bookworm-10 -- bash -c ' set -e make install mkdir -p tmp/build_ext-tea && cd tmp/build_ext-tea @@ -170,7 +170,7 @@ source tree (see [index.md](index.md) for details). ```bash .claude/ci/dockerh --cache ext-tea-8.3-debug --overlayfs --php debug \ - datadog/dd-trace-ci:php-8.3_bookworm-6 -- bash -c ' + datadog/dd-trace-ci:php-8.3_bookworm-10 -- bash -c ' cd tmp/build_ext-tea ctest --output-on-failure -R "" ' @@ -182,7 +182,7 @@ ctest --output-on-failure -R "" ```bash .claude/ci/dockerh --cache components-asan \ - datadog/dd-trace-ci:bookworm-6 -- bash -c ' + datadog/dd-trace-ci:bookworm-10 -- bash -c ' set -e mkdir -p tmp/build_php_components_asan && cd tmp/build_php_components_asan CMAKE_PREFIX_PATH=/opt/catch2 cmake \ @@ -238,7 +238,7 @@ local reproduction, but if needed: ```bash .claude/ci/dockerh --cache config-consistency --overlayfs \ --php nts \ - datadog/dd-trace-ci:php-8.5_bookworm-6 -- bash -c ' + datadog/dd-trace-ci:php-8.5_bookworm-10 -- bash -c ' bash tooling/generate-supported-configurations.sh ' ``` @@ -268,7 +268,7 @@ running the script locally and committing the result. tested. This image is not easily reproducible locally since the shared-ext images are custom CI builds. -- **C components tests do not require PHP.** The `bookworm-6` base +- **C components tests do not require PHP.** The `bookworm-10` base image (no PHP version suffix) is sufficient. The centos-7 and alpine images need the `PKG_CONFIG_PATH` / `CMAKE_PREFIX_PATH` env vars for libuv and Catch2 respectively. diff --git a/.claude/ci/tracer-integration-tests.md b/.claude/ci/tracer-integration-tests.md index db1019dae95..18e5e9ac14a 100644 --- a/.claude/ci/tracer-integration-tests.md +++ b/.claude/ci/tracer-integration-tests.md @@ -17,7 +17,7 @@ | CI Job | Image | What it does | |--------|-------|-------------| -| `test_integrations_amqp2: [{php}]` | `datadog/dd-trace-ci:php-{php}_bookworm-6` | AMQP v2 (RabbitMQ) integration tests | +| `test_integrations_amqp2: [{php}]` | `datadog/dd-trace-ci:php-{php}_bookworm-10` | AMQP v2 (RabbitMQ) integration tests | | `test_integrations_amqp_latest: [{php}]` | same | AMQP latest version tests | | `test_integrations_curl: [{php}]` | same | Curl integration tests | | `test_integrations_deferred_loading: [{php}]` | same | Deferred loading tests (needs mysql + redis) | @@ -130,7 +130,7 @@ Use the **same cache name** as the test step (`tracer-integ-83`) so the built ```bash .claude/ci/dockerh --cache tracer-integ-83 --overlayfs --php debug \ - datadog/dd-trace-ci:php-8.3_bookworm-6 \ + datadog/dd-trace-ci:php-8.3_bookworm-10 \ -e CI_COMMIT_TAG=local \ -e SHARED=1 \ -- bash -c ' @@ -187,7 +187,7 @@ name from Step 2). The `--network` flag must appear **after** the image name. ```bash .claude/ci/dockerh --cache tracer-integ-83 --overlayfs --php debug \ - datadog/dd-trace-ci:php-8.3_bookworm-6 \ + datadog/dd-trace-ci:php-8.3_bookworm-10 \ --network ${PROJECT}_default \ -e COMPOSER_MEMORY_LIMIT=-1 \ -e DD_TRACE_ASSUME_COMPILED=1 \ @@ -239,7 +239,7 @@ docker compose -p $PROJECT -f .claude/ci/docker-compose.services.yml down - **phpredis version-specific .so files**: `test_integrations_phpredis3` through `phpredis5` load specific pre-built `.so` files via - `TEST_EXTRA_INI=-d extension=redis-X.Y.Z.so`. The `bookworm-6` CI image + `TEST_EXTRA_INI=-d extension=redis-X.Y.Z.so`. The `bookworm-10` CI image only ships `redis-5.3.7.so` — **`redis-3.1.6.so` and `redis-4.3.0.so` are absent**, so `test_integrations_phpredis3` and `test_integrations_phpredis4` cannot be run locally against a PHP 8.3 image. Run them with a PHP 7.x image diff --git a/.claude/ci/tracer-unit-tests.md b/.claude/ci/tracer-unit-tests.md index d6a306fa858..8ebaea8b647 100644 --- a/.claude/ci/tracer-unit-tests.md +++ b/.claude/ci/tracer-unit-tests.md @@ -12,9 +12,9 @@ | CI Job | Image | What it does | |--------|-------|-------------| -| `compile extension: debug` | `dd-trace-ci:php-{ver}_bookworm-6` | Compiles ddtrace.so in debug mode; produces artifact consumed by all test jobs below | +| `compile extension: debug` | `dd-trace-ci:php-{ver}_bookworm-10` | Compiles ddtrace.so in debug mode; produces artifact consumed by all test jobs below | | `compile extension: debug-zts-asan` | same | Compiles ddtrace.so with ASAN+ZTS; used by ASAN test jobs | -| `Unit tests: [{ver}]` | `dd-trace-ci:php-{ver}_bookworm-6` | Runs PHPUnit `--testsuite=unit` | +| `Unit tests: [{ver}]` | `dd-trace-ci:php-{ver}_bookworm-10` | Runs PHPUnit `--testsuite=unit` | | `API unit tests: [{ver}]` | same | Runs PHPUnit API unit tests | | `test_extension_ci: [{ver}]` | same | Runs .phpt extension tests (normal pass), with test-agent | | `test_extension_ci: [{ver}, valgrind]` | same | Same suite under valgrind for leak checking; much slower, so it is a separate job | @@ -56,7 +56,7 @@ Build the extension and install PHPUnit prerequisites once: ```bash .claude/ci/dockerh --cache tracer-8.3-debug --overlayfs --php debug \ - datadog/dd-trace-ci:php-8.3_bookworm-6 -- bash -c ' + datadog/dd-trace-ci:php-8.3_bookworm-10 -- bash -c ' set -e; make -j$(nproc) all; make install_all; composer update --no-interaction; make generate' ``` @@ -65,7 +65,7 @@ Then reuse the cache for any test target: ```bash .claude/ci/dockerh --cache tracer-8.3-debug --overlayfs --php debug \ - datadog/dd-trace-ci:php-8.3_bookworm-6 \ + datadog/dd-trace-ci:php-8.3_bookworm-10 \ -e DD_TRACE_ASSUME_COMPILED=1 -- bash -c 'make test_unit' ``` @@ -123,7 +123,7 @@ runs. ```bash .claude/ci/dockerh --cache tracer-8.3-debug --overlayfs --php debug \ - datadog/dd-trace-ci:php-8.3_bookworm-6 -- bash -c ' + datadog/dd-trace-ci:php-8.3_bookworm-10 -- bash -c ' set -e make -j$(nproc) all make test_c @@ -138,7 +138,7 @@ skipped (most .phpt tests are self-contained). ```bash .claude/ci/dockerh --cache tracer-8.3-debug --overlayfs --php debug \ - datadog/dd-trace-ci:php-8.3_bookworm-6 -- bash -c ' + datadog/dd-trace-ci:php-8.3_bookworm-10 -- bash -c ' make test_c TESTS=tests/ext/sandbox/auto_flush.phpt ' ``` @@ -160,7 +160,7 @@ August 2022 and never matches. ```bash .claude/ci/dockerh --cache tracer-8.3-debug --overlayfs --php debug \ - datadog/dd-trace-ci:php-8.3_bookworm-6 -- bash -c ' + datadog/dd-trace-ci:php-8.3_bookworm-10 -- bash -c ' make test_c_disabled ' ``` @@ -173,7 +173,7 @@ For the ASAN variant, see the [ASAN Tests](#asan-tests) section. ```bash .claude/ci/dockerh --cache tracer-8.3-debug --overlayfs --php debug \ - datadog/dd-trace-ci:php-8.3_bookworm-6 -- bash -c ' + datadog/dd-trace-ci:php-8.3_bookworm-10 -- bash -c ' set -e make -j$(nproc) all make install_all @@ -187,7 +187,7 @@ make test_unit ```bash .claude/ci/dockerh --cache tracer-8.3-debug --overlayfs --php debug \ - datadog/dd-trace-ci:php-8.3_bookworm-6 -- bash -c ' + datadog/dd-trace-ci:php-8.3_bookworm-10 -- bash -c ' make test_unit FILTER=testSomething ' ``` @@ -207,7 +207,7 @@ make test_unit TESTS=tests/Unit/SomeTest.php FILTER=testSomething ```bash .claude/ci/dockerh --cache tracer-8.3-debug --overlayfs --php debug \ - datadog/dd-trace-ci:php-8.3_bookworm-6 -- bash -c ' + datadog/dd-trace-ci:php-8.3_bookworm-10 -- bash -c ' set -e make -j$(nproc) all make test_opcache @@ -222,7 +222,7 @@ flag is passed automatically. ```bash .claude/ci/dockerh --cache tracer-8.3-debug --overlayfs --php debug \ - datadog/dd-trace-ci:php-8.3_bookworm-6 -- bash -c ' + datadog/dd-trace-ci:php-8.3_bookworm-10 -- bash -c ' make test_opcache TESTS=tests/opcache/some_test.phpt ' ``` @@ -233,7 +233,7 @@ Requires a compiled `ddtrace.so` (see [Prerequisites](#prerequisites)). ```bash .claude/ci/dockerh --cache tracer-8.3-debug --overlayfs --php debug \ - datadog/dd-trace-ci:php-8.3_bookworm-6 -- bash -c ' + datadog/dd-trace-ci:php-8.3_bookworm-10 -- bash -c ' set -e make -j$(nproc) all make test_internal_api_randomized @@ -254,7 +254,7 @@ docker compose -p $PROJECT -f .claude/ci/docker-compose.services.yml \ up -d test-agent request-replayer .claude/ci/dockerh --cache tracer-8.3-debug --overlayfs --root --php debug \ - datadog/dd-trace-ci:php-8.3_bookworm-6 \ + datadog/dd-trace-ci:php-8.3_bookworm-10 \ --network ${PROJECT}_default \ -e DD_AGENT_HOST=test-agent \ -e DD_TRACE_AGENT_PORT=9126 \ @@ -288,7 +288,7 @@ inside `/usr/local/src/php/`. ```bash .claude/ci/dockerh --cache tracer-8.3-debug --overlayfs --root --php debug \ - datadog/dd-trace-ci:php-8.3_bookworm-6 -- bash -c ' + datadog/dd-trace-ci:php-8.3_bookworm-10 -- bash -c ' set -e make -j$(nproc) all install_all php /usr/local/src/php/run-tests.php -g FAIL,XFAIL,BORK,WARN,LEAK,XLEAK,SKIP \ @@ -324,7 +324,7 @@ Wait a few seconds for the test-agent to be ready before running tests. ```bash .claude/ci/dockerh --cache tracer-integ-83 --overlayfs --php debug \ - datadog/dd-trace-ci:php-8.3_bookworm-6 \ + datadog/dd-trace-ci:php-8.3_bookworm-10 \ --network tracer-integ-83_default \ -e DD_TRACE_ASSUME_COMPILED=1 \ -e DDAGENT_HOSTNAME=test-agent \ @@ -380,7 +380,7 @@ them if not already running (see `test_auto_instrumentation` section). ```bash .claude/ci/dockerh --cache tracer-integ-83 --overlayfs --php debug \ - datadog/dd-trace-ci:php-8.3_bookworm-6 \ + datadog/dd-trace-ci:php-8.3_bookworm-10 \ --network tracer-integ-83_default \ -e DD_TRACE_ASSUME_COMPILED=1 \ -e DDAGENT_HOSTNAME=test-agent \ @@ -421,7 +421,7 @@ silent wrong behaviour. ```bash .claude/ci/dockerh --cache tracer-8.3-asan --overlayfs \ --php debug-zts-asan \ - datadog/dd-trace-ci:php-8.3_bookworm-6 -- bash -c ' + datadog/dd-trace-ci:php-8.3_bookworm-10 -- bash -c ' set -e export COMPILE_ASAN=1 make -j$(nproc) all @@ -441,7 +441,7 @@ when `ASAN=1` is detected, which adds `-fsanitize=address ```bash .claude/ci/dockerh --cache tracer-8.3-asan --overlayfs \ --php debug-zts-asan \ - datadog/dd-trace-ci:php-8.3_bookworm-6 -- bash -c ' + datadog/dd-trace-ci:php-8.3_bookworm-10 -- bash -c ' set -e export ASAN_OPTIONS=abort_on_error=1:disable_coredump=0:unmap_shadow_on_exit=1 make test_c @@ -465,7 +465,7 @@ run). Tests that use `getenv("SKIP_ASAN")` in `--SKIPIF--` sections ```bash .claude/ci/dockerh --cache tracer-8.3-asan --overlayfs \ --php debug-zts-asan \ - datadog/dd-trace-ci:php-8.3_bookworm-6 -- bash -c ' + datadog/dd-trace-ci:php-8.3_bookworm-10 -- bash -c ' export ASAN_OPTIONS=abort_on_error=1:disable_coredump=0:unmap_shadow_on_exit=1 make test_internal_api_randomized ' @@ -490,7 +490,7 @@ Then run: ```bash .claude/ci/dockerh --cache tracer-8.3-asan --overlayfs \ --php debug-zts-asan \ - datadog/dd-trace-ci:php-8.3_bookworm-6 \ + datadog/dd-trace-ci:php-8.3_bookworm-10 \ --network tracer-asan-83_default \ -e HTTPBIN_HOSTNAME=httpbin-integration \ -e HTTPBIN_PORT=8080 \ @@ -513,7 +513,7 @@ PHP 7.x — do not run against PHP 7.x. ```bash .claude/ci/dockerh --cache tracer-8.3-asan --overlayfs \ --php debug-zts-asan \ - datadog/dd-trace-ci:php-8.3_bookworm-6 -- bash -c ' + datadog/dd-trace-ci:php-8.3_bookworm-10 -- bash -c ' export ASAN_OPTIONS=abort_on_error=1:disable_coredump=0:unmap_shadow_on_exit=1 make test_c_observer ' @@ -524,7 +524,7 @@ make test_c_observer ```bash .claude/ci/dockerh --cache tracer-8.3-asan --overlayfs \ --php debug-zts-asan \ - datadog/dd-trace-ci:php-8.3_bookworm-6 -- bash -c ' + datadog/dd-trace-ci:php-8.3_bookworm-10 -- bash -c ' export ASAN_OPTIONS=abort_on_error=1:disable_coredump=0:unmap_shadow_on_exit=1 make test_opcache ' diff --git a/.claude/ci/tracer-web-tests.md b/.claude/ci/tracer-web-tests.md index 0f7f1b8e6a3..4537e9eb83d 100644 --- a/.claude/ci/tracer-web-tests.md +++ b/.claude/ci/tracer-web-tests.md @@ -15,7 +15,7 @@ | CI Job | Image | What it does | |--------|-------|-------------| -| `test_web_laravel_{ver}: [{php}, {sapi}]` | `datadog/dd-trace-ci:php-{php}_bookworm-6` | Laravel integration tests (4.2, 5.7, 5.8, 8.x, 9.x, 10.x, 11.x, latest, octane) | +| `test_web_laravel_{ver}: [{php}, {sapi}]` | `datadog/dd-trace-ci:php-{php}_bookworm-10` | Laravel integration tests (4.2, 5.7, 5.8, 8.x, 9.x, 10.x, 11.x, latest, octane) | | `test_web_symfony_{ver}: [{php}, {sapi}]` | same | Symfony integration tests (2.3--latest) | | `test_web_wordpress_{ver}: [{php}, {sapi}]` | same | WordPress integration tests (4.8, 5.5, 5.9, 6.1) | | `test_web_drupal_{ver}: [{php}, {sapi}]` | same | Drupal integration tests (8.9, 9.5, 10.1) | @@ -94,7 +94,7 @@ between the compile and test containers: ```bash .claude/ci/dockerh --cache tracer-web-83 --overlayfs \ - datadog/dd-trace-ci:php-8.3_bookworm-6 \ + datadog/dd-trace-ci:php-8.3_bookworm-10 \ -e CI_COMMIT_TAG=local \ -e SHARED=1 \ -- bash -c ' @@ -178,7 +178,7 @@ the toplevel `Makefile`. ```bash PROJECT=tracer-web-83-laravel-latest .claude/ci/dockerh --cache tracer-web-83 --overlayfs --php \ - datadog/dd-trace-ci:php-8.3_bookworm-6 \ + datadog/dd-trace-ci:php-8.3_bookworm-10 \ --network ${PROJECT}_default \ -e COMPOSER_MEMORY_LIMIT=-1 \ -e DD_TRACE_ASSUME_COMPILED=1 \ diff --git a/.claude/project/profiling.md b/.claude/project/profiling.md index dea4aa23613..cbf3bceb84a 100644 --- a/.claude/project/profiling.md +++ b/.claude/project/profiling.md @@ -1,48 +1,61 @@ -# profiling/ — Rust profiler extension +# profiling/ — PHP SDK profiling module ## What it is -A separate Rust `cdylib` extension for continuous CPU/wall-time profiling, -heap allocation, exception, and I/O profiling. Uploads pprof directly to the -agent. - -## Key files & dirs - -- `profiling/Cargo.toml` — `cdylib`; depends on `libdd-profiling`/`alloc`/ - `common` from libdatadog. -- `profiling/rust-toolchain.toml` — pins Rust (distinct from the workspace - toolchain). -- `profiling/src/lib.rs` — module entry: `minit`/`rinit`/`prshutdown`, Zend - interrupt registration. -- `profiling/src/profiling/` — `mod.rs` (`Profiler` + `SampleValues`), - `interrupts.rs`, `backtrace.rs`, `stack_walking.rs`, `uploader.rs`, - `thread_utils.rs`. -- `profiling/src/allocation/` — `allocation_ge84.rs` (PHP 8.4+), - `allocation_le83.rs` (≤8.3). +The Rust profiling product for continuous CPU/wall-time, allocation, heap, +exception, and I/O profiling. It can be built into the combined `ddtrace.so` +or, for OTel and future integrations, as the standalone +`datadog-profiling.so`. Profiles upload directly to the agent. + +## Key files and directories + +- Root `Cargo.toml` — owns the `datadog-php` package and its `profiling` + feature; profiling is not a separate Cargo package. +- `profiling/src/lib.rs` — shared profiler lifecycle and the standalone PHP + module/Zend-extension wrappers. +- `profiling/src/lifecycle.h` — C lifecycle API used by combined `ddtrace.so`. +- `profiling/src/profiling/` — profiler, interrupts, stack walking, uploader, + and thread utilities. +- `profiling/src/allocation/` — version-specific PHP allocation hooks. +- `profiling/configuration.h` and `profiling/src/configuration.c` — generated + configuration declarations and the standalone ZAI host. - `profiling/src/config.rs`, `profiling/src/capi.rs`. -- `profiling/build.rs` — bindgen + `php-config` feature detection. +- `profiling/build.rs` — bindgen, PHP capability detection, and private C + support compiled as part of the root package build. - `profiling/src/php_ffi.{c,h}`. ## How it fits -`minit` registers a hybrid module + zend_extension; `rinit` starts a -per-request `Profiler` sampling on VM interrupt (~10ms) and hooks -`zend_execute_internal`. Samples wall/CPU/alloc/heap/exception; uploads -pprof via a background thread; `prshutdown` flushes. +Both artifacts call the same profiler lifecycle implementation. Standalone +mode supplies its own PHP module, Zend extension, globals, and ZAI runtime. +Combined mode is dispatched by `ext/datadog.c`, uses ddtrace-owned globals and +aggregate ZAI configuration, and links tracer/common integrations directly. +The standalone profiler may coexist with the OTel SDK tracer, but loading it +alongside any `ddtrace.so` artifact is unsupported and rejected. -Builds independently from the tracer; depends on libdatadog for pprof. The -main tracer looks up `ddog_php_prof_interrupt_function` by symbol to call on -interrupt (see [tracer.md](tracer.md)). Not sidecar-dependent — it uploads -directly (contrast with [sidecar.md](sidecar.md)). `build.rs` reads -`../VERSION`. +## Building + +Loadable PHP artifacts must be built from the repository root through +phpize/configure/Make; direct Cargo output is not a supported extension: + +```sh +phpize +./configure --disable-ddtrace-tracer --enable-ddtrace-profiling # standalone +# or: ./configure --enable-ddtrace-tracer --enable-ddtrace-profiling # combined +make -j"$(nproc)" +``` + +Make/configure select the Cargo features, target directory, PHP headers, +sanitizer flags, and output module name. Cargo remains appropriate for Rust +unit tests, clippy, and benchmarks, but not extension validation. ## Gotchas -- Pinned `rust-toolchain.toml`, not the workspace default. -- `CARGO_TARGET_DIR` must be set (the Makefile uses `tmp/build_profiler`). -- `cdylib` is release-only — phpt tests fail on debug builds. -- Allocation hook differs: PHP 8.4+ vs ≤8.3 use different modules. +- The toolchain is pinned by the repository's Rust toolchain files. +- PHPT and integration validation must load `modules/datadog-profiling.so` or + `modules/ddtrace.so`, never a Cargo target-directory cdylib. +- Allocation hooks differ between PHP 8.4+ and earlier PHP versions. - `io_profiling` is Linux/macOS only. -- `trigger_time_sample` is a debug-only build feature. +- `trigger_time_sample` is a test/benchmark build feature. - For build/test detail see [../ci/building-locally.md](../ci/building-locally.md). diff --git a/.github/workflows/prof_asan.yml b/.github/workflows/prof_asan.yml index eb374e690bb..dd2590d6e2f 100644 --- a/.github/workflows/prof_asan.yml +++ b/.github/workflows/prof_asan.yml @@ -20,6 +20,7 @@ jobs: RUSTUP_HOME: /rust/rustup CARGO_TARGET_DIR: /tmp/build-cargo RUST_TOOLCHAIN: nightly-2025-06-13 + PROFILE_EXTENSION: ${{ matrix.php-version == 8.5 && matrix.php-build == 'nts-asan' && matrix.runner == 'ubuntu-8-core-latest' && 'ddtrace.so' || 'datadog-profiling.so' }} container: image: datadog/dd-trace-ci:php-${{matrix.php-version}}_bookworm-10 # https://docs.github.com/en/actions/creating-actions/dockerfile-support-for-github-actions#user @@ -62,9 +63,13 @@ jobs: export DDTRACE_PROFILING_TARGET="$(uname -m)-unknown-linux-gnu" export DDTRACE_PROFILING_CARGO_BUILD_FLAGS='-Zbuild-std=std,panic_abort' phpize - ./configure --disable-ddtrace-tracer --enable-ddtrace-profiling --disable-ddtrace-rust-debug + if [ "${PROFILE_EXTENSION}" = "ddtrace.so" ]; then + ./configure --enable-ddtrace-tracer --enable-ddtrace-profiling --disable-ddtrace-rust-debug + else + ./configure --disable-ddtrace-tracer --enable-ddtrace-profiling --disable-ddtrace-rust-debug + fi make -j"$(nproc)" - cp -v modules/datadog-profiling.so "$(php-config --extension-dir)/datadog-profiling.so" + cp -v "modules/${PROFILE_EXTENSION}" "$(php-config --extension-dir)/${PROFILE_EXTENSION}" - name: Run phpt tests run: | @@ -73,9 +78,12 @@ jobs: cd profiling/tests cp -v $(php-config --prefix)/lib/php/build/run-tests.php . export DD_PROFILING_OUTPUT_PPROF=/tmp/pprof + export DD_TRACE_ENABLED=false + export DD_INSTRUMENTATION_TELEMETRY_ENABLED=false + export DD_REMOTE_CONFIG_ENABLED=false export ASAN_OPTIONS="${ASAN_OPTIONS:+$ASAN_OPTIONS:}abort_on_error=1:halt_on_error=1:detect_leaks=1:strict_string_checks=1:detect_stack_use_after_return=1:check_initialization_order=1:detect_container_overflow=1:detect_odr_violation=2:symbolize=1:disable_coredump=0:unmap_shadow_on_exit=1" export LSAN_OPTIONS="${LSAN_OPTIONS:+$LSAN_OPTIONS:}exitcode=23:print_suppressions=0:report_objects=1" - php run-tests.php -j$(nproc) --show-diff --asan -d extension=datadog-profiling.so phpt + php run-tests.php -j$(nproc) --show-diff --asan -d "extension=${PROFILE_EXTENSION}" phpt prof-ubsan: name: PHP 8.5 ${{ matrix.php-build }} UBSAN (${{ matrix.runner }}) diff --git a/.github/workflows/prof_correctness.yml b/.github/workflows/prof_correctness.yml index a9ba768c7c3..caac982e695 100644 --- a/.github/workflows/prof_correctness.yml +++ b/.github/workflows/prof_correctness.yml @@ -13,6 +13,13 @@ jobs: runs-on: ubuntu-24.04 env: LLVM_VERSION: "20" + # Keep profiler correctness isolated when the tests use a combined build. + DD_PROFILING_ENABLED: "true" + DD_TRACE_ENABLED: "false" + DD_INSTRUMENTATION_TELEMETRY_ENABLED: "false" + DD_REMOTE_CONFIG_ENABLED: "false" + PROFILE_EXTENSION: ${{ matrix.php-version == '8.5' && 'ddtrace.so' || 'datadog-profiling.so' }} + PROFILE_RI: ${{ matrix.php-version == '8.5' && 'ddtrace' || 'datadog-profiling' }} strategy: fail-fast: false matrix: @@ -48,6 +55,7 @@ jobs: ~/.cargo/registry/cache/ ~/.cargo/git/db/ target-profiling/ + target-common/ key: ${{ runner.os }}-cargo-llvm-${{ env.LLVM_VERSION }}-${{ hashFiles('**/Cargo.lock', 'rust-toolchain.toml') }}-${{ matrix.php-version }}-${{ matrix.phpts }} - name: Build profiler @@ -79,7 +87,11 @@ jobs: version_number=$(awk -F' = ' '$1 == "channel" { gsub(/"/, "", $2); print $2 }' rust-toolchain.toml) curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal -y --default-toolchain "$version_number" phpize - DDTRACE_PROFILING_FEATURES=trigger_time_sample ./configure --disable-ddtrace-tracer --enable-ddtrace-profiling + if [ "${{ matrix.php-version }}" = "8.5" ]; then + DDTRACE_PROFILING_FEATURES=trigger_time_sample ./configure --enable-ddtrace-tracer --enable-ddtrace-profiling + else + DDTRACE_PROFILING_FEATURES=trigger_time_sample ./configure --disable-ddtrace-tracer --enable-ddtrace-profiling + fi make -j"$(nproc)" - name: Cache build dependencies @@ -91,6 +103,7 @@ jobs: ~/.cargo/registry/cache/ ~/.cargo/git/db/ target-profiling/ + target-common/ key: ${{ runner.os }}-cargo-llvm-${{ env.LLVM_VERSION }}-${{ hashFiles('**/Cargo.lock', 'rust-toolchain.toml') }}-${{ matrix.php-version }}-${{ matrix.phpts }} - name: Run no profile test @@ -99,11 +112,11 @@ jobs: export DD_PROFILING_EXPERIMENTAL_FEATURES_ENABLED=1 export DD_PROFILING_EXCEPTION_MESSAGE_ENABLED=1 php -v - php -d extension=modules/datadog-profiling.so --ri datadog-profiling + php -d extension="modules/${PROFILE_EXTENSION}" --ri "${PROFILE_RI}" for test_case in "allocations" "time" "strange_frames" "timeline" "exceptions" "io" "io_upscaling" "allocation_time_combined" "generators"; do mkdir -p profiling/tests/correctness/"$test_case"/ export DD_PROFILING_OUTPUT_PPROF=$PWD/profiling/tests/correctness/"$test_case"/test.pprof - php -d extension="${PWD}/modules/datadog-profiling.so" "profiling/tests/correctness/${test_case}.php" + php -d extension="${PWD}/modules/${PROFILE_EXTENSION}" "profiling/tests/correctness/${test_case}.php" if compgen -G "$DD_PROFILING_OUTPUT_PPROF.*" > /dev/null; then echo "Profile output should not exist:" ls -l "$DD_PROFILING_OUTPUT_PPROF".* @@ -118,22 +131,74 @@ jobs: export DD_PROFILING_EXPERIMENTAL_EXCEPTION_SAMPLING_DISTANCE=1 export DD_PROFILING_EXCEPTION_MESSAGE_ENABLED=1 php -v - php -d extension=modules/datadog-profiling.so --ri datadog-profiling + php -d extension="modules/${PROFILE_EXTENSION}" --ri "${PROFILE_RI}" for test_case in "allocations" "time" "strange_frames" "timeline" "exceptions" "io" "io_upscaling" "allocation_time_combined" "generators"; do mkdir -p profiling/tests/correctness/"$test_case"/ export DD_PROFILING_OUTPUT_PPROF=$PWD/profiling/tests/correctness/"$test_case"/test.pprof - php -d extension=$PWD/modules/datadog-profiling.so profiling/tests/correctness/"$test_case".php + php -d extension="$PWD/modules/${PROFILE_EXTENSION}" profiling/tests/correctness/"$test_case".php done mkdir -p profiling/tests/correctness/allocations_1byte/ export DD_PROFILING_OUTPUT_PPROF=$PWD/profiling/tests/correctness/allocations_1byte/test.pprof export DD_PROFILING_ALLOCATION_SAMPLING_DISTANCE=1 - php -d extension=$PWD/modules/datadog-profiling.so profiling/tests/correctness/allocations.php + php -d extension="$PWD/modules/${PROFILE_EXTENSION}" profiling/tests/correctness/allocations.php mkdir -p profiling/tests/correctness/allocations_1byte_no_zend_alloc/ export DD_PROFILING_OUTPUT_PPROF=$PWD/profiling/tests/correctness/allocations_1byte_no_zend_alloc/test.pprof export DD_PROFILING_ALLOCATION_SAMPLING_DISTANCE=1 - USE_ZEND_ALLOC=0 php -d extension=$PWD/modules/datadog-profiling.so profiling/tests/correctness/allocations.php + USE_ZEND_ALLOC=0 php -d extension="$PWD/modules/${PROFILE_EXTENSION}" profiling/tests/correctness/allocations.php unset DD_PROFILING_ALLOCATION_SAMPLING_DISTANCE + - name: Run standalone profiler with the OpenTelemetry SDK tracer + if: matrix.php-version == '8.4' && matrix.phpts == 'nts' + run: | + mkdir -p /tmp/otel-sdk + composer require --working-dir=/tmp/otel-sdk --no-interaction --no-progress open-telemetry/sdk:^1.0 + cat >/tmp/otel-sdk/coexistence.php <<'PHP' + getTracer('datadog-profiler-coexistence'); + $span = $tracer->spanBuilder('coexistence')->startSpan(); + $scope = $span->activate(); + + if (!extension_loaded('datadog-profiling') || extension_loaded('ddtrace')) { + throw new RuntimeException('expected only the standalone profiler extension'); + } + if (!filter_var(ini_get('datadog.profiling.enabled'), FILTER_VALIDATE_BOOL)) { + throw new RuntimeException('profiling is not enabled'); + } + + $scope->detach(); + $span->end(); + $provider->shutdown(); + echo "standalone profiler and OpenTelemetry SDK tracer coexist\n"; + PHP + php -d "extension=${PWD}/modules/datadog-profiling.so" /tmp/otel-sdk/coexistence.php + + - name: Run combined tracer and profiler integration tests + if: matrix.php-version == '8.5' && matrix.phpts == 'nts' + env: + DD_TRACE_ENABLED: "true" + run: | + export TEST_PHP_EXECUTABLE="$(command -v php)" + php run-tests.php -q \ + -d "extension=${PWD}/modules/ddtrace.so" \ + tests/ext/extension_disabled.phpt \ + tests/ext/profiling/runtime_id_01.phpt \ + tests/ext/profiling/runtime_id_02.phpt + + endpoint_output=$( + DD_TRACE_CLI_ENABLED=true \ + DD_PROFILING_ENABLED=true \ + DD_PROFILING_LOG_LEVEL=trace \ + DD_PROFILING_OUTPUT_PPROF=/tmp/combined-endpoint.pprof \ + php -d "extension=${PWD}/modules/ddtrace.so" \ + -r '$span = DDTrace\active_span(); $span->type = "web"; $span->resource = "combined-endpoint-test";' \ + 2>&1 + ) + echo "${endpoint_output}" + grep -q 'Enqueued endpoint profiling information for span id:' <<<"${endpoint_output}" + - name: Run ZTS tests if: matrix.phpts == 'zts' run: | @@ -142,11 +207,11 @@ jobs: export DD_PROFILING_EXPERIMENTAL_EXCEPTION_SAMPLING_DISTANCE=1 export DD_PROFILING_EXCEPTION_MESSAGE_ENABLED=1 php -v - php -d extension=modules/datadog-profiling.so --ri datadog-profiling + php -d extension="modules/${PROFILE_EXTENSION}" --ri "${PROFILE_RI}" for test_case in "exceptions_zts"; do mkdir -p profiling/tests/correctness/"$test_case"/ export DD_PROFILING_OUTPUT_PPROF=$PWD/profiling/tests/correctness/"$test_case"/test.pprof - php -d extension=$PWD/modules/datadog-profiling.so profiling/tests/correctness/"$test_case".php + php -d extension="$PWD/modules/${PROFILE_EXTENSION}" profiling/tests/correctness/"$test_case".php done - name: Check profiler correctness for allocations @@ -222,6 +287,26 @@ jobs: expected_json: profiling/tests/correctness/exceptions.json pprof_path: profiling/tests/correctness/exceptions/ + - name: Run standalone and ddtrace conflict tests + if: matrix.php-version == '8.5' && matrix.phpts == 'nts' + run: | + cp modules/ddtrace.so /tmp/ddtrace-combined.so + make distclean + phpize + export CC=clang-${LLVM_VERSION} + export CXX=clang++-${LLVM_VERSION} + export LLVM_CONFIG_PATH=/usr/bin/llvm-config-${LLVM_VERSION} + export LIBCLANG_PATH=/usr/lib/llvm-${LLVM_VERSION}/lib + export LD_LIBRARY_PATH="${LIBCLANG_PATH}:${LD_LIBRARY_PATH:-}" + ./configure --disable-ddtrace-tracer --enable-ddtrace-profiling + make -j"$(nproc)" + export TEST_PHP_EXECUTABLE="$(command -v php)" + export DDTRACE_TEST_TRACER_EXTENSION=/tmp/ddtrace-combined.so + export DDTRACE_TEST_PROFILER_EXTENSION="${PWD}/modules/datadog-profiling.so" + php run-tests.php -q \ + profiling/tests/phpt/standalone_conflict_ddtrace_first.phpt \ + profiling/tests/phpt/standalone_conflict_profiler_first.phpt + - name: Notify Slack if: failure() && github.ref == 'refs/heads/master' run: | diff --git a/.gitlab/build-profiler.sh b/.gitlab/build-profiler.sh index c0dcccc0ff7..dae14a88ed6 100755 --- a/.gitlab/build-profiler.sh +++ b/.gitlab/build-profiler.sh @@ -16,25 +16,34 @@ fi set -u prefix="$1" thread_safety="${2:-nts}" +artifact_mode="${3:-standalone}" mkdir -vp "${prefix}" prefix="$(cd "${prefix}" && pwd)" +if [ "$artifact_mode" = "combined" ]; then + configure_products="--enable-ddtrace-tracer --enable-ddtrace-profiling" + extension_name="ddtrace" +else + configure_products="--disable-ddtrace-tracer --enable-ddtrace-profiling" + extension_name="datadog-profiling" +fi + if [ "$thread_safety" = "zts" ]; then switch-php "${PHP_VERSION}-zts" - output_file="${prefix}/datadog-profiling-zts.so" + output_file="${prefix}/${extension_name}-zts.so" else switch-php "${PHP_VERSION}" - output_file="${prefix}/datadog-profiling.so" + output_file="${prefix}/${extension_name}.so" fi -# Loadable profiler artifacts must go through the supported PHP build path. +# Loadable profiling artifacts must go through the supported PHP build path. build_dir="/tmp/ddtrace-build-profiler-${thread_safety}" rm -rf "${build_dir}" mkdir -p "${build_dir}/src" tar -cf - --exclude=.git --exclude=tmp . | tar -xf - -C "${build_dir}/src" cd "${build_dir}/src" phpize -./configure --disable-ddtrace-tracer --enable-ddtrace-profiling +./configure ${configure_products} make -j"$(nproc)" -cp -v modules/datadog-profiling.so "${output_file}" +cp -v "modules/${extension_name}.so" "${output_file}" objcopy --compress-debug-sections "${output_file}" diff --git a/.gitlab/generate-package.php b/.gitlab/generate-package.php index ba8ad8cac3a..8e6cfe1e170 100644 --- a/.gitlab/generate-package.php +++ b/.gitlab/generate-package.php @@ -96,7 +96,7 @@ function appsec_image_from_tag_mapping(string $tag): string stages: - prepare - - profiler + - combined - appsec - tracing - packaging @@ -230,8 +230,8 @@ function appsec_image_from_tag_mapping(string $tag): string foreach ($profiler_minor_major_targets as $major_minor) { $abi_no = $php_versions_to_abi[$major_minor] ?> -"compile profiler extension: [, , ]": - stage: profiler +"compile combined extension: [, , ]": + stage: combined image: "" tags: [ "arch:$ARCH" ] needs: @@ -249,8 +249,8 @@ function appsec_image_from_tag_mapping(string $tag): string KUBERNETES_MEMORY_REQUEST: 4Gi KUBERNETES_MEMORY_LIMIT: 8Gi script: - - .gitlab/build-profiler.sh "datadog-profiling/${TRIPLET}/lib/php/${ABI_NO}" "nts" - - .gitlab/build-profiler.sh "datadog-profiling/${TRIPLET}/lib/php/${ABI_NO}" "zts" + - .gitlab/build-profiler.sh "combined-ddtrace/${TRIPLET}/lib/php/${ABI_NO}" "nts" "combined" + - .gitlab/build-profiler.sh "combined-ddtrace/${TRIPLET}/lib/php/${ABI_NO}" "zts" "combined" cache: - key: prefix: cargo-cache-${TRIPLET} @@ -261,7 +261,7 @@ function appsec_image_from_tag_mapping(string $tag): string policy: pull # `Cache Cargo Deps` is used to update/push the cache artifacts: paths: - - "datadog-profiling" + - "combined-ddtrace" " TRIPLET: "" script: + # Overlay the PHP-version-specific combined artifacts onto the historical + # ddtrace paths consumed by package generation. PHP 7.0 and debug builds + # remain tracer-only because profiling does not support those builds. + - | + for module in combined-ddtrace/${TRIPLET}/lib/php/*/ddtrace*.so; do + api=$(basename "$(dirname "$module")") + variant=$(basename "$module" .so) + variant=${variant#ddtrace} + cp -v "$module" "extensions_/ddtrace-${api}${variant}.so" + done - make -j 4 - ./tooling/bin/generate-final-artifact.sh $( - # Profiler extension - - job: "compile profiler extension: [, , ]" + # Combined tracer and profiler extension + - job: "compile combined extension: [, , ]" artifacts: true - - job: "compile profiler extension: [, , ]" + - job: "compile combined extension: [, , ]" artifacts: true /dev/null 2>&1; then switch-php "${PHP_MAJOR_MINOR}-zts"; fi - - touch ../profiling/build.rs # force regeneration after switch-php changes the php-config symlink target - (cd ..; make distclean || true; phpize && DDTRACE_PROFILING_FEATURES="debug_stats,stack_walking_tests,test,tracing,tracing-subscriber,trigger_time_sample" ./configure --disable-ddtrace-tracer --enable-ddtrace-profiling && make -j$(nproc)) - (cd ../; TEST_PHP_JUNIT="${CI_PROJECT_DIR}/artifacts/profiler-tests/zts-results.xml" php profiling/tests/run-tests.php -d "extension=${CI_PROJECT_DIR}/modules/datadog-profiling.so" --show-diff -g "FAIL,XFAIL,BORK,WARN,LEAK,XLEAK,SKIP" "profiling/tests/phpt") after_script: @@ -121,10 +120,9 @@ libdir: /tmp/datadog-profiling script: - switch-php nts - - cargo test --no-default-features --features profiling,test,debug_stats,stack_walking_tests,tracing,tracing-subscriber,trigger_time_sample - - touch profiling/build.rs # make sure the build helper runs after switch-php + - PHP_CONFIG="$(realpath "$(command -v php-config)")" cargo test --no-default-features --features profiling,test,debug_stats,stack_walking_tests,tracing,tracing-subscriber,trigger_time_sample - switch-php zts - - cargo test --no-default-features --features profiling,test,debug_stats,stack_walking_tests,tracing,tracing-subscriber,trigger_time_sample + - PHP_CONFIG="$(realpath "$(command -v php-config)")" cargo test --no-default-features --features profiling,test,debug_stats,stack_walking_tests,tracing,tracing-subscriber,trigger_time_sample "PHP language tests": stage: test diff --git a/Makefile b/Makefile index 40997cdd95d..d6b2ae0f8d7 100644 --- a/Makefile +++ b/Makefile @@ -482,6 +482,9 @@ define FPM_FILES extensions_$(shell test $(1) = arm64 && echo aarch64 || echo $(1))/=$(EXT_DIR)/extensions \ $(shell test $(1) = windows || echo package/post-install.sh=$(EXT_DIR)/bin/post-install.sh) package/ddtrace.ini.example=$(EXT_DIR)/etc/ \ docs=$(EXT_DIR)/docs README.md=$(EXT_DIR)/docs/README.md \ + profiling/LICENSE=$(EXT_DIR)/docs/profiling/LICENSE \ + profiling/LICENSE-3rdparty.csv=$(EXT_DIR)/docs/profiling/LICENSE-3rdparty.csv \ + profiling/NOTICE=$(EXT_DIR)/docs/profiling/NOTICE \ src=$(EXT_DIR)/dd-trace-sources endef define FPM_OPTS diff --git a/benchmark/otel-profiler-context/README.md b/benchmark/otel-profiler-context/README.md index 22f5c182f24..044fbcd1646 100644 --- a/benchmark/otel-profiler-context/README.md +++ b/benchmark/otel-profiler-context/README.md @@ -1,9 +1,9 @@ # OTel profiler-context benchmark This Linux benchmark measures the real PHP profiler time-sampling path while it -reads OTel Process and Thread Context. It loads optimized NTS builds of both -`ddtrace.so` and `datadog-profiling.so`; the profiler is built with the existing -`trigger_time_sample` benchmark feature. +reads OTel Process and Thread Context. It loads an optimized NTS combined +`ddtrace.so`, built through Make with the existing `trigger_time_sample` +benchmark feature. The workload calls `Datadog\Profiling\trigger_time_sample()` from PHP. On the PHP 8.3 benchmark runtime, returning from that internal function processes the diff --git a/benchmark/otel-profiler-context/run.sh b/benchmark/otel-profiler-context/run.sh index 3479b3236f4..7c2b87dc7b6 100755 --- a/benchmark/otel-profiler-context/run.sh +++ b/benchmark/otel-profiler-context/run.sh @@ -51,9 +51,10 @@ build_extensions() { -v "$VOLUME:/work" \ "$IMAGE" \ sh -c ' - make -C /work -j"$(nproc)" all \ + DDTRACE_PROFILING_FEATURES=trigger_time_sample \ + make -C /work -j"$(nproc)" all \ + EXTRA_CONFIGURE_OPTIONS="--enable-ddtrace-profiling" \ "CFLAGS=-O2 -g0 -DNDEBUG -Wall -Wextra" - make -C /work compile_profiler ' } @@ -90,7 +91,6 @@ benchmark_env=( php_command=( php -n -d extension=/work/tmp/build_extension/modules/ddtrace.so - -d extension=/work/tmp/build_profiler/modules/datadog-profiling.so /work/benchmark/otel-profiler-context/workload.php ) diff --git a/components-rs/build.rs b/components-rs/build.rs index 06e67a853f3..0d7ac424a45 100644 --- a/components-rs/build.rs +++ b/components-rs/build.rs @@ -1,14 +1,9 @@ +#[path = "config_codegen.rs"] +mod config_codegen; #[path = "../profiling/build.rs"] mod profiling_build; fn main() { - println!("cargo:rustc-check-cfg=cfg(standalone_profiler)"); - if std::env::var_os("CARGO_FEATURE_PROFILING").is_some() - && std::env::var_os("CARGO_FEATURE_TRACER").is_none() - { - println!("cargo:rustc-cfg=standalone_profiler"); - } - // This entry point belongs only to the common/tracer cdylib used by SSI. // The standalone profiler must remain an ordinary PHP shared library. if std::env::var_os("CARGO_FEATURE_TRACER").is_some() @@ -20,4 +15,6 @@ fn main() { if std::env::var_os("CARGO_FEATURE_PROFILING").is_some() { profiling_build::build(); } + + config_codegen::build(); } diff --git a/components-rs/config.rs b/components-rs/config.rs new file mode 100644 index 00000000000..62da12e0c07 --- /dev/null +++ b/components-rs/config.rs @@ -0,0 +1,9 @@ +//! Rust IDs for the aggregate common/product ZAI configuration table. +//! +//! Products keep their typed conversion helpers because validation, defaults, +//! and representation can be product-specific (for example, `DD_TAGS` is a +//! map in ddtrace and an encoded string in the standalone profiler table). +//! Generating only IDs and the table count avoids mirroring Zend/ZAI layouts +//! in this common Rust module. + +include!(concat!(env!("OUT_DIR"), "/generated_config.rs")); diff --git a/components-rs/config_codegen.rs b/components-rs/config_codegen.rs new file mode 100644 index 00000000000..340dd5719e4 --- /dev/null +++ b/components-rs/config_codegen.rs @@ -0,0 +1,115 @@ +//! Generates Rust configuration IDs from the same X-macro list used by C. +//! +//! The C preprocessor reduces each CONFIG/CALIAS declaration to a type/name +//! record. Keeping the records in preprocessor order makes the generated Rust +//! discriminants match `datadog_config_id` without maintaining a second list. + +use std::env; +use std::fs; +use std::path::Path; + +const MARKER: &str = "RUST_CONFIG_MARKER"; + +const PROBE_SOURCE: &str = r#" +#if defined(PROFILING) && !defined(TRACER) +#include "profiling/configuration.h" +#define RUST_CONFIGURATIONS DDTRACE_PROFILING_ALL_CONFIGURATIONS +#else +#include "ext/configuration.h" +#define RUST_CONFIGURATIONS DD_ALL_CONFIGURATIONS +#endif + +#define CUSTOM(type) type +#define SET MAP +#define SET_LOWERCASE MAP +#define SET_OR_MAP_LOWERCASE MAP +#define JSON MAP +#define CALIAS CONFIG +#define CONFIG(type, name, ...) type, name |NEXT_CONFIG| + +RUST_CONFIG_MARKER +RUST_CONFIGURATIONS +"#; + +pub fn build() { + let out_dir = env::var("OUT_DIR").expect("OUT_DIR not set"); + let probe_path = Path::new(&out_dir).join("config_probe.c"); + fs::write(&probe_path, PROBE_SOURCE).expect("failed to write config_probe.c"); + + let mut cc_build = cc::Build::new(); + cc_build + .file(&probe_path) + .include(".") + .include("ext") + .include("zend_abstract_interface") + .include("src/dogstatsd") + .include("components-rs"); + if env::var_os("CARGO_FEATURE_TRACER").is_some() { + cc_build.define("TRACER", None); + } + if env::var_os("CARGO_FEATURE_PROFILING").is_some() { + cc_build.define("PROFILING", None); + } + for inc in php_config_includes() { + cc_build.include(inc); + } + + let expanded = String::from_utf8(cc_build.expand()) + .expect("config preprocessor output was not valid UTF-8"); + let start = expanded + .find(MARKER) + .unwrap_or_else(|| panic!("marker `{MARKER}` not found in preprocessor output")) + + MARKER.len(); + + let mut variants = String::new(); + let mut count = 0; + for record in expanded[start..].split("|NEXT_CONFIG|") { + let record = record.trim(); + if record.is_empty() { + continue; + } + let (_, name) = record + .split_once(',') + .unwrap_or_else(|| panic!("malformed config record: {record:?}")); + variants.push_str(&format!(" {},\n", name.trim())); + count += 1; + } + + fs::write( + Path::new(&out_dir).join("generated_config.rs"), + format!( + "pub const CONFIG_COUNT: usize = {count};\n\n#[repr(u16)]\n#[derive(Clone, Copy, Debug)]\n#[allow(non_camel_case_types)]\npub enum ConfigId {{\n{variants}}}\n" + ), + ) + .expect("failed to write generated_config.rs"); + + for path in [ + "ext/configuration.h", + "ext/configuration_helpers.h", + "ext/configuration_shared.h", + "tracer/configuration.h", + "profiling/configuration.h", + ] { + println!("cargo:rerun-if-changed={path}"); + } +} + +fn php_config_includes() -> Vec { + let php_config = env::var("PHP_CONFIG").unwrap_or_else(|_| "php-config".to_string()); + let output = std::process::Command::new(&php_config) + .arg("--includes") + .output() + .unwrap_or_else(|e| panic!("failed to run `{php_config} --includes`: {e}")); + if !output.status.success() { + panic!( + "`{php_config} --includes` failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + String::from_utf8(output.stdout) + .expect("php-config output was not valid UTF-8") + .split_whitespace() + .filter_map(|flag| flag.strip_prefix("-I")) + .map(str::to_string) + .collect() +} diff --git a/components-rs/lib.rs b/components-rs/lib.rs index cddfa542480..878c635ff6f 100644 --- a/components-rs/lib.rs +++ b/components-rs/lib.rs @@ -33,29 +33,30 @@ pub mod tracer; #[path = "../profiling/src/lib.rs"] pub mod profiling; -#[cfg(not(standalone_profiler))] +#[cfg(not(all(feature = "profiling", not(feature = "tracer"))))] pub mod agent_info; -#[cfg(not(standalone_profiler))] +#[cfg(not(all(feature = "profiling", not(feature = "tracer"))))] pub mod bytes; -#[cfg(not(standalone_profiler))] +pub mod config; +#[cfg(not(all(feature = "profiling", not(feature = "tracer"))))] pub mod ffe; -#[cfg(not(standalone_profiler))] +#[cfg(not(all(feature = "profiling", not(feature = "tracer"))))] pub mod log; -#[cfg(not(standalone_profiler))] +#[cfg(not(all(feature = "profiling", not(feature = "tracer"))))] pub mod remote_config; -#[cfg(not(standalone_profiler))] +#[cfg(not(all(feature = "profiling", not(feature = "tracer"))))] pub mod sidecar; -#[cfg(not(standalone_profiler))] +#[cfg(not(all(feature = "profiling", not(feature = "tracer"))))] pub mod stats; -#[cfg(not(standalone_profiler))] +#[cfg(not(all(feature = "profiling", not(feature = "tracer"))))] pub mod telemetry; -#[cfg(not(standalone_profiler))] +#[cfg(not(all(feature = "profiling", not(feature = "tracer"))))] pub mod trace_filter; // A standalone profiler must retain the existing profiler-only ABI and size. // Cargo's `cdylib` keeps every `no_mangle` common export alive, even though the // profiler does not use them, so omit those exports only in profiler-only builds. -#[cfg(not(standalone_profiler))] +#[cfg(not(all(feature = "profiling", not(feature = "tracer"))))] #[rustfmt::skip] mod common_exports { #[cfg(unix)] @@ -563,5 +564,5 @@ pub extern "C" fn ddog_free_normalized_tag_value(ptr: *const c_char) { } } -#[cfg(not(standalone_profiler))] +#[cfg(not(all(feature = "profiling", not(feature = "tracer"))))] pub use common_exports::*; diff --git a/config.m4 b/config.m4 index b88fc88676c..ac8fb22b219 100644 --- a/config.m4 +++ b/config.m4 @@ -8,8 +8,8 @@ PHP_ARG_ENABLE(ddtrace, whether to enable Datadog support, PHP_ARG_ENABLE(ddtrace-tracer, whether to enable Datadog tracing support, [ --disable-ddtrace-tracer Disable Datadog tracing support], yes, no) -PHP_ARG_ENABLE(ddtrace-profiling, whether to build the standalone Datadog profiler, - [ --enable-ddtrace-profiling Build the standalone datadog-profiling extension], no, no) +PHP_ARG_ENABLE(ddtrace-profiling, whether to enable Datadog profiling support, + [ --enable-ddtrace-profiling Build Datadog profiling support], no, no) PHP_ARG_ENABLE(ddtrace-sanitize, whether to enable AddressSanitizer for ddtrace, [ --enable-ddtrace-sanitize Build Datadog tracing with AddressSanitizer support], no, no) @@ -30,6 +30,9 @@ PHP_ARG_ENABLE(ddtrace-rust-library-split, whether to not link the rust library [ --enable-ddtrace-rust-library-split Do not build nor link against the rust code], no, no) DDTRACE_PHP_CONFIG=$(command -v "$PHP_CONFIG" 2>/dev/null || echo "$PHP_CONFIG") +if command -v realpath >/dev/null 2>&1; then + DDTRACE_PHP_CONFIG=$(realpath "$DDTRACE_PHP_CONFIG") +fi PHP_SUBST(DDTRACE_PHP_CONFIG) dnl Keep make clean out of Cargo targets; distclean removes all product-specific targets. @@ -43,17 +46,13 @@ AC_DEFUN([DDTRACE_GEN_GLOBAL_MAKEFILE_WRAP], [ ]) DDTRACE_GEN_GLOBAL_MAKEFILE_WRAP -if test "$PHP_DDTRACE_TRACER" != "no" && test "$PHP_DDTRACE_PROFILING" != "no"; then - AC_MSG_ERROR([A combined tracer and profiler extension is deferred to a later milestone. Build the standalone profiler with: ./configure --disable-ddtrace-tracer --enable-ddtrace-profiling]) -fi - if test "$PHP_DDTRACE_PROFILING" != "no"; then dnl PHP_NEW_EXTENSION normally initializes this for libtool's configure probes. RM="rm -f" case "$host_os" in mingw*|cygwin*|msys*|windows*) - AC_MSG_ERROR([The standalone Datadog profiler is not supported on Windows]) + AC_MSG_ERROR([Datadog profiling support is not available on Windows]) ;; esac @@ -61,7 +60,7 @@ if test "$PHP_DDTRACE_PROFILING" != "no"; then PHP_VERSION_ID=$("$PHP_CONFIG" --vernum) fi if test "$PHP_VERSION_ID" -lt 70100; then - AC_MSG_ERROR([The standalone Datadog profiler requires PHP 7.1 or newer; PHP 7.0 is not supported]) + AC_MSG_ERROR([Datadog profiling support requires PHP 7.1 or newer; PHP 7.0 is not supported]) fi if test -n "$PHP_DDTRACE_CARGO" && test "$PHP_DDTRACE_CARGO" != "cargo"; then @@ -78,6 +77,8 @@ if test "$PHP_DDTRACE_PROFILING" != "no"; then profiler_cargo_profile=$(test "$PHP_DDTRACE_RUST_DEBUG" != "no" && echo debug || echo profiler-release) profiler_target_dir="${CARGO_TARGET_DIR:-\$(builddir)/target-profiling}" + + if test "$PHP_DDTRACE_TRACER" = "no"; then profiler_target_path= profiler_target_arg= if test -n "$DDTRACE_PROFILING_TARGET"; then @@ -94,7 +95,7 @@ if test "$PHP_DDTRACE_PROFILING" != "no"; then PHP_SUBST(PHP_MODULES) cat <> Makefile.fragments -$profiler_rust_lib: \$(shell for dir in \$(srcdir)/components-rs \$(srcdir)/profiling \$(srcdir)/libdatadog \$(srcdir)/../../libdatadog; do test ! -d \$\$dir || find \$\$dir \( -type f -o -type l \) \( -name '*.rs' -o -name '*.c' -o -name '*.h' -o -name Cargo.toml \) -not -path '*/target/*' -not -path '*/.git/*'; done) \$(srcdir)/ext/handlers_api.c \$(srcdir)/ext/handlers_api.h \$(srcdir)/Cargo.toml \$(srcdir)/Cargo.lock \$(srcdir)/VERSION +$profiler_rust_lib: \$(shell for dir in \$(srcdir)/components-rs \$(srcdir)/profiling \$(srcdir)/libdatadog \$(srcdir)/../../libdatadog; do test ! -d \$\$dir || find \$\$dir \( -type f -o -type l \) \( -name '*.rs' -o -name '*.c' -o -name '*.h' -o -name Cargo.toml \) -not -path '*/target/*' -not -path '*/.git/*'; done) \$(srcdir)/ext/configuration.h \$(srcdir)/ext/configuration_helpers.h \$(srcdir)/ext/configuration_shared.h \$(srcdir)/tracer/configuration.h \$(srcdir)/ext/handlers_api.c \$(srcdir)/ext/handlers_api.h \$(srcdir)/Cargo.toml \$(srcdir)/Cargo.lock \$(srcdir)/VERSION (cd "\$(srcdir)"; PHP_CONFIG="\$(DDTRACE_PHP_CONFIG)" CARGO_TARGET_DIR="$profiler_target_dir" RUSTFLAGS="\$(RUSTFLAGS) --cfg tokio_unstable" "\$(DDTRACE_CARGO)" build $DDTRACE_PROFILING_CARGO_BUILD_FLAGS $profiler_target_arg --no-default-features --features "profiling${DDTRACE_PROFILING_FEATURES:+,$DDTRACE_PROFILING_FEATURES}" $(test "$profiler_cargo_profile" = debug || echo --profile "$profiler_cargo_profile") \$(shell echo "\$(MAKEFLAGS)" | $EGREP -o "[[-]]j[[0-9]]+")) \$(phplibdir)/datadog-profiling.la: $profiler_rust_lib @@ -108,9 +109,10 @@ clean-profiler: clean: clean-profiler EOT + fi fi -if test "$PHP_DDTRACE" != "no" && test "$PHP_DDTRACE_PROFILING" = "no"; then +if test "$PHP_DDTRACE" != "no" && { test "$PHP_DDTRACE_PROFILING" = "no" || test "$PHP_DDTRACE_TRACER" != "no"; }; then AC_CHECK_SIZEOF([long]) AC_MSG_CHECKING([for 64-bit platform]) AS_IF([test "$ac_cv_sizeof_long" -eq 4],[ @@ -382,7 +384,11 @@ if test "$PHP_DDTRACE" != "no" && test "$PHP_DDTRACE_PROFILING" = "no"; then $DD_TRACE_PHP_SOURCES " - DATADOG_EXTENSION_FLAGS="$DATADOG_EXTENSION_FLAGS -DDDTRACE" + DATADOG_EXTENSION_FLAGS="$DATADOG_EXTENSION_FLAGS -DTRACER" + fi + + if test "$PHP_DDTRACE_PROFILING" != "no"; then + DATADOG_EXTENSION_FLAGS="$DATADOG_EXTENSION_FLAGS -DPROFILING" fi PHP_NEW_EXTENSION(ddtrace, $ALL_DATADOG_SOURCES, $ext_shared,, $DATADOG_EXTENSION_FLAGS) @@ -525,16 +531,27 @@ EOT else dnl consider it debug if -g is specified (but not -g0) ddtrace_cargo_profile=$(test "$PHP_DDTRACE_RUST_DEBUG" != "no" && echo debug || echo tracer-release) - ddtrace_rust_lib="\$(builddir)/target-common/$ddtrace_cargo_profile/libdatadog_php.a" - if test "$PHP_DDTRACE_TRACER" != "no"; then + ddtrace_target_dir="${CARGO_TARGET_DIR:-\$(builddir)/target-common}" + ddtrace_target_path= + ddtrace_target_args= + if test "$PHP_DDTRACE_TRACER" != "no" && test "$PHP_DDTRACE_PROFILING" != "no" && test -n "$DDTRACE_PROFILING_TARGET"; then + ddtrace_target_path="/$DDTRACE_PROFILING_TARGET" + ddtrace_target_args="--target $DDTRACE_PROFILING_TARGET" + fi + ddtrace_rust_lib="$ddtrace_target_dir$ddtrace_target_path/$ddtrace_cargo_profile/libdatadog_php.a" + if test "$PHP_DDTRACE_TRACER" != "no" && test "$PHP_DDTRACE_PROFILING" != "no"; then + ddtrace_cargo_features="--no-default-features --features tracer,profiling${DDTRACE_PROFILING_FEATURES:+,$DDTRACE_PROFILING_FEATURES}" + ddtrace_cargo_build_flags="$DDTRACE_PROFILING_CARGO_BUILD_FLAGS $ddtrace_target_args" + elif test "$PHP_DDTRACE_TRACER" != "no"; then ddtrace_cargo_features="--features tracer" else ddtrace_cargo_features="--no-default-features" fi + ddtrace_cargo_build_flags="${ddtrace_cargo_build_flags:-}" cat <> Makefile.fragments -$ddtrace_rust_lib: $( (find "$ext_srcdir/components-rs" -name "*.rs"; find "$ext_srcdir" -maxdepth 1 -name "Cargo.toml"; find "$ext_srcdir/../../libdatadog" -name "*.rs" -not -path "*/target/*"; find "$ext_srcdir/libdatadog" -name "*.rs" -not -path "*/target/*") 2>/dev/null | tr '\n' ' ' ) - (cd "$ext_srcdir"; PHP_CONFIG="\$(DDTRACE_PHP_CONFIG)" CARGO_FEATURES="$ddtrace_cargo_features" CARGO_TARGET_DIR=\$(builddir)/target-common/ SHARED=$(test "$ext_shared" = "yes" && echo 1) PROFILE="$ddtrace_cargo_profile" host_os="$host_os" DDTRACE_CARGO="\$(DDTRACE_CARGO)" $(if test "$PHP_DDTRACE_SANITIZE" != "no"; then echo COMPILE_ASAN=1; fi) sh ./compile_rust.sh \$(shell echo "\$(MAKEFLAGS)" | $EGREP -o "[[-]]j[[0-9]]+")) +$ddtrace_rust_lib: $( (find "$ext_srcdir/components-rs" -name "*.rs"; find "$ext_srcdir/profiling" \( -name "*.rs" -o -name "*.c" -o -name "*.h" \); find "$ext_srcdir/zend_abstract_interface" \( -name "*.c" -o -name "*.h" \); find "$ext_srcdir" -maxdepth 1 -name "Cargo.toml"; find "$ext_srcdir/../../libdatadog" -name "*.rs" -not -path "*/target/*"; find "$ext_srcdir/libdatadog" -name "*.rs" -not -path "*/target/*") 2>/dev/null | tr '\n' ' ' ) $ext_srcdir/ext/configuration.h $ext_srcdir/ext/configuration_helpers.h $ext_srcdir/ext/configuration_shared.h $ext_srcdir/tracer/configuration.h + (cd "$ext_srcdir"; PHP_CONFIG="\$(DDTRACE_PHP_CONFIG)" CARGO_FEATURES="$ddtrace_cargo_features" CARGO_TARGET_DIR="$ddtrace_target_dir/" SHARED=$(test "$ext_shared" = "yes" && echo 1) PROFILE="$ddtrace_cargo_profile" host_os="$host_os" DDTRACE_CARGO="\$(DDTRACE_CARGO)" $(if test "$PHP_DDTRACE_SANITIZE" != "no"; then echo COMPILE_ASAN=1; fi) sh ./compile_rust.sh $ddtrace_cargo_build_flags \$(shell echo "\$(MAKEFLAGS)" | $EGREP -o "[[-]]j[[0-9]]+")) EOT fi diff --git a/config.w32 b/config.w32 index dc407a3dfe0..ab56b01f556 100644 --- a/config.w32 +++ b/config.w32 @@ -1,7 +1,7 @@ ARG_ENABLE("ddtrace", "Datadog tracing support", "shared"); -ARG_ENABLE("ddtrace-profiling", "standalone Datadog profiler (unsupported on Windows)", "no"); +ARG_ENABLE("ddtrace-profiling", "Datadog profiling support (unsupported on Windows)", "no"); if (PHP_DDTRACE_PROFILING != 'no') { - ERROR('The standalone Datadog profiler is not supported on Windows'); + ERROR('Datadog profiling support is not available on Windows'); } var ddtrace_cargo_profile = "debug"; @@ -111,7 +111,7 @@ if (PHP_DDTRACE != 'no') { ADD_FLAG("CFLAGS_DDTRACE", " /Zc:preprocessor "); } - ADD_FLAG("CFLAGS_DDTRACE", " /DDDTRACE "); + ADD_FLAG("CFLAGS_DDTRACE", " /DTRACER "); ADD_SOURCES(configure_module_dirname + "/ext", DDTRACE_EXT_SOURCES, "ddtrace"); ADD_SOURCES(configure_module_dirname + "/tracer", DDTRACE_TRACER_SOURCES, "ddtrace"); diff --git a/datadog-setup.php b/datadog-setup.php index f4aec408712..d7a8ef23850 100644 --- a/datadog-setup.php +++ b/datadog-setup.php @@ -55,7 +55,7 @@ /** * The number of items to shift off `get_ini_settings` for config commands. */ -const CMD_CONFIG_NUM_SHIFT = 3; +const CMD_CONFIG_NUM_SHIFT = 2; function main() { @@ -109,7 +109,7 @@ function print_help() --extension-dir Specify the extension directory. Default: PHP's extension directory. --ini Specify the INI file to use. Default: /98-ddtrace.ini --enable-appsec Enable the application security monitoring module. - --enable-profiling Enable the profiling module. + --enable-profiling Enable profiling in the ddtrace module. -d setting[=value] Used in conjunction with `config ` command to specify the INI setting to get or set. @@ -191,7 +191,7 @@ function config_list(array $options) * * $ php datadog-setup.php config list --php-bin all * Searching for available php binaries, this operation might take a while. - * datadog.profiling.enabled = On; default: 1, binary: /opt/php/8.2/bin/php, INI file: /opt/php/etc/conf.d/98-ddtrace.ini + * datadog.profiling.enabled = On; default: Off, binary: /opt/php/8.2/bin/php, INI file: /opt/php/etc/conf.d/98-ddtrace.ini * datadog.profiling.experimental_allocation_enabled = On; default: 1, binary: /opt/php/8.2/bin/php, INI file: /opt/php/etc/conf.d/98-ddtrace.ini * * @see get_ini_settings @@ -533,7 +533,6 @@ function install($options) $tmpArchiveAppsecRoot = $tmpDir . '/dd-library-php/appsec'; $tmpArchiveAppsecLib = "{$tmpArchiveAppsecRoot}/lib"; $tmpArchiveAppsecEtc = "{$tmpArchiveAppsecRoot}/etc"; - $tmpArchiveProfilingRoot = $tmpDir . '/dd-library-php/profiling'; $tmpSrcDir = $tmpArchiveTraceRoot . '/src'; execute_or_exit("Cannot create directory '$tmpDir'. Try setting a different temporary directory by setting the sys_temp_dir INI variable. This directory must exist. E.g. php -d sys_temp_dir=" . (IS_WINDOWS ? 'C:\path\to\temp\dir' : "/path/to/temp/dir") . (isset($_SERVER["argv"][0]) ? " {$_SERVER["argv"][0]}" : ""), "mkdir " . (IS_WINDOWS ? "" : "-m 700 ") . escapeshellarg($tmpDir)); @@ -657,15 +656,11 @@ function install($options) $extensionDestination = $extDir . '/' . EXTENSION_PREFIX . 'ddtrace.' . EXTENSION_SUFFIX; safe_copy_extension($extensionRealPath, $extensionDestination); - // Profiling - $profilingExtensionRealPath = "$tmpArchiveProfilingRoot/ext/$extensionVersion/" - . EXTENSION_PREFIX . "datadog-profiling$extensionSuffix." . EXTENSION_SUFFIX; - $shouldInstallProfiling = file_exists($profilingExtensionRealPath); - - if ($shouldInstallProfiling) { - $profilingExtensionDestination = $extDir . '/' . EXTENSION_PREFIX . 'datadog-profiling.' . EXTENSION_SUFFIX; - safe_copy_extension($profilingExtensionRealPath, $profilingExtensionDestination); - } + // Profiling is compiled into supported ddtrace artifacts. The marker + // distinguishes combined builds from legacy tracer-only variants such + // as PHP 7.0 and PHP debug builds. + $profilingMarker = "$tmpArchiveTraceRoot/ext/$extensionVersion/.ddtrace$extensionSuffix.profiling"; + $shouldInstallProfiling = file_exists($profilingMarker); // Appsec $appsecExtensionRealPath = "{$tmpArchiveAppsecRoot}/ext/{$extensionVersion}/" @@ -722,6 +717,11 @@ function install($options) ]; } + // The standalone profiler cannot coexist with combined ddtrace. + // Disable legacy entries while preserving them for users who + // intentionally install it separately. + $replacements['(^\s*(zend_)?extension\s*=\s*.*datadog-profiling.*)m'] = '; $0'; + if (isset($options[OPT_EXTENSION_DIR])) { $replacements += [ '(^\s*;?\s*extension\s*=\s*.*ddtrace.*)m' => "extension = $extensionDestination", @@ -733,28 +733,18 @@ function install($options) * `;`, hence not automatically re-activating the extension if the user had commented it out. */ '(^\s*;?\s*extension\s*=\s*.*ddtrace.*)m' => "extension = ddtrace" . (IS_WINDOWS ? "" : "." . EXTENSION_SUFFIX), - // Support upgrading from the C based zend_extension. - '(zend_extension\s*=\s*.*datadog-profiling.*)' => "extension = datadog-profiling" . (IS_WINDOWS ? "" : "." . EXTENSION_SUFFIX), ]; } - // Enabling profiling + // Profiling is opt-in for installer-managed combined artifacts. if (is_truthy($options[OPT_ENABLE_PROFILING])) { - // phpcs:disable Generic.Files.LineLength.TooLong - if ($shouldInstallProfiling) { - if (isset($options[OPT_EXTENSION_DIR])) { - $replacements['(zend_extension\s*=\s*.*datadog-profiling.*)'] = "extension = $profilingExtensionDestination"; - $replacements['(^\s*;?\s*extension\s*=\s*.*datadog-profiling.*)m'] = "extension = $profilingExtensionDestination"; - } else { - $replacements['(^\s*;?\s*extension\s*=\s*.*datadog-profiling.*)m'] = "extension = datadog-profiling" . (IS_WINDOWS ? "" : "." . EXTENSION_SUFFIX); - } - } else { + if (!$shouldInstallProfiling) { $enableProfiling = OPT_ENABLE_PROFILING; print_error_and_exit( "Option --{$enableProfiling} was provided, but it is not supported on this PHP build or version.\n" ); } - // phpcs:enable Generic.Files.LineLength.TooLong + $replacements['(^[\s;]*datadog\.profiling\.enabled\s*=.*)m'] = 'datadog.profiling.enabled = On'; } // Load AppSec and enable/disable as required @@ -2234,13 +2224,7 @@ function get_ini_settings($sourcesDir, $appsecHelperPath, $appsecRulesPath) 'name' => 'extension', 'default' => 'ddtrace' . (IS_WINDOWS ? "" : "." . EXTENSION_SUFFIX), 'commented' => false, - 'description' => 'Enables or disables tracing (set by the installer, do not change it)', - ], - [ - 'name' => 'extension', - 'default' => 'datadog-profiling' . (IS_WINDOWS ? "" : "." . EXTENSION_SUFFIX), - 'commented' => true, - 'description' => 'Enables the profiling module', + 'description' => 'Loads the Datadog PHP SDK (set by the installer, do not change it)', ], [ 'name' => 'extension', @@ -2256,9 +2240,9 @@ function get_ini_settings($sourcesDir, $appsecHelperPath, $appsecRulesPath) [ 'name' => 'datadog.profiling.enabled', - 'default' => '1', - 'commented' => true, - 'description' => 'Enable the Datadog profiling module.', + 'default' => 'Off', + 'commented' => false, + 'description' => 'Enable profiling in the Datadog ddtrace module.', ], [ 'name' => 'datadog.trace.endpoint_collection_enabled', diff --git a/dockerfiles/verify_packages/installer/test_install_custom_ext_dir_and_ini.sh b/dockerfiles/verify_packages/installer/test_install_custom_ext_dir_and_ini.sh index 73b9d15e90b..99cc85a25f3 100644 --- a/dockerfiles/verify_packages/installer/test_install_custom_ext_dir_and_ini.sh +++ b/dockerfiles/verify_packages/installer/test_install_custom_ext_dir_and_ini.sh @@ -13,6 +13,7 @@ custom_ini_file="$(get_php_conf_dir)/40-ddtrace.ini" php ./build/packages/datadog-setup.php --php-bin php --extension-dir /custom-ext-dir --enable-profiling --ini "$custom_ini_file" assert_file_exists /custom-ext-dir/ddtrace.so +assert_file_not_exists /custom-ext-dir/datadog-profiling.so assert_ddtrace_version "${trace_version}" assert_profiler_installed diff --git a/dockerfiles/verify_packages/installer/test_install_custom_extension_directory.sh b/dockerfiles/verify_packages/installer/test_install_custom_extension_directory.sh index ed941dcf127..33ba9fc0e60 100644 --- a/dockerfiles/verify_packages/installer/test_install_custom_extension_directory.sh +++ b/dockerfiles/verify_packages/installer/test_install_custom_extension_directory.sh @@ -12,6 +12,7 @@ trace_version=$(cat VERSION) php ./build/packages/datadog-setup.php --php-bin php --extension-dir /custom-ext-dir --enable-profiling assert_file_exists /custom-ext-dir/ddtrace.so +assert_file_not_exists /custom-ext-dir/datadog-profiling.so assert_ddtrace_version "${trace_version}" assert_profiler_installed diff --git a/dockerfiles/verify_packages/installer/test_install_uninstall_install_tracer_profiling.sh b/dockerfiles/verify_packages/installer/test_install_uninstall_install_tracer_profiling.sh index 2d11190912c..07c29036118 100644 --- a/dockerfiles/verify_packages/installer/test_install_uninstall_install_tracer_profiling.sh +++ b/dockerfiles/verify_packages/installer/test_install_uninstall_install_tracer_profiling.sh @@ -45,14 +45,8 @@ assert_no_profiler php ./build/packages/datadog-setup.php --enable-profiling --php-bin php --file "./build/packages/dd-library-php-${version}-${arch}-linux-gnu.tar.gz" extension_dir="$(php -i | grep '^extension_dir' | awk '{ print $NF }')" -for extension in ddtrace datadog-profiling ; do - if [ -f "${extension_dir}/${extension}.so" ]; then - echo "Ok: File ${extension_dir}/${extension}.so exists." - else - echo "Error. File ${extension_dir}/${extension}.so should exist." - exit 1 - fi -done +assert_file_exists "${extension_dir}/ddtrace.so" +assert_file_not_exists "${extension_dir}/datadog-profiling.so" assert_ddtrace_version "${version}" assert_profiler_version "${version}" diff --git a/dockerfiles/verify_packages/installer/test_profiler_install_disabled.sh b/dockerfiles/verify_packages/installer/test_profiler_install_disabled.sh index 9d6f7ced5b8..c0dfa20a8d0 100644 --- a/dockerfiles/verify_packages/installer/test_profiler_install_disabled.sh +++ b/dockerfiles/verify_packages/installer/test_profiler_install_disabled.sh @@ -12,6 +12,7 @@ version=$(cat VERSION) php ./build/packages/datadog-setup.php --php-bin php assert_ddtrace_version "${version}" -assert_file_exists "$(get_php_extension_dir)"/datadog-profiling.so - +assert_file_exists "$(get_php_extension_dir)"/ddtrace.so +assert_file_not_exists "$(get_php_extension_dir)"/datadog-profiling.so assert_no_profiler +assert_file_contains "$(get_php_conf_dir)/98-ddtrace.ini" "datadog.profiling.enabled = Off" diff --git a/dockerfiles/verify_packages/installer/test_profiler_install_enabled.sh b/dockerfiles/verify_packages/installer/test_profiler_install_enabled.sh index 3c3cc92bed8..0511d35e0a7 100644 --- a/dockerfiles/verify_packages/installer/test_profiler_install_enabled.sh +++ b/dockerfiles/verify_packages/installer/test_profiler_install_enabled.sh @@ -12,6 +12,7 @@ version=$(cat VERSION) php ./build/packages/datadog-setup.php --php-bin php --enable-profiling assert_ddtrace_version "${version}" -assert_file_exists "$(get_php_extension_dir)"/datadog-profiling.so - +assert_file_exists "$(get_php_extension_dir)"/ddtrace.so +assert_file_not_exists "$(get_php_extension_dir)"/datadog-profiling.so assert_profiler_version "${version}" +assert_file_contains "$(get_php_conf_dir)/98-ddtrace.ini" "datadog.profiling.enabled = On" diff --git a/dockerfiles/verify_packages/installer/utils.sh b/dockerfiles/verify_packages/installer/utils.sh index 6d87b8c2cf5..3c5c56fa6fc 100755 --- a/dockerfiles/verify_packages/installer/utils.sh +++ b/dockerfiles/verify_packages/installer/utils.sh @@ -32,12 +32,11 @@ assert_no_ddtrace() { } assert_no_profiler() { - output="$(php -v)" - if [ -z "${output##*datadog-profiling*}" ]; then - echo "---\nError: profiler should not be installed\n---\n${1}\n---\n" + if php -r 'exit(filter_var(ini_get("datadog.profiling.enabled"), FILTER_VALIDATE_BOOL) ? 0 : 1);'; then + echo "---\nError: profiler should not be enabled\n---\n${1}\n---\n" exit 1 fi - echo "Ok: profiler is not installed" + echo "Ok: profiler is not enabled" } assert_ddtrace_version() ( @@ -72,13 +71,8 @@ assert_no_appsec() { } assert_profiler_version() { - output="$(php -v)" - if [ -z "${output##*datadog-profiling v${1}*}" ]; then - echo "---\nOk: datadog-profiling version '${1}' is correctly installed\n---\n${output}\n---\n" - else - echo "---\nError: Wrong datadog-profiling version. Expected: ${1}\n---\n${output}\n---\n" - exit 1 - fi + assert_ddtrace_version "${1}" + assert_profiler_installed } assert_tracer_installed() { @@ -94,11 +88,10 @@ assert_tracer_installed() { assert_profiler_installed() { php_bin=${1:-php} - output="$($php_bin -v)" - if [ -z "${output##*with datadog-profiling*}" ]; then - echo "---\nOk: Profiler is installed\n---\n${output}\n---\n" + if "$php_bin" -r '$value = ini_get("datadog.profiling.enabled"); exit($value !== false && filter_var($value, FILTER_VALIDATE_BOOL) ? 0 : 1);'; then + echo "Ok: Profiling is available and enabled in ddtrace" else - echo "---\nError: Profiler should be installed\n---\n${output}\n---\n" + echo "Error: Profiling should be available and enabled in ddtrace" exit 1 fi } diff --git a/ext/configuration.c b/ext/configuration.c index 78e2fcd0970..9e7af35f1fc 100644 --- a/ext/configuration.c +++ b/ext/configuration.c @@ -14,7 +14,7 @@ ZEND_EXTERN_MODULE_GLOBALS(datadog); #include -#ifndef DDTRACE +#ifndef TRACER bool datadog_alter_dd_service(zval *old_value, zval *new_value, zend_string *new_str) { UNUSED(old_value, new_value); if (DATADOG_G(request_initialized)) { @@ -67,7 +67,7 @@ bool datadog_alter_dd_trace_disabled_config(zval *old_value, zval *new_value, ze #define CALIAS CONFIG #define CONFIG(...) 1, #define NUMBER_OF_CONFIGURATIONS sizeof((uint8_t[]){DD_ALL_CONFIGURATIONS}) -_Static_assert(NUMBER_OF_CONFIGURATIONS < ZAI_CONFIG_ENTRIES_COUNT_MAX, +_Static_assert(NUMBER_OF_CONFIGURATIONS <= ZAI_CONFIG_ENTRIES_COUNT_MAX, "There are more config entries than ZAI_CONFIG_ENTRIES_COUNT_MAX."); #undef CONFIG #define CONFIG(type, name, ...) \ @@ -154,6 +154,8 @@ static void dd_ini_env_to_ini_name(const zai_str env_name, zai_config_name *ini_ if (env_name.ptr == strstr(env_name.ptr, "DD_TRACE_")) { ini_name->ptr[sizeof("datadog.trace") - 1] = '.'; + } else if (env_name.ptr == strstr(env_name.ptr, "DD_PROFILING_")) { + ini_name->ptr[sizeof("datadog.profiling") - 1] = '.'; } else if (env_name.ptr == strstr(env_name.ptr, "DD_APPSEC_")) { ini_name->ptr[sizeof("datadog.appsec") - 1] = '.'; } else if (env_name.ptr == strstr(env_name.ptr, "DD_DYNAMIC_INSTRUMENTATION_")) { @@ -190,7 +192,7 @@ bool datadog_config_minit(int module_number) { } void datadog_config_first_rinit() { -#ifdef DDTRACE +#ifdef TRACER zend_ini_entry *internal_functions_ini = zai_config_memoized_entries[DATADOG_CONFIG_DD_TRACE_TRACED_INTERNAL_FUNCTIONS].ini_entries[0]; zend_string *internal_functions_old = zend_string_copy( @@ -200,7 +202,7 @@ void datadog_config_first_rinit() { zai_config_first_time_rinit(true); zai_config_rinit(); -#ifdef DDTRACE +#ifdef TRACER zend_string *internal_functions_new = internal_functions_ini->modified ? internal_functions_ini->orig_value : internal_functions_ini->value; diff --git a/ext/configuration.h b/ext/configuration.h index 6e9c4f8c2ec..65676370495 100644 --- a/ext/configuration.h +++ b/ext/configuration.h @@ -40,22 +40,20 @@ enum datadog_sidecar_connection_mode { #define DD_CRASHTRACKING_ENABLED_DEFAULT "true" #endif +#include "configuration_shared.h" + #define DATADOG_CONFIGURATION \ - CONFIG(STRING, DD_TRACE_AGENT_URL, "", .ini_change = zai_config_system_ini_change) \ - CONFIG(STRING, DD_AGENT_HOST, "localhost", .ini_change = zai_config_system_ini_change) \ + DD_COMMON_TRACE_AGENT_URL_CONFIGURATION \ + DD_COMMON_AGENT_HOST_CONFIGURATION \ CONFIG(STRING, DD_DOGSTATSD_URL, "http://localhost:8125") \ CONFIG(STRING, DD_DOGSTATSD_HOST, "localhost") \ CONFIG(STRING, DD_API_KEY, "", .ini_change = zai_config_system_ini_change) \ CONFIG(INT, DD_DOGSTATSD_PORT, "8125") \ - CONFIG(STRING, DD_ENV, "", .ini_change = datadog_alter_dd_env, \ - .env_config_fallback = ddtrace_conf_otel_resource_attributes_env) \ - CONFIG(STRING, DD_SERVICE, "", .ini_change = datadog_alter_dd_service, \ - .env_config_fallback = ddtrace_conf_otel_service_name) \ + DD_COMMON_ENV_CONFIGURATION \ + DD_COMMON_SERVICE_CONFIGURATION \ CONFIG(MAP, DD_SERVICE_MAPPING, "") \ - CONFIG(CUSTOM(MAP), DD_TAGS, "", \ - .env_config_fallback = ddtrace_conf_otel_resource_attributes_tags, \ - .parser = dd_parse_tags) \ - CONFIG(INT, DD_TRACE_AGENT_PORT, "8126", .ini_change = zai_config_system_ini_change) \ + DD_COMMON_TAGS_CONFIGURATION \ + DD_COMMON_TRACE_AGENT_PORT_CONFIGURATION \ CONFIG(BOOL, DD_TRACE_CLI_ENABLED, "true") \ CONFIG(BOOL, DD_TRACE_DEBUG, "false", .ini_change = datadog_alter_dd_trace_debug) \ CONFIG(BOOL, DD_TRACE_ENABLED, "true", .ini_change = datadog_alter_dd_trace_disabled_config, \ @@ -86,8 +84,7 @@ enum datadog_sidecar_connection_mode { CONFIG(BOOL, DD_TRACE_STARTUP_LOGS, "true") \ CONFIG(BOOL, DD_TRACE_ONCE_LOGS, "true") \ CONFIG(BOOL, DD_TRACE_AGENTLESS, "false", .ini_change = zai_config_system_ini_change) \ - CONFIG(STRING, DD_VERSION, "", .ini_change = datadog_alter_dd_version, \ - .env_config_fallback = ddtrace_conf_otel_resource_attributes_version) \ + DD_COMMON_VERSION_CONFIGURATION \ CONFIG(STRING, OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, "", \ .ini_change = zai_config_system_ini_change, \ .env_config_fallback = ddtrace_conf_otel_otlp_endpoint) \ @@ -103,8 +100,8 @@ enum datadog_sidecar_connection_mode { .env_config_fallback = ddtrace_conf_otel_log_level) \ CONFIG(BOOL, DD_APPSEC_SCA_ENABLED, "false", .ini_change = zai_config_system_ini_change) \ CONFIG(BOOL, DD_TRACE_GIT_METADATA_ENABLED, "true") \ - CONFIG(STRING, DD_GIT_COMMIT_SHA, "") \ - CONFIG(STRING, DD_GIT_REPOSITORY_URL, "") \ + DD_COMMON_GIT_COMMIT_SHA_CONFIGURATION \ + DD_COMMON_GIT_REPOSITORY_URL_CONFIGURATION \ CONFIG(BOOL, DD_INJECT_FORCE, "false", .ini_change = zai_config_system_ini_change) \ CONFIG(DOUBLE, DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS, "5.0", .ini_change = zai_config_system_ini_change) \ CONFIG(BOOL, DD_REMOTE_CONFIG_ENABLED, "true", .ini_change = zai_config_system_ini_change) \ @@ -113,22 +110,30 @@ enum datadog_sidecar_connection_mode { CONFIG(BOOL, DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED, "false") #define DD_CONFIGURATIONS_ONLY -#ifdef DDTRACE +#ifdef TRACER #include #else #define DDTRACE_CONFIGURATION #endif #undef DD_CONFIGURATIONS_ONLY +#ifdef PROFILING +#include +#else +#define DDTRACE_PROFILING_CONFIGURATION +#endif + #define DD_ALL_CONFIGURATIONS \ DATADOG_CONFIGURATION \ DDTRACE_CONFIGURATION \ + DDTRACE_PROFILING_CONFIGURATION \ #define CALIAS CONFIG #define CONFIG(type, name, ...) DATADOG_CONFIG_##name, typedef enum { DD_ALL_CONFIGURATIONS + DATADOG_CONFIG_COUNT } datadog_config_id; #undef CONFIG diff --git a/ext/configuration_shared.h b/ext/configuration_shared.h new file mode 100644 index 00000000000..d2094d22d79 --- /dev/null +++ b/ext/configuration_shared.h @@ -0,0 +1,69 @@ +#ifndef DATADOG_CONFIGURATION_SHARED_H +#define DATADOG_CONFIGURATION_SHARED_H + +#include +#include
+#include + +/* + * Shared configuration declarations are named individually so the common + * extension can preserve its historical ID ordering while standalone products + * can compose just the shared subset in their own table. + */ +#if defined(PROFILING) && !defined(TRACER) +bool ddog_php_prof_config_parse_utf8_string(zai_str value, zval *decoded_value, bool persistent); + +#define DD_COMMON_AGENT_HOST_CONFIGURATION \ + CONFIG(STRING, DD_AGENT_HOST, "", .ini_change = zai_config_system_ini_change, \ + .parser = ddog_php_prof_config_parse_utf8_string) +#define DD_COMMON_ENV_CONFIGURATION \ + CONFIG(STRING, DD_ENV, "", .parser = ddog_php_prof_config_parse_utf8_string) +#define DD_COMMON_SERVICE_CONFIGURATION \ + CONFIG(STRING, DD_SERVICE, "", .parser = ddog_php_prof_config_parse_utf8_string) +#define DD_COMMON_TAGS_CONFIGURATION CONFIG(STRING, DD_TAGS, "") +#define DD_COMMON_TRACE_AGENT_PORT_CONFIGURATION \ + CONFIG(INT, DD_TRACE_AGENT_PORT, "0", .ini_change = zai_config_system_ini_change) +#define DD_COMMON_TRACE_AGENT_URL_CONFIGURATION \ + CONFIG(STRING, DD_TRACE_AGENT_URL, "", .ini_change = zai_config_system_ini_change, \ + .parser = ddog_php_prof_config_parse_utf8_string) +#define DD_COMMON_VERSION_CONFIGURATION \ + CONFIG(STRING, DD_VERSION, "", .parser = ddog_php_prof_config_parse_utf8_string) +#define DD_COMMON_GIT_COMMIT_SHA_CONFIGURATION \ + CONFIG(STRING, DD_GIT_COMMIT_SHA, "", .parser = ddog_php_prof_config_parse_utf8_string) +#define DD_COMMON_GIT_REPOSITORY_URL_CONFIGURATION \ + CONFIG(STRING, DD_GIT_REPOSITORY_URL, "", .parser = ddog_php_prof_config_parse_utf8_string) +#else +#define DD_COMMON_AGENT_HOST_CONFIGURATION \ + CONFIG(STRING, DD_AGENT_HOST, "localhost", .ini_change = zai_config_system_ini_change) +#define DD_COMMON_ENV_CONFIGURATION \ + CONFIG(STRING, DD_ENV, "", .ini_change = datadog_alter_dd_env, \ + .env_config_fallback = ddtrace_conf_otel_resource_attributes_env) +#define DD_COMMON_SERVICE_CONFIGURATION \ + CONFIG(STRING, DD_SERVICE, "", .ini_change = datadog_alter_dd_service, \ + .env_config_fallback = ddtrace_conf_otel_service_name) +#define DD_COMMON_TAGS_CONFIGURATION \ + CONFIG(CUSTOM(MAP), DD_TAGS, "", \ + .env_config_fallback = ddtrace_conf_otel_resource_attributes_tags, .parser = dd_parse_tags) +#define DD_COMMON_TRACE_AGENT_PORT_CONFIGURATION \ + CONFIG(INT, DD_TRACE_AGENT_PORT, "8126", .ini_change = zai_config_system_ini_change) +#define DD_COMMON_TRACE_AGENT_URL_CONFIGURATION \ + CONFIG(STRING, DD_TRACE_AGENT_URL, "", .ini_change = zai_config_system_ini_change) +#define DD_COMMON_VERSION_CONFIGURATION \ + CONFIG(STRING, DD_VERSION, "", .ini_change = datadog_alter_dd_version, \ + .env_config_fallback = ddtrace_conf_otel_resource_attributes_version) +#define DD_COMMON_GIT_COMMIT_SHA_CONFIGURATION CONFIG(STRING, DD_GIT_COMMIT_SHA, "") +#define DD_COMMON_GIT_REPOSITORY_URL_CONFIGURATION CONFIG(STRING, DD_GIT_REPOSITORY_URL, "") +#endif + +#define DATADOG_SHARED_CONFIGURATION \ + DD_COMMON_AGENT_HOST_CONFIGURATION \ + DD_COMMON_ENV_CONFIGURATION \ + DD_COMMON_SERVICE_CONFIGURATION \ + DD_COMMON_TAGS_CONFIGURATION \ + DD_COMMON_TRACE_AGENT_PORT_CONFIGURATION \ + DD_COMMON_TRACE_AGENT_URL_CONFIGURATION \ + DD_COMMON_VERSION_CONFIGURATION \ + DD_COMMON_GIT_COMMIT_SHA_CONFIGURATION \ + DD_COMMON_GIT_REPOSITORY_URL_CONFIGURATION + +#endif /* DATADOG_CONFIGURATION_SHARED_H */ diff --git a/ext/datadog.c b/ext/datadog.c index 6ef4880a457..6767a134fb0 100644 --- a/ext/datadog.c +++ b/ext/datadog.c @@ -6,6 +6,9 @@ #include #include +#ifdef PROFILING +#include +#endif #include "configuration.h" #include "excluded_modules.h" @@ -47,8 +50,19 @@ ddog_CharSlice php_version_rt; ZEND_DECLARE_MODULE_GLOBALS(datadog) +#ifdef PROFILING +static bool datadog_profiling_initialized; +#endif + #ifdef COMPILE_DL_DDTRACE +#ifdef PROFILING +ZEND_DLEXPORT zend_module_entry *get_module(void) { + datadog_module_entry.functions = ddog_php_prof_functions; + return &datadog_module_entry; +} +#else ZEND_GET_MODULE(datadog) +#endif #ifdef ZTS TSRM_TLS void *TSRMLS_CACHE = NULL; #endif @@ -138,19 +152,30 @@ static int datadog_startup(zend_extension *extension) { #endif } -#ifdef DDTRACE - ddtrace_startup(); +#ifdef TRACER + if (ddtrace_startup() != SUCCESS) { + return FAILURE; + } +#endif +#ifdef PROFILING + if (datadog_profiling_initialized && ddog_php_prof_zend_startup(extension) != SUCCESS) { + return FAILURE; + } #endif return SUCCESS; } static void datadog_shutdown(zend_extension *extension) { - UNUSED(extension); - - #ifdef DDTRACE +#ifdef PROFILING + if (datadog_profiling_initialized) { + ddog_php_prof_zend_shutdown(extension); + } +#endif +#ifdef TRACER ddtrace_shutdown(); #endif + UNUSED(extension); } static void dd_activate_once(void) { @@ -167,13 +192,14 @@ static void dd_activate_once(void) { if (enable_sidecar) { datadog_sidecar_setup(flags); } -#ifdef DDTRACE - ddtrace_activate_once(); -#endif } } static pthread_once_t dd_activate_once_control = PTHREAD_ONCE_INIT; +#ifdef TRACER +static pthread_once_t dd_tracer_activate_once_control = PTHREAD_ONCE_INIT; +static pthread_once_t dd_tracer_first_rinit_control = PTHREAD_ONCE_INIT; +#endif static bool dd_is_cli_autodisabled(const char *arg) { const char *slashend = strrchr(arg, '/'); @@ -191,10 +217,6 @@ static void datadog_activate(void) { datadog_telemetry_rinit(); -#ifdef DDTRACE - ddtrace_activate_early(); -#endif - // ZAI config is always set up pthread_once(&dd_activate_once_control, dd_activate_once); zai_config_rinit(); @@ -217,9 +239,18 @@ static void datadog_activate(void) { } } -#ifdef DDTRACE +#ifdef TRACER + // Tracer activation follows completion of common activation. Its one-time + // setup must still precede its first zai_hook activation. + pthread_once(&dd_tracer_activate_once_control, ddtrace_activate_once); + ddtrace_activate_early(); ddtrace_activate_late(); #endif +#ifdef PROFILING + if (datadog_profiling_initialized) { + ddog_php_prof_zend_activate(); + } +#endif } static void datadog_deactivate(void) { @@ -241,7 +272,7 @@ static zend_extension dd_zend_extension_entry = {"ddtrace", datadog_activate, datadog_deactivate, NULL, -#if PHP_VERSION_ID < 80000 && defined(DDTRACE) +#if PHP_VERSION_ID < 80000 && defined(TRACER) zai_interceptor_op_array_pass_two, #else NULL, @@ -249,12 +280,12 @@ static zend_extension dd_zend_extension_entry = {"ddtrace", NULL, NULL, NULL, -#if PHP_VERSION_ID < 80000 && defined(DDTRACE) +#if PHP_VERSION_ID < 80000 && defined(TRACER) zai_interceptor_op_array_ctor, #else NULL, #endif -#ifdef DDTRACE +#ifdef TRACER zai_hook_unresolve_op_array, #else NULL, @@ -274,11 +305,23 @@ static PHP_GINIT_FUNCTION(datadog) { datadog_globals->sidecar_universal_service_tags_mutex = tsrm_mutex_alloc(); zend_hash_init(&datadog_globals->git_metadata, 8, unused, (dtor_func_t)datadog_git_metadata_dtor, 1); -#ifdef DDTRACE +#ifdef TRACER ddtrace_ginit(datadog_globals); #endif +#ifdef PROFILING + datadog_globals->profiling_globals = pemalloc(ddog_php_prof_globals_size(), true); + if (datadog_globals->profiling_globals) { + ddog_php_prof_ginit(datadog_globals->profiling_globals); + } +#endif } +#ifdef PROFILING +void *datadog_php_profiling_globals(void) { + return DATADOG_G(profiling_globals); +} +#endif + // Rust code will call __cxa_thread_atexit_impl. This is a weak symbol; it's defined by glibc. // The problem is that calls to __cxa_thread_atexit_impl cause shared libraries to remain referenced until the calling thread terminates. // However in NTS builds the calling thread is the main thread and thus the shared object (i.e. ddtrace.so) WILL remain loaded. @@ -348,6 +391,16 @@ static void dd_clean_main_thread_locals() { #endif static PHP_GSHUTDOWN_FUNCTION(datadog) { +#ifdef PROFILING + if (datadog_globals->profiling_globals) { + ddog_php_prof_gshutdown(datadog_globals->profiling_globals); + pefree(datadog_globals->profiling_globals, true); + datadog_globals->profiling_globals = NULL; + } +#endif +#ifdef TRACER + ddtrace_gshutdown(datadog_globals); +#endif #if ZTS datadog_thread_gshutdown(); #endif @@ -367,10 +420,6 @@ static PHP_GSHUTDOWN_FUNCTION(datadog) { zend_hash_destroy(&datadog_globals->git_metadata); -#ifdef DDTRACE - ddtrace_gshutdown(datadog_globals); -#endif - // Drop the per-thread sidecar transport (thread-lifetime, one per thread). datadog_sidecar_gshutdown(datadog_globals); @@ -407,8 +456,38 @@ static void dd_disable_if_incompatible_sapi_detected(void) { } } +static PHP_MSHUTDOWN_FUNCTION(datadog); + +static int datadog_register_zend_extension(void) { + /* Allow extension=ddtrace.so to install Zend Engine hooks without being + * loadable as zend_extension=ddtrace.so. Normal startup registers only + * after product MINIT succeeds; the legacy hard-disabled path registers + * after creating only the tracer's required hook infrastructure. */ + zend_module_entry *mod_ptr = + zend_hash_str_find_ptr(&module_registry, PHP_DDTRACE_EXTNAME, sizeof(PHP_DDTRACE_EXTNAME) - 1); + if (mod_ptr == NULL) { + zend_error(E_CORE_WARNING, + "Failed to find ddtrace extension in registered modules. " + "Please open a bug report."); + return FAILURE; + } + zend_register_extension(&dd_zend_extension_entry, datadog_module_entry.handle); + mod_ptr->handle = NULL; + return SUCCESS; +} + static PHP_MINIT_FUNCTION(datadog) { UNUSED(type); +#ifdef PROFILING + datadog_profiling_initialized = false; +#endif + if (zend_hash_str_exists(&module_registry, ZEND_STRL("datadog-profiling"))) { + zend_error(E_CORE_WARNING, + "datadog-profiling cannot be loaded alongside ddtrace; " + "use combined ddtrace profiling support instead"); + return FAILURE; + } + zval *php_version = zend_get_constant_str(ZEND_STRL("PHP_VERSION")); if (php_version && Z_TYPE_P(php_version) == IS_STRING) { php_version_rt = (ddog_CharSlice){Z_STRVAL_P(php_version), Z_STRLEN_P(php_version)}; @@ -430,7 +509,10 @@ static PHP_MINIT_FUNCTION(datadog) { // Reset on every minit for `apachectl graceful`. dd_activate_once_control = (pthread_once_t)PTHREAD_ONCE_INIT; - +#ifdef TRACER + dd_tracer_activate_once_control = (pthread_once_t)PTHREAD_ONCE_INIT; + dd_tracer_first_rinit_control = (pthread_once_t)PTHREAD_ONCE_INIT; +#endif #if PHP_VERSION_ID < 70300 || (defined(_WIN32) && PHP_VERSION_ID >= 80300 && PHP_VERSION_ID < 80400) datadog_startup_hrtime(); @@ -449,7 +531,7 @@ static PHP_MINIT_FUNCTION(datadog) { // config initialization needs to be at the top // This also initialiyzed logging, so no logs may be emitted before this. datadog_log_init(); -#ifdef DDTRACE +#ifdef TRACER ddtrace_pre_config_minit(); #endif if (!datadog_config_minit(module_number)) { @@ -458,33 +540,13 @@ static PHP_MINIT_FUNCTION(datadog) { dd_disable_if_incompatible_sapi_detected(); -#ifdef DDTRACE - ddtrace_minit_early(module_number); -#endif - - /* This allows an extension (e.g. extension=ddtrace.so) to have zend_engine - * hooks too, but not loadable as zend_extension=ddtrace.so. - * See http://www.phpinternalsbook.com/php7/extensions_design/zend_extensions.html#hybrid-extensions - * {{{ */ - zend_register_extension(&dd_zend_extension_entry, datadog_module_entry.handle); - // The original entry is copied into the module registry when the module is - // registered, so we search the module registry to find the right - // zend_module_entry to modify. - zend_module_entry *mod_ptr = - zend_hash_str_find_ptr(&module_registry, PHP_DDTRACE_EXTNAME, sizeof(PHP_DDTRACE_EXTNAME) - 1); - if (mod_ptr == NULL) { - // This shouldn't happen, possibly a bug if it does. - zend_error(E_CORE_WARNING, - "Failed to find ddtrace extension in " - "registered modules. Please open a bug report."); - - return FAILURE; - } - mod_ptr->handle = NULL; - /* }}} */ - if (datadog_disable) { - return SUCCESS; +#ifdef TRACER + // Preserve the legacy hard-disabled module's PHP API and hook + // infrastructure without starting either product. + ddtrace_minit_early(module_number); +#endif + return datadog_register_zend_extension(); } #ifndef _WIN32 @@ -495,10 +557,6 @@ static PHP_MINIT_FUNCTION(datadog) { datadog_sidecar_minit(); -#ifdef DDTRACE - ddtrace_minit_late(); -#endif - datadog_minit_remote_config(); #ifndef _WIN32 @@ -506,22 +564,44 @@ static PHP_MINIT_FUNCTION(datadog) { #endif ddtrace_set_container_cgroup_path((ddog_CharSlice){ .ptr = DATADOG_G(cgroup_file), .len = strlen(DATADOG_G(cgroup_file)) }); +#ifdef TRACER + ddtrace_minit_early(module_number); + ddtrace_minit_late(); +#endif +#ifdef PROFILING + if (ddog_php_prof_minit(type, module_number) != SUCCESS) { + PHP_MSHUTDOWN(datadog)(type, module_number); + return FAILURE; + } + datadog_profiling_initialized = true; +#endif + if (datadog_register_zend_extension() != SUCCESS) { + PHP_MSHUTDOWN(datadog)(type, module_number); + return FAILURE; + } return SUCCESS; } static PHP_MSHUTDOWN_FUNCTION(datadog) { UNUSED(module_number, type); - UNREGISTER_INI_ENTRIES(); - -#ifdef DDTRACE +#ifdef PROFILING + int profiler_result = datadog_profiling_initialized ? ddog_php_prof_mshutdown(type, module_number) : SUCCESS; +#endif +#ifdef TRACER ddtrace_mshutdown(); #endif + UNREGISTER_INI_ENTRIES(); + if (datadog_disable == 1) { zai_config_mshutdown(); zai_json_shutdown_bindings(); +#ifdef PROFILING + return profiler_result; +#else return SUCCESS; +#endif } datadog_mshutdown_remote_config(); @@ -539,7 +619,11 @@ static PHP_MSHUTDOWN_FUNCTION(datadog) { datadog_process_tags_mshutdown(); +#ifdef PROFILING + return profiler_result; +#else return SUCCESS; +#endif } static void dd_rinit_once(void) { @@ -563,10 +647,6 @@ static void dd_rinit_once(void) { #endif datadog_startup_logging_first_rinit(); - -#ifdef DDTRACE - ddtrace_first_rinit(); -#endif } static pthread_once_t dd_rinit_once_control = PTHREAD_ONCE_INIT; @@ -593,18 +673,26 @@ static PHP_RINIT_FUNCTION(datadog) { // Single combined read: applies env, container-hash, and concentrator config. datadog_apply_agent_info(); -#ifdef DDTRACE - ddtrace_rinit_early(); -#endif - // Do after env check, so that RC data is not updated before RC init DATADOG_G(request_initialized) = true; datadog_sidecar_rinit(); -#ifdef DDTRACE +#ifdef TRACER + pthread_once(&dd_tracer_first_rinit_control, ddtrace_first_rinit); + ddtrace_rinit_early(); ddtrace_rinit(); #endif +#ifdef PROFILING + if (datadog_profiling_initialized) { + if (ddog_php_prof_rinit(type, module_number) != SUCCESS) { + return FAILURE; + } +#ifdef TRACER + ddtrace_set_profiling_notify_enabled(ddog_php_prof_is_enabled()); +#endif + } +#endif return SUCCESS; } @@ -633,6 +721,10 @@ static void dd_shutdown_observer() { static PHP_RSHUTDOWN_FUNCTION(datadog) { UNUSED(module_number, type); +#ifdef PROFILING + int profiler_result = datadog_profiling_initialized ? ddog_php_prof_rshutdown(type, module_number) : SUCCESS; +#endif + // We deliberately select to not free some data structures, as to avoid the overhead of freeing them. // Just proper destruction can have significant and easily measurable overhead on applications. // Prior to PHP 7.2 fast shutdown was an opcache only feature @@ -648,6 +740,16 @@ static PHP_RSHUTDOWN_FUNCTION(datadog) { bool fast_shutdown = is_zend_mm() && !EG(full_tables_cleanup); #endif +#ifdef TRACER + ddtrace_rshutdown(fast_shutdown); +#ifdef PROFILING + // Keep Endpoint Profiling notification available while tracer RSHUTDOWN + // closes the request's automatic root span. Profiler request locals remain + // valid until post-deactivate. + ddtrace_set_profiling_notify_enabled(false); +#endif +#endif + if (DATADOG_G(remote_config_state)) { datadog_rshutdown_remote_config(); } @@ -656,10 +758,6 @@ static PHP_RSHUTDOWN_FUNCTION(datadog) { dd_shutdown_observer(); } -#ifdef DDTRACE - ddtrace_rshutdown(fast_shutdown); -#endif - datadog_sidecar_finalize(true); DATADOG_G(request_initialized) = false; @@ -681,7 +779,11 @@ static PHP_RSHUTDOWN_FUNCTION(datadog) { DATADOG_G(last_version) = NULL; } +#ifdef PROFILING + return profiler_result; +#else return SUCCESS; +#endif } #if PHP_VERSION_ID < 80000 @@ -689,14 +791,21 @@ int datadog_post_deactivate(void) { #else zend_result datadog_post_deactivate(void) { #endif -#ifdef DDTRACE +#ifdef PROFILING + int profiler_result = datadog_profiling_initialized ? ddog_php_prof_post_deactivate() : SUCCESS; +#endif +#ifdef TRACER ddtrace_post_deactivate(); #endif // zai config may be accessed indirectly via other modules RSHUTDOWN, so delay this until the last possible time zai_config_rshutdown(); +#ifdef PROFILING + return profiler_result; +#else return SUCCESS; +#endif } static PHP_MINFO_FUNCTION(datadog) { @@ -705,6 +814,11 @@ static PHP_MINFO_FUNCTION(datadog) { datadog_phpinfo(); DISPLAY_INI_ENTRIES(); +#ifdef PROFILING + if (datadog_profiling_initialized) { + ddog_php_prof_minfo(zend_module); + } +#endif } void datadog_internal_handle_fork(void) { @@ -716,7 +830,7 @@ void datadog_internal_handle_fork(void) { datadog_publish_otel_process_context(dd_zend_string_to_CharSlice(process_tags)); #endif -#ifdef DDTRACE +#ifdef TRACER ddtrace_internal_handle_fork(); #else ddtrace_sidecar_submit_span_data_direct_defaults(&DATADOG_G(sidecar), NULL); @@ -727,6 +841,12 @@ static const zend_module_dep datadog_module_deps[] = { ZEND_MOD_REQUIRED("json") ZEND_MOD_REQUIRED("standard") ZEND_MOD_OPTIONAL("opentelemetry") // make sure we load after otel to insert the hook function if it doesn't exist yet +#ifdef PROFILING + ZEND_MOD_OPTIONAL("ev") + ZEND_MOD_OPTIONAL("event") + ZEND_MOD_OPTIONAL("libevent") + ZEND_MOD_OPTIONAL("uv") +#endif ZEND_MOD_END}; zend_module_entry datadog_module_entry = {STANDARD_MODULE_HEADER_EX, NULL, diff --git a/ext/datadog.h b/ext/datadog.h index c6f9fb8835e..07caad31375 100644 --- a/ext/datadog.h +++ b/ext/datadog.h @@ -83,7 +83,11 @@ ZEND_BEGIN_MODULE_GLOBALS(datadog) char *cgroup_file; zend_bool backtrace_handler_already_run; -#if DDTRACE +#ifdef PROFILING + void *profiling_globals; +#endif + +#ifdef TRACER ddtrace_globals ddtrace; #endif ZEND_END_MODULE_GLOBALS(datadog) diff --git a/ext/handlers_pcntl.c b/ext/handlers_pcntl.c index 6df41d4510a..79865c0b836 100644 --- a/ext/handlers_pcntl.c +++ b/ext/handlers_pcntl.c @@ -16,7 +16,7 @@ static zif_handler dd_pcntl_forkx_handler = NULL; static void dd_prefork() { -#ifdef DDTRACE +#ifdef TRACER ddtrace_internal_handle_prefork(); #endif } @@ -25,7 +25,7 @@ static void dd_handle_fork(zval *return_value) { if (Z_LVAL_P(return_value) == 0) { datadog_internal_handle_fork(); } else { -#ifdef DDTRACE +#ifdef TRACER ddtrace_internal_handle_postfork(); #endif } diff --git a/ext/phpinfo.c b/ext/phpinfo.c index ff0e7d6523a..ce337258224 100644 --- a/ext/phpinfo.c +++ b/ext/phpinfo.c @@ -84,7 +84,7 @@ void datadog_phpinfo() { php_info_print_box_end(); php_info_print_table_start(); -#ifdef DDTRACE +#ifdef TRACER php_info_print_table_row(2, "Datadog tracing support", datadog_disable ? "disabled" : "enabled"); #else php_info_print_table_row(2, "Datadog tracing support", "disabled (not built)"); diff --git a/ext/remote_config.c b/ext/remote_config.c index 81aabbc7f2e..d0723b7db3c 100644 --- a/ext/remote_config.c +++ b/ext/remote_config.c @@ -70,7 +70,7 @@ static void dd_sigvtalarm_handler(int signal, siginfo_t *siginfo, void *ctx) { } #endif -#ifdef DDTRACE +#ifdef TRACER ddtrace_live_debugger_handle_sigvtalarm(); #endif } @@ -114,7 +114,7 @@ static zend_string *dd_dynamic_configuration_update(ddog_CharSlice config, zend_ } void datadog_minit_remote_config(void) { -#ifdef DDTRACE +#ifdef TRACER ddog_setup_remote_config(dd_dynamic_configuration_update, &ddtrace_live_debugger_setup); #else ddog_setup_remote_config(dd_dynamic_configuration_update, NULL); diff --git a/ext/sidecar.c b/ext/sidecar.c index 863ff2ac771..d52844d7e26 100644 --- a/ext/sidecar.c +++ b/ext/sidecar.c @@ -409,7 +409,7 @@ bool datadog_sidecar_should_enable(ddog_RemoteConfigFlags *flags) { flags->appsec_config = appsec_config; enable_sidecar = enable_sidecar || get_global_DD_INSTRUMENTATION_TELEMETRY_ENABLED(); -#ifdef DDTRACE +#ifdef TRACER enable_sidecar = ddtrace_update_remote_config_flags(flags) || enable_sidecar; #endif @@ -637,7 +637,7 @@ void datadog_sidecar_push_tag(ddog_Vec_Tag *vec, ddog_CharSlice key, ddog_CharSl void datadog_sidecar_push_tags(ddog_Vec_Tag *vec, zval *tags) { // Global tags (https://github.com/DataDog/php-datadogstatsd/blob/0efdd1c38f6d3dd407efbb899ad1fd2e5cd18085/src/DogStatsd.php#L113-L125) zend_string *service_string, *env_string, *version_string; -#ifdef DDTRACE +#ifdef TRACER ddtrace_span_data *span = ddtrace_active_span(); #else ddtrace_span_data *span = NULL; @@ -861,7 +861,7 @@ bool datadog_alter_test_session_token(zval *old_value, zval *new_value, zend_str datadog_ffi_try("Failed updating test session token", ddog_sidecar_set_test_session_token(&DATADOG_G(sidecar), dd_zend_string_to_CharSlice(Z_STR_P(new_value)))); } -#if !defined(_WIN32) && defined(DDTRACE) +#if !defined(_WIN32) && defined(TRACER) ddtrace_coms_set_test_session_token(Z_STRVAL_P(new_value), Z_STRLEN_P(new_value)); #endif return true; diff --git a/ext/signals.c b/ext/signals.c index 2432234f896..31d38398c77 100644 --- a/ext/signals.c +++ b/ext/signals.c @@ -65,7 +65,7 @@ static void dd_sigsegv_handler(int sig) { DATADOG_G(backtrace_handler_already_run) = true; datadog_signal_safe_logf("[crash] Segmentation fault encountered"); -#if HAVE_SIGACTION && defined(DDTRACE) +#if HAVE_SIGACTION && defined(TRACER) bool health_metrics_enabled = get_DD_TRACE_HEALTH_METRICS_ENABLED(); if (health_metrics_enabled) { // TODO: emit in sidecar diff --git a/ext/startup_logging.c b/ext/startup_logging.c index 1bf374e82da..4c61f89276a 100644 --- a/ext/startup_logging.c +++ b/ext/startup_logging.c @@ -66,7 +66,7 @@ static size_t dd_curl_write_noop(void *ptr, size_t size, size_t nmemb, void *use static size_t dd_check_for_agent_error(char *error, bool quick) { CURL *curl = curl_easy_init(); -#ifdef DDTRACE +#ifdef TRACER ddtrace_curl_set_hostname(curl); #else char *url = datadog_agent_url(); @@ -87,7 +87,7 @@ static size_t dd_check_for_agent_error(char *error, bool quick) { curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, DATADOG_AGENT_QUICK_TIMEOUT); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, DATADOG_AGENT_QUICK_CONNECT_TIMEOUT); } else { -#ifdef DDTRACE +#ifdef TRACER ddtrace_curl_set_timeout(curl); ddtrace_curl_set_connect_timeout(curl); #else @@ -137,7 +137,7 @@ static void dd_check_for_excluded_module(HashTable *ht, zend_module_entry *modul * To support other types, update phpinfo.c:_dd_info_diagnostics_table(); */ void datadog_startup_diagnostics(HashTable *ht, bool quick) { -#ifdef DDTRACE +#ifdef TRACER ddtrace_startup_diagnostics(ht, quick); #endif #ifndef _WIN32 @@ -164,7 +164,7 @@ void datadog_startup_diagnostics(HashTable *ht, bool quick) { zai_config_memoized_entry *cfg = &zai_config_memoized_entries[i]; // DD_TRACE_LOGS_ENABLED would be the proper name, but for compatibility with other tracers, we also support DD_LOGS_INJECTION officially bool deprecated_alias = cfg->name_index > 0; -#ifdef DDTRACE +#ifdef TRACER deprecated_alias = deprecated_alias && i != DATADOG_CONFIG_DD_TRACE_LOGS_ENABLED; #endif if (deprecated_alias) { @@ -195,7 +195,7 @@ void datadog_startup_logging_json(smart_str *buf, int options) { zend_hash_init(ht, DATADOG_STARTUP_STAT_COUNT, NULL, ZVAL_PTR_DTOR, 0); dd_get_startup_config(ht); -#ifdef DDTRACE +#ifdef TRACER ddtrace_populate_startup_config(ht); #endif dd_add_assoc_bool(ht, ZEND_STRL("loaded_by_ssi"), datadog_loaded_by_ssi); @@ -239,7 +239,7 @@ static void dd_print_startup_logs(void (*log)(const char *format, ...)) { datadog_startup_diagnostics(ht, true); dd_get_startup_config(ht); -#ifdef DDTRACE +#ifdef TRACER ddtrace_populate_startup_config(ht); #endif dd_add_assoc_bool(ht, ZEND_STRL("loaded_by_ssi"), datadog_loaded_by_ssi); @@ -252,7 +252,7 @@ static void dd_print_startup_logs(void (*log)(const char *format, ...)) { "page. Alternatively set DD_TRACE_DEBUG=Error,Startup to add diagnostic checks to the error logs on the first request " "of a new PHP process. Set DD_TRACE_STARTUP_LOGS=0 to disable this tracer configuration message."); -#ifdef DDTRACE +#ifdef TRACER ddtrace_startup_logging_extra(log); #endif diff --git a/ext/telemetry.c b/ext/telemetry.c index 90623a017db..d76ecf1f096 100644 --- a/ext/telemetry.c +++ b/ext/telemetry.c @@ -46,7 +46,7 @@ void datadog_telemetry_rshutdown(void) { // Register in the sidecar services not bound to the request lifetime void datadog_telemetry_register_services(ddog_SidecarTransport **sidecar) { -#ifdef DDTRACE +#ifdef TRACER ddtrace_telemetry_register_services(sidecar); #endif } @@ -71,7 +71,7 @@ void datadog_telemetry_finalize() { ddog_SidecarActionsBuffer *buffer = datadog_telemetry_buffer(); -#ifdef DDTRACE +#ifdef TRACER // Must be called before clearing telemetry_buffer so ddtrace_telemetry_finalize // uses the same buffer (via datadog_telemetry_buffer()) that we'll flush below. ddtrace_telemetry_finalize(); diff --git a/loader/bin/test_apache_reload.sh b/loader/bin/test_apache_reload.sh index ba9a952eb06..b5d0e15ab0a 100755 --- a/loader/bin/test_apache_reload.sh +++ b/loader/bin/test_apache_reload.sh @@ -4,7 +4,7 @@ # # On "apachectl graceful" Apache re-runs php_module_startup() in the very same process, after # having dlclose()'d and re-dlopen()'d dd_library_loader.so (which lands at a *different* address), -# while the previously injected ddtrace.so/datadog-profiling.so usually stay resident (glibc refuses +# while the previously injected combined ddtrace.so usually stays resident (glibc refuses # to unmap libraries that started threads). If the loader leaves its in-place patches on the # injected module's static zend_module_entry behind, the second startup re-captures those already # patched values -- pointers into the now unmapped previous loader image -- installs and calls them, @@ -112,7 +112,7 @@ printf( extension_loaded('ddtrace') ? 'yes' : 'no', phpversion('ddtrace') ?: '-', function_exists('DDTrace\\trace_function') ? 'yes' : 'no', - extension_loaded('datadog-profiling') ? 'yes' : 'no' + ini_get('datadog.profiling.enabled') !== false ? 'yes' : 'no' ); PHP diff --git a/loader/dd_library_loader.c b/loader/dd_library_loader.c index 6d7b40ce08a..a60c366b291 100644 --- a/loader/dd_library_loader.c +++ b/loader/dd_library_loader.c @@ -41,6 +41,7 @@ static unsigned int php_api_no = 0; static const char *runtime_version = "unknown"; static bool injection_forced = false; static bool ddtrace_disabled_by_incompatible_runtime = false; +static bool ddtrace_has_profiling = false; static char ddtrace_disabled_result_reason[384] = {0}; static bool already_done = false; @@ -89,7 +90,12 @@ static void ddloader_set_ddtrace_disabled_by_incompatible_runtime(const char *fo } static char *ddtrace_pre_load_hook(injected_ext *config) { - // Load libdatadog_php.so, on which ddtrace.so implicitly depends. Implicit + // Combined ddtrace artifacts contain their PHP-version-specific Rust code. + if (ddtrace_has_profiling) { + return NULL; + } + + // Load libdatadog_php.so, on which split ddtrace.so implicitly depends. Implicit // because there's no DT_NEEDED(libdatadog_php.so) entry in ddtrace.so. // This has unfortunate side effects. Resolution of libdatadog_php.so // symbols against the handle of ddtrace.so (usually stored in @@ -270,6 +276,10 @@ static void ddtrace_pre_minit_hook(injected_ext *config, zend_module_entry *modu ddloader_ini_set_configuration(config, ZEND_STRL("ddtrace.disable"), ZEND_STRL("1")); } + if (ddtrace_has_profiling && !ddloader_ini_get_configuration(ZEND_STRL("datadog.profiling.enabled"))) { + ddloader_ini_set_configuration(config, ZEND_STRL("datadog.profiling.enabled"), ZEND_STRL("0")); + } + // Let ddtrace knows that it was loaded by the loader bool *datadog_loaded_by_ssi = (bool *)DL_FETCH_SYMBOL(config->so_handle, "datadog_loaded_by_ssi"); if (datadog_loaded_by_ssi) { @@ -778,6 +788,13 @@ static PHP_MINIT_FUNCTION(ddloader_injected_extension_minit) { TELEMETRY(REASON_COMPLETE, config, NULL, "Application instrumentation bootstrapping complete ('%s')", config->ext_name) } + if (strcmp(config->ext_name, "ddtrace") == 0 && ddtrace_has_profiling) { + injected_ext *profiling = &ddloader_injected_ext_config[EXT_DATADOG_PROFILING]; + profiling->injection_success = config->injection_success; + profiling->injection_error = config->injection_error; + profiling->version = config->version; + } + return ret; } @@ -799,6 +816,32 @@ static void ddloader_restore_so_module_entry(injected_ext *config) { config->orig_module_functions = NULL; } +static bool ddloader_combined_profiling_available(unsigned int api_no, bool is_zts, bool is_debug) { + if (is_debug) { + return false; + } + + char *marker = NULL; + const char *zts_suffix = is_zts ? "-zts" : ""; + if (asprintf(&marker, "%s/%strace/ext/%u/.ddtrace%s.profiling", package_path, OS_PATH, api_no, + zts_suffix) == -1) { + return false; + } + bool available = access(marker, F_OK) == 0; + free(marker); + if (available) { + return true; + } + + if (asprintf(&marker, "%s/trace/ext/%u/.ddtrace%s.profiling", package_path, api_no, + zts_suffix) == -1) { + return false; + } + available = access(marker, F_OK) == 0; + free(marker); + return available; +} + static int ddloader_load_extension(unsigned int php_api_no, char *module_build_id, bool is_zts, bool is_debug, injected_ext *config) { if (php_api_no < config->ext_min_version) { TELEMETRY(REASON_INCOMPATIBLE_RUNTIME, config, NULL, "'%s' extension is not supported on this PHP version", config->ext_name, php_api_no); @@ -1048,8 +1091,19 @@ static int ddloader_build_id_check(const char *build_id) { return SUCCESS; } - // Load the extensions declared in ddloader_injected_ext_config + ddtrace_has_profiling = ddloader_combined_profiling_available(php_api_no, is_zts, is_debug); + + // Load the extensions declared in ddloader_injected_ext_config. Combined + // ddtrace owns profiling, so it must not load the standalone profiler too. for (unsigned int i = 0; i < sizeof(ddloader_injected_ext_config) / sizeof(ddloader_injected_ext_config[0]); ++i) { + if (i == EXT_DATADOG_PROFILING && ddtrace_has_profiling) { + injected_ext *profiling = &ddloader_injected_ext_config[i]; + injected_ext *ddtrace = &ddloader_injected_ext_config[EXT_DDTRACE]; + profiling->version = ddtrace->version; + safe_str_append(profiling->extra_config, sizeof(profiling->extra_config), + "provided by combined ddtrace"); + continue; + } ddloader_load_extension(php_api_no, module_build_id, is_zts, is_debug, &ddloader_injected_ext_config[i]); } @@ -1094,6 +1148,7 @@ static void ddloader_zend_extension_shutdown(zend_extension *ext) { DL_UNLOAD(libdatadog_php_handle); libdatadog_php_handle = NULL; } + ddtrace_has_profiling = false; injection_forced = false; ddtrace_disabled_by_incompatible_runtime = false; diff --git a/loader/tests/functional/test_php_output.php b/loader/tests/functional/test_php_output.php index 3dea1f0a028..20f9c6da982 100644 --- a/loader/tests/functional/test_php_output.php +++ b/loader/tests/functional/test_php_output.php @@ -12,9 +12,6 @@ $output = runCLI('-v', true); assertContains($output, 'with ddtrace v'); assertContains($output, 'with dd_library_loader v'); -if ('7.0' !== php_minor_version()) { - assertContains($output, 'with datadog-profiling v'); -} assertContains($output, 'with ddappsec v'); $output = runCLI('-m', false); @@ -26,7 +23,4 @@ $output = runCLI('-m', true); assertContains($output, 'ddtrace'); assertContains($output, 'dd_library_loader'); -if ('7.0' !== php_minor_version()) { - assertContains($output, 'datadog-profiling'); -} assertContains($output, 'ddappsec'); diff --git a/loader/tests/functional/test_phpinfo.php b/loader/tests/functional/test_phpinfo.php index 4501fd0bad6..ac7f2e57b8a 100644 --- a/loader/tests/functional/test_phpinfo.php +++ b/loader/tests/functional/test_phpinfo.php @@ -60,6 +60,7 @@ Injection success => true Injection error => Extra config => datadog.trace.sources_path=%s/trace/src +datadog.profiling.enabled=0 Logs => Found extension file: %s %A @@ -70,11 +71,8 @@ Version => %s Injection success => true Injection error => -Extra config => datadog.profiling.enabled=0 - -Logs => Found extension file: %s -%A -Application instrumentation bootstrapping complete ('datadog-profiling') +Extra config => provided by combined ddtrace +Logs => %A => ddappsec diff --git a/package/ddtrace.ini.example b/package/ddtrace.ini.example index 5d3f1077acf..0d91dcff6a2 100644 --- a/package/ddtrace.ini.example +++ b/package/ddtrace.ini.example @@ -1,2 +1,3 @@ [ddtrace] extension=/opt/datadog-php/extensions/ddtrace-11111.so +datadog.profiling.enabled=Off diff --git a/profiling/README.md b/profiling/README.md index df5dc505417..e62d014a94b 100644 --- a/profiling/README.md +++ b/profiling/README.md @@ -26,11 +26,17 @@ the process. ## Compiling -From the repository root, run `phpize`, then -`./configure --disable-ddtrace-tracer --enable-ddtrace-profiling`, and `make`. -This invokes the root Cargo package and its profiler build helper, which is how -it adapts to various PHP versions. The -[bindgen](https://crates.io/crates/bindgen) crate is used to generate Rust +From the repository root, run `phpize`, then select one profiler build: + +- Standalone `datadog-profiling.so`: `./configure --disable-ddtrace-tracer --enable-ddtrace-profiling` +- Combined tracer and profiler `ddtrace.so`: `./configure --enable-ddtrace-tracer --enable-ddtrace-profiling` + +Run `make` after configuring. Both modes invoke the root Cargo package and its +profiler build helper, which is how the profiler adapts to various PHP versions. +Standalone profiling defaults on when its module is explicitly loaded; combined +`ddtrace.so` defaults profiling off and is enabled with `DD_PROFILING_ENABLED=1`. +A direct `cargo build` does not produce a supported PHP extension artifact. +The [bindgen](https://crates.io/crates/bindgen) crate is used to generate Rust bindings to the Zend Engine. Although bindgen is pretty good, there are things like complex macro expansions which it doesn't understand, so there is a bit of C code to do things that Rust/bindgen isn't good at. @@ -47,9 +53,10 @@ From the repository root, `cargo test --no-default-features --features profiling runs the Rust tests. To also run the stack walking tests, add the `stack_walking_tests` feature. -To see if the profiler is recognised by your PHP version as an extension, run -`/path/to/php -d extension=modules/datadog-profiling.so --ri datadog-profiling` -and check the output. +To inspect a standalone profiler, run +`/path/to/php -d extension=modules/datadog-profiling.so --ri datadog-profiling`. +For a combined build, load `modules/ddtrace.so` and inspect `--ri ddtrace`; it +registers only the `ddtrace` module and Zend extension identities. The following command will help you run the [PHPT tests](tests/phpt): @@ -57,6 +64,16 @@ The following command will help you run the [PHPT tests](tests/phpt): /path/to/php /path/to/run-tests.php -d extension=modules/datadog-profiling.so -n profiling/tests/phpt ``` +For profiler-focused testing of a combined artifact, disable the other runtime +products while leaving common configuration active: + +```sh +DD_TRACE_ENABLED=0 \ +DD_INSTRUMENTATION_TELEMETRY_ENABLED=0 \ +DD_REMOTE_CONFIG_ENABLED=0 \ +/path/to/php /path/to/run-tests.php -d extension=modules/ddtrace.so -n profiling/tests/phpt +``` + Be aware that the PHPT tests will fail with the debug version of the profiler, if you haven't already, build the release version through configure and make. Also the `run-tests.php` version has to match the PHP version used to run the @@ -80,6 +97,7 @@ behind a feature flag. #### Can't find the profiler library on macOS -The supported PHP build copies the loadable extension to -`modules/datadog-profiling.so`. Cargo's intermediate cdylib uses the native -`.dylib` suffix and is stored under `target-profiling//libdatadog_php.dylib`. +Only the phpize/configure/Make output is a supported loadable extension. A +standalone build produces `modules/datadog-profiling.so`; a combined build +produces `modules/ddtrace.so`. Do not load or copy Cargo's intermediate cdylib +from the target directory. diff --git a/profiling/build.rs b/profiling/build.rs index 2a04ae599e1..c445b0bdf5c 100644 --- a/profiling/build.rs +++ b/profiling/build.rs @@ -10,6 +10,10 @@ fn php_config() -> std::ffi::OsString { } pub fn build() { + // Configure resolves PHP_CONFIG to the selected toolchain's real path, so + // switching PHP builds invalidates Cargo's cached bindings and C probes. + println!("cargo:rerun-if-env-changed=PHP_CONFIG"); + let php_config_includes_output = Command::new(php_config()) .arg("--includes") .output() @@ -116,6 +120,9 @@ fn build_zend_php_ffis( ) { println!("cargo:rerun-if-changed=profiling/src/php_ffi.h"); println!("cargo:rerun-if-changed=profiling/src/php_ffi.c"); + println!("cargo:rerun-if-changed=profiling/src/configuration.c"); + println!("cargo:rerun-if-changed=profiling/configuration.h"); + println!("cargo:rerun-if-changed=ext/configuration_shared.h"); println!("cargo:rerun-if-changed=ext/handlers_api.c"); println!("cargo:rerun-if-changed=ext/handlers_api.h"); @@ -161,9 +168,16 @@ fn build_zend_php_ffis( #[cfg(not(feature = "stack_walking_tests"))] let stack_walking_tests = "0"; + let combined = env::var_os("CARGO_FEATURE_TRACER").is_some(); let mut build = cc::Build::new(); build - .files(files.into_iter().chain(zai_c_files)) + .files(files) + .files(if combined { &[][..] } else { &zai_c_files[..] }) + .files(if combined { + &[][..] + } else { + &["profiling/src/configuration.c"][..] + }) .define("CFG_POST_STARTUP_CB", post_startup_cb) .define("CFG_PRELOAD", preload) .define("CFG_FIBERS", fibers) @@ -172,6 +186,7 @@ fn build_zend_php_ffis( .define("CFG_STACK_WALKING_TESTS", stack_walking_tests) .define("CFG_TRIGGER_TIME_SAMPLE", trigger_time_sample) .define("CFG_ZEND_ERROR_OBSERVER", zend_error_observer) + .define("PROFILING", None) .includes([Path::new("ext")]) .includes( str::replace(php_config_includes, "-I", "") @@ -182,6 +197,9 @@ fn build_zend_php_ffis( ) .flag_if_supported("-std=gnu11") .flag_if_supported("-std=gnu17"); + if combined { + build.define("TRACER", None); + } #[cfg(feature = "test")] build.define("CFG_TEST", "1"); build.compile("php_ffi"); @@ -230,20 +248,19 @@ fn generate_bindings(php_config_includes: &str, fibers: bool, zend_error_observe .collect(), ); - let mut clang_args = if fibers { - vec!["-D", "CFG_FIBERS=1"] + let mut clang_args: Vec = if fibers { + vec!["-D".into(), "CFG_FIBERS=1".into()] } else { - vec!["-D", "CFG_FIBERS=0"] + vec!["-D".into(), "CFG_FIBERS=0".into()] }; if zend_error_observer { - clang_args.push("-D"); - clang_args.push("CFG_ZEND_ERROR_OBSERVER=1"); + clang_args.push("-D".into()); + clang_args.push("CFG_ZEND_ERROR_OBSERVER=1".into()); } else { - clang_args.push("-D"); - clang_args.push("CFG_ZEND_ERROR_OBSERVER=0"); + clang_args.push("-D".into()); + clang_args.push("CFG_ZEND_ERROR_OBSERVER=0".into()); } - let bindings = bindgen::Builder::default() .ctypes_prefix("libc") .clang_args(clang_args) @@ -502,12 +519,13 @@ int main() { /// `-undefined dynamic_lookup` for debug builds. /// /// To regenerate the symbol list after adding new PHP/C API calls: -/// 1. Edit the profile != release check to always be true e.g. if true { -/// 2. Build a release: `cargo build --no-default-features --features profiling --release` +/// 1. Temporarily make the release-like profile check below take the debug branch. +/// 2. Build the standalone profiler through its supported PHP build path: +/// `phpize && ./configure --disable-ddtrace-tracer --enable-ddtrace-profiling --disable-ddtrace-rust-debug && make` /// 3. Extract symbols: -/// `nm -u target/release/libdatadog_php.dylib | sort -u | +/// `nm -u modules/datadog-profiling.so | sort -u | /// grep -v '^$\|^ERROR\|dyld_stub' | sed 's/^/-Wl,-U,/' | tr '\n' ' '` -/// 4. Update the ALLOWED_UNDEFINED_SYMBOLS list below. +/// 4. Update the ALLOWED_UNDEFINED_SYMBOLS list below and restore the check. /// /// ## Why list the symbols manually? /// @@ -525,8 +543,10 @@ fn apple_linker_flags() { } let profile = env::var("PROFILE").unwrap_or_default(); - if profile != "release" { - // Debug builds: allow all undefined symbols. + // Matches both Cargo's `release` profile and our `profiler-release` profile. + let release_like = profile.ends_with("release"); + if !release_like { + // Debug and test builds: allow all undefined symbols. println!("cargo:rustc-cdylib-link-arg=-undefined"); println!("cargo:rustc-cdylib-link-arg=dynamic_lookup"); println!("cargo:rustc-link-arg=-undefined"); diff --git a/profiling/configuration.h b/profiling/configuration.h new file mode 100644 index 00000000000..bac1c693435 --- /dev/null +++ b/profiling/configuration.h @@ -0,0 +1,53 @@ +#ifndef DDTRACE_PROFILING_CONFIGURATION_H +#define DDTRACE_PROFILING_CONFIGURATION_H + +#include +#include
+#include +#include + +bool ddog_php_prof_config_parse_sampling_distance(zai_str value, zval *decoded_value, bool persistent); +bool ddog_php_prof_config_parse_log_level(zai_str value, zval *decoded_value, bool persistent); +bool ddog_php_prof_config_parse_enabled(zai_str value, zval *decoded_value, bool persistent); +void ddog_php_prof_config_display_enabled(zend_ini_entry *ini_entry, int type); +bool ddog_php_prof_config_parse_utf8_string(zai_str value, zval *decoded_value, bool persistent); + +#ifdef TRACER +#define DDTRACE_PROFILING_ENABLED_DEFAULT "0" +#else +#define DDTRACE_PROFILING_ENABLED_DEFAULT "1" +#endif + +#define DDTRACE_PROFILING_CONFIGURATION \ + CONFIG(CUSTOM(BOOL), DD_PROFILING_ENABLED, DDTRACE_PROFILING_ENABLED_DEFAULT, \ + .ini_change = zai_config_system_ini_change, \ + .parser = ddog_php_prof_config_parse_enabled, .displayer = ddog_php_prof_config_display_enabled) \ + CONFIG(BOOL, DD_PROFILING_EXPERIMENTAL_FEATURES_ENABLED, "0", .ini_change = zai_config_system_ini_change) \ + CONFIG(BOOL, DD_PROFILING_ENDPOINT_COLLECTION_ENABLED, "1", .ini_change = zai_config_system_ini_change) \ + CALIAS(BOOL, DD_PROFILING_EXPERIMENTAL_CPU_TIME_ENABLED, "1", \ + CALIASES("DD_PROFILING_EXPERIMENTAL_CPU_ENABLED"), .ini_change = zai_config_system_ini_change) \ + CALIAS(BOOL, DD_PROFILING_ALLOCATION_ENABLED, "1", CALIASES("DD_PROFILING_EXPERIMENTAL_ALLOCATION_ENABLED"), \ + .ini_change = zai_config_system_ini_change) \ + CONFIG(CUSTOM(INT), DD_PROFILING_ALLOCATION_SAMPLING_DISTANCE, "4194304", \ + .ini_change = zai_config_system_ini_change, .parser = ddog_php_prof_config_parse_sampling_distance) \ + CONFIG(BOOL, DD_PROFILING_EXPERIMENTAL_HEAP_LIVE_ENABLED, "0", .ini_change = zai_config_system_ini_change) \ + CALIAS(BOOL, DD_PROFILING_TIMELINE_ENABLED, "1", CALIASES("DD_PROFILING_EXPERIMENTAL_TIMELINE_ENABLED"), \ + .ini_change = zai_config_system_ini_change) \ + CALIAS(BOOL, DD_PROFILING_EXCEPTION_ENABLED, "1", CALIASES("DD_PROFILING_EXPERIMENTAL_EXCEPTION_ENABLED"), \ + .ini_change = zai_config_system_ini_change) \ + CONFIG(BOOL, DD_PROFILING_EXCEPTION_MESSAGE_ENABLED, "0", .ini_change = zai_config_system_ini_change) \ + CALIAS(CUSTOM(INT), DD_PROFILING_EXCEPTION_SAMPLING_DISTANCE, "100", \ + CALIASES("DD_PROFILING_EXPERIMENTAL_EXCEPTION_SAMPLING_DISTANCE"), \ + .ini_change = zai_config_system_ini_change, .parser = ddog_php_prof_config_parse_sampling_distance) \ + CONFIG(BOOL, DD_PROFILING_EXPERIMENTAL_IO_ENABLED, "0", .ini_change = zai_config_system_ini_change) \ + CONFIG(CUSTOM(INT), DD_PROFILING_LOG_LEVEL, "off", .ini_change = zai_config_system_ini_change, \ + .parser = ddog_php_prof_config_parse_log_level) \ + CONFIG(STRING, DD_PROFILING_OUTPUT_PPROF, "", .ini_change = zai_config_system_ini_change, \ + .parser = ddog_php_prof_config_parse_utf8_string) \ + CONFIG(BOOL, DD_PROFILING_WALLTIME_ENABLED, "1", .ini_change = zai_config_system_ini_change) + +#define DDTRACE_PROFILING_ALL_CONFIGURATIONS \ + DDTRACE_PROFILING_CONFIGURATION \ + DATADOG_SHARED_CONFIGURATION + +#endif /* DDTRACE_PROFILING_CONFIGURATION_H */ diff --git a/profiling/src/bindings/mod.rs b/profiling/src/bindings/mod.rs index 92cf258e325..3bd22ed7841 100644 --- a/profiling/src/bindings/mod.rs +++ b/profiling/src/bindings/mod.rs @@ -362,6 +362,13 @@ extern "C" { /// until mshutdown. pub(crate) fn ddog_php_prof_get_memoized_config(config_id: ConfigId) -> *mut zval; + /// Returns the current encoded common DD_TAGS value in combined mode. + pub(crate) fn ddog_php_prof_config_count() -> u16; + pub(crate) fn datadog_php_profiling_config_count_error(c_count: u16, rust_count: usize); + #[cfg(all(feature = "profiling", not(feature = "tracer")))] + pub(crate) fn ddog_php_prof_config_minit(module_number: libc::c_int) -> bool; + pub(crate) fn ddog_php_prof_get_common_tags() -> ZaiStr<'static>; + /// Returns true if the config value was explicitly set by the user (not /// just the compiled-in default), false otherwise. pub(crate) fn ddog_php_prof_config_is_set_by_user(config_id: ConfigId) -> bool; diff --git a/profiling/src/capi.rs b/profiling/src/capi.rs index cfaf987d72d..60ff0a21c24 100644 --- a/profiling/src/capi.rs +++ b/profiling/src/capi.rs @@ -32,7 +32,7 @@ impl From<&uuid::Uuid> for Uuid { /// Only call this from a PHP thread. #[no_mangle] pub extern "C" fn datadog_profiling_runtime_id() -> Uuid { - Uuid::from(runtime_id()) + Uuid(runtime_id()) } #[cfg(feature = "trigger_time_sample")] diff --git a/profiling/src/config.rs b/profiling/src/config.rs index 5cd583d5f45..4089e25cdc6 100644 --- a/profiling/src/config.rs +++ b/profiling/src/config.rs @@ -1,9 +1,7 @@ -use crate::profiling::bindings::zai_config_type::*; use crate::profiling::bindings::{ datadog_php_profiling_copy_string_view_into_zval, ddog_php_prof_config_is_set_by_user, - ddog_php_prof_get_memoized_config, zai_config_entry, zai_config_get_value, zai_config_minit, - zai_config_name, zai_config_system_ini_change, zend_ini_entry, zend_long, zend_string, - zend_write, zval, StringError, ZaiStr, IS_FALSE, IS_LONG, IS_TRUE, ZAI_CONFIG_NAME_BUFSIZ, + ddog_php_prof_get_memoized_config, zai_config_get_value, zend_ini_entry, zend_long, + zend_string, zend_write, zval, StringError, ZaiStr, IS_FALSE, IS_LONG, IS_TRUE, ZEND_INI_DISPLAY_ORIG, }; use crate::profiling::zend::zai_str_from_zstr; @@ -13,7 +11,7 @@ use core::mem::transmute; use core::ptr; use core::str::FromStr; pub use http::Uri; -use libc::{c_char, c_int}; +use libc::c_int; use log::{debug, error, warn, LevelFilter}; use std::borrow::Cow; use std::ffi::CString; @@ -324,71 +322,11 @@ fn detect_uri_from_config( AgentEndpoint::default() } -unsafe extern "C" fn env_to_ini_name(env_name: ZaiStr, ini_name: *mut zai_config_name) { - assert!(!ini_name.is_null()); - let ini_name = &mut *ini_name; - - let name: &str = env_name.into_utf8().unwrap(); - - assert!(name.starts_with("DD_")); - - // Env var name needs to fit. - let projection = "datadog.".len() - "DD_".len(); - let null_byte = 1usize; - assert!(name.len() + projection + null_byte <= (ZAI_CONFIG_NAME_BUFSIZ as usize)); - - let (dest_prefix, src_prefix) = if name.starts_with("DD_TRACE_") { - ("datadog.trace.", "DD_TRACE_") - } else if name.starts_with("DD_PROFILING_") { - ("datadog.profiling.", "DD_PROFILING_") - } else if name.starts_with("DD_APPSEC_") { - ("datadog.appsec.", "DD_APPSEC_") - } else { - ("datadog.", "DD_") - }; - - { - /* Safety: - * 1. The src buffer's length is coming from a safe rust slice - * 2. The length of all these prefixes is less than the size of the - * dst buffer (currently 60 bytes); - * 3. Both pointers are dealing with bytes, and so they are aligned. - * 4. These pointers do not overlap, the src string is a constant - * and the destination is an in-place array in a struct. - */ - ptr::copy_nonoverlapping( - dest_prefix.as_ptr() as *const c_char, - ini_name.ptr.as_mut_ptr(), - dest_prefix.len(), - ); - - // Miri doesn't like uninitialized bytes - let buffer = &mut ini_name.ptr[dest_prefix.len()..]; - buffer.fill(c_char::default()); - } - - // Copy in the parts after the prefix, lowercasing as we go. For example, - // with DD_PROFILING_ENABLED copy `ENABLED` as `enabled` into the - // destination slice. - let dest_suffix = &mut ini_name.ptr[dest_prefix.len()..]; - let src_suffix = &name[src_prefix.len()..]; - for (dest, src) in dest_suffix.iter_mut().zip(src_suffix.bytes()) { - // Casting between same-sized integers is a no-op. - *dest = src.to_ascii_lowercase() as c_char; - } - - // Add the null terminator. - dest_suffix[src_suffix.len()] = b'\0' as c_char; - - // Store the length without the null. - ini_name.len = dest_prefix.len() + src_suffix.len(); -} - /// # Safety /// This function must only be called after config has been initialized in /// rinit, and before it is uninitialized in rshutdown. pub(crate) unsafe fn get_value(id: ConfigId) -> &'static mut zval { - let value = zai_config_get_value(transmute::(id)); + let value = zai_config_get_value(id as u16); // Panic: the implementation makes this guarantee. assert!(!value.is_null()); &mut *value @@ -401,78 +339,27 @@ unsafe fn get_system_value(id: ConfigId) -> &'static mut zval { &mut *value } -#[repr(u16)] -#[derive(Clone, Copy)] -pub(crate) enum ConfigId { - ProfilingEnabled = 0, - ProfilingExperimentalFeaturesEnabled, - ProfilingEndpointCollectionEnabled, - ProfilingExperimentalCpuTimeEnabled, - ProfilingAllocationEnabled, - ProfilingAllocationSamplingDistance, - ProfilingExperimentalHeapLiveEnabled, - ProfilingTimelineEnabled, - ProfilingExceptionEnabled, - ProfilingExceptionMessageEnabled, - ProfilingExceptionSamplingDistance, - ProfilingExperimentalIOEnabled, - ProfilingLogLevel, - ProfilingOutputPprof, - ProfilingWallTimeEnabled, - - // todo: do these need to be kept in sync with the tracer? - AgentHost, - Env, - Service, - Tags, - TraceAgentPort, - TraceAgentUrl, - Version, - GitCommitSha, - GitRepositoryUrl, -} - -use ConfigId::*; - -impl ConfigId { - const fn env_var_name(&self) -> ZaiStr<'_> { - let bytes: &'static [u8] = match self { - ProfilingEnabled => b"DD_PROFILING_ENABLED\0", - ProfilingExperimentalFeaturesEnabled => b"DD_PROFILING_EXPERIMENTAL_FEATURES_ENABLED\0", - ProfilingEndpointCollectionEnabled => b"DD_PROFILING_ENDPOINT_COLLECTION_ENABLED\0", - ProfilingExperimentalCpuTimeEnabled => b"DD_PROFILING_EXPERIMENTAL_CPU_TIME_ENABLED\0", - ProfilingAllocationEnabled => b"DD_PROFILING_ALLOCATION_ENABLED\0", - ProfilingAllocationSamplingDistance => b"DD_PROFILING_ALLOCATION_SAMPLING_DISTANCE\0", - ProfilingExperimentalHeapLiveEnabled => { - b"DD_PROFILING_EXPERIMENTAL_HEAP_LIVE_ENABLED\0" - } - ProfilingTimelineEnabled => b"DD_PROFILING_TIMELINE_ENABLED\0", - ProfilingExceptionEnabled => b"DD_PROFILING_EXCEPTION_ENABLED\0", - ProfilingExceptionMessageEnabled => b"DD_PROFILING_EXCEPTION_MESSAGE_ENABLED\0", - ProfilingExceptionSamplingDistance => b"DD_PROFILING_EXCEPTION_SAMPLING_DISTANCE\0", - ProfilingExperimentalIOEnabled => b"DD_PROFILING_EXPERIMENTAL_IO_ENABLED\0", - ProfilingLogLevel => b"DD_PROFILING_LOG_LEVEL\0", - - // Note: this group is meant only for debugging and testing. Please - // don't advertise this group of settings in the docs. - ProfilingOutputPprof => b"DD_PROFILING_OUTPUT_PPROF\0", - ProfilingWallTimeEnabled => b"DD_PROFILING_WALLTIME_ENABLED\0", - - AgentHost => b"DD_AGENT_HOST\0", - Env => b"DD_ENV\0", - Service => b"DD_SERVICE\0", - Tags => b"DD_TAGS\0", - TraceAgentPort => b"DD_TRACE_AGENT_PORT\0", - TraceAgentUrl => b"DD_TRACE_AGENT_URL\0", - Version => b"DD_VERSION\0", - GitCommitSha => b"DD_GIT_COMMIT_SHA\0", - GitRepositoryUrl => b"DD_GIT_REPOSITORY_URL\0", - }; - - // Safety: all these byte strings are [CStr::from_bytes_with_nul_unchecked] compatible. - unsafe { ZaiStr::literal(bytes) } - } -} +pub(crate) use crate::config::ConfigId; +use crate::config::ConfigId::{ + DD_AGENT_HOST as AgentHost, DD_ENV as Env, DD_GIT_COMMIT_SHA as GitCommitSha, + DD_GIT_REPOSITORY_URL as GitRepositoryUrl, + DD_PROFILING_ALLOCATION_ENABLED as ProfilingAllocationEnabled, + DD_PROFILING_ALLOCATION_SAMPLING_DISTANCE as ProfilingAllocationSamplingDistance, + DD_PROFILING_ENABLED as ProfilingEnabled, + DD_PROFILING_ENDPOINT_COLLECTION_ENABLED as ProfilingEndpointCollectionEnabled, + DD_PROFILING_EXCEPTION_ENABLED as ProfilingExceptionEnabled, + DD_PROFILING_EXCEPTION_MESSAGE_ENABLED as ProfilingExceptionMessageEnabled, + DD_PROFILING_EXCEPTION_SAMPLING_DISTANCE as ProfilingExceptionSamplingDistance, + DD_PROFILING_EXPERIMENTAL_CPU_TIME_ENABLED as ProfilingExperimentalCpuTimeEnabled, + DD_PROFILING_EXPERIMENTAL_FEATURES_ENABLED as ProfilingExperimentalFeaturesEnabled, + DD_PROFILING_EXPERIMENTAL_HEAP_LIVE_ENABLED as ProfilingExperimentalHeapLiveEnabled, + DD_PROFILING_EXPERIMENTAL_IO_ENABLED as ProfilingExperimentalIOEnabled, + DD_PROFILING_LOG_LEVEL as ProfilingLogLevel, DD_PROFILING_OUTPUT_PPROF as ProfilingOutputPprof, + DD_PROFILING_TIMELINE_ENABLED as ProfilingTimelineEnabled, + DD_PROFILING_WALLTIME_ENABLED as ProfilingWallTimeEnabled, DD_SERVICE as Service, + DD_TRACE_AGENT_PORT as TraceAgentPort, DD_TRACE_AGENT_URL as TraceAgentUrl, + DD_VERSION as Version, +}; /// Keep these in sync with the INI defaults. static DEFAULT_SYSTEM_SETTINGS: SystemSettings = SystemSettings { @@ -499,7 +386,7 @@ static DEFAULT_SYSTEM_SETTINGS: SystemSettings = SystemSettings { /// # Safety /// This function must only be called after config has been initialized in /// rinit, and before it is uninitialized in mshutdown. -unsafe fn profiling_enabled() -> bool { +pub(crate) unsafe fn profiling_enabled() -> bool { get_system_bool(ProfilingEnabled, DEFAULT_SYSTEM_SETTINGS.profiling_enabled) } @@ -695,7 +582,7 @@ unsafe fn get_system_str(config_id: ConfigId) -> Option> { } } Err(err) => { - let env_var = config_id.env_var_name().into_string_lossy(); + let env_var = format!("{config_id:?}"); match err { StringError::Null => panic!("When fetching {env_var}, found a null string pointer inside a zval of type string"), StringError::Type(type_code) => panic!("When fetching {env_var}, expected type IS_STRING, found {type_code}"), @@ -776,7 +663,11 @@ pub(crate) unsafe fn git_repository_url() -> Option { /// This function must only be called after config has been initialized in /// rinit, and before it is uninitialized in mshutdown. pub(crate) unsafe fn tags() -> Option { - get_str(Tags) + let tags = unsafe { bindings::ddog_php_prof_get_common_tags() }; + tags.into_utf8() + .ok() + .filter(|tags| !tags.is_empty()) + .map(str::to_owned) } /// # Safety @@ -819,7 +710,8 @@ unsafe fn profiling_log_level() -> LevelFilter { } /// Parses the sampling distance and makes sure it is ℤ+ (positive integer > 0) -unsafe extern "C" fn parse_sampling_distance_filter( +#[no_mangle] +pub unsafe extern "C" fn ddog_php_prof_config_parse_sampling_distance( value: ZaiStr, decoded_value: *mut zval, _persistent: bool, @@ -848,7 +740,8 @@ unsafe extern "C" fn parse_sampling_distance_filter( } } -unsafe extern "C" fn parse_level_filter( +#[no_mangle] +pub unsafe extern "C" fn ddog_php_prof_config_parse_log_level( value: ZaiStr, decoded_value: *mut zval, _persistent: bool, @@ -881,8 +774,9 @@ unsafe extern "C" fn parse_level_filter( } /// This function is used to parse the profiling enabled config value. -/// It behaves similarlry to the "zai_config_decode_bool" but also accepts "auto" as true. -unsafe extern "C" fn parse_profiling_enabled( +/// It behaves similarly to `zai_config_decode_bool` but also accepts "auto" as true. +#[no_mangle] +pub unsafe extern "C" fn ddog_php_prof_config_parse_enabled( value: ZaiStr, decoded_value: *mut zval, _persistent: bool, @@ -911,7 +805,11 @@ unsafe extern "C" fn parse_profiling_enabled( } /// Display the profiling enabled config value -unsafe extern "C" fn display_profiling_enabled(ini_entry: *mut zend_ini_entry, type_: c_int) { +#[no_mangle] +pub unsafe extern "C" fn ddog_php_prof_config_display_enabled( + ini_entry: *mut zend_ini_entry, + type_: c_int, +) { // PHP 8.6 changed this field from u8 to bool, so the cast is redundant only on older PHP. #[allow(clippy::unnecessary_cast)] let tmp_value: *mut zend_string = @@ -951,7 +849,8 @@ unsafe extern "C" fn display_profiling_enabled(ini_entry: *mut zend_ini_entry, t } } -unsafe extern "C" fn parse_utf8_string( +#[no_mangle] +pub unsafe extern "C" fn ddog_php_prof_config_parse_utf8_string( value: ZaiStr, decoded_value: *mut zval, persistent: bool, @@ -973,352 +872,19 @@ unsafe extern "C" fn parse_utf8_string( } } -pub(crate) fn minit(module_number: libc::c_int) { +pub(crate) fn minit(_module_number: libc::c_int) { unsafe { - const CPU_TIME_ALIASES: &[ZaiStr] = - unsafe { &[ZaiStr::literal(b"DD_PROFILING_EXPERIMENTAL_CPU_ENABLED\0")] }; - - const ALLOCATION_ALIASES: &[ZaiStr] = unsafe { - &[ZaiStr::literal( - b"DD_PROFILING_EXPERIMENTAL_ALLOCATION_ENABLED\0", - )] - }; - - const EXCEPTION_ALIASES: &[ZaiStr] = unsafe { - &[ZaiStr::literal( - b"DD_PROFILING_EXPERIMENTAL_EXCEPTION_ENABLED\0", - )] - }; - - const EXCEPTION_SAMPLING_DISTANCE_ALIASES: &[ZaiStr] = unsafe { - &[ZaiStr::literal( - b"DD_PROFILING_EXPERIMENTAL_EXCEPTION_SAMPLING_DISTANCE\0", - )] - }; - - const TIMELINE_ALIASES: &[ZaiStr] = unsafe { - &[ZaiStr::literal( - b"DD_PROFILING_EXPERIMENTAL_TIMELINE_ENABLED\0", - )] - }; - - // Note that function pointers cannot appear in const functions, so we - // can't extract each entry into a helper function. - static mut ENTRIES: &mut [zai_config_entry] = unsafe { - &mut [ - zai_config_entry { - id: transmute::(ProfilingEnabled), - name: ProfilingEnabled.env_var_name(), - type_: ZAI_CONFIG_TYPE_CUSTOM, - default_encoded_value: ZaiStr::literal(b"1\0"), - aliases: ptr::null_mut(), - aliases_count: 0, - ini_change: Some(zai_config_system_ini_change), - parser: Some(parse_profiling_enabled), - displayer: Some(display_profiling_enabled), - env_config_fallback: None, - }, - zai_config_entry { - id: transmute::(ProfilingExperimentalFeaturesEnabled), - name: ProfilingExperimentalFeaturesEnabled.env_var_name(), - type_: ZAI_CONFIG_TYPE_BOOL, - default_encoded_value: ZaiStr::literal(b"0\0"), - aliases: ptr::null_mut(), - aliases_count: 0, - ini_change: Some(zai_config_system_ini_change), - parser: None, - displayer: None, - env_config_fallback: None, - }, - zai_config_entry { - id: transmute::(ProfilingEndpointCollectionEnabled), - name: ProfilingEndpointCollectionEnabled.env_var_name(), - type_: ZAI_CONFIG_TYPE_BOOL, - default_encoded_value: ZaiStr::literal(b"1\0"), - aliases: ptr::null_mut(), - aliases_count: 0, - ini_change: Some(zai_config_system_ini_change), - parser: None, - displayer: None, - env_config_fallback: None, - }, - zai_config_entry { - id: transmute::(ProfilingExperimentalCpuTimeEnabled), - name: ProfilingExperimentalCpuTimeEnabled.env_var_name(), - type_: ZAI_CONFIG_TYPE_BOOL, - default_encoded_value: ZaiStr::literal(b"1\0"), - aliases: CPU_TIME_ALIASES.as_ptr(), - aliases_count: CPU_TIME_ALIASES.len() as u8, - ini_change: Some(zai_config_system_ini_change), - parser: None, - displayer: None, - env_config_fallback: None, - }, - zai_config_entry { - id: transmute::(ProfilingAllocationEnabled), - name: ProfilingAllocationEnabled.env_var_name(), - type_: ZAI_CONFIG_TYPE_BOOL, - default_encoded_value: ZaiStr::literal(b"1\0"), - aliases: ALLOCATION_ALIASES.as_ptr(), - aliases_count: ALLOCATION_ALIASES.len() as u8, - ini_change: Some(zai_config_system_ini_change), - parser: None, - displayer: None, - env_config_fallback: None, - }, - zai_config_entry { - id: transmute::(ProfilingAllocationSamplingDistance), - name: ProfilingAllocationSamplingDistance.env_var_name(), - type_: ZAI_CONFIG_TYPE_CUSTOM, - default_encoded_value: ZaiStr::literal(b"4194304\0"), // crate::profiling::allocation::DEFAULT_ALLOCATION_SAMPLING_INTERVAL - aliases: ptr::null_mut(), - aliases_count: 0, - ini_change: Some(zai_config_system_ini_change), - parser: Some(parse_sampling_distance_filter), - displayer: None, - env_config_fallback: None, - }, - zai_config_entry { - id: transmute::(ProfilingExperimentalHeapLiveEnabled), - name: ProfilingExperimentalHeapLiveEnabled.env_var_name(), - type_: ZAI_CONFIG_TYPE_BOOL, - default_encoded_value: ZaiStr::literal(b"0\0"), - aliases: ptr::null_mut(), - aliases_count: 0, - ini_change: Some(zai_config_system_ini_change), - parser: None, - displayer: None, - env_config_fallback: None, - }, - zai_config_entry { - id: transmute::(ProfilingTimelineEnabled), - name: ProfilingTimelineEnabled.env_var_name(), - type_: ZAI_CONFIG_TYPE_BOOL, - default_encoded_value: ZaiStr::literal(b"1\0"), - aliases: TIMELINE_ALIASES.as_ptr(), - aliases_count: TIMELINE_ALIASES.len() as u8, - ini_change: Some(zai_config_system_ini_change), - parser: None, - displayer: None, - env_config_fallback: None, - }, - zai_config_entry { - id: transmute::(ProfilingExceptionEnabled), - name: ProfilingExceptionEnabled.env_var_name(), - type_: ZAI_CONFIG_TYPE_BOOL, - default_encoded_value: ZaiStr::literal(b"1\0"), - aliases: EXCEPTION_ALIASES.as_ptr(), - aliases_count: EXCEPTION_ALIASES.len() as u8, - ini_change: Some(zai_config_system_ini_change), - parser: None, - displayer: None, - env_config_fallback: None, - }, - zai_config_entry { - id: transmute::(ProfilingExceptionMessageEnabled), - name: ProfilingExceptionMessageEnabled.env_var_name(), - type_: ZAI_CONFIG_TYPE_BOOL, - default_encoded_value: ZaiStr::literal(b"0\0"), - aliases: ptr::null_mut(), - aliases_count: 0, - ini_change: Some(zai_config_system_ini_change), - parser: None, - displayer: None, - env_config_fallback: None, - }, - zai_config_entry { - id: transmute::(ProfilingExceptionSamplingDistance), - name: ProfilingExceptionSamplingDistance.env_var_name(), - type_: ZAI_CONFIG_TYPE_CUSTOM, - default_encoded_value: ZaiStr::literal(b"100\0"), - aliases: EXCEPTION_SAMPLING_DISTANCE_ALIASES.as_ptr(), - aliases_count: EXCEPTION_SAMPLING_DISTANCE_ALIASES.len() as u8, - ini_change: Some(zai_config_system_ini_change), - parser: Some(parse_sampling_distance_filter), - displayer: None, - env_config_fallback: None, - }, - zai_config_entry { - id: transmute::(ProfilingExperimentalIOEnabled), - name: ProfilingExperimentalIOEnabled.env_var_name(), - type_: ZAI_CONFIG_TYPE_BOOL, - default_encoded_value: ZaiStr::literal(b"0\0"), - aliases: ptr::null_mut(), - aliases_count: 0, - ini_change: Some(zai_config_system_ini_change), - parser: None, - displayer: None, - env_config_fallback: None, - }, - zai_config_entry { - id: transmute::(ProfilingLogLevel), - name: ProfilingLogLevel.env_var_name(), - type_: ZAI_CONFIG_TYPE_CUSTOM, // store it as an int - default_encoded_value: ZaiStr::literal(b"off\0"), - aliases: ptr::null_mut(), - aliases_count: 0, - ini_change: Some(zai_config_system_ini_change), - parser: Some(parse_level_filter), - displayer: None, - env_config_fallback: None, - }, - zai_config_entry { - id: transmute::(ProfilingOutputPprof), - name: ProfilingOutputPprof.env_var_name(), - type_: ZAI_CONFIG_TYPE_STRING, - default_encoded_value: ZaiStr::new(), - aliases: ptr::null_mut(), - aliases_count: 0, - ini_change: Some(zai_config_system_ini_change), - parser: Some(parse_utf8_string), - displayer: None, - env_config_fallback: None, - }, - // At the moment, wall-time cannot be fully disabled. This only - // controls automatic collection (manual collection is still - // possible). - zai_config_entry { - id: transmute::(ProfilingWallTimeEnabled), - name: ProfilingWallTimeEnabled.env_var_name(), - type_: ZAI_CONFIG_TYPE_BOOL, - default_encoded_value: ZaiStr::literal(b"1\0"), - aliases: ptr::null_mut(), - aliases_count: 0, - ini_change: Some(zai_config_system_ini_change), - parser: None, - displayer: None, - env_config_fallback: None, - }, - zai_config_entry { - id: transmute::(AgentHost), - name: AgentHost.env_var_name(), - type_: ZAI_CONFIG_TYPE_STRING, - default_encoded_value: ZaiStr::new(), - aliases: ptr::null_mut(), - aliases_count: 0, - ini_change: Some(zai_config_system_ini_change), - parser: Some(parse_utf8_string), - displayer: None, - env_config_fallback: None, - }, - zai_config_entry { - id: transmute::(Env), - name: Env.env_var_name(), - type_: ZAI_CONFIG_TYPE_STRING, - default_encoded_value: ZaiStr::new(), - aliases: ptr::null_mut(), - aliases_count: 0, - ini_change: None, - parser: Some(parse_utf8_string), - displayer: None, - env_config_fallback: None, - }, - zai_config_entry { - id: transmute::(Service), - name: Service.env_var_name(), - type_: ZAI_CONFIG_TYPE_STRING, - default_encoded_value: ZaiStr::new(), - aliases: ptr::null_mut(), - aliases_count: 0, - ini_change: None, - parser: Some(parse_utf8_string), - displayer: None, - env_config_fallback: None, - }, - zai_config_entry { - id: transmute::(Tags), - name: Tags.env_var_name(), - // Using a string here means we're going to parse the - // string into tags over and over, but since it needs to - // be a valid zval for destruction, we can't just use a - // Box::leak of Vec or something. - type_: ZAI_CONFIG_TYPE_STRING, - default_encoded_value: ZaiStr::new(), - aliases: ptr::null_mut(), - aliases_count: 0, - ini_change: None, - parser: None, - displayer: None, - env_config_fallback: None, - }, - zai_config_entry { - id: transmute::(TraceAgentPort), - name: TraceAgentPort.env_var_name(), - type_: ZAI_CONFIG_TYPE_INT, - default_encoded_value: ZaiStr::literal(b"0\0"), - aliases: ptr::null_mut(), - aliases_count: 0, - ini_change: Some(zai_config_system_ini_change), - parser: Some(parse_utf8_string), - displayer: None, - env_config_fallback: None, - }, - zai_config_entry { - id: transmute::(TraceAgentUrl), - name: TraceAgentUrl.env_var_name(), - type_: ZAI_CONFIG_TYPE_STRING, // TYPE? - default_encoded_value: ZaiStr::new(), - aliases: ptr::null_mut(), - aliases_count: 0, - ini_change: Some(zai_config_system_ini_change), - parser: Some(parse_utf8_string), - displayer: None, - env_config_fallback: None, - }, - zai_config_entry { - id: transmute::(Version), - name: Version.env_var_name(), - type_: ZAI_CONFIG_TYPE_STRING, - default_encoded_value: ZaiStr::new(), - aliases: ptr::null_mut(), - aliases_count: 0, - ini_change: None, - parser: Some(parse_utf8_string), - displayer: None, - env_config_fallback: None, - }, - zai_config_entry { - id: transmute::(GitCommitSha), - name: GitCommitSha.env_var_name(), - type_: ZAI_CONFIG_TYPE_STRING, - default_encoded_value: ZaiStr::new(), - aliases: ptr::null_mut(), - aliases_count: 0, - ini_change: None, - parser: Some(parse_utf8_string), - displayer: None, - env_config_fallback: None, - }, - zai_config_entry { - id: transmute::(GitRepositoryUrl), - name: GitRepositoryUrl.env_var_name(), - type_: ZAI_CONFIG_TYPE_STRING, - default_encoded_value: ZaiStr::new(), - aliases: ptr::null_mut(), - aliases_count: 0, - ini_change: None, - parser: Some(parse_utf8_string), - displayer: None, - env_config_fallback: None, - }, - ] - }; - - let entries = &mut *ptr::addr_of_mut!(ENTRIES); - let tmp = zai_config_minit( - entries.as_mut_ptr(), - entries.len(), - Some(env_to_ini_name), - module_number, - ); - assert!(tmp); // It's literally return true in the source. + #[cfg(all(feature = "profiling", not(feature = "tracer")))] + { + assert!(bindings::ddog_php_prof_config_minit(_module_number)); - // We set this so that we can access config for system INI settings during - // minit, for example for allocation_sampling_distance. - let in_request = false; - bindings::zai_config_first_time_rinit(in_request); + // Make system INI settings available during MINIT, for example + // for allocation_sampling_distance. + bindings::zai_config_first_time_rinit(false); + } - // SAFETY: just initialized zai config. + // SAFETY: common configuration has already initialized the aggregate + // table in combined mode; standalone initialized it above. let mut system_settings = SystemSettings::new(); // Initialize logging before allocation's rinit, as it logs. @@ -1358,98 +924,6 @@ pub(crate) unsafe fn on_fork_in_child() { #[cfg(test)] mod tests { use super::*; - use core::mem::MaybeUninit; - use libc::memcmp; - - #[test] - fn test_env_to_ini_name() { - let cases: &[(&[u8], &str)] = &[ - (b"DD_SERVICE\0", "datadog.service"), - (b"DD_ENV\0", "datadog.env"), - (b"DD_VERSION\0", "datadog.version"), - (b"DD_GIT_COMMIT_SHA\0", "datadog.git_commit_sha"), - (b"DD_GIT_REPOSITORY_URL\0", "datadog.git_repository_url"), - (b"DD_TRACE_AGENT_URL\0", "datadog.trace.agent_url"), - (b"DD_TRACE_AGENT_PORT\0", "datadog.trace.agent_port"), - (b"DD_AGENT_HOST\0", "datadog.agent_host"), - (b"DD_PROFILING_ENABLED\0", "datadog.profiling.enabled"), - ( - b"DD_PROFILING_EXPERIMENTAL_FEATURES_ENABLED\0", - "datadog.profiling.experimental_features_enabled", - ), - ( - b"DD_PROFILING_ENDPOINT_COLLECTION_ENABLED\0", - "datadog.profiling.endpoint_collection_enabled", - ), - ( - b"DD_PROFILING_EXPERIMENTAL_CPU_TIME_ENABLED\0", - "datadog.profiling.experimental_cpu_time_enabled", - ), - ( - b"DD_PROFILING_EXPERIMENTAL_ALLOCATION_ENABLED\0", - "datadog.profiling.experimental_allocation_enabled", - ), - ( - b"DD_PROFILING_ALLOCATION_ENABLED\0", - "datadog.profiling.allocation_enabled", - ), - ( - b"DD_PROFILING_EXPERIMENTAL_EXCEPTION_SAMPLING_DISTANCE\0", - "datadog.profiling.experimental_exception_sampling_distance", - ), - ( - b"DD_PROFILING_EXPERIMENTAL_TIMELINE_ENABLED\0", - "datadog.profiling.experimental_timeline_enabled", - ), - ( - b"DD_PROFILING_TIMELINE_ENABLED\0", - "datadog.profiling.timeline_enabled", - ), - ( - b"DD_PROFILING_EXPERIMENTAL_IO_ENABLED\0", - "datadog.profiling.experimental_io_enabled", - ), - (b"DD_PROFILING_LOG_LEVEL\0", "datadog.profiling.log_level"), - ( - b"DD_PROFILING_OUTPUT_PPROF\0", - "datadog.profiling.output_pprof", - ), - ( - b"DD_PROFILING_WALL_TIME_ENABLED\0", - "datadog.profiling.wall_time_enabled", - ), - ]; - - for (env_name, expected_ini_name) in cases { - unsafe { - let env = ZaiStr::literal(env_name); - let mut ini = MaybeUninit::uninit(); - env_to_ini_name(env, ini.as_mut_ptr()); - let ini = ini.assume_init(); - - // Check that .len matches. - assert_eq!( - expected_ini_name.len(), - { ini.len }, - "Env: {}, expected ini: {}", - std::str::from_utf8(env_name).unwrap(), - expected_ini_name - ); - - // Check that the bytes match. - let cmp = memcmp( - expected_ini_name.as_ptr().cast(), - ini.ptr.as_ptr().cast(), - expected_ini_name.len(), - ); - assert_eq!(0, cmp); - - // Check that it is null terminated. - assert_eq!(ini.ptr[ini.len] as u8, b'\0'); - } - } - } - #[test] fn detect_uri_from_config_works() { // expected diff --git a/profiling/src/configuration.c b/profiling/src/configuration.c new file mode 100644 index 00000000000..e2269c324bd --- /dev/null +++ b/profiling/src/configuration.c @@ -0,0 +1,80 @@ +#include + +#include +#include + +#define APPLY_0(...) +#define APPLY_1(macro, arg, ...) macro(arg) +#define APPLY_2(macro, arg, ...) macro(arg) APPLY_1(macro, __VA_ARGS__) +#define APPLY_3(macro, arg, ...) macro(arg) APPLY_2(macro, __VA_ARGS__) +#define APPLY_4(macro, arg, ...) macro(arg) APPLY_3(macro, __VA_ARGS__) +#define APPLY_NAME_EXPAND(count) APPLY_##count +#define APPLY_NAME(count) APPLY_NAME_EXPAND(count) +#define APPLY_COUNT(_0, _1, _2, _3, _4, N, ...) N +#define APPLY_N(macro, ...) APPLY_NAME(APPLY_COUNT(0, ##__VA_ARGS__, 4, 3, 2, 1, 0))(macro, ##__VA_ARGS__) + +#define CALIAS CONFIG +#define CONFIG(type, name, ...) DDTRACE_PROFILING_CONFIG_##name, +typedef enum { + DDTRACE_PROFILING_ALL_CONFIGURATIONS + DDTRACE_PROFILING_CONFIG_COUNT +} ddprof_config_id; +#undef CONFIG +#undef CALIAS + +#ifndef _WIN32 +#define CALIAS CONFIG +#define CONFIG(...) 1, +#define DDTRACE_PROFILING_NUMBER_OF_CONFIGURATIONS sizeof((uint8_t[]){DDTRACE_PROFILING_ALL_CONFIGURATIONS}) +_Static_assert(DDTRACE_PROFILING_NUMBER_OF_CONFIGURATIONS <= ZAI_CONFIG_ENTRIES_COUNT_MAX, + "There are more profiler config entries than ZAI_CONFIG_ENTRIES_COUNT_MAX"); +#undef CONFIG +#undef CALIAS +#endif + +#define CALIAS_EXPAND(name) {.ptr = name, .len = sizeof(name) - 1}, +#define CUSTOM(...) CUSTOM +#define CONFIG(type, name, ...) ZAI_CONFIG_ENTRY(DDTRACE_PROFILING_CONFIG_##name, name, type, __VA_ARGS__), +#define CALIASES(...) ((zai_str[]){APPLY_N(CALIAS_EXPAND, ##__VA_ARGS__)}) +#define CALIAS(type, name, ...) ZAI_CONFIG_ALIASED_ENTRY(DDTRACE_PROFILING_CONFIG_##name, name, type, __VA_ARGS__), +static zai_config_entry ddprof_config_entries[] = {DDTRACE_PROFILING_ALL_CONFIGURATIONS}; +#undef CALIAS +#undef CALIASES +#undef CONFIG +#undef CUSTOM + +static char ddprof_tolower_ascii(char c) { return c >= 'A' && c <= 'Z' ? c - ('A' - 'a') : c; } + +static void ddprof_copy_tolower(char *restrict dst, const char *restrict src) { + while (*src) { + *(dst++) = ddprof_tolower_ascii(*(src++)); + } +} + +static void ddprof_env_to_ini_name(const zai_str env_name, zai_config_name *ini_name) { + enum { DD_TO_DATADOG_INC = 5 }; /* "DD" expands to "datadog". */ + + if (env_name.len + DD_TO_DATADOG_INC >= ZAI_CONFIG_NAME_BUFSIZ) { + assert(false && "Expanded profiler env name is larger than the INI name buffer"); + return; + } + + assert(env_name.ptr == strstr(env_name.ptr, "DD_") && "Profiler env name is missing its DD_ prefix"); + ddprof_copy_tolower(ini_name->ptr + DD_TO_DATADOG_INC, env_name.ptr); + memcpy(ini_name->ptr, "datadog.", sizeof("datadog.") - 1); + ini_name->len = env_name.len + DD_TO_DATADOG_INC; + + if (env_name.ptr == strstr(env_name.ptr, "DD_TRACE_")) { + ini_name->ptr[sizeof("datadog.trace") - 1] = '.'; + } else if (env_name.ptr == strstr(env_name.ptr, "DD_PROFILING_")) { + ini_name->ptr[sizeof("datadog.profiling") - 1] = '.'; + } + + ini_name->ptr[ini_name->len] = '\0'; +} + +bool ddog_php_prof_config_minit(int module_number) { + return zai_config_minit(ddprof_config_entries, DDTRACE_PROFILING_CONFIG_COUNT, ddprof_env_to_ini_name, module_number); +} + +uint16_t ddog_php_prof_config_count(void) { return DDTRACE_PROFILING_CONFIG_COUNT; } diff --git a/profiling/src/lib.rs b/profiling/src/lib.rs index 62157c7e1ea..e6218ef51e1 100644 --- a/profiling/src/lib.rs +++ b/profiling/src/lib.rs @@ -38,6 +38,7 @@ use bindings::{ use clocks::*; use core::ffi::{c_char, c_int, CStr}; use core::ptr; +#[cfg(all(feature = "profiling", not(feature = "tracer")))] use libdd_common::cstr; use log::{debug, error, info, trace, warn}; use profile_tags::{ProfileTagSegment, UnifiedServiceTagSegment}; @@ -47,7 +48,9 @@ use std::borrow::Cow; use std::cell::{BorrowError, BorrowMutError, RefCell}; use std::ops::{Deref, DerefMut}; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; -use std::sync::{Arc, LazyLock, Once, OnceLock}; +#[cfg(not(all(feature = "profiling", feature = "tracer")))] +use std::sync::OnceLock; +use std::sync::{Arc, LazyLock, Once}; use std::thread::{AccessError, LocalKey}; use std::time::{Duration, Instant}; use uuid::Uuid; @@ -55,6 +58,8 @@ use uuid::Uuid; /// Name of the profiling module and zend_extension. Must not contain any /// interior null bytes and must be null terminated. static PROFILER_NAME: &CStr = c"datadog-profiling"; +#[cfg(all(feature = "profiling", not(feature = "tracer")))] +static DDTRACE_NAME: &CStr = c"ddtrace"; // SAFETY: PROFILER_NAME is a valid utf8 string. static PROFILER_NAME_STR: &str = match PROFILER_NAME.to_str() { @@ -163,15 +168,16 @@ static SAPI: LazyLock = LazyLock::new(|| { /// Additionally, the tracer is going to ask for this in its ACTIVATE handler, /// so whatever it is replaced with needs to also follow the /// initialize-on-first-use pattern. +#[cfg(not(all(feature = "profiling", feature = "tracer")))] static RUNTIME_ID: OnceLock = OnceLock::new(); -/// Module dependencies for the profiler extension. -static MODULE_DEPS: [zend::ModuleDep; 9] = [ +/// Module dependencies for the standalone profiler extension. +#[cfg(all(feature = "profiling", not(feature = "tracer")))] +static MODULE_DEPS: [zend::ModuleDep; 8] = [ zend::ModuleDep::required(cstr!("standard")), zend::ModuleDep::required(cstr!("json")), - // Load after optional context publishers so their Process and Thread Context - // are available when profiling starts. - zend::ModuleDep::optional(cstr!("ddtrace")), + // Load after the optional OTel context publisher so its Process and Thread + // Context are available when profiling starts. zend::ModuleDep::optional(cstr!("opentelemetry")), // Optionally, be dependent on these event extensions so that the functions they provide // are registered in the function table and we can hook into them. @@ -182,8 +188,9 @@ static MODULE_DEPS: [zend::ModuleDep; 9] = [ zend::ModuleDep::end(), ]; -/// The module entry for the profiler extension. Fields that aren't +/// The module entry for the standalone profiler extension. Fields that aren't /// const-compatible are set in get_module(). +#[cfg(all(feature = "profiling", not(feature = "tracer")))] static mut MODULE: zend::ModuleEntry = zend::ModuleEntry { deps: MODULE_DEPS.as_ptr(), name: PROFILER_NAME.as_ptr(), @@ -214,6 +221,7 @@ static mut MODULE: zend::ModuleEntry = zend::ModuleEntry { /// Generally it is only called once, but if someone accidentally loads the /// module twice then it might get called more than once, though it will warn /// and not use the consecutive return value. +#[cfg(all(feature = "profiling", not(feature = "tracer")))] #[no_mangle] pub unsafe extern "C" fn get_module() -> *mut zend::ModuleEntry { let module = ptr::addr_of_mut!(MODULE); @@ -239,6 +247,39 @@ pub unsafe extern "C" fn get_module() -> *mut zend::ModuleEntry { // mechanisms like std::sync::Once::call_once may not be suitable. // Be careful out there! extern "C" fn minit(_type: c_int, module_number: c_int) -> ZendResult { + #[cfg(all(feature = "profiling", not(feature = "tracer")))] + if !unsafe { + bindings::datadog_get_module_entry(DDTRACE_NAME.as_ptr(), DDTRACE_NAME.to_bytes().len()) + } + .is_null() + { + unsafe { bindings::datadog_php_profiling_conflicting_extension_error() }; + return ZendResult::Failure; + } + + #[cfg(all(feature = "profiling", feature = "tracer"))] + if !unsafe { + bindings::datadog_get_module_entry(PROFILER_NAME.as_ptr(), PROFILER_NAME_STR.len()) + } + .is_null() + { + unsafe { bindings::datadog_php_profiling_conflicting_extension_error() }; + return ZendResult::Failure; + } + + { + let c_count = unsafe { bindings::ddog_php_prof_config_count() }; + if c_count as usize != crate::config::CONFIG_COUNT { + unsafe { + bindings::datadog_php_profiling_config_count_error( + c_count, + crate::config::CONFIG_COUNT, + ) + }; + return ZendResult::Failure; + } + } + // todo: merge these lifecycle things to tracing feature? // When developing the extension, it's useful to see log messages that // occur before the user can configure the log level. However, if we @@ -330,12 +371,10 @@ extern "C" fn minit(_type: c_int, module_number: c_int) -> ZendResult { _ = std::sync::LazyLock::force(&libdd_common::entity_id::DD_EXTERNAL_ENV); _ = std::sync::LazyLock::force(&libdd_common::azure_app_services::AAS_METADATA); - // Use a hybrid extension hack to load as a module but have the - // zend_extension hooks available: - // https://www.phpinternalsbook.com/php7/extensions_design/zend_extensions.html#hybrid-extensions - // In this case, use the same technique as the tracer: transfer the module - // handle to the zend_extension as extensions have longer lifetimes than - // modules in the engine. + // The standalone profiler uses a hybrid extension hack to load as a module + // while retaining zend_extension hooks. The combined extension dispatches + // these hooks through ddtrace's single Zend extension instead. + #[cfg(all(feature = "profiling", not(feature = "tracer")))] let handle = { // Levi modified the engine for PHP 8.2 to stop copying the module: // https://github.com/php/php-src/pull/8551 @@ -368,6 +407,7 @@ extern "C" fn minit(_type: c_int, module_number: c_int) -> ZendResult { // zend_llist_element. Every time a new PHP version is released, we should // double-check zend_register_extension to ensure the address is not // mutated nor stored. Well, hopefully we catch it _before_ a release. + #[cfg(all(feature = "profiling", not(feature = "tracer")))] let extension = ZendExtension { name: PROFILER_NAME.as_ptr(), version: PROFILER_VERSION.as_ptr().cast::(), @@ -385,7 +425,10 @@ extern "C" fn minit(_type: c_int, module_number: c_int) -> ZendResult { // SAFETY: all arguments are valid for this C call. // Note that on PHP 7 this never fails, and on PHP 8 it returns void. - unsafe { zend::zend_register_extension(&extension, handle) }; + #[cfg(all(feature = "profiling", not(feature = "tracer")))] + unsafe { + zend::zend_register_extension(&extension, handle) + }; timeline::timeline_minit(); @@ -411,7 +454,10 @@ extern "C" fn prshutdown() -> ZendResult { // ZAI config may be accessed indirectly via other modules RSHUTDOWN, so // delay this until the last possible time. - unsafe { bindings::zai_config_rshutdown() }; + #[cfg(all(feature = "profiling", not(feature = "tracer")))] + unsafe { + bindings::zai_config_rshutdown() + }; timeline::timeline_prshutdown(); @@ -530,11 +576,19 @@ thread_local! { } /// Gets the runtime-id for the process. Do not call before RINIT! -fn runtime_id() -> &'static Uuid { - RUNTIME_ID.get_or_init(|| { - // Resolve dynamically so the separately loaded tracer remains authoritative. The root - // package also contains a common runtime-id symbol, so treating this as an extern pointer - // would both use the wrong ABI and make a standalone profiler unsafe. +#[cfg(all(feature = "profiling", feature = "tracer"))] +fn runtime_id() -> Uuid { + // Copy from the common extension's authoritative storage. Returning a + // shared reference to mutable C-owned storage would violate Rust aliasing + // when the common extension refreshes the ID after a fork. + unsafe { crate::datadog_runtime_id } +} + +#[cfg(not(all(feature = "profiling", feature = "tracer")))] +fn runtime_id() -> Uuid { + *RUNTIME_ID.get_or_init(|| { + // Retain compatibility with embedders that export Datadog's runtime-ID + // symbol without loading the ddtrace PHP extension. let symbol = unsafe { libc::dlsym(libc::RTLD_DEFAULT, c"datadog_runtime_id".as_ptr()) } .cast::(); unsafe { symbol.as_ref() } @@ -576,11 +630,15 @@ extern "C" fn rinit(_type: c_int, _module_number: c_int) -> ZendResult { // SAFETY: not being mutated during rinit. let once = unsafe { &*ptr::addr_of!(ZAI_CONFIG_ONCE) }; once.call_once(|| unsafe { + #[cfg(all(feature = "profiling", not(feature = "tracer")))] bindings::zai_config_first_time_rinit(true); config::first_rinit(); }); - unsafe { bindings::zai_config_rinit() }; + #[cfg(all(feature = "profiling", not(feature = "tracer")))] + unsafe { + bindings::zai_config_rinit() + }; // Needs to come after config::first_rinit, because that's what sets the // values to the ones in the configuration. @@ -1008,6 +1066,7 @@ unsafe extern "C" fn minfo(module_ptr: *mut zend::ModuleEntry) { zend::php_info_print_table_end(); + #[cfg(all(feature = "profiling", not(feature = "tracer")))] zend::display_ini_entries(module_ptr); }); @@ -1087,10 +1146,68 @@ extern "C" fn shutdown(extension: *mut ZendExtension) { // data race condition. unsafe { config::shutdown() }; - // SAFETY: zai_config_mshutdown should be safe to call in shutdown instead - // of mshutdown. - unsafe { bindings::zai_config_mshutdown() }; - unsafe { bindings::zai_json_shutdown_bindings() }; + // SAFETY: standalone owns its ZAI configuration implementation. Combined + // mode uses the common extension's aggregate ZAI lifecycle. + #[cfg(all(feature = "profiling", not(feature = "tracer")))] + unsafe { + bindings::zai_config_mshutdown(); + bindings::zai_json_shutdown_bindings(); + } +} + +// Combined-extension lifecycle entry points. The standalone module and Zend +// extension entries above call the same implementations, so the two build +// modes cannot drift apart. +#[no_mangle] +pub extern "C" fn ddog_php_prof_minit(type_: c_int, module_number: c_int) -> ZendResult { + minit(type_, module_number) +} + +#[no_mangle] +pub extern "C" fn ddog_php_prof_mshutdown(type_: c_int, module_number: c_int) -> ZendResult { + mshutdown(type_, module_number) +} + +#[no_mangle] +pub extern "C" fn ddog_php_prof_rinit(type_: c_int, module_number: c_int) -> ZendResult { + rinit(type_, module_number) +} + +#[no_mangle] +pub extern "C" fn ddog_php_prof_rshutdown(type_: c_int, module_number: c_int) -> ZendResult { + rshutdown(type_, module_number) +} + +#[no_mangle] +pub extern "C" fn ddog_php_prof_is_enabled() -> bool { + // SAFETY: the combined lifecycle calls this after profiler RINIT and before + // profiler RSHUTDOWN tears down request configuration. + unsafe { config::profiling_enabled() } +} + +#[no_mangle] +pub extern "C" fn ddog_php_prof_post_deactivate() -> ZendResult { + prshutdown() +} + +#[no_mangle] +pub unsafe extern "C" fn ddog_php_prof_minfo(module: *mut zend::ModuleEntry) { + unsafe { minfo(module) } +} + +#[no_mangle] +pub extern "C" fn ddog_php_prof_zend_startup(extension: *mut ZendExtension) -> ZendResult { + startup(extension) +} + +#[no_mangle] +pub extern "C" fn ddog_php_prof_zend_activate() { + activate() +} + +#[no_mangle] +pub extern "C" fn ddog_php_prof_zend_shutdown(extension: *mut ZendExtension) { + shutdown(extension) } /// Notifies the profiler a trace has finished so it can update information diff --git a/profiling/src/lifecycle.h b/profiling/src/lifecycle.h new file mode 100644 index 00000000000..fd7066cfa3a --- /dev/null +++ b/profiling/src/lifecycle.h @@ -0,0 +1,25 @@ +#ifndef DATADOG_PHP_PROFILING_LIFECYCLE_H +#define DATADOG_PHP_PROFILING_LIFECYCLE_H + +#include +#include + +extern const zend_function_entry *ddog_php_prof_functions; + +size_t ddog_php_prof_globals_size(void); +void ddog_php_prof_ginit(void *globals); +void ddog_php_prof_gshutdown(void *globals); + +int ddog_php_prof_minit(int type, int module_number); +int ddog_php_prof_mshutdown(int type, int module_number); +int ddog_php_prof_rinit(int type, int module_number); +int ddog_php_prof_rshutdown(int type, int module_number); +bool ddog_php_prof_is_enabled(void); +int ddog_php_prof_post_deactivate(void); +void ddog_php_prof_minfo(zend_module_entry *module); + +int ddog_php_prof_zend_startup(zend_extension *extension); +void ddog_php_prof_zend_activate(void); +void ddog_php_prof_zend_shutdown(zend_extension *extension); + +#endif diff --git a/profiling/src/module_globals.rs b/profiling/src/module_globals.rs index f76e2aab07b..b15939a20a6 100644 --- a/profiling/src/module_globals.rs +++ b/profiling/src/module_globals.rs @@ -2,6 +2,11 @@ use crate::profiling::allocation; use core::cell::{Cell, UnsafeCell}; use core::ffi::c_void; use core::mem::MaybeUninit; +#[cfg(any( + not(all(feature = "profiling", feature = "tracer")), + target_os = "linux", + test +))] use core::ptr; use core::sync::atomic::AtomicU32; @@ -38,13 +43,16 @@ pub struct ProfilerGlobals { /// [`ModuleEntry::globals_id_ptr`] in the `zend_module_entry` and the TSRM /// will store it's thread-safe-resource id here; see: /// -#[cfg(php_zts)] +#[cfg(all(php_zts, all(feature = "profiling", not(feature = "tracer"))))] pub static mut GLOBALS_ID: i32 = 0; /// Module globals for NTS builds. In NTS mode, PHP uses this static directly. /// The `globals_ctor` function will re-initialize this (though it's already /// initialized here). -#[cfg(not(php_zts))] +#[cfg(all( + not(php_zts), + any(not(all(feature = "profiling", feature = "tracer")), test) +))] pub static mut GLOBALS: ProfilerGlobals = ProfilerGlobals { zend_mm_state: Cell::new(ZendMMState::new()), interrupt_count: AtomicU32::new(0), @@ -89,7 +97,7 @@ pub unsafe fn get_tsrm_resource_from_cache(ls_cache: *mut c_void, id: i32) -> *m zts::tsrmg_bulk(ls_cache, id) } -#[cfg(php_zts)] +#[cfg(all(php_zts, all(feature = "profiling", not(feature = "tracer"))))] #[inline] pub unsafe fn get_profiler_globals_from_cache(ls_cache: *mut c_void) -> *mut ProfilerGlobals { // SAFETY: As long as this is called during the times documented by @@ -98,6 +106,14 @@ pub unsafe fn get_profiler_globals_from_cache(ls_cache: *mut c_void) -> *mut Pro get_tsrm_resource_from_cache(ls_cache, id).cast() } +#[cfg(all(php_zts, all(feature = "profiling", feature = "tracer")))] +#[inline] +pub unsafe fn get_profiler_globals_from_cache(_ls_cache: *mut c_void) -> *mut ProfilerGlobals { + // Combined storage is owned by the current thread's ddtrace globals. The C + // accessor uses PHP's static TSRMLS cache, matching DATADOG_G access. + get_profiler_globals() +} + /// Returns a pointer to the profiler globals for the current thread. /// /// # Safety @@ -109,12 +125,23 @@ pub unsafe fn get_profiler_globals_from_cache(ls_cache: *mut c_void) -> *mut Pro /// - Must not be called after `GSHUTDOWN`. #[inline] pub unsafe fn get_profiler_globals() -> *mut ProfilerGlobals { - #[cfg(php_zts)] + #[cfg(all(all(feature = "profiling", feature = "tracer"), not(test)))] + { + unsafe extern "C" { + fn datadog_php_profiling_globals() -> *mut c_void; + } + datadog_php_profiling_globals().cast() + } + + #[cfg(all(not(all(feature = "profiling", feature = "tracer")), php_zts))] { get_profiler_globals_from_cache(get_tsrm_ls_cache()) } - #[cfg(not(php_zts))] + #[cfg(all( + not(php_zts), + any(not(all(feature = "profiling", feature = "tracer")), test) + ))] { ptr::addr_of_mut!(GLOBALS) } @@ -131,7 +158,7 @@ pub unsafe extern "C" fn ginit(_globals_ptr: *mut c_void) { // Initialize PHP globals for ZTS builds. For NTS builds, this was already // done in its const initializer. - #[cfg(php_zts)] + #[cfg(any(php_zts, all(feature = "profiling", feature = "tracer")))] { let globals = _globals_ptr.cast::(); (*globals).zend_mm_state = Cell::new(ZendMMState::new()); @@ -161,7 +188,7 @@ pub unsafe extern "C" fn gshutdown(_globals_ptr: *mut c_void) { if let Ok(mut cache) = (*globals).process_context.try_borrow_mut() { cache.reset(); } - #[cfg(php_zts)] + #[cfg(any(php_zts, all(feature = "profiling", feature = "tracer")))] ptr::drop_in_place(ptr::addr_of_mut!((*globals).process_context)); } @@ -169,6 +196,11 @@ pub unsafe extern "C" fn gshutdown(_globals_ptr: *mut c_void) { allocation::gshutdown(); } +#[no_mangle] +pub extern "C" fn ddog_php_prof_globals_size() -> usize { + core::mem::size_of::() +} + // Unit tests are not loaded by PHP, so provide the PHP globals and TSRM symbol // needed to link code retained in the test executable. #[cfg(test)] diff --git a/profiling/src/php_ffi.c b/profiling/src/php_ffi.c index 3d0cc6285bd..97b3f6c98a0 100644 --- a/profiling/src/php_ffi.c +++ b/profiling/src/php_ffi.c @@ -1,5 +1,11 @@ #include "php_ffi.h" +#if defined(TRACER) && defined(PROFILING) +#include +#include +#include +#endif + #include #include #include @@ -33,26 +39,6 @@ const void *datadog_php_profiling_get_otel_thread_context(void) { } #endif -static void locate_ddtrace_get_profiling_context(const zend_extension *extension) { - ddtrace_profiling_context (*get_profiling)(void) = - DL_FETCH_SYMBOL(extension->handle, "ddtrace_get_profiling_context"); - if (EXPECTED(get_profiling)) { - datadog_php_profiling_get_profiling_context = get_profiling; - } -} - -static void locate_datadog_process_tags_get_serialized(const zend_extension *extension) { - zend_string *(*get_process_tags)(void) = - DL_FETCH_SYMBOL(extension->handle, "datadog_process_tags_get_serialized"); - if (EXPECTED(get_process_tags)) { - datadog_php_profiling_get_process_tags_serialized = get_process_tags; - } -} - -static bool is_ddtrace_extension(const zend_extension *ext) { - return ext && ext->name && strcmp(ext->name, "ddtrace") == 0; -} - static ddtrace_profiling_context noop_get_profiling_context(void) { return (ddtrace_profiling_context){0, 0}; } @@ -163,27 +149,23 @@ static post_startup_cb_result ddog_php_prof_post_startup_cb(void) { static bool _ignore_run_time_cache = false; #endif +#if defined(TRACER) && defined(PROFILING) +static ddtrace_profiling_context combined_get_profiling_context(void) { + struct ddtrace_profiling_context context = ddtrace_get_profiling_context(); + return (ddtrace_profiling_context){context.local_root_span_id, context.span_id}; +} +#endif + void datadog_php_profiling_startup(zend_extension *extension) { #if CFG_RUN_TIME_CACHE // defined by build.rs _ignore_run_time_cache = strcmp(sapi_module.name, "cli") == 0; #endif - datadog_php_profiling_get_profiling_context = noop_get_profiling_context; - datadog_php_profiling_get_process_tags_serialized = noop_get_process_tags_serialized; - - /* Due to the optional dependency on ddtrace, the profiling module will be - * loaded after ddtrace if it's present, so ddtrace should always be found - * on startup and not need a message handler. - */ - const zend_llist *list = &zend_extensions; - for (const zend_llist_element *item = list->head; item; item = item->next) { - const zend_extension *maybe_ddtrace = (zend_extension *)item->data; - if (maybe_ddtrace != extension && is_ddtrace_extension(maybe_ddtrace)) { - locate_ddtrace_get_profiling_context(maybe_ddtrace); - locate_datadog_process_tags_get_serialized(maybe_ddtrace); - break; - } - } + (void)extension; +#if defined(TRACER) && defined(PROFILING) + datadog_php_profiling_get_profiling_context = combined_get_profiling_context; + datadog_php_profiling_get_process_tags_serialized = datadog_process_tags_get_serialized; +#endif #if CFG_POST_STARTUP_CB // defined by build.rs _is_post_startup = false; @@ -195,10 +177,31 @@ void datadog_php_profiling_startup(zend_extension *extension) { void *datadog_php_profiling_vm_interrupt_addr(void) { return &EG(vm_interrupt); } +void datadog_php_profiling_conflicting_extension_error(void) { + php_error_docref(NULL, E_CORE_WARNING, + "datadog-profiling cannot be loaded alongside ddtrace; " + "use combined ddtrace profiling support instead"); +} + +void datadog_php_profiling_config_count_error(uint16_t c_count, uintptr_t rust_count) { + php_error_docref(NULL, E_CORE_WARNING, + "generated configuration table mismatch (C=%u, Rust=%zu)", + (unsigned)c_count, (size_t)rust_count); +} + zend_module_entry *datadog_get_module_entry(const char *str, uintptr_t len) { return zend_hash_str_find_ptr(&module_registry, str, len); } +zai_str ddog_php_prof_get_common_tags(void) { + const char *tags = zend_ini_string(ZEND_STRL("datadog.tags"), false); + return tags ? (zai_str)ZAI_STR_FROM_CSTR(tags) : (zai_str)ZAI_STR_EMPTY; +} + +#if defined(TRACER) && defined(PROFILING) +uint16_t ddog_php_prof_config_count(void) { return DATADOG_CONFIG_COUNT; } +#endif + ddtrace_profiling_context (*datadog_php_profiling_get_profiling_context)(void) = noop_get_profiling_context; diff --git a/profiling/src/php_ffi.h b/profiling/src/php_ffi.h index ec7dcba417b..dc2672290d7 100644 --- a/profiling/src/php_ffi.h +++ b/profiling/src/php_ffi.h @@ -77,6 +77,9 @@ zend_module_entry *datadog_get_module_entry(const char *str, uintptr_t len); */ void *datadog_php_profiling_vm_interrupt_addr(void); +/** Reports that standalone profiler and ddtrace were loaded together. */ +void datadog_php_profiling_conflicting_extension_error(void); + /** * For Code Hotspots, we need the tracer's local root span id and the current * span id. This is a cross-product struct, so keep it in sync with tracer's @@ -88,15 +91,11 @@ typedef struct ddtrace_profiling_context_s { } ddtrace_profiling_context; /** - * A pointer to the tracer's ddtrace_get_profiling_context function if it was - * found, otherwise points to a function which just returns {0, 0}. + * Context providers used by profiler code. Combined builds install direct + * tracer/common providers during Zend startup; standalone builds retain the + * no-op providers. */ extern ddtrace_profiling_context (*datadog_php_profiling_get_profiling_context)(void); - -/** - * A pointer to the tracer's ddtrace_get_process_tags_serialized function if it - * was found, otherwise points to a function which just returns NULL; - */ extern zend_string *(*datadog_php_profiling_get_process_tags_serialized)(void); /** @@ -107,11 +106,7 @@ extern zend_string *(*datadog_php_profiling_get_process_tags_serialized)(void); const void *datadog_php_profiling_get_otel_thread_context(void); #endif -/** - * Called by this zend_extension's .startup handler. Does things that are - * burdensome in Rust, like locating the ddtrace extension in the module - * registry and finding the ddtrace_get_profiling_context function. - */ +/** Installs the direct combined-build context providers, when available. */ void datadog_php_profiling_startup(zend_extension *extension); /** diff --git a/profiling/tests/correctness/timeline.php b/profiling/tests/correctness/timeline.php index 8d18799a980..3d8ca932097 100644 --- a/profiling/tests/correctness/timeline.php +++ b/profiling/tests/correctness/timeline.php @@ -6,4 +6,6 @@ function foobar () { include(__DIR__.'/timeline_call.php'); -eval('usleep(1);'); +// Give the eval compilation event enough duration to survive percentage rounding +// in the correctness analyzer on older PHP versions and fast CI runners. +eval(str_repeat('$timelineEvalValue = 1;', 512) . 'usleep(1);'); diff --git a/profiling/tests/phpt/agent_host_port_01.phpt b/profiling/tests/phpt/agent_host_port_01.phpt new file mode 100644 index 00000000000..cfdbc95a384 --- /dev/null +++ b/profiling/tests/phpt/agent_host_port_01.phpt @@ -0,0 +1,23 @@ +--TEST-- +[profiling] DD_AGENT_HOST and DD_TRACE_AGENT_PORT configure the profiler endpoint +--SKIPIF-- + +--ENV-- +DD_AGENT_HOST=example.test +DD_TRACE_AGENT_PORT=4321 +DD_PROFILING_ENABLED=1 +--FILE-- +info(); +$output = ob_get_clean(); + +preg_match('/Profiling Agent Endpoint\s*=>\s*([^\r\n]+)/', $output, $matches); +echo trim($matches[1] ?? 'missing'), "\n"; +?> +--EXPECT-- +http://example.test:4321/ diff --git a/profiling/tests/phpt/allocation_bind_static_01.phpt b/profiling/tests/phpt/allocation_bind_static_01.phpt index 33a5cf4789c..d4eb3e20bf8 100644 --- a/profiling/tests/phpt/allocation_bind_static_01.phpt +++ b/profiling/tests/phpt/allocation_bind_static_01.phpt @@ -19,7 +19,7 @@ DD_PROFILING_ALLOCATION_SAMPLING_DISTANCE=1 --FILE-- diff --git a/profiling/tests/phpt/allocation_func_get_args.phpt b/profiling/tests/phpt/allocation_func_get_args.phpt index ce14cbc29c2..a220eb2a6a3 100644 --- a/profiling/tests/phpt/allocation_func_get_args.phpt +++ b/profiling/tests/phpt/allocation_func_get_args.phpt @@ -19,7 +19,7 @@ TODO: run this in some mode which will look at the opline on every allocation. --FILE-- diff --git a/profiling/tests/phpt/allocation_generator_01.phpt b/profiling/tests/phpt/allocation_generator_01.phpt index 7bcae9640df..947ce497188 100644 --- a/profiling/tests/phpt/allocation_generator_01.phpt +++ b/profiling/tests/phpt/allocation_generator_01.phpt @@ -12,7 +12,7 @@ Note: this does not trigger with PHP 7 when the tracer is enabled, as the tracer restores the opline in a opcode handler! --SKIPIF-- = 8.0", PHP_EOL; diff --git a/profiling/tests/phpt/allocation_sampling_distance.phpt b/profiling/tests/phpt/allocation_sampling_distance.phpt index 46c0c94c66d..f534570be53 100644 --- a/profiling/tests/phpt/allocation_sampling_distance.phpt +++ b/profiling/tests/phpt/allocation_sampling_distance.phpt @@ -5,7 +5,7 @@ This code path had a regression, so it seems worth adding a test to ensure it cannot regress again. --SKIPIF-- --INI-- diff --git a/profiling/tests/phpt/combined_default_disabled.phpt b/profiling/tests/phpt/combined_default_disabled.phpt new file mode 100644 index 00000000000..597bb315515 --- /dev/null +++ b/profiling/tests/phpt/combined_default_disabled.phpt @@ -0,0 +1,15 @@ +--TEST-- +[profiling] combined ddtrace defaults profiling to disabled +--SKIPIF-- + +--FILE-- + +--EXPECT-- +string(1) "0" diff --git a/profiling/tests/phpt/combined_identity.phpt b/profiling/tests/phpt/combined_identity.phpt new file mode 100644 index 00000000000..cb5e27fc766 --- /dev/null +++ b/profiling/tests/phpt/combined_identity.phpt @@ -0,0 +1,23 @@ +--TEST-- +[profiling] combined build registers only the ddtrace extension identity +--SKIPIF-- + +--FILE-- +getName()); +var_dump(in_array('ddtrace', get_loaded_extensions(true), true)); +var_dump(in_array('datadog-profiling', get_loaded_extensions(true), true)); +?> +--EXPECT-- +bool(true) +bool(false) +string(7) "ddtrace" +bool(true) +bool(false) diff --git a/profiling/tests/phpt/disabled_hooks_01.phpt b/profiling/tests/phpt/disabled_hooks_01.phpt index c590efc86a7..8e1bbc22bde 100644 --- a/profiling/tests/phpt/disabled_hooks_01.phpt +++ b/profiling/tests/phpt/disabled_hooks_01.phpt @@ -13,7 +13,7 @@ PHP timeout limit is implemented using the VM interrupt handler, which is why it is set up for this test. --SKIPIF-- --EXPECTREGEX-- .* Exception profiling initialized with sampling distance: 20 -.* Sent stack sample of 2 frames, 3 labels with Exception RuntimeException to profiler. +.* Sent stack sample of 2 frames, (3|5) labels with Exception RuntimeException to profiler. .*Done\..* .* diff --git a/profiling/tests/phpt/exceptions_fibers_throw.phpt b/profiling/tests/phpt/exceptions_fibers_throw.phpt index e46bc204ec7..064546c9132 100644 --- a/profiling/tests/phpt/exceptions_fibers_throw.phpt +++ b/profiling/tests/phpt/exceptions_fibers_throw.phpt @@ -2,7 +2,7 @@ [profiling] test exceptions being sampled in throwing fibers --SKIPIF-- --EXPECTREGEX-- -.* Exception profiling initialized with sampling distance: 20 -.* Sent stack sample of 1 frames, 3 labels with Exception RuntimeException to profiler. -.*Worker [0-9] exited -.*Done..* -.* +(?s).* Exception profiling initialized with sampling distance: 20.* Sent stack sample of 1 frames, (3|5) labels with Exception RuntimeException to profiler\..*Worker [0-9] exited.*Done\..* diff --git a/profiling/tests/phpt/fibers_01.phpt b/profiling/tests/phpt/fibers_01.phpt index 5a051cdb74b..a6dab507bcb 100644 --- a/profiling/tests/phpt/fibers_01.phpt +++ b/profiling/tests/phpt/fibers_01.phpt @@ -5,7 +5,7 @@ At this point we do not have active support for fibers, but we at least want to make sure to not crash when fibers are in use --SKIPIF-- --INI-- diff --git a/profiling/tests/phpt/gc_collect_cycles_01.phpt b/profiling/tests/phpt/gc_collect_cycles_01.phpt index 26c86ad8d3b..0cabe98ea1b 100644 --- a/profiling/tests/phpt/gc_collect_cycles_01.phpt +++ b/profiling/tests/phpt/gc_collect_cycles_01.phpt @@ -5,7 +5,7 @@ This will check if for every garbage collection event a debug print will happen, indicating that gc events in the engine are sampled for the timeline feature. --SKIPIF-- --ENV-- diff --git a/profiling/tests/phpt/gc_mem_caches_02.phpt b/profiling/tests/phpt/gc_mem_caches_02.phpt index bb426658ccf..58f87ebf128 100644 --- a/profiling/tests/phpt/gc_mem_caches_02.phpt +++ b/profiling/tests/phpt/gc_mem_caches_02.phpt @@ -2,7 +2,7 @@ [profiling] allocation profiling not crashing with system allocator in `gc_mem_caches()` call --SKIPIF-- --ENV-- diff --git a/profiling/tests/phpt/heap_live_02.phpt b/profiling/tests/phpt/heap_live_02.phpt index 6f7b45e90e8..35656a6a8ac 100644 --- a/profiling/tests/phpt/heap_live_02.phpt +++ b/profiling/tests/phpt/heap_live_02.phpt @@ -5,7 +5,7 @@ Verify that heap live profiling untracks allocations when they are freed. Uses trace log level to verify allocations are being untracked. --SKIPIF-- --ENV-- diff --git a/profiling/tests/phpt/heap_live_phpinfo_01.phpt b/profiling/tests/phpt/heap_live_phpinfo_01.phpt index 6ebbe655591..0cc25d7bc46 100644 --- a/profiling/tests/phpt/heap_live_phpinfo_01.phpt +++ b/profiling/tests/phpt/heap_live_phpinfo_01.phpt @@ -5,7 +5,7 @@ Verify that the heap live profiling setting is properly displayed in phpinfo when allocation profiling is enabled. --SKIPIF-- --ENV-- @@ -20,7 +20,7 @@ opcache.jit=off info(); $output = ob_get_clean(); diff --git a/profiling/tests/phpt/heap_live_phpinfo_02.phpt b/profiling/tests/phpt/heap_live_phpinfo_02.phpt index 2ab4590bacb..a0f4abd4782 100644 --- a/profiling/tests/phpt/heap_live_phpinfo_02.phpt +++ b/profiling/tests/phpt/heap_live_phpinfo_02.phpt @@ -5,7 +5,7 @@ Verify that heap live profiling shows "requires allocation profiling" when allocation profiling is disabled. --SKIPIF-- --ENV-- @@ -20,7 +20,7 @@ opcache.jit=off info(); $output = ob_get_clean(); diff --git a/profiling/tests/phpt/heap_live_phpinfo_03.phpt b/profiling/tests/phpt/heap_live_phpinfo_03.phpt index d8f5451c45c..4ec399c9c0a 100644 --- a/profiling/tests/phpt/heap_live_phpinfo_03.phpt +++ b/profiling/tests/phpt/heap_live_phpinfo_03.phpt @@ -5,7 +5,7 @@ Verify that setting the umbrella DD_PROFILING_EXPERIMENTAL_FEATURES_ENABLED=true turns on heap live profiling without needing the specific opt-in. --SKIPIF-- --ENV-- @@ -20,7 +20,7 @@ opcache.jit=off info(); $output = ob_get_clean(); diff --git a/profiling/tests/phpt/heap_live_phpinfo_04.phpt b/profiling/tests/phpt/heap_live_phpinfo_04.phpt index a7ee8c59861..72e3cb6784a 100644 --- a/profiling/tests/phpt/heap_live_phpinfo_04.phpt +++ b/profiling/tests/phpt/heap_live_phpinfo_04.phpt @@ -5,7 +5,7 @@ Verify that heap live profiling is disabled if allocation profiling is disabled and works normally otherwise. --SKIPIF-- = 8.0\n"; @@ -33,7 +33,7 @@ opcache.jit_buffer_size=4M info(); $output = ob_get_clean(); diff --git a/profiling/tests/phpt/jit_01.phpt b/profiling/tests/phpt/jit_01.phpt index e9cfa0cfd55..6b1ea1d3c59 100644 --- a/profiling/tests/phpt/jit_01.phpt +++ b/profiling/tests/phpt/jit_01.phpt @@ -12,7 +12,7 @@ if (PHP_VERSION_ID < 80000) echo "skip: JIT requires PHP >= 8.0", PHP_EOL; if (PHP_VERSION_ID >= 80300) echo "skip: not affected version", PHP_EOL; -if (!extension_loaded('datadog-profiling')) +if (!(extension_loaded('datadog-profiling') || ini_get('datadog.profiling.enabled') !== false)) echo "skip: test requires datadog-profiling", PHP_EOL; $arch = php_uname('m'); if (PHP_VERSION_ID < 80100 && in_array($arch, ['aarch64', 'arm64'])) diff --git a/profiling/tests/phpt/jit_02.phpt b/profiling/tests/phpt/jit_02.phpt index 18ce815356d..e74721c6091 100644 --- a/profiling/tests/phpt/jit_02.phpt +++ b/profiling/tests/phpt/jit_02.phpt @@ -11,7 +11,7 @@ if (PHP_VERSION_ID < 80000) echo "skip: JIT requires PHP >= 8.0", PHP_EOL; if (PHP_VERSION_ID >= 80300) echo "skip: not affected version", PHP_EOL; -if (!extension_loaded('datadog-profiling')) +if (!(extension_loaded('datadog-profiling') || ini_get('datadog.profiling.enabled') !== false)) echo "skip: test requires datadog-profiling", PHP_EOL; $arch = php_uname('m'); if (PHP_VERSION_ID < 80100 && in_array($arch, ['aarch64', 'arm64'])) diff --git a/profiling/tests/phpt/jit_03.phpt b/profiling/tests/phpt/jit_03.phpt index 504b4481544..b3a89d9ec08 100644 --- a/profiling/tests/phpt/jit_03.phpt +++ b/profiling/tests/phpt/jit_03.phpt @@ -10,7 +10,7 @@ if (PHP_VERSION_ID < 80400) echo "skip: PHP Version < 8.4 are not affected", PHP_EOL; if (PHP_VERSION_ID >= 80407) echo "skip: fixed since PHP version 8.4.7", PHP_EOL; -if (!extension_loaded('datadog-profiling')) +if (!(extension_loaded('datadog-profiling') || ini_get('datadog.profiling.enabled') !== false)) echo "skip: test requires datadog-profiling", PHP_EOL; if (php_uname("s") === "Darwin") echo "skip: 'Darwin' has no JIT", PHP_EOL; diff --git a/profiling/tests/phpt/memory_get_peak_usage_01.phpt b/profiling/tests/phpt/memory_get_peak_usage_01.phpt index 9ae95c0bb3d..a94d5b62445 100644 --- a/profiling/tests/phpt/memory_get_peak_usage_01.phpt +++ b/profiling/tests/phpt/memory_get_peak_usage_01.phpt @@ -6,7 +6,7 @@ https://github.com/DataDog/dd-trace-php/issues/3360 --ENV-- diff --git a/profiling/tests/phpt/native_thread_alloc_01.phpt b/profiling/tests/phpt/native_thread_alloc_01.phpt index 3404e1c4450..8b7121ad796 100644 --- a/profiling/tests/phpt/native_thread_alloc_01.phpt +++ b/profiling/tests/phpt/native_thread_alloc_01.phpt @@ -8,7 +8,7 @@ Both of these are uninitialized for non-PHP threads since they never went throug See https://github.com/DataDog/dd-trace-php/pull/3542 for the fix --SKIPIF-- --ENV-- @@ -27,7 +27,7 @@ opcache.jit=off info(); $output = ob_get_clean(); diff --git a/profiling/tests/phpt/phpinfo_02.phpt b/profiling/tests/phpt/phpinfo_02.phpt index 9a9058ead8e..698586f5751 100644 --- a/profiling/tests/phpt/phpinfo_02.phpt +++ b/profiling/tests/phpt/phpinfo_02.phpt @@ -9,7 +9,7 @@ if (PHP_VERSION_ID >= 80208 || PHP_VERSION_ID >= 80121 && PHP_VERSION_ID < 80200 echo "skip: PHP Version >= 8.1.21 and >= 8.2.8 have a fix for this"; if (PHP_VERSION_ID < 80000) echo "skip: JIT requires PHP >= 8.0", PHP_EOL; -if (!extension_loaded('datadog-profiling')) +if (!(extension_loaded('datadog-profiling') || ini_get('datadog.profiling.enabled') !== false)) echo "skip: test requires datadog-profiling", PHP_EOL; $arch = php_uname('m'); if (PHP_VERSION_ID < 80100 && in_array($arch, ['aarch64', 'arm64'])) @@ -30,7 +30,7 @@ opcache.jit_buffer_size=4M info(); $output = ob_get_clean(); diff --git a/profiling/tests/phpt/phpinfo_03.phpt b/profiling/tests/phpt/phpinfo_03.phpt index b0a854c7bcc..dff666de49a 100644 --- a/profiling/tests/phpt/phpinfo_03.phpt +++ b/profiling/tests/phpt/phpinfo_03.phpt @@ -5,7 +5,7 @@ The profiler's phpinfo section contains important debugging information. This test verifies that certain information is present. --SKIPIF-- --ENV-- @@ -27,7 +27,7 @@ opcache.jit=off info(); $output = ob_get_clean(); diff --git a/profiling/tests/phpt/phpinfo_04.phpt b/profiling/tests/phpt/phpinfo_04.phpt index c34aa5e6ca6..eb0e454722d 100644 --- a/profiling/tests/phpt/phpinfo_04.phpt +++ b/profiling/tests/phpt/phpinfo_04.phpt @@ -9,7 +9,7 @@ if (PHP_VERSION_ID < 80120 || PHP_VERSION_ID >= 80200 && PHP_VERSION_ID < 80207 echo "skip: unpatched PHP version, so JIT should be inactive"; if (PHP_VERSION_ID < 80000) echo "skip: JIT requires PHP >= 8.0", PHP_EOL; -if (!extension_loaded('datadog-profiling')) +if (!(extension_loaded('datadog-profiling') || ini_get('datadog.profiling.enabled') !== false)) echo "skip: test requires datadog-profiling", PHP_EOL; $arch = php_uname('m'); if (PHP_VERSION_ID < 80100 && in_array($arch, ['aarch64', 'arm64'])) @@ -30,7 +30,7 @@ opcache.jit_buffer_size=4M info(); $output = ob_get_clean(); diff --git a/profiling/tests/phpt/phpinfo_05.phpt b/profiling/tests/phpt/phpinfo_05.phpt index d3ec73c23fe..85a28837139 100644 --- a/profiling/tests/phpt/phpinfo_05.phpt +++ b/profiling/tests/phpt/phpinfo_05.phpt @@ -5,7 +5,7 @@ This test shall validate that the "old" EXPERIMENTAL env variables get overwritten by the new ones without EXPERIMENTAL in them. --SKIPIF-- --ENV-- @@ -21,7 +21,7 @@ opcache.jit=off info(); $output = ob_get_clean(); diff --git a/profiling/tests/phpt/phpinfo_06.phpt b/profiling/tests/phpt/phpinfo_06.phpt index 79a0d14500a..f28bccd7bef 100644 --- a/profiling/tests/phpt/phpinfo_06.phpt +++ b/profiling/tests/phpt/phpinfo_06.phpt @@ -5,7 +5,7 @@ The profiler's phpinfo section contains important debugging information. This test verifies that certain information is present. --SKIPIF-- --ENV-- @@ -23,7 +23,7 @@ opcache.jit=off info(); $output = ob_get_clean(); diff --git a/profiling/tests/phpt/phpinfo_07.phpt b/profiling/tests/phpt/phpinfo_07.phpt index 526777e724b..b4aa4b70749 100644 --- a/profiling/tests/phpt/phpinfo_07.phpt +++ b/profiling/tests/phpt/phpinfo_07.phpt @@ -5,7 +5,7 @@ The profiler's phpinfo section contains important debugging information. This test verifies that certain information is present. --SKIPIF-- --ENV-- @@ -23,7 +23,7 @@ opcache.jit=off info(); $output = ob_get_clean(); diff --git a/profiling/tests/phpt/phpinfo_08.phpt b/profiling/tests/phpt/phpinfo_08.phpt index a4bac30d89b..9203307a699 100644 --- a/profiling/tests/phpt/phpinfo_08.phpt +++ b/profiling/tests/phpt/phpinfo_08.phpt @@ -5,7 +5,7 @@ The profiler's phpinfo section contains important debugging information. This test verifies that certain information is present. --SKIPIF-- --ENV-- @@ -16,7 +16,7 @@ assert.exception=1 info(); $output = ob_get_clean(); diff --git a/profiling/tests/phpt/phpinfo_ini_01.phpt b/profiling/tests/phpt/phpinfo_ini_01.phpt index 6a80ee0233f..861ead25893 100644 --- a/profiling/tests/phpt/phpinfo_ini_01.phpt +++ b/profiling/tests/phpt/phpinfo_ini_01.phpt @@ -5,7 +5,7 @@ The profiler's phpinfo section contains important debugging information. This test verifies that certain information is present when configured by .ini. --SKIPIF-- --INI-- @@ -26,7 +26,7 @@ datadog.trace.agent_url=http://datadog:8126 info(); $output = ob_get_clean(); diff --git a/profiling/tests/phpt/phpinfo_ini_02.phpt b/profiling/tests/phpt/phpinfo_ini_02.phpt index e16076e518c..8e37e2e96b6 100644 --- a/profiling/tests/phpt/phpinfo_ini_02.phpt +++ b/profiling/tests/phpt/phpinfo_ini_02.phpt @@ -5,7 +5,7 @@ The profiler's phpinfo section contains important debugging information. This test verifies that certain information is present when configured by .ini. --SKIPIF-- --INI-- @@ -15,7 +15,7 @@ datadog.profiling.enabled=auto info(); $output = ob_get_clean(); diff --git a/profiling/tests/phpt/preload_01.phpt b/profiling/tests/phpt/preload_01.phpt index 4ca447877f3..c70a7af0cbc 100644 --- a/profiling/tests/phpt/preload_01.phpt +++ b/profiling/tests/phpt/preload_01.phpt @@ -18,7 +18,7 @@ USE_ZEND_ALLOC=0 = 7.4.0", PHP_EOL; -if (!extension_loaded('datadog-profiling')) +if (!(extension_loaded('datadog-profiling') || ini_get('datadog.profiling.enabled') !== false)) echo "skip: test requires datadog-profiling", PHP_EOL; if (!extension_loaded('Zend OPcache')) echo "skip: test requires opcache", PHP_EOL; @@ -41,6 +41,6 @@ echo "Done.", PHP_EOL; ?> --EXPECTREGEX-- .*zend_post_startup_cb hasn't happened yet; not enabling profiler. -preloading +(.*\n)*preloading .*Started with an upload period of [0-9]+ seconds and approximate wall-time period of [0-9]+ milliseconds. .*Done.* diff --git a/profiling/tests/phpt/service_cli_01.phpt b/profiling/tests/phpt/service_cli_01.phpt index 98a8d015acd..f3dcd77aa17 100644 --- a/profiling/tests/phpt/service_cli_01.phpt +++ b/profiling/tests/phpt/service_cli_01.phpt @@ -8,7 +8,7 @@ of "Standard input code." This behavior matches the tracer's and should be kept in sync. --SKIPIF-- --ENV-- @@ -20,7 +20,7 @@ assert.exception=1 info(); $output = ob_get_clean(); diff --git a/profiling/tests/phpt/service_web_01.phpt b/profiling/tests/phpt/service_web_01.phpt index 00b1eca0fb5..96dc62849e9 100644 --- a/profiling/tests/phpt/service_web_01.phpt +++ b/profiling/tests/phpt/service_web_01.phpt @@ -25,29 +25,26 @@ assert.exception=1 assert(php_sapi_name() !== 'cli'); // Can't be in the SKIPIF for CGI requests for some reason -assert(extension_loaded('datadog-profiling')); +assert((extension_loaded('datadog-profiling') || ini_get('datadog.profiling.enabled') !== false)); assert(extension_loaded('dom')); ob_start(); -$extension = new ReflectionExtension('datadog-profiling'); +$extension = new ReflectionExtension(extension_loaded('datadog-profiling') ? 'datadog-profiling' : 'ddtrace'); $extension->info(); $output = ob_get_clean(); $values = []; -/* Expect two tables. - * 1. Two column layout, first column is the key and second is the value. - * 2. Three column layout for .ini settings. First is the directive, second - * is the local value, and last is the master value. - * For this test, we're after the first table for DD_SERVICE. - */ +// Standalone profiling has two tables. Combined ddtrace includes its common +// diagnostics and INI tables as well, so locate the profiler row by content. $dom = new DOMDocument(); assert($dom->loadHTML($output)); -$tables = iterator_to_array($dom->getElementsByTagName('table')); -assert(count($tables) === 2); - -foreach ($tables[0]->getElementsByTagName('tr') as $row) { - [$key, $value] = iterator_to_array($row->getElementsByTagName('td')); +foreach ($dom->getElementsByTagName('tr') as $row) { + $columns = iterator_to_array($row->getElementsByTagName('td')); + if (count($columns) !== 2) { + continue; + } + [$key, $value] = $columns; $key = html_entity_decode(trim($key->nodeValue)); $value = html_entity_decode(trim($value->nodeValue)); $values[$key] = $value; diff --git a/profiling/tests/phpt/standalone_conflict_ddtrace_first.phpt b/profiling/tests/phpt/standalone_conflict_ddtrace_first.phpt new file mode 100644 index 00000000000..4aa21503ddd --- /dev/null +++ b/profiling/tests/phpt/standalone_conflict_ddtrace_first.phpt @@ -0,0 +1,37 @@ +--TEST-- +[profiling] standalone profiler rejects a separately loaded ddtrace extension +--SKIPIF-- + +--FILE-- +&1'; +exec($command, $output, $status); +$text = implode("\n", $output); + +var_dump($status !== 0); +var_dump(strpos($text, 'datadog-profiling cannot be loaded alongside ddtrace; use combined ddtrace profiling support instead') !== false); +?> +--EXPECT-- +bool(true) +bool(true) diff --git a/profiling/tests/phpt/standalone_conflict_profiler_first.phpt b/profiling/tests/phpt/standalone_conflict_profiler_first.phpt new file mode 100644 index 00000000000..dc4eb37b1a6 --- /dev/null +++ b/profiling/tests/phpt/standalone_conflict_profiler_first.phpt @@ -0,0 +1,37 @@ +--TEST-- +[profiling] ddtrace rejects a separately loaded standalone profiler +--SKIPIF-- + +--FILE-- +&1'; +exec($command, $output, $status); +$text = implode("\n", $output); + +var_dump($status !== 0); +var_dump(strpos($text, 'datadog-profiling cannot be loaded alongside ddtrace; use combined ddtrace profiling support instead') !== false); +?> +--EXPECT-- +bool(true) +bool(true) diff --git a/profiling/tests/phpt/timeline_fatal_error_null_filename.phpt b/profiling/tests/phpt/timeline_fatal_error_null_filename.phpt index 0aea9d2a784..5905759ee2c 100644 --- a/profiling/tests/phpt/timeline_fatal_error_null_filename.phpt +++ b/profiling/tests/phpt/timeline_fatal_error_null_filename.phpt @@ -12,7 +12,7 @@ Reproduces the upstream Zend/tests/bug50005.phpt and bug64821.3.phpt crashes that only triggered with the profiler loaded and timeline enabled. --SKIPIF-- --ENV-- @@ -18,7 +18,7 @@ assert.exception=1 info(); $output = ob_get_clean(); diff --git a/profiling/tests/phpt/uds_02.phpt b/profiling/tests/phpt/uds_02.phpt index 8a18f270439..7897cfdbdc4 100644 --- a/profiling/tests/phpt/uds_02.phpt +++ b/profiling/tests/phpt/uds_02.phpt @@ -6,7 +6,7 @@ DD_TRACE_AGENT_URL will cause it to fall back to the default socket path (as long as DD_AGENT_HOST is not set or is empty). --SKIPIF-- info(); $output = ob_get_clean(); diff --git a/profiling/tests/phpt/zts_01.phpt b/profiling/tests/phpt/zts_01.phpt index 39832d71290..c9cf09c7473 100644 --- a/profiling/tests/phpt/zts_01.phpt +++ b/profiling/tests/phpt/zts_01.phpt @@ -2,7 +2,7 @@ [profiling] test that the profiler works in a ZTS build, loads and exists and does not segfault --SKIPIF-- --FILE-- --FILE-- [false], 'opcache.jit_buffer_size' => ['256M'], - 'extension' => ['datadog-profiling.so'], ]; diff --git a/tooling/bin/build-debug-artifact b/tooling/bin/build-debug-artifact index 8739fbe656d..0040a75ee03 100755 --- a/tooling/bin/build-debug-artifact +++ b/tooling/bin/build-debug-artifact @@ -38,7 +38,7 @@ Target format: {libc}-{arch}-{php_version}-{thread_safety} thread_safety: nts | zts Options: - --profiler Also build the profiling extension + --profiler Build profiling into the combined ddtrace extension --appsec Also build the AppSec extension If the current directory contains a Dockerfile, it will be patched to COPY the @@ -242,16 +242,12 @@ _preamble() { # $1 = shell commands to run as the host user fi } -TRACE_SCRIPT="$(_preamble "DD_TRACE_DOCKER_DEBUG=1 CFLAGS='-std=gnu11 -O0 -g' make -cp tmp/build_extension/modules/ddtrace.so /output/ddtrace.so")" - +TRACE_PRODUCTS="" if [[ "$BUILD_PROFILER" == "1" ]]; then - PROFILER_SCRIPT="$(_preamble "if [ -f /sbin/apk ] && [ \$(uname -m) = 'aarch64' ]; then - ln -sf ../lib/llvm20/bin/clang /usr/bin/clang -fi -make BUILD_SUFFIX=profiler PROFILING=1 RUST_DEBUG_BUILD=1 EXTRA_CONFIGURE_OPTIONS='--disable-ddtrace-tracer --enable-ddtrace-profiling' -cp ${HOME_DIR}/tmp/build_profiler/modules/datadog-profiling.so /output/datadog-profiling${EXT_SUFFIX}.so")" + TRACE_PRODUCTS="EXTRA_CONFIGURE_OPTIONS=--enable-ddtrace-profiling" fi +TRACE_SCRIPT="$(_preamble "DD_TRACE_DOCKER_DEBUG=1 CFLAGS='-std=gnu11 -O0 -g' make ${TRACE_PRODUCTS} +cp tmp/build_extension/modules/ddtrace.so /output/ddtrace.so")" if [[ "$BUILD_APPSEC" == "1" ]]; then # Resolve libddwaf commit on the host (avoids git inside the container, @@ -317,9 +313,6 @@ fi _launch "trace" docker run -w "${HOME_DIR}" --rm "${_common_volumes[@]}" "$DOCKER_IMAGE" "$DOCKER_SHELL" -c "$TRACE_SCRIPT" -[[ "$BUILD_PROFILER" == "1" ]] && \ - _launch "profiler" docker run -w "${HOME_DIR}" --rm "${_common_volumes[@]}" "$DOCKER_IMAGE" "$DOCKER_SHELL" -c "$PROFILER_SCRIPT" - if [[ "$BUILD_APPSEC" == "1" ]]; then _launch "appsec" docker run -w "${HOME_DIR}" --rm "${_common_volumes[@]}" "$DOCKER_IMAGE" "$DOCKER_SHELL" -c "$APPSEC_SCRIPT" @@ -368,11 +361,10 @@ cp -r "${REPO_ROOT}/src" "${TRACE_BASE}/src" echo "$VERSION" > "${TMP_PKG}/dd-library-php/VERSION" if [[ "$BUILD_PROFILER" == "1" ]]; then - PROFILING_BASE="${TMP_PKG}/dd-library-php/profiling" - mkdir -p "${PROFILING_BASE}/ext/${php_api}" - cp "${TMP_OUT}/datadog-profiling${EXT_SUFFIX}.so" "${PROFILING_BASE}/ext/${php_api}/datadog-profiling${EXT_SUFFIX}.so" - cp "${REPO_ROOT}/profiling/LICENSE"* "${PROFILING_BASE}/" 2>/dev/null || true - cp "${REPO_ROOT}/profiling/NOTICE" "${PROFILING_BASE}/" 2>/dev/null || true + touch "${TRACE_BASE}/ext/${php_api}/.ddtrace${EXT_SUFFIX}.profiling" + mkdir -p "${TRACE_BASE}/profiling-licenses" + cp "${REPO_ROOT}/profiling/LICENSE"* "${TRACE_BASE}/profiling-licenses/" 2>/dev/null || true + cp "${REPO_ROOT}/profiling/NOTICE" "${TRACE_BASE}/profiling-licenses/" 2>/dev/null || true fi if [[ "$BUILD_APPSEC" == "1" ]]; then diff --git a/tooling/bin/generate-final-artifact.sh b/tooling/bin/generate-final-artifact.sh index d69c5c6e50c..88e28b70533 100755 --- a/tooling/bin/generate-final-artifact.sh +++ b/tooling/bin/generate-final-artifact.sh @@ -70,16 +70,11 @@ for architecture in "${architectures[@]}"; do cp_with_dir ${ddtrace_ext_path} ${trace_base_dir}/ext/${php_api}/$(if [[ $target == "windows" ]]; then echo php_; fi)ddtrace${config}.${ext} cp -r ./src ${trace_base_dir} - profiling_ext_path=./datadog-profiling/${architecture}-${full_target}/lib/php/${php_api}/datadog-profiling${config}.${ext} - if [[ -f ${profiling_ext_path} ]]; then - profiling_base_dir=${tmp_folder_final}/dd-library-php/profiling - cp_with_dir ${profiling_ext_path} ${profiling_base_dir}/ext/${php_api}/datadog-profiling${config}.${ext} - - # Licenses - cp \ - ./profiling/LICENSE* \ - ./profiling/NOTICE \ - ${profiling_base_dir}/ + combined_ext_path=./combined-ddtrace/${architecture}-${full_target}/lib/php/${php_api}/ddtrace${config}.${ext} + if [[ -f ${combined_ext_path} ]]; then + touch ${trace_base_dir}/ext/${php_api}/.ddtrace${config}.profiling + mkdir -p ${trace_base_dir}/profiling-licenses + cp ./profiling/LICENSE* ./profiling/NOTICE ${trace_base_dir}/profiling-licenses/ fi appsec_ext_path=./appsec_${architecture}/ddappsec-${php_api}${alpine}${config}.${ext} @@ -140,11 +135,21 @@ for architecture in "${architectures[@]}"; do cp ./extensions_${architecture}/ddtrace-$php_api.so ${tmp_folder_final_gnu_trace}/ext/$php_api/ddtrace.so; cp ./extensions_${architecture}/ddtrace-$php_api-zts.so ${tmp_folder_final_gnu_trace}/ext/$php_api/ddtrace-zts.so; cp ./extensions_${architecture}/ddtrace-$php_api-debug.so ${tmp_folder_final_gnu_trace}/ext/$php_api/ddtrace-debug.so; + for profiling_config in "" -zts; do + if [[ -f ./combined-ddtrace/${architecture}-${full_target}/lib/php/${php_api}/ddtrace${profiling_config}.so ]]; then + touch ${tmp_folder_final_gnu_trace}/ext/$php_api/.ddtrace${profiling_config}.profiling + fi + done fi if [[ $target == "linux-musl" ]]; then mkdir -p ${tmp_folder_final_musl_trace}/ext/$php_api; cp ./extensions_${architecture}/ddtrace-$php_api-alpine.so ${tmp_folder_final_musl_trace}/ext/$php_api/ddtrace.so; cp ./extensions_${architecture}/ddtrace-$php_api-alpine-zts.so ${tmp_folder_final_musl_trace}/ext/$php_api/ddtrace-zts.so; + for profiling_config in "" -zts; do + if [[ -f ./combined-ddtrace/${architecture}-${full_target}/lib/php/${php_api}/ddtrace${profiling_config}.so ]]; then + touch ${tmp_folder_final_musl_trace}/ext/$php_api/.ddtrace${profiling_config}.profiling + fi + done fi if [[ $target == "windows" && ${php_api} -ge 20170718 && $architecture == "x86_64" ]]; then # Windows support starts on 7.2 mkdir -p ${tmp_folder_final_windows_trace}/ext/$php_api; @@ -166,11 +171,19 @@ for architecture in "${architectures[@]}"; do if [[ $target == "linux-gnu" ]]; then mkdir -p ${tmp_folder_final_gnu_trace} cp -r ./src ${tmp_folder_final_gnu_trace}; + if [[ -d ./combined-ddtrace/${architecture}-${full_target} ]]; then + mkdir -p ${tmp_folder_final_gnu_trace}/profiling-licenses + cp ./profiling/LICENSE* ./profiling/NOTICE ${tmp_folder_final_gnu_trace}/profiling-licenses/ + fi fi if [[ $target == "linux-musl" ]]; then mkdir -p ${tmp_folder_final_musl_trace} cp -r ./src ${tmp_folder_final_musl_trace}; + if [[ -d ./combined-ddtrace/${architecture}-${full_target} ]]; then + mkdir -p ${tmp_folder_final_musl_trace}/profiling-licenses + cp ./profiling/LICENSE* ./profiling/NOTICE ${tmp_folder_final_musl_trace}/profiling-licenses/ + fi fi if [[ $target == "windows" && $architecture == "x86_64" ]]; then @@ -179,62 +192,6 @@ for architecture in "${architectures[@]}"; do fi done - ######################## - # Profiling - ######################## - if [[ -z ${DDTRACE_MAKE_PACKAGES_ASAN:-} ]]; then - # Extension - php_apis=(20160303 20170718 20180731 20190902 20200930 20210902 20220829 20230831 20240924 20250925) - for version in "${php_apis[@]}" - do - for full_target in "${targets[@]}"; do - target=${full_target#*-} - if [[ $target == "linux-gnu" ]]; then - mkdir -v -p \ - $tmp_folder_final_gnu/dd-library-php/profiling/ext/$version - - cp -v \ - ./datadog-profiling/$architecture-unknown-linux-gnu/lib/php/$version/datadog-profiling.so \ - $tmp_folder_final_gnu/dd-library-php/profiling/ext/$version/datadog-profiling.so - cp -v \ - ./datadog-profiling/$architecture-unknown-linux-gnu/lib/php/$version/datadog-profiling-zts.so \ - $tmp_folder_final_gnu/dd-library-php/profiling/ext/$version/datadog-profiling-zts.so - fi - - if [[ $target == "linux-musl" ]]; then - mkdir -v -p \ - $tmp_folder_final_musl/dd-library-php/profiling/ext/$version - - cp -v \ - ./datadog-profiling/$architecture-alpine-linux-musl/lib/php/$version/datadog-profiling.so \ - $tmp_folder_final_musl/dd-library-php/profiling/ext/$version/datadog-profiling.so - cp -v \ - ./datadog-profiling/$architecture-alpine-linux-musl/lib/php/$version/datadog-profiling-zts.so \ - $tmp_folder_final_musl/dd-library-php/profiling/ext/$version/datadog-profiling-zts.so - fi - done - done - - # Licenses - for full_target in "${targets[@]}"; do - target=${full_target#*-} - if [[ $target == "linux-gnu" ]]; then - mkdir -p $tmp_folder_final_gnu/dd-library-php/profiling - cp -v \ - ./profiling/LICENSE* \ - ./profiling/NOTICE \ - $tmp_folder_final_gnu/dd-library-php/profiling/ - fi - if [[ $target == "linux-musl" ]]; then - mkdir -p $tmp_folder_final_musl/dd-library-php/profiling - cp -v \ - ./profiling/LICENSE* \ - ./profiling/NOTICE \ - $tmp_folder_final_musl/dd-library-php/profiling/ - fi - done - fi - ######################## # AppSec ######################## diff --git a/tooling/bin/generate-ssi-package.sh b/tooling/bin/generate-ssi-package.sh index cec1a604977..765c1c60553 100755 --- a/tooling/bin/generate-ssi-package.sh +++ b/tooling/bin/generate-ssi-package.sh @@ -75,29 +75,27 @@ for architecture in "${architectures[@]}"; do ######################## mkdir -p ${gnu}/trace/ext/${php_api} ${musl}/trace/ext/${php_api} - # gnu - stripto ./standalone_${architecture}/ddtrace-${php_api}.so ${gnu}/trace/ext/${php_api}/ddtrace.so - stripto ./standalone_${architecture}/ddtrace-${php_api}-zts.so ${gnu}/trace/ext/${php_api}/ddtrace-zts.so - # musl - stripto ./standalone_${architecture}/ddtrace-${php_api}-alpine.so ${musl}/trace/ext/${php_api}/ddtrace.so - stripto ./standalone_${architecture}/ddtrace-${php_api}-alpine-zts.so ${musl}/trace/ext/${php_api}/ddtrace-zts.so - - ######################## - # Profiling - ######################## - if [[ ${php_api} -ge 20160303 ]]; then - mkdir -p ${gnu}/profiling/ext/${php_api} ${musl}/profiling/ext/${php_api} - # gnu - stripto ./datadog-profiling/${architecture}-unknown-linux-gnu/lib/php/${php_api}/datadog-profiling.so \ - ${gnu}/profiling/ext/${php_api}/datadog-profiling.so - stripto ./datadog-profiling/${architecture}-unknown-linux-gnu/lib/php/${php_api}/datadog-profiling-zts.so \ - ${gnu}/profiling/ext/${php_api}/datadog-profiling-zts.so - # musl - stripto ./datadog-profiling/${architecture}-alpine-linux-musl/lib/php/${php_api}/datadog-profiling.so \ - ${musl}/profiling/ext/${php_api}/datadog-profiling.so - stripto ./datadog-profiling/${architecture}-alpine-linux-musl/lib/php/${php_api}/datadog-profiling-zts.so \ - ${musl}/profiling/ext/${php_api}/datadog-profiling-zts.so + # Profiling-capable PHP versions use the self-contained combined artifact. + stripto ./combined-ddtrace/${architecture}-unknown-linux-gnu/lib/php/${php_api}/ddtrace.so \ + ${gnu}/trace/ext/${php_api}/ddtrace.so + stripto ./combined-ddtrace/${architecture}-unknown-linux-gnu/lib/php/${php_api}/ddtrace-zts.so \ + ${gnu}/trace/ext/${php_api}/ddtrace-zts.so + touch ${gnu}/trace/ext/${php_api}/.ddtrace.profiling + touch ${gnu}/trace/ext/${php_api}/.ddtrace-zts.profiling + + stripto ./combined-ddtrace/${architecture}-alpine-linux-musl/lib/php/${php_api}/ddtrace.so \ + ${musl}/trace/ext/${php_api}/ddtrace.so + stripto ./combined-ddtrace/${architecture}-alpine-linux-musl/lib/php/${php_api}/ddtrace-zts.so \ + ${musl}/trace/ext/${php_api}/ddtrace-zts.so + touch ${musl}/trace/ext/${php_api}/.ddtrace.profiling + touch ${musl}/trace/ext/${php_api}/.ddtrace-zts.profiling + else + # PHP 7.0 remains tracer-only. + stripto ./standalone_${architecture}/ddtrace-${php_api}.so ${gnu}/trace/ext/${php_api}/ddtrace.so + stripto ./standalone_${architecture}/ddtrace-${php_api}-zts.so ${gnu}/trace/ext/${php_api}/ddtrace-zts.so + stripto ./standalone_${architecture}/ddtrace-${php_api}-alpine.so ${musl}/trace/ext/${php_api}/ddtrace.so + stripto ./standalone_${architecture}/ddtrace-${php_api}-alpine-zts.so ${musl}/trace/ext/${php_api}/ddtrace-zts.so fi ######################## @@ -113,9 +111,10 @@ for architecture in "${architectures[@]}"; do stripto ./appsec_${architecture}/ddappsec-${php_api}-alpine-zts.so ${musl}/appsec/ext/${php_api}/ddappsec-zts.so done - # Trace - mkdir -p "${root}/trace" + # Trace and profiling licenses + mkdir -p "${root}/trace/profiling-licenses" cp -r ./src "${root}/trace/" + cp ./profiling/LICENSE* ./profiling/NOTICE "${root}/trace/profiling-licenses/" # AppSec mkdir -p "${root}/appsec/lib" "${root}/appsec/etc" diff --git a/tooling/bin/pecl-build b/tooling/bin/pecl-build index 4c82e6ec4ac..7b47bdc1a03 100755 --- a/tooling/bin/pecl-build +++ b/tooling/bin/pecl-build @@ -39,8 +39,5 @@ pkg_xml=${pkg_xml//'${changelog}'/${dd_changelog}} echo "$pkg_xml" > package.xml -# no profiling in pecl -sed -i 's/, "profiling"//' "Cargo.toml" - pear package-validate package.xml pear package diff --git a/tracer/ddtrace.c b/tracer/ddtrace.c index df9b73ef12c..03a43c95762 100644 --- a/tracer/ddtrace.c +++ b/tracer/ddtrace.c @@ -516,7 +516,7 @@ void ddtrace_rinit(void) { if (datadog_endpoint) { DDTRACE_G(agent_config_reader) = ddog_agent_remote_config_reader_for_endpoint(datadog_endpoint); } -#if !defined(_WIN32) && defined(DDTRACE) +#if !defined(_WIN32) && defined(TRACER) } else if (ddtrace_coms_agent_config_handle) { ddog_agent_remote_config_reader_for_anon_shm(ddtrace_coms_agent_config_handle, &DDTRACE_G(agent_config_reader)); #endif diff --git a/tracer/engine_hooks.c b/tracer/engine_hooks.c index e2b2eaea976..7ff4e2a97bb 100644 --- a/tracer/engine_hooks.c +++ b/tracer/engine_hooks.c @@ -35,6 +35,13 @@ profiling_notify_trace_finished_t profiling_notify_trace_finished = NULL; void (*profiling_interrupt_function)(zend_execute_data *) = NULL; +#ifdef PROFILING +extern void ddog_php_prof_interrupt_function(zend_execute_data *execute_data); +extern void datadog_profiling_notify_trace_finished(uint64_t local_root_span_id, + zai_str span_type, + zai_str resource); +#endif + // Check if the profiler is loaded, and if, so it will locate certain symbols so cross-product features can be enabled. void dd_search_for_profiling_symbols(void *arg) { zend_extension *extension = (zend_extension *)arg; @@ -55,9 +62,21 @@ void dd_search_for_profiling_symbols(void *arg) { } void ddtrace_fetch_profiling_symbols(void) { +#ifdef PROFILING + profiling_interrupt_function = ddog_php_prof_interrupt_function; + // Notification follows the profiler's request-level enabled state. + profiling_notify_trace_finished = NULL; +#else zend_llist_apply(&zend_extensions, dd_search_for_profiling_symbols); +#endif } +#ifdef PROFILING +void ddtrace_set_profiling_notify_enabled(bool enabled) { + profiling_notify_trace_finished = enabled ? datadog_profiling_notify_trace_finished : NULL; +} +#endif + void ddtrace_engine_hooks_minit(void) { _compile_minit(); diff --git a/tracer/engine_hooks.h b/tracer/engine_hooks.h index 5e8682b7f66..24e7bcbf206 100644 --- a/tracer/engine_hooks.h +++ b/tracer/engine_hooks.h @@ -16,6 +16,9 @@ ZEND_EXTERN_MODULE_GLOBALS(datadog) void ddtrace_engine_hooks_minit(void); void ddtrace_fetch_profiling_symbols(void); +#ifdef PROFILING +void ddtrace_set_profiling_notify_enabled(bool enabled); +#endif void ddtrace_engine_hooks_mshutdown(void); void ddtrace_compile_time_reset(void); diff --git a/tracer/tracer_api.h b/tracer/tracer_api.h index 032bff78a55..759db98333a 100644 --- a/tracer/tracer_api.h +++ b/tracer/tracer_api.h @@ -1,7 +1,7 @@ #include #include -#ifdef DDTRACE +#ifdef TRACER // Primary lifecycle int ddtrace_startup(void); void ddtrace_shutdown(void); @@ -19,6 +19,9 @@ void ddtrace_rinit_early(void); void ddtrace_rinit(void); void ddtrace_rshutdown(bool fast_shutdown); void ddtrace_post_deactivate(void); +#ifdef PROFILING +void ddtrace_set_profiling_notify_enabled(bool enabled); +#endif // fork handling void ddtrace_internal_handle_fork(void); @@ -39,7 +42,7 @@ void ddtrace_live_debugger_rshutdown(void); #endif // Miscellaneous stuff with fallback functions -#ifdef DDTRACE +#ifdef TRACER ddog_DynamicInstrumentationConfigState ddtrace_dynamic_instrumentation_state(void); void ddtrace_populate_span_data(ddtrace_span_data *span, zend_string **service, zend_string **env, zend_string **version); #else diff --git a/zend_abstract_interface/config/config.h b/zend_abstract_interface/config/config.h index 3a9c8d34f50..65d2b34f23a 100644 --- a/zend_abstract_interface/config/config.h +++ b/zend_abstract_interface/config/config.h @@ -18,7 +18,7 @@ typedef uint16_t zai_config_id; #include "config_ini.h" #include "config_stable_file.h" -#define ZAI_CONFIG_ENTRIES_COUNT_MAX 320 +#define ZAI_CONFIG_ENTRIES_COUNT_MAX 330 #define ZAI_CONFIG_NAMES_COUNT_MAX 4 #define ZAI_CONFIG_NAME_BUFSIZ 72 diff --git a/zend_abstract_interface/config/config_stable_file.c b/zend_abstract_interface/config/config_stable_file.c index c3b8dffbbe0..558dbb179b3 100644 --- a/zend_abstract_interface/config/config_stable_file.c +++ b/zend_abstract_interface/config/config_stable_file.c @@ -40,9 +40,20 @@ static void stable_config_entry_dtor(zval *el) { } void zai_config_stable_file_minit(void) { - // Resolve symbols at runtime, as they are not part of the AppSec extension - // but are provided by ddtrace if it is loaded. + // Resolve symbols at runtime for separate artifacts. A combined artifact + // binds directly because the functions are linked into the same library. if (!_ddog_library_configurator_new) { +#if defined(TRACER) && defined(PROFILING) + _ddog_library_configurator_new = ddog_library_configurator_new; + _ddog_library_configurator_with_local_path = ddog_library_configurator_with_local_path; + _ddog_library_configurator_with_fleet_path = ddog_library_configurator_with_fleet_path; + _ddog_library_configurator_with_detect_process_info = ddog_library_configurator_with_detect_process_info; + _ddog_library_configurator_get = ddog_library_configurator_get; + _ddog_library_config_source_to_string = ddog_library_config_source_to_string; + _ddog_library_config_drop = ddog_library_config_drop; + _ddog_Error_drop = ddog_Error_drop; + _ddog_library_configurator_drop = ddog_library_configurator_drop; +#else zend_module_entry *ext = NULL; ext = zend_hash_str_find_ptr(&module_registry, ZEND_STRL("ddtrace")); if (!ext) { @@ -61,6 +72,7 @@ void zai_config_stable_file_minit(void) { RESOLVE_SYMBOL(ddog_library_config_drop); RESOLVE_SYMBOL(ddog_Error_drop); RESOLVE_SYMBOL(ddog_library_configurator_drop); +#endif } ddog_Configurator *configurator = _ddog_library_configurator_new(false, DDOG_CHARSLICE_C("php")); From 971b95573c2235c166abde82bb390c317fbccf52 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Sun, 30 Aug 2026 07:01:52 -0600 Subject: [PATCH 02/47] fix: validate standalone profiler CI artifacts --- .gitlab/generate-profiler.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitlab/generate-profiler.php b/.gitlab/generate-profiler.php index 466501afa16..76900e5a4be 100644 --- a/.gitlab/generate-profiler.php +++ b/.gitlab/generate-profiler.php @@ -30,7 +30,6 @@ KUBERNETES_HELPER_MEMORY_REQUEST: 2Gi KUBERNETES_HELPER_MEMORY_LIMIT: 2Gi CARGO_TARGET_DIR: /mnt/ramdisk/cargo # ramdisk?? - libdir: /tmp/datadog-profiling parallel: matrix: - PHP_MAJOR_MINOR: *all_profiler_targets @@ -60,12 +59,16 @@ - '# Use if/then instead of `command -v switch-php && switch-php` — the && form exits 1 when switch-php is absent, which FF_ENABLE_BASH_EXIT_CODE_CHECK treats as a job failure' - if command -v switch-php > /dev/null 2>&1; then switch-php "${PHP_MAJOR_MINOR}"; fi - (cd ..; phpize && DDTRACE_PROFILING_FEATURES="debug_stats,stack_walking_tests,test,tracing,tracing-subscriber,trigger_time_sample" ./configure --disable-ddtrace-tracer --enable-ddtrace-profiling && make -j$(nproc)) + - test -f "${CI_PROJECT_DIR}/modules/datadog-profiling.so" || { echo "ERROR standalone profiler build did not produce modules/datadog-profiling.so"; find "${CI_PROJECT_DIR}/modules" -maxdepth 1 -type f -print; exit 1; } + - php -d "extension=${CI_PROJECT_DIR}/modules/datadog-profiling.so" -r 'if (!extension_loaded("datadog-profiling") || ini_get("datadog.profiling.enabled") === false) { exit(1); }' - (cd ../; TEST_PHP_JUNIT="${CI_PROJECT_DIR}/artifacts/profiler-tests/nts-results.xml" php profiling/tests/run-tests.php -d "extension=${CI_PROJECT_DIR}/modules/datadog-profiling.so" --show-diff -g "FAIL,XFAIL,BORK,WARN,LEAK,XLEAK,SKIP" "profiling/tests/phpt") - '# ZTS' - if command -v switch-php > /dev/null 2>&1; then switch-php "${PHP_MAJOR_MINOR}-zts"; fi - (cd ..; make distclean || true; phpize && DDTRACE_PROFILING_FEATURES="debug_stats,stack_walking_tests,test,tracing,tracing-subscriber,trigger_time_sample" ./configure --disable-ddtrace-tracer --enable-ddtrace-profiling && make -j$(nproc)) + - test -f "${CI_PROJECT_DIR}/modules/datadog-profiling.so" || { echo "ERROR standalone profiler ZTS build did not produce modules/datadog-profiling.so"; find "${CI_PROJECT_DIR}/modules" -maxdepth 1 -type f -print; exit 1; } + - php -d "extension=${CI_PROJECT_DIR}/modules/datadog-profiling.so" -r 'if (!extension_loaded("datadog-profiling") || ini_get("datadog.profiling.enabled") === false) { exit(1); }' - (cd ../; TEST_PHP_JUNIT="${CI_PROJECT_DIR}/artifacts/profiler-tests/zts-results.xml" php profiling/tests/run-tests.php -d "extension=${CI_PROJECT_DIR}/modules/datadog-profiling.so" --show-diff -g "FAIL,XFAIL,BORK,WARN,LEAK,XLEAK,SKIP" "profiling/tests/phpt") after_script: - | @@ -95,7 +98,6 @@ KUBERNETES_HELPER_MEMORY_REQUEST: 2Gi KUBERNETES_HELPER_MEMORY_LIMIT: 2Gi # CARGO_TARGET_DIR: /mnt/ramdisk/cargo # ramdisk?? - libdir: /tmp/datadog-profiling parallel: matrix: - PHP_MAJOR_MINOR: *all_profiler_targets @@ -117,7 +119,6 @@ KUBERNETES_HELPER_MEMORY_REQUEST: 2Gi KUBERNETES_HELPER_MEMORY_LIMIT: 2Gi # CARGO_TARGET_DIR: /mnt/ramdisk/cargo # ramdisk?? - libdir: /tmp/datadog-profiling script: - switch-php nts - PHP_CONFIG="$(realpath "$(command -v php-config)")" cargo test --no-default-features --features profiling,test,debug_stats,stack_walking_tests,tracing,tracing-subscriber,trigger_time_sample @@ -138,7 +139,6 @@ KUBERNETES_HELPER_MEMORY_REQUEST: 2Gi KUBERNETES_HELPER_MEMORY_LIMIT: 2Gi CARGO_TARGET_DIR: /tmp/cargo - libdir: /tmp/datadog-profiling SKIP_ONLINE_TESTS: "1" REPORT_EXIT_STATUS: "1" TEST_PHP_JUNIT: "${CI_PROJECT_DIR}/artifacts/tests/php-tests.xml" From ca6a1d19a3c6028a395afacce1ca12c896f8ac0a Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Sun, 30 Aug 2026 11:58:18 -0600 Subject: [PATCH 03/47] fix: use configured PHP metadata in Rust builds --- .gitlab/generate-profiler.php | 6 +- .gitlab/generate-tracer.php | 8 +- benchmark/runall.sh | 4 +- components-rs/config_codegen.rs | 33 +++--- config.m4 | 10 +- config.w32 | 4 + profiling/README.md | 13 ++- profiling/build.rs | 184 +++++++++----------------------- 8 files changed, 94 insertions(+), 168 deletions(-) diff --git a/.gitlab/generate-profiler.php b/.gitlab/generate-profiler.php index 76900e5a4be..c640cb00227 100644 --- a/.gitlab/generate-profiler.php +++ b/.gitlab/generate-profiler.php @@ -103,7 +103,7 @@ - PHP_MAJOR_MINOR: *all_profiler_targets script: - switch-php nts # not compatible with debug - - cargo clippy --all-targets --no-default-features --features profiling,test,debug_stats,stack_walking_tests,tracing,tracing-subscriber,trigger_time_sample -- -D warnings -Aunknown-lints + - DDTRACE_PHP_INCLUDES="$(php-config --includes)" cargo clippy --all-targets --no-default-features --features profiling,test,debug_stats,stack_walking_tests,tracing,tracing-subscriber,trigger_time_sample -- -D warnings -Aunknown-lints "Cargo test": stage: test @@ -121,9 +121,9 @@ # CARGO_TARGET_DIR: /mnt/ramdisk/cargo # ramdisk?? script: - switch-php nts - - PHP_CONFIG="$(realpath "$(command -v php-config)")" cargo test --no-default-features --features profiling,test,debug_stats,stack_walking_tests,tracing,tracing-subscriber,trigger_time_sample + - DDTRACE_PHP_INCLUDES="$(php-config --includes)" cargo test --no-default-features --features profiling,test,debug_stats,stack_walking_tests,tracing,tracing-subscriber,trigger_time_sample - switch-php zts - - PHP_CONFIG="$(realpath "$(command -v php-config)")" cargo test --no-default-features --features profiling,test,debug_stats,stack_walking_tests,tracing,tracing-subscriber,trigger_time_sample + - DDTRACE_PHP_INCLUDES="$(php-config --includes)" cargo test --no-default-features --features profiling,test,debug_stats,stack_walking_tests,tracing,tracing-subscriber,trigger_time_sample "PHP language tests": stage: test diff --git a/.gitlab/generate-tracer.php b/.gitlab/generate-tracer.php index 26e8045b2d4..721d9b96c10 100644 --- a/.gitlab/generate-tracer.php +++ b/.gitlab/generate-tracer.php @@ -141,11 +141,14 @@ function before_script_steps($with_docker_auth = false) { docker exec ${CONTAINER_NAME} powershell.exe -Command "`$ErrorActionPreference='Stop'; Set-ItemProperty -Path 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\FileSystem' -Name LongPathsEnabled -Value 1 -Type DWord" if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # local registry tweak, not network — fail fast (no retry) - # Build nts + # Build nts. PowerShell does not automatically fail when a native command + # returns non-zero, so propagate docker/nmake failures explicitly. docker exec ${CONTAINER_NAME} powershell.exe "cd app; switch-php nts; C:\php\SDK\phpize.bat; .\configure.bat --enable-debug-pack; nmake" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Set test environment variables docker exec ${CONTAINER_NAME} powershell.exe "setx DD_AUTOLOAD_NO_COMPILE true; setx DATADOG_HAVE_DEV_ENV 1; setx DD_TRACE_GIT_METADATA_ENABLED 0" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Exclude tests that deadlock the php-cgi SKIPIF skip-task on Windows. # Run extension tests - docker exec ${CONTAINER_NAME} powershell.exe 'cd app; $env:_DD_DEBUG_SIDECAR_LOG_LEVEL=trace; $env:_DD_DEBUG_SIDECAR_LOG_METHOD="""file://${pwd}\sidecar.log"""; C:\php\php.exe -n -d memory_limit=-1 -d output_buffering=0 run-tests.php -g FAIL,XFAIL,BORK,WARN,LEAK,XLEAK,SKIP --show-diff -p C:\php\php.exe -d "extension=${pwd}\x64\Release\php_ddtrace.dll" "${pwd}\tests\ext"' + docker exec --env _DD_DEBUG_SIDECAR_LOG_LEVEL=trace --env _DD_DEBUG_SIDECAR_LOG_METHOD=file://C:/Users/ContainerAdministrator/app/sidecar.log ${CONTAINER_NAME} powershell.exe 'cd app; C:\php\php.exe -n -d memory_limit=-1 -d output_buffering=0 run-tests.php -g FAIL,XFAIL,BORK,WARN,LEAK,XLEAK,SKIP --show-diff -p C:\php\php.exe -d "extension=${pwd}\x64\Release\php_ddtrace.dll" "${pwd}\tests\ext"' + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } after_script: - | docker exec ${CONTAINER_NAME} cmd.exe /s /c xcopy /y /c /s /e C:\ProgramData\Microsoft\Windows\WER\ReportQueue .\app\dumps\ diff --git a/benchmark/runall.sh b/benchmark/runall.sh index 4e60be6c967..ada31ddf01b 100755 --- a/benchmark/runall.sh +++ b/benchmark/runall.sh @@ -12,8 +12,8 @@ if [ "$SCENARIO" = "profiler" ]; then sirun benches/exceptions.json > "$ARTIFACTS_DIR/sirun_exceptions.ndjson" - PHP_CONFIG="$(command -v php-config)" cargo bench --no-default-features \ - --features profiling,test,stack_walking_tests -- --noplot + DDTRACE_PHP_INCLUDES="$(php-config --includes)" \ + cargo bench --no-default-features --features profiling,test,stack_walking_tests -- --noplot elif [ "$SCENARIO" = "tracer" ]; then # Run Trace Benchmarks cd .. diff --git a/components-rs/config_codegen.rs b/components-rs/config_codegen.rs index 340dd5719e4..fb383009786 100644 --- a/components-rs/config_codegen.rs +++ b/components-rs/config_codegen.rs @@ -50,7 +50,7 @@ pub fn build() { if env::var_os("CARGO_FEATURE_PROFILING").is_some() { cc_build.define("PROFILING", None); } - for inc in php_config_includes() { + for inc in php_include_dirs() { cc_build.include(inc); } @@ -94,20 +94,25 @@ pub fn build() { } } -fn php_config_includes() -> Vec { - let php_config = env::var("PHP_CONFIG").unwrap_or_else(|_| "php-config".to_string()); - let output = std::process::Command::new(&php_config) - .arg("--includes") - .output() - .unwrap_or_else(|e| panic!("failed to run `{php_config} --includes`: {e}")); - if !output.status.success() { - panic!( - "`{php_config} --includes` failed: {}", - String::from_utf8_lossy(&output.stderr) - ); +fn php_include_dirs() -> Vec { + println!("cargo:rerun-if-env-changed=DDTRACE_PHP_INCLUDES"); + if let Ok(includes) = env::var("DDTRACE_PHP_INCLUDES") { + return parse_include_flags(&includes); } - String::from_utf8(output.stdout) - .expect("php-config output was not valid UTF-8") + + // php-config does not exist in the Windows SDK. config.w32 exports the + // PHP build's CFLAGS for cc-rs instead, including the PHP header paths. + if env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") { + return Vec::new(); + } + + panic!( + "DDTRACE_PHP_INCLUDES is required; loadable extensions must pass Make's INCLUDES to Cargo" + ); +} + +fn parse_include_flags(flags: &str) -> Vec { + flags .split_whitespace() .filter_map(|flag| flag.strip_prefix("-I")) .map(str::to_string) diff --git a/config.m4 b/config.m4 index ac8fb22b219..feb684d0b5a 100644 --- a/config.m4 +++ b/config.m4 @@ -29,12 +29,6 @@ PHP_ARG_ENABLE(ddtrace-rust-debug, whether to compile rust in debug mode, PHP_ARG_ENABLE(ddtrace-rust-library-split, whether to not link the rust library against the extension at compile time, [ --enable-ddtrace-rust-library-split Do not build nor link against the rust code], no, no) -DDTRACE_PHP_CONFIG=$(command -v "$PHP_CONFIG" 2>/dev/null || echo "$PHP_CONFIG") -if command -v realpath >/dev/null 2>&1; then - DDTRACE_PHP_CONFIG=$(realpath "$DDTRACE_PHP_CONFIG") -fi -PHP_SUBST(DDTRACE_PHP_CONFIG) - dnl Keep make clean out of Cargo targets; distclean removes all product-specific targets. AC_DEFUN([DDTRACE_GEN_GLOBAL_MAKEFILE_WRAP], [ pushdef([PHP_GEN_GLOBAL_MAKEFILE], [ @@ -96,7 +90,7 @@ if test "$PHP_DDTRACE_PROFILING" != "no"; then cat <> Makefile.fragments $profiler_rust_lib: \$(shell for dir in \$(srcdir)/components-rs \$(srcdir)/profiling \$(srcdir)/libdatadog \$(srcdir)/../../libdatadog; do test ! -d \$\$dir || find \$\$dir \( -type f -o -type l \) \( -name '*.rs' -o -name '*.c' -o -name '*.h' -o -name Cargo.toml \) -not -path '*/target/*' -not -path '*/.git/*'; done) \$(srcdir)/ext/configuration.h \$(srcdir)/ext/configuration_helpers.h \$(srcdir)/ext/configuration_shared.h \$(srcdir)/tracer/configuration.h \$(srcdir)/ext/handlers_api.c \$(srcdir)/ext/handlers_api.h \$(srcdir)/Cargo.toml \$(srcdir)/Cargo.lock \$(srcdir)/VERSION - (cd "\$(srcdir)"; PHP_CONFIG="\$(DDTRACE_PHP_CONFIG)" CARGO_TARGET_DIR="$profiler_target_dir" RUSTFLAGS="\$(RUSTFLAGS) --cfg tokio_unstable" "\$(DDTRACE_CARGO)" build $DDTRACE_PROFILING_CARGO_BUILD_FLAGS $profiler_target_arg --no-default-features --features "profiling${DDTRACE_PROFILING_FEATURES:+,$DDTRACE_PROFILING_FEATURES}" $(test "$profiler_cargo_profile" = debug || echo --profile "$profiler_cargo_profile") \$(shell echo "\$(MAKEFLAGS)" | $EGREP -o "[[-]]j[[0-9]]+")) + (cd "\$(srcdir)"; DDTRACE_PHP_INCLUDES="\$(INCLUDES)" CARGO_TARGET_DIR="$profiler_target_dir" RUSTFLAGS="\$(RUSTFLAGS) --cfg tokio_unstable" "\$(DDTRACE_CARGO)" build $DDTRACE_PROFILING_CARGO_BUILD_FLAGS $profiler_target_arg --no-default-features --features "profiling${DDTRACE_PROFILING_FEATURES:+,$DDTRACE_PROFILING_FEATURES}" $(test "$profiler_cargo_profile" = debug || echo --profile "$profiler_cargo_profile") \$(shell echo "\$(MAKEFLAGS)" | $EGREP -o "[[-]]j[[0-9]]+")) \$(phplibdir)/datadog-profiling.la: $profiler_rust_lib @mkdir -p \$(phplibdir)/.libs @@ -551,7 +545,7 @@ EOT cat <> Makefile.fragments $ddtrace_rust_lib: $( (find "$ext_srcdir/components-rs" -name "*.rs"; find "$ext_srcdir/profiling" \( -name "*.rs" -o -name "*.c" -o -name "*.h" \); find "$ext_srcdir/zend_abstract_interface" \( -name "*.c" -o -name "*.h" \); find "$ext_srcdir" -maxdepth 1 -name "Cargo.toml"; find "$ext_srcdir/../../libdatadog" -name "*.rs" -not -path "*/target/*"; find "$ext_srcdir/libdatadog" -name "*.rs" -not -path "*/target/*") 2>/dev/null | tr '\n' ' ' ) $ext_srcdir/ext/configuration.h $ext_srcdir/ext/configuration_helpers.h $ext_srcdir/ext/configuration_shared.h $ext_srcdir/tracer/configuration.h - (cd "$ext_srcdir"; PHP_CONFIG="\$(DDTRACE_PHP_CONFIG)" CARGO_FEATURES="$ddtrace_cargo_features" CARGO_TARGET_DIR="$ddtrace_target_dir/" SHARED=$(test "$ext_shared" = "yes" && echo 1) PROFILE="$ddtrace_cargo_profile" host_os="$host_os" DDTRACE_CARGO="\$(DDTRACE_CARGO)" $(if test "$PHP_DDTRACE_SANITIZE" != "no"; then echo COMPILE_ASAN=1; fi) sh ./compile_rust.sh $ddtrace_cargo_build_flags \$(shell echo "\$(MAKEFLAGS)" | $EGREP -o "[[-]]j[[0-9]]+")) + (cd "$ext_srcdir"; DDTRACE_PHP_INCLUDES="\$(INCLUDES)" CARGO_FEATURES="$ddtrace_cargo_features" CARGO_TARGET_DIR="$ddtrace_target_dir/" SHARED=$(test "$ext_shared" = "yes" && echo 1) PROFILE="$ddtrace_cargo_profile" host_os="$host_os" DDTRACE_CARGO="\$(DDTRACE_CARGO)" $(if test "$PHP_DDTRACE_SANITIZE" != "no"; then echo COMPILE_ASAN=1; fi) sh ./compile_rust.sh $ddtrace_cargo_build_flags \$(shell echo "\$(MAKEFLAGS)" | $EGREP -o "[[-]]j[[0-9]]+")) EOT fi diff --git a/config.w32 b/config.w32 index ab56b01f556..921bc3e566c 100644 --- a/config.w32 +++ b/config.w32 @@ -180,6 +180,10 @@ if (PHP_DDTRACE != 'no') { // Promote to env var frag_out.WriteLine("!if [set CARGO_TARGET_DIR=$(CARGO_TARGET_DIR)]"); frag_out.WriteLine("!endif"); + // cc-rs needs the PHP SDK include flags while preprocessing the generated + // configuration table; Windows does not provide php-config. + frag_out.WriteLine("!if [set CFLAGS=$(CFLAGS) $(CFLAGS_DDTRACE)]"); + frag_out.WriteLine("!endif"); frag_out.WriteLine("!if [set RUSTFLAGS=--cfg windows_seh_wrapper]"); // Not supported yet on Windows: --cfg tokio_unstable --cfg tokio_taskdump frag_out.WriteLine("!endif"); diff --git a/profiling/README.md b/profiling/README.md index e62d014a94b..a62b493b180 100644 --- a/profiling/README.md +++ b/profiling/README.md @@ -49,9 +49,15 @@ cases where they are ABI compatible. ## Testing -From the repository root, `cargo test --no-default-features --features profiling,test` -runs the Rust tests. To also run the stack walking tests, add the -`stack_walking_tests` feature. +From the repository root, Rust tests can be run directly when the selected PHP +toolchain is supplied explicitly: + +```sh +DDTRACE_PHP_INCLUDES="$(php-config --includes)" \ +cargo test --no-default-features --features profiling,test +``` + +To also run the stack walking tests, add the `stack_walking_tests` feature. To inspect a standalone profiler, run `/path/to/php -d extension=modules/datadog-profiling.so --ri datadog-profiling`. @@ -86,6 +92,7 @@ Benchmarks are implemented using execute them from the repository root using: ```sh +DDTRACE_PHP_INCLUDES="$(php-config --includes)" \ cargo bench --no-default-features --features profiling,test,stack_walking_tests ``` diff --git a/profiling/build.rs b/profiling/build.rs index c445b0bdf5c..84b0bcf3bab 100644 --- a/profiling/build.rs +++ b/profiling/build.rs @@ -2,34 +2,15 @@ use bindgen::callbacks::IntKind; use std::collections::HashSet; use std::path::Path; use std::path::PathBuf; -use std::process::Command; use std::{env, fs}; -fn php_config() -> std::ffi::OsString { - env::var_os("PHP_CONFIG").unwrap_or_else(|| "php-config".into()) -} - pub fn build() { - // Configure resolves PHP_CONFIG to the selected toolchain's real path, so - // switching PHP builds invalidates Cargo's cached bindings and C probes. - println!("cargo:rerun-if-env-changed=PHP_CONFIG"); - - let php_config_includes_output = Command::new(php_config()) - .arg("--includes") - .output() - .expect( - "Unable to run `php-config`. Set PHP_CONFIG to the php-config selected by configure.", - ); - - if !php_config_includes_output.status.success() { - match String::from_utf8(php_config_includes_output.stderr) { - Ok(stderr) => panic!("`php-config failed: {stderr}"), - Err(err) => panic!( - "`php-config` failed, not utf8: {}", - String::from_utf8_lossy(err.as_bytes()) - ), - } - } + // Make owns PHP toolchain selection and passes its generated include flags + // into Cargo. Do not rediscover a potentially different PHP installation. + println!("cargo:rerun-if-env-changed=DDTRACE_PHP_INCLUDES"); + let php_includes = env::var("DDTRACE_PHP_INCLUDES") + .expect("DDTRACE_PHP_INCLUDES is required; build through phpize/configure/Make"); + let php_include_root = php_include_root(&php_includes); // Read the version from the VERSION file let version = fs::read_to_string("VERSION") @@ -39,10 +20,7 @@ pub fn build() { println!("cargo:rustc-env=PROFILER_VERSION={version}"); println!("cargo:rerun-if-changed=VERSION"); - let php_config_includes = std::str::from_utf8(php_config_includes_output.stdout.as_slice()) - .expect("`php-config`'s stdout to be valid utf8"); - - let vernum = php_config_vernum(); + let vernum = php_version_id(&php_include_root); let post_startup_cb = cfg_post_startup_cb(vernum); let preload = cfg_preload(vernum); let fibers = cfg_fibers(vernum); @@ -51,9 +29,9 @@ pub fn build() { let trigger_time_sample = cfg_trigger_time_sample(); let zend_error_observer = cfg_zend_error_observer(vernum); - generate_bindings(php_config_includes, fibers, zend_error_observer); + generate_bindings(&php_includes, fibers, zend_error_observer); build_zend_php_ffis( - php_config_includes, + &php_includes, post_startup_cb, preload, run_time_cache, @@ -65,32 +43,40 @@ pub fn build() { cfg_php_major_version(vernum); cfg_php_feature_flags(vernum); - cfg_php_build(); + cfg_php_build(&php_include_root); apple_linker_flags(); } -fn php_config_vernum() -> u64 { - let output = Command::new(php_config()).arg("--vernum").output().expect( - "Unable to run `php-config`. Set PHP_CONFIG to the php-config selected by configure.", - ); - - if !output.status.success() { - match String::from_utf8(output.stderr) { - Ok(stderr) => panic!("`php-config --vernum` failed: {stderr}"), - Err(err) => panic!( - "`php-config --vernum` failed, not utf8: {}", - String::from_utf8_lossy(err.as_bytes()) - ), - } - } +fn php_include_root(includes: &str) -> PathBuf { + includes + .split_whitespace() + .filter_map(|flag| flag.strip_prefix("-I")) + .map(PathBuf::from) + .find(|path| { + path.join("main/php_version.h").is_file() && path.join("main/php_config.h").is_file() + }) + .unwrap_or_else(|| { + panic!("DDTRACE_PHP_INCLUDES does not contain the PHP include root: {includes}") + }) +} - let vernum = std::str::from_utf8(output.stdout.as_slice()) - .expect("`php-config`'s stdout to be valid utf8"); +fn php_version_id(include_root: &Path) -> u64 { + let version_header = include_root.join("main/php_version.h"); + println!("cargo:rerun-if-changed={}", version_header.display()); + let contents = fs::read_to_string(&version_header).expect("failed to read main/php_version.h"); + macro_value(&contents, "PHP_VERSION_ID") + .expect("PHP_VERSION_ID is missing from main/php_version.h") + .parse() + .expect("PHP_VERSION_ID is not an integer") +} - vernum - .trim() - .parse::() - .expect("output to be a number and fit in u64") +fn macro_value<'a>(contents: &'a str, name: &str) -> Option<&'a str> { + contents.lines().find_map(|line| { + let rest = line.trim().strip_prefix("#define ")?; + let split_at = rest.find(char::is_whitespace)?; + let (macro_name, value) = rest.split_at(split_at); + (macro_name == name).then(|| value.trim()) + }) } const ZAI_H_FILES: &[&str] = &[ @@ -109,7 +95,7 @@ const ZAI_H_FILES: &[&str] = &[ #[allow(clippy::too_many_arguments)] fn build_zend_php_ffis( - php_config_includes: &str, + php_includes: &str, post_startup_cb: bool, preload: bool, run_time_cache: bool, @@ -143,16 +129,6 @@ fn build_zend_php_ffis( println!("cargo:rerun-if-changed={file}"); } - let output = Command::new(php_config()).arg("--prefix").output().expect( - "Unable to run `php-config`. Set PHP_CONFIG to the php-config selected by configure.", - ); - - let prefix = String::from_utf8(output.stdout).expect("only utf8 chars work"); - println!( - "cargo:rustc-link-search=native={prefix}/lib", - prefix = prefix.trim() - ); - let files = ["profiling/src/php_ffi.c", "ext/handlers_api.c"]; let post_startup_cb = if post_startup_cb { "1" } else { "0" }; let preload = if preload { "1" } else { "0" }; @@ -189,7 +165,7 @@ fn build_zend_php_ffis( .define("PROFILING", None) .includes([Path::new("ext")]) .includes( - str::replace(php_config_includes, "-I", "") + str::replace(php_includes, "-I", "") .split(' ') .map(Path::new) .chain([Path::new("zend_abstract_interface")]) @@ -232,7 +208,7 @@ impl bindgen::callbacks::ParseCallbacks for IgnoreMacros { } } -fn generate_bindings(php_config_includes: &str, fibers: bool, zend_error_observer: bool) { +fn generate_bindings(php_includes: &str, fibers: bool, zend_error_observer: bool) { println!("cargo:rerun-if-changed=profiling/src/php_ffi.h"); println!("cargo:rerun-if-changed=ext/handlers_api.h"); let ignored_macros = IgnoreMacros( @@ -310,7 +286,7 @@ fn generate_bindings(php_config_includes: &str, fibers: bool, zend_error_observe .rustified_enum("datadog_php_profiling_log_level") .rustified_enum("zai_config_type") .parse_callbacks(Box::new(ignored_macros)) - .clang_args(php_config_includes.split(' ')) + .clang_args(php_includes.split(' ')) .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) .layout_tests(false) // this prevents bindgen from copying C comments to Rust, as otherwise @@ -430,80 +406,16 @@ fn cfg_php_feature_flags(vernum: u64) { } } -fn cfg_php_build() { +fn cfg_php_build(include_root: &Path) { println!("cargo::rustc-check-cfg=cfg(php_zts)"); println!("cargo::rustc-check-cfg=cfg(php_debug)"); - let output = Command::new(php_config()) - .arg("--include-dir") - .output() - .expect( - "Unable to run `php-config`. Set PHP_CONFIG to the php-config selected by configure.", - ); - - if !output.status.success() { - match String::from_utf8(output.stderr) { - Ok(stderr) => panic!("`php-config --include-dir` failed: {stderr}"), - Err(err) => panic!("`php-config --include-dir` failed, not utf8: {err}"), - } - } - - let include_dir = std::str::from_utf8(output.stdout.as_slice()) - .expect("`php-config`'s stdout to be valid utf8") - .trim(); - - let out_dir = env::var("OUT_DIR").unwrap(); - let probe_path = Path::new(&out_dir).join("php_build_probe.c"); - fs::write( - &probe_path, - r#" -#include "main/php_config.h" -#include -int main() { -#ifdef ZTS - printf("1"); -#else - printf("0"); -#endif -#if ZEND_DEBUG - printf("1"); -#else - printf("0"); -#endif - return 0; -} -"#, - ) - .expect("Failed to write PHP build probe file"); - - let compiler = cc::Build::new().get_compiler(); - let probe_exe = Path::new(&out_dir).join("php_build_probe"); - let compile_status = Command::new(compiler.path()) - .arg(format!("-I{}", include_dir)) - .arg(&probe_path) - .arg("-o") - .arg(&probe_exe) - .status() - .expect("Failed to compile PHP build probe"); - - if !compile_status.success() { - panic!("Failed to compile PHP build probe"); - } + let config_header = include_root.join("main/php_config.h"); + println!("cargo:rerun-if-changed={}", config_header.display()); + let contents = fs::read_to_string(&config_header).expect("failed to read main/php_config.h"); - let probe_output = Command::new(&probe_exe) - .output() - .expect("Failed to run PHP build probe"); - - let probe_value = std::str::from_utf8(&probe_output.stdout) - .expect("PHP build probe output not UTF-8") - .trim(); - let (zts, debug) = match probe_value { - "00" => (false, false), - "01" => (false, true), - "10" => (true, false), - "11" => (true, true), - _ => panic!("Unexpected PHP build probe output: {probe_value:?}"), - }; + let zts = macro_value(&contents, "ZTS").is_some_and(|value| value != "0"); + let debug = macro_value(&contents, "ZEND_DEBUG").is_some_and(|value| value != "0"); if zts { println!("cargo:rustc-cfg=php_zts"); From 602408346aa832a796710597244a9736d22ab8b8 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Sun, 30 Aug 2026 12:27:25 -0600 Subject: [PATCH 04/47] fix: generate combined configuration metadata --- metadata/supported-configurations.json | 29 +++- tooling/generate-supported-configurations.sh | 163 +++---------------- 2 files changed, 45 insertions(+), 147 deletions(-) diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 20f598cbc02..3c456e60994 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -547,7 +547,10 @@ { "implementation": "B", "type": "boolean", - "default": "true" + "default": "true", + "aliases": [ + "DD_PROFILING_EXPERIMENTAL_ALLOCATION_ENABLED" + ] } ], "DD_PROFILING_ALLOCATION_SAMPLING_DISTANCE": [ @@ -561,7 +564,7 @@ { "implementation": "D", "type": "boolean", - "default": "true" + "default": "false" } ], "DD_PROFILING_ENDPOINT_COLLECTION_ENABLED": [ @@ -575,7 +578,10 @@ { "implementation": "A", "type": "boolean", - "default": "true" + "default": "true", + "aliases": [ + "DD_PROFILING_EXPERIMENTAL_EXCEPTION_ENABLED" + ] } ], "DD_PROFILING_EXCEPTION_MESSAGE_ENABLED": [ @@ -589,7 +595,10 @@ { "implementation": "A", "type": "int", - "default": "100" + "default": "100", + "aliases": [ + "DD_PROFILING_EXPERIMENTAL_EXCEPTION_SAMPLING_DISTANCE" + ] } ], "DD_PROFILING_EXPERIMENTAL_CPU_TIME_ENABLED": [ @@ -609,6 +618,13 @@ "default": "false" } ], + "DD_PROFILING_EXPERIMENTAL_HEAP_LIVE_ENABLED": [ + { + "implementation": "A", + "type": "boolean", + "default": "false" + } + ], "DD_PROFILING_EXPERIMENTAL_IO_ENABLED": [ { "implementation": "A", @@ -634,7 +650,10 @@ { "implementation": "A", "type": "boolean", - "default": "true" + "default": "true", + "aliases": [ + "DD_PROFILING_EXPERIMENTAL_TIMELINE_ENABLED" + ] } ], "DD_PROFILING_WALLTIME_ENABLED": [ diff --git a/tooling/generate-supported-configurations.sh b/tooling/generate-supported-configurations.sh index df1479c7746..2bcc4c7ad1f 100755 --- a/tooling/generate-supported-configurations.sh +++ b/tooling/generate-supported-configurations.sh @@ -9,17 +9,21 @@ readonly CONFIG_HEADER_FILES=( "ext/configuration.h" "appsec/src/extension/configuration.h" ) +readonly CONFIG_DEPENDENCY_FILES=( + "ext/configuration_shared.h" + "tracer/configuration.h" + "profiling/configuration.h" +) readonly OTEL_CONFIG_FILES=( "ext/otel_config.c" "tracer/tracer_otel_config.c" "src/DDTrace/OpenTelemetry/Configuration.php" ) -readonly PROFILING_CONFIG_FILE="profiling/src/config.rs" readonly GENERATOR_SCRIPT_FILE="tooling/generate-supported-configurations.sh" readonly CONFIG_GENERATION_INPUT_FILES=( "${CONFIG_HEADER_FILES[@]}" + "${CONFIG_DEPENDENCY_FILES[@]}" "${OTEL_CONFIG_FILES[@]}" - "${PROFILING_CONFIG_FILE}" "${GENERATOR_SCRIPT_FILE}" ) @@ -38,14 +42,15 @@ trap 'rm -f "$PHP_CODE_FILE"' EXIT cat >"$PHP_CODE_FILE" <<'ENDPHP' 'boolean', - 'ZAI_CONFIG_TYPE_STRING' => 'string', - 'ZAI_CONFIG_TYPE_INT' => 'int', - 'ZAI_CONFIG_TYPE_DOUBLE' => 'decimal', - 'ZAI_CONFIG_TYPE_MAP' => 'map', - 'ZAI_CONFIG_TYPE_JSON' => 'array', - 'ZAI_CONFIG_TYPE_SET' => 'array', - 'ZAI_CONFIG_TYPE_SET_LOWERCASE' => 'array', - 'ZAI_CONFIG_TYPE_SET_OR_MAP_LOWERCASE' => 'map', - ]; - if (isset($map[$rawType])) { - return $map[$rawType]; - } - if ($rawType === 'ZAI_CONFIG_TYPE_CUSTOM') { - if ($parser === 'parse_profiling_enabled') { - return 'boolean'; - } - if ($parser === 'parse_sampling_distance_filter') { - return 'int'; - } - return 'string'; - } - return 'string'; -} - -function parse_rust_default($raw) { - $raw = trim(preg_replace('/\/\/.*$/', '', $raw)); - if ($raw === 'ZaiStr::new()') { - return ''; - } - if (preg_match('/ZaiStr::literal\(b"((?:\\\\.|[^"\\\\])*)\\\\0"\)/', $raw, $m)) { - return stripcslashes($m[1]); - } - return ''; -} - -function extract_rust_alias_groups($source) { - $groups = []; - if (preg_match_all('/const\s+([A-Z0-9_]+)\s*:\s*&\[ZaiStr\]\s*=\s*unsafe\s*\{\s*&\[(.*?)\]\s*\};/s', $source, $matches, PREG_SET_ORDER)) { - foreach ($matches as $match) { - $aliases = []; - if (preg_match_all('/ZaiStr::literal\(b"((?:\\\\.|[^"\\\\])*)\\\\0"\)/', $match[2], $aliasMatches)) { - foreach ($aliasMatches[1] as $alias) { - $aliases[] = stripcslashes($alias); - } - } - $groups[$match[1]] = $aliases; - } - } - return $groups; -} - -function extract_rust_env_var_names($source) { - $names = []; - if (preg_match_all('/([A-Za-z0-9_]+)\s*=>\s*b"([A-Z0-9_]+)\\\\0"/', $source, $matches, PREG_SET_ORDER)) { - foreach ($matches as $match) { - $names[$match[1]] = $match[2]; - } - } - return $names; -} - -function add_rust_profiling_configurations(&$supported, $path) { - if (!file_exists($path)) { - return; - } - $source = file_get_contents($path); - if ($source === false || $source === '') { - return; - } - - $envVarByConfigId = extract_rust_env_var_names($source); - if (empty($envVarByConfigId)) { - return; - } - $aliasesByConstName = extract_rust_alias_groups($source); - - if (!preg_match_all('/zai_config_entry\s*\{(.*?)\n\s*},/s', $source, $entryMatches, PREG_SET_ORDER)) { - return; - } - - foreach ($entryMatches as $entryMatch) { - $entryBlock = $entryMatch[1]; - if (!preg_match('/name:\s*([A-Za-z0-9_]+)\.env_var_name\(\),/', $entryBlock, $nameMatch)) { - continue; - } - $configId = $nameMatch[1]; - if (!isset($envVarByConfigId[$configId])) { - continue; - } - $name = $envVarByConfigId[$configId]; - - if (!preg_match('/type_:\s*(ZAI_CONFIG_TYPE_[A-Z_]+),/', $entryBlock, $typeMatch)) { - continue; - } - if (!preg_match('/default_encoded_value:\s*([^\n]+),/', $entryBlock, $defaultMatch)) { - continue; - } - - $parser = ''; - if (preg_match('/parser:\s*([^\n,]+),/', $entryBlock, $parserMatch)) { - $parserRaw = trim($parserMatch[1]); - if (preg_match('/Some\(([^)]+)\)/', $parserRaw, $parserNameMatch)) { - $parser = trim($parserNameMatch[1]); - } - } - - $aliases = []; - if (preg_match('/aliases:\s*([A-Z0-9_]+)\.as_ptr\(\),/', $entryBlock, $aliasesMatch)) { - $aliases = $aliasesByConstName[$aliasesMatch[1]] ?? []; - } - - $mappedType = map_rust_type(trim($typeMatch[1]), $parser); - $entry = [ - "implementation" => "A", - "type" => $mappedType, - "default" => normalize_default(parse_rust_default($defaultMatch[1]), $mappedType, $name), - ]; - $normAliases = normalize_aliases($aliases, $name); - if (!empty($normAliases)) { - sort($normAliases); - $entry["aliases"] = $normAliases; - } - add_supported_entry($supported, $name, $entry); - } -} - $supported = []; foreach (explode("|NEXT_CONFIG|", file_get_contents("php://stdin")) as $configLine) { + [$configLine, $options] = array_pad(explode("|CONFIG_OPTIONS|", $configLine, 2), 2, ''); $configLine = preg_replace('((\\\\{2})*\K"\s*")', '', $configLine); $config = str_getcsv(trim($configLine), ",", '"', '\\'); if (count($config) < 3) { @@ -278,7 +154,9 @@ foreach (explode("|NEXT_CONFIG|", file_get_contents("php://stdin")) as $configLi } [$type, $name, $default] = array_map('trim', array_slice($config, 0, 3)); $aliases = count($config) > 3 ? array_slice($config, 3) : []; - $mappedType = map_type($type); + $parser = preg_match('/\\.parser\\s*=\\s*([A-Za-z0-9_]+)/', $options, $parserMatch) + ? $parserMatch[1] : ''; + $mappedType = map_type($type, $parser); $entry = [ "implementation" => "A", "type" => $mappedType, @@ -354,9 +232,6 @@ if (file_exists($otelWhitelistPath) add_otel_entries($supported, $m[1], $otelMetadata); } -$profilingPath = "../profiling/src/config.rs"; -add_rust_profiling_configurations($supported, $profilingPath); - if (empty($supported)) { fwrite(STDERR, "Error: no supported configurations were generated\n"); exit(1); @@ -458,6 +333,8 @@ extract_c_supported_configurations() { "${CPP_COMPILER_CMD[@]}" $(php-config --includes) -I.. -I../ext -I../zend_abstract_interface -I../src/dogstatsd -I../components-rs -x c -E - < Date: Sun, 30 Aug 2026 17:08:55 -0600 Subject: [PATCH 05/47] fix: align profiling metadata with registry --- metadata/supported-configurations.json | 24 ++++---------------- tooling/generate-supported-configurations.sh | 24 +++++++++++++++++++- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 3c456e60994..cafbae854a6 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -547,10 +547,7 @@ { "implementation": "B", "type": "boolean", - "default": "true", - "aliases": [ - "DD_PROFILING_EXPERIMENTAL_ALLOCATION_ENABLED" - ] + "default": "true" } ], "DD_PROFILING_ALLOCATION_SAMPLING_DISTANCE": [ @@ -562,7 +559,7 @@ ], "DD_PROFILING_ENABLED": [ { - "implementation": "D", + "implementation": "A", "type": "boolean", "default": "false" } @@ -578,10 +575,7 @@ { "implementation": "A", "type": "boolean", - "default": "true", - "aliases": [ - "DD_PROFILING_EXPERIMENTAL_EXCEPTION_ENABLED" - ] + "default": "true" } ], "DD_PROFILING_EXCEPTION_MESSAGE_ENABLED": [ @@ -595,10 +589,7 @@ { "implementation": "A", "type": "int", - "default": "100", - "aliases": [ - "DD_PROFILING_EXPERIMENTAL_EXCEPTION_SAMPLING_DISTANCE" - ] + "default": "100" } ], "DD_PROFILING_EXPERIMENTAL_CPU_TIME_ENABLED": [ @@ -618,13 +609,6 @@ "default": "false" } ], - "DD_PROFILING_EXPERIMENTAL_HEAP_LIVE_ENABLED": [ - { - "implementation": "A", - "type": "boolean", - "default": "false" - } - ], "DD_PROFILING_EXPERIMENTAL_IO_ENABLED": [ { "implementation": "A", diff --git a/tooling/generate-supported-configurations.sh b/tooling/generate-supported-configurations.sh index 2bcc4c7ad1f..58193a21535 100755 --- a/tooling/generate-supported-configurations.sh +++ b/tooling/generate-supported-configurations.sh @@ -87,9 +87,25 @@ function normalize_default($v, $type, $name) { } function normalize_aliases($aliases, $canonical) { + // Local aliases only need to be a subset of the configuration registry. + // Keep runtime-compatible legacy aliases unpublished until the registry's + // existing implementation versions include them. + $unpublishedAliases = [ + 'DD_PROFILING_ALLOCATION_ENABLED' => [ + 'DD_PROFILING_EXPERIMENTAL_ALLOCATION_ENABLED', + ], + 'DD_PROFILING_EXCEPTION_ENABLED' => [ + 'DD_PROFILING_EXPERIMENTAL_EXCEPTION_ENABLED', + ], + 'DD_PROFILING_EXCEPTION_SAMPLING_DISTANCE' => [ + 'DD_PROFILING_EXPERIMENTAL_EXCEPTION_SAMPLING_DISTANCE', + ], + ]; + $excluded = array_flip($unpublishedAliases[$canonical] ?? []); + $out = []; foreach ($aliases as $a) { - if ($a !== '' && $a !== $canonical) { + if ($a !== '' && $a !== $canonical && !isset($excluded[$a])) { $out[$a] = true; } } @@ -120,6 +136,12 @@ function normalize_supported_entries($entries, $canonical) { } function add_supported_entry(&$supported, $name, $entry) { + // This experimental runtime setting predates its registry metadata. Do not + // publish it until an implementation version exists in the registry. + if ($name === 'DD_PROFILING_EXPERIMENTAL_HEAP_LIVE_ENABLED') { + return; + } + // Keep the first-seen source as canonical for duplicate names. if (!isset($supported[$name])) { $supported[$name] = [$entry]; From 88621a8f5a39d5e291b72e87f386b12d530fb19b Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Sun, 30 Aug 2026 17:13:00 -0600 Subject: [PATCH 06/47] fix: pass PHP includes to ZAI Rust build --- zend_abstract_interface/CMakeLists.txt | 4 ++-- zend_abstract_interface/components_rs.cmake | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/zend_abstract_interface/CMakeLists.txt b/zend_abstract_interface/CMakeLists.txt index 2a080d1609c..d9439357cfd 100644 --- a/zend_abstract_interface/CMakeLists.txt +++ b/zend_abstract_interface/CMakeLists.txt @@ -9,6 +9,8 @@ project( # Get our FindPhpConfig helper. list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/../cmake/Modules") +find_package(PhpConfig REQUIRED) + option(BUILD_ZAI_TESTING "Enable tests" OFF) if(${BUILD_ZAI_TESTING}) # Tests uses the C++ testing framework Catch2 @@ -40,8 +42,6 @@ include(GNUInstallDirs) add_library(zai_zend_abstract_interface INTERFACE) -find_package(PhpConfig REQUIRED) - if(PhpConfig_VERNUM LESS "70000") set(PHP_VERSION_DIRECTORY "php5") message(STATUS "Detected PHP 5 from version id: ${PhpConfig_VERNUM}") diff --git a/zend_abstract_interface/components_rs.cmake b/zend_abstract_interface/components_rs.cmake index c2ea2036e76..892e6e223b8 100644 --- a/zend_abstract_interface/components_rs.cmake +++ b/zend_abstract_interface/components_rs.cmake @@ -52,11 +52,17 @@ add_custom_command( ) add_custom_target(update_version_h ALL DEPENDS "${VERSION_H_PATH}") +set(DDTRACE_PHP_INCLUDE_FLAGS ${PhpConfig_INCLUDE_DIRS}) +list(TRANSFORM DDTRACE_PHP_INCLUDE_FLAGS PREPEND "-I") +list(JOIN DDTRACE_PHP_INCLUDE_FLAGS " " DDTRACE_PHP_INCLUDES) + ExternalProject_Add(components_rs_proj PREFIX ${CMAKE_BINARY_DIR}/components_rs SOURCE_DIR ${CMAKE_SOURCE_DIR}/../components-rs CONFIGURE_COMMAND "" - BUILD_COMMAND ${CARGO_BUILD_ENV} ${CARGO_BUILD_CMD} --target-dir=${CMAKE_BINARY_DIR}/components_rs + BUILD_COMMAND ${CMAKE_COMMAND} -E env + "DDTRACE_PHP_INCLUDES=${DDTRACE_PHP_INCLUDES}" + ${CARGO_BUILD_ENV} ${CARGO_BUILD_CMD} --target-dir=${CMAKE_BINARY_DIR}/components_rs INSTALL_COMMAND "" DEPENDS libdatadog_stamp BUILD_IN_SOURCE TRUE From 9cf8445165871537353e7772b15274ef31b06c2c Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Sun, 30 Aug 2026 17:36:01 -0600 Subject: [PATCH 07/47] fix: configure split Rust extension builds --- .gitlab/compile_extension.sh | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.gitlab/compile_extension.sh b/.gitlab/compile_extension.sh index b43416e47b7..0e89becc7d1 100755 --- a/.gitlab/compile_extension.sh +++ b/.gitlab/compile_extension.sh @@ -19,10 +19,17 @@ if [ "${WITH_ASAN}" -eq "1" ]; then export ASAN=1 export COMPILE_ASAN=1 fi -# Compile Rust and PHP in parallel -SHARED=1 ./compile_rust.sh & +# Compile Rust and PHP in parallel. This split-library build links the Rust +# archive manually, so pass the PHP toolchain selected above explicitly. +DDTRACE_PHP_INCLUDES="$(php-config --includes)" SHARED=1 ./compile_rust.sh & +rust_pid=$! make -j static & -wait +php_pid=$! + +build_status=0 +wait "${rust_pid}" || build_status=$? +wait "${php_pid}" || build_status=$? +test "${build_status}" -eq 0 # Link extension if [ "$(uname -s)" = "Linux" ]; then From de2d8119064e876994395d962b9debc7ac42cc89 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Sun, 30 Aug 2026 20:34:17 -0600 Subject: [PATCH 08/47] fix: configure tracing sidecar Rust builds --- .gitlab/build-sidecar.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitlab/build-sidecar.sh b/.gitlab/build-sidecar.sh index b3314c4d8ca..5ae74ebafff 100755 --- a/.gitlab/build-sidecar.sh +++ b/.gitlab/build-sidecar.sh @@ -19,7 +19,8 @@ if [ "${suffix}" = "-alpine" ]; then export PATH="/root/.cargo/bin:$PATH" fi -SHARED=1 PROFILE=tracer-release host_os="${HOST_OS}" ./compile_rust.sh +DDTRACE_PHP_INCLUDES="$(php-config --includes)" \ + SHARED=1 PROFILE=tracer-release host_os="${HOST_OS}" ./compile_rust.sh cp -v "${CARGO_TARGET_DIR:-target}/tracer-release/libdatadog_php.a" "libdatadog_php_$(uname -m)${suffix}.a" output="libdatadog_php_$(uname -m)${suffix}.so" objcopy --compress-debug-sections "${CARGO_TARGET_DIR:-target}/tracer-release/libdatadog_php.so" "${output}" From 266c20a0e10259ad3d673b528a87b5c25fa12fdf Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Sun, 30 Aug 2026 20:39:46 -0600 Subject: [PATCH 09/47] fix: rebuild profiler for selected PHP ABI --- config.m4 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/config.m4 b/config.m4 index feb684d0b5a..dbeb000bec7 100644 --- a/config.m4 +++ b/config.m4 @@ -89,7 +89,10 @@ if test "$PHP_DDTRACE_PROFILING" != "no"; then PHP_SUBST(PHP_MODULES) cat <> Makefile.fragments -$profiler_rust_lib: \$(shell for dir in \$(srcdir)/components-rs \$(srcdir)/profiling \$(srcdir)/libdatadog \$(srcdir)/../../libdatadog; do test ! -d \$\$dir || find \$\$dir \( -type f -o -type l \) \( -name '*.rs' -o -name '*.c' -o -name '*.h' -o -name Cargo.toml \) -not -path '*/target/*' -not -path '*/.git/*'; done) \$(srcdir)/ext/configuration.h \$(srcdir)/ext/configuration_helpers.h \$(srcdir)/ext/configuration_shared.h \$(srcdir)/tracer/configuration.h \$(srcdir)/ext/handlers_api.c \$(srcdir)/ext/handlers_api.h \$(srcdir)/Cargo.toml \$(srcdir)/Cargo.lock \$(srcdir)/VERSION +.PHONY: ddtrace-profiler-rust-build +ddtrace-profiler-rust-build: + +$profiler_rust_lib: ddtrace-profiler-rust-build \$(shell for dir in \$(srcdir)/components-rs \$(srcdir)/profiling \$(srcdir)/libdatadog \$(srcdir)/../../libdatadog; do test ! -d \$\$dir || find \$\$dir \( -type f -o -type l \) \( -name '*.rs' -o -name '*.c' -o -name '*.h' -o -name Cargo.toml \) -not -path '*/target/*' -not -path '*/.git/*'; done) \$(srcdir)/ext/configuration.h \$(srcdir)/ext/configuration_helpers.h \$(srcdir)/ext/configuration_shared.h \$(srcdir)/tracer/configuration.h \$(srcdir)/ext/handlers_api.c \$(srcdir)/ext/handlers_api.h \$(srcdir)/Cargo.toml \$(srcdir)/Cargo.lock \$(srcdir)/VERSION (cd "\$(srcdir)"; DDTRACE_PHP_INCLUDES="\$(INCLUDES)" CARGO_TARGET_DIR="$profiler_target_dir" RUSTFLAGS="\$(RUSTFLAGS) --cfg tokio_unstable" "\$(DDTRACE_CARGO)" build $DDTRACE_PROFILING_CARGO_BUILD_FLAGS $profiler_target_arg --no-default-features --features "profiling${DDTRACE_PROFILING_FEATURES:+,$DDTRACE_PROFILING_FEATURES}" $(test "$profiler_cargo_profile" = debug || echo --profile "$profiler_cargo_profile") \$(shell echo "\$(MAKEFLAGS)" | $EGREP -o "[[-]]j[[0-9]]+")) \$(phplibdir)/datadog-profiling.la: $profiler_rust_lib From 80941dea2a372a1cb2c43125d8da5a36b3f1172c Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Sun, 30 Aug 2026 20:43:38 -0600 Subject: [PATCH 10/47] fix: isolate PHP compiler flags on Windows --- components-rs/config_codegen.rs | 97 +++++++++++++++++++++++++++++---- config.w32 | 6 +- 2 files changed, 88 insertions(+), 15 deletions(-) diff --git a/components-rs/config_codegen.rs b/components-rs/config_codegen.rs index fb383009786..1b4de6c7037 100644 --- a/components-rs/config_codegen.rs +++ b/components-rs/config_codegen.rs @@ -50,9 +50,7 @@ pub fn build() { if env::var_os("CARGO_FEATURE_PROFILING").is_some() { cc_build.define("PROFILING", None); } - for inc in php_include_dirs() { - cc_build.include(inc); - } + configure_php_build(&mut cc_build); let expanded = String::from_utf8(cc_build.expand()) .expect("config preprocessor output was not valid UTF-8"); @@ -94,21 +92,23 @@ pub fn build() { } } -fn php_include_dirs() -> Vec { +fn configure_php_build(build: &mut cc::Build) { println!("cargo:rerun-if-env-changed=DDTRACE_PHP_INCLUDES"); - if let Ok(includes) = env::var("DDTRACE_PHP_INCLUDES") { - return parse_include_flags(&includes); - } + println!("cargo:rerun-if-env-changed=DDTRACE_PHP_CFLAGS"); - // php-config does not exist in the Windows SDK. config.w32 exports the - // PHP build's CFLAGS for cc-rs instead, including the PHP header paths. if env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") { - return Vec::new(); + let flags = env::var("DDTRACE_PHP_CFLAGS") + .expect("DDTRACE_PHP_CFLAGS is required; config.w32 must pass PHP's CFLAGS to Cargo"); + apply_msvc_php_flags(build, &flags); + return; } - panic!( - "DDTRACE_PHP_INCLUDES is required; loadable extensions must pass Make's INCLUDES to Cargo" + let includes = env::var("DDTRACE_PHP_INCLUDES").expect( + "DDTRACE_PHP_INCLUDES is required; loadable extensions must pass Make's INCLUDES to Cargo", ); + for include in parse_include_flags(&includes) { + build.include(include); + } } fn parse_include_flags(flags: &str) -> Vec { @@ -118,3 +118,76 @@ fn parse_include_flags(flags: &str) -> Vec { .map(str::to_string) .collect() } + +fn apply_msvc_php_flags(build: &mut cc::Build, flags: &str) { + let args = split_windows_command_line(flags); + let mut index = 0; + while index < args.len() { + let arg = &args[index]; + let upper = arg.to_ascii_uppercase(); + match upper.as_str() { + "/I" => { + index += 1; + build.include( + args.get(index) + .expect("/I is missing its include directory"), + ); + } + "/D" => { + index += 1; + apply_msvc_define( + build, + args.get(index).expect("/D is missing its definition"), + ); + } + "/FI" => { + index += 1; + build.flag("/FI"); + build.flag(args.get(index).expect("/FI is missing its header")); + } + "/EXPERIMENTAL:PREPROCESSOR" | "/ZC:PREPROCESSOR" => { + build.flag(arg); + } + _ if upper.starts_with("/I") => { + build.include(&arg[2..]); + } + _ if upper.starts_with("/D") => apply_msvc_define(build, &arg[2..]), + _ if upper.starts_with("/FI") => { + build.flag(arg); + } + _ => {} + } + index += 1; + } +} + +fn apply_msvc_define(build: &mut cc::Build, definition: &str) { + if let Some((name, value)) = definition.split_once('=') { + build.define(name, Some(value)); + } else { + build.define(definition, None); + } +} + +fn split_windows_command_line(command_line: &str) -> Vec { + let mut args = Vec::new(); + let mut current = String::new(); + let mut quoted = false; + + for ch in command_line.chars() { + match ch { + '"' => quoted = !quoted, + ch if ch.is_whitespace() && !quoted => { + if !current.is_empty() { + args.push(std::mem::take(&mut current)); + } + } + _ => current.push(ch), + } + } + assert!(!quoted, "unterminated quote in DDTRACE_PHP_CFLAGS"); + if !current.is_empty() { + args.push(current); + } + args +} diff --git a/config.w32 b/config.w32 index 921bc3e566c..c6b8da0a680 100644 --- a/config.w32 +++ b/config.w32 @@ -180,9 +180,9 @@ if (PHP_DDTRACE != 'no') { // Promote to env var frag_out.WriteLine("!if [set CARGO_TARGET_DIR=$(CARGO_TARGET_DIR)]"); frag_out.WriteLine("!endif"); - // cc-rs needs the PHP SDK include flags while preprocessing the generated - // configuration table; Windows does not provide php-config. - frag_out.WriteLine("!if [set CFLAGS=$(CFLAGS) $(CFLAGS_DDTRACE)]"); + // The configuration generator needs the PHP SDK compiler settings, but + // exporting CFLAGS would also affect every C/C++ build script in Cargo. + frag_out.WriteLine("!if [set DDTRACE_PHP_CFLAGS=$(CFLAGS) $(CFLAGS_DDTRACE)]"); frag_out.WriteLine("!endif"); frag_out.WriteLine("!if [set RUSTFLAGS=--cfg windows_seh_wrapper]"); // Not supported yet on Windows: --cfg tokio_unstable --cfg tokio_taskdump From 383a07f92bf735241413140498914bb2e4b43f3f Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Mon, 31 Aug 2026 08:18:32 -0600 Subject: [PATCH 11/47] fix: initialize tracer defaults before request config --- ext/datadog.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ext/datadog.c b/ext/datadog.c index 6767a134fb0..4a3152d947d 100644 --- a/ext/datadog.c +++ b/ext/datadog.c @@ -219,6 +219,11 @@ static void datadog_activate(void) { // ZAI config is always set up pthread_once(&dd_activate_once_control, dd_activate_once); +#ifdef TRACER + // This setup may change tracer config defaults and must run before the + // request snapshots them. Common one-time activation still runs first. + pthread_once(&dd_tracer_activate_once_control, ddtrace_activate_once); +#endif zai_config_rinit(); if (!datadog_disable) { @@ -240,9 +245,6 @@ static void datadog_activate(void) { } #ifdef TRACER - // Tracer activation follows completion of common activation. Its one-time - // setup must still precede its first zai_hook activation. - pthread_once(&dd_tracer_activate_once_control, ddtrace_activate_once); ddtrace_activate_early(); ddtrace_activate_late(); #endif From fd887c85f9f65234c50c253e4c109407a9ca6dbc Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Mon, 31 Aug 2026 13:16:44 -0600 Subject: [PATCH 12/47] build: silence Cargo with quiet Make --- compile_rust.sh | 5 ++++- config.m4 | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/compile_rust.sh b/compile_rust.sh index 0e165316274..92ac3dc3ad3 100755 --- a/compile_rust.sh +++ b/compile_rust.sh @@ -12,7 +12,10 @@ case "${host_os}" in ;; esac -set -x +case " $* " in + *" --quiet "*|*" -q "*) ;; + *) set -x ;; +esac if test -n "$COMPILE_ASAN"; then # We need -lresolv due to https://github.com/llvm/llvm-project/issues/59007 diff --git a/config.m4 b/config.m4 index dbeb000bec7..ff3c8226298 100644 --- a/config.m4 +++ b/config.m4 @@ -93,7 +93,7 @@ if test "$PHP_DDTRACE_PROFILING" != "no"; then ddtrace-profiler-rust-build: $profiler_rust_lib: ddtrace-profiler-rust-build \$(shell for dir in \$(srcdir)/components-rs \$(srcdir)/profiling \$(srcdir)/libdatadog \$(srcdir)/../../libdatadog; do test ! -d \$\$dir || find \$\$dir \( -type f -o -type l \) \( -name '*.rs' -o -name '*.c' -o -name '*.h' -o -name Cargo.toml \) -not -path '*/target/*' -not -path '*/.git/*'; done) \$(srcdir)/ext/configuration.h \$(srcdir)/ext/configuration_helpers.h \$(srcdir)/ext/configuration_shared.h \$(srcdir)/tracer/configuration.h \$(srcdir)/ext/handlers_api.c \$(srcdir)/ext/handlers_api.h \$(srcdir)/Cargo.toml \$(srcdir)/Cargo.lock \$(srcdir)/VERSION - (cd "\$(srcdir)"; DDTRACE_PHP_INCLUDES="\$(INCLUDES)" CARGO_TARGET_DIR="$profiler_target_dir" RUSTFLAGS="\$(RUSTFLAGS) --cfg tokio_unstable" "\$(DDTRACE_CARGO)" build $DDTRACE_PROFILING_CARGO_BUILD_FLAGS $profiler_target_arg --no-default-features --features "profiling${DDTRACE_PROFILING_FEATURES:+,$DDTRACE_PROFILING_FEATURES}" $(test "$profiler_cargo_profile" = debug || echo --profile "$profiler_cargo_profile") \$(shell echo "\$(MAKEFLAGS)" | $EGREP -o "[[-]]j[[0-9]]+")) + (cd "\$(srcdir)"; DDTRACE_PHP_INCLUDES="\$(INCLUDES)" CARGO_TARGET_DIR="$profiler_target_dir" RUSTFLAGS="\$(RUSTFLAGS) --cfg tokio_unstable" "\$(DDTRACE_CARGO)" build $DDTRACE_PROFILING_CARGO_BUILD_FLAGS $profiler_target_arg --no-default-features --features "profiling${DDTRACE_PROFILING_FEATURES:+,$DDTRACE_PROFILING_FEATURES}" $(test "$profiler_cargo_profile" = debug || echo --profile "$profiler_cargo_profile") \$(shell echo "\$(MAKEFLAGS)" | $EGREP -o "[[-]]j[[0-9]]+") \$(shell echo "\$(MAKEFLAGS)" | $EGREP -q -e '--silent' -e '^[[^ -]]*s' && echo --quiet)) \$(phplibdir)/datadog-profiling.la: $profiler_rust_lib @mkdir -p \$(phplibdir)/.libs @@ -517,7 +517,7 @@ EOT if test "$PHP_DDTRACE_SIDECAR_MOCKGEN" != "-"; then ddtrace_mockgen_invocation="HOST= TARGET= $PHP_DDTRACE_SIDECAR_MOCKGEN" else - ddtrace_mockgen_invocation="cd \"$ext_srcdir/components-rs/php_sidecar_mockgen\"; HOST= TARGET= CARGO_TARGET_DIR=\$(builddir)/target_mockgen/ \$(DDTRACE_CARGO) run" + ddtrace_mockgen_invocation="cd \"$ext_srcdir/components-rs/php_sidecar_mockgen\"; HOST= TARGET= CARGO_TARGET_DIR=\$(builddir)/target_mockgen/ \$(DDTRACE_CARGO) run \$(shell echo \"\$(MAKEFLAGS)\" | $EGREP -q -e '--silent' -e '^[[^ -]]*s' && echo --quiet)" fi fi @@ -548,7 +548,7 @@ EOT cat <> Makefile.fragments $ddtrace_rust_lib: $( (find "$ext_srcdir/components-rs" -name "*.rs"; find "$ext_srcdir/profiling" \( -name "*.rs" -o -name "*.c" -o -name "*.h" \); find "$ext_srcdir/zend_abstract_interface" \( -name "*.c" -o -name "*.h" \); find "$ext_srcdir" -maxdepth 1 -name "Cargo.toml"; find "$ext_srcdir/../../libdatadog" -name "*.rs" -not -path "*/target/*"; find "$ext_srcdir/libdatadog" -name "*.rs" -not -path "*/target/*") 2>/dev/null | tr '\n' ' ' ) $ext_srcdir/ext/configuration.h $ext_srcdir/ext/configuration_helpers.h $ext_srcdir/ext/configuration_shared.h $ext_srcdir/tracer/configuration.h - (cd "$ext_srcdir"; DDTRACE_PHP_INCLUDES="\$(INCLUDES)" CARGO_FEATURES="$ddtrace_cargo_features" CARGO_TARGET_DIR="$ddtrace_target_dir/" SHARED=$(test "$ext_shared" = "yes" && echo 1) PROFILE="$ddtrace_cargo_profile" host_os="$host_os" DDTRACE_CARGO="\$(DDTRACE_CARGO)" $(if test "$PHP_DDTRACE_SANITIZE" != "no"; then echo COMPILE_ASAN=1; fi) sh ./compile_rust.sh $ddtrace_cargo_build_flags \$(shell echo "\$(MAKEFLAGS)" | $EGREP -o "[[-]]j[[0-9]]+")) + (cd "$ext_srcdir"; DDTRACE_PHP_INCLUDES="\$(INCLUDES)" CARGO_FEATURES="$ddtrace_cargo_features" CARGO_TARGET_DIR="$ddtrace_target_dir/" SHARED=$(test "$ext_shared" = "yes" && echo 1) PROFILE="$ddtrace_cargo_profile" host_os="$host_os" DDTRACE_CARGO="\$(DDTRACE_CARGO)" $(if test "$PHP_DDTRACE_SANITIZE" != "no"; then echo COMPILE_ASAN=1; fi) sh ./compile_rust.sh $ddtrace_cargo_build_flags \$(shell echo "\$(MAKEFLAGS)" | $EGREP -o "[[-]]j[[0-9]]+") \$(shell echo "\$(MAKEFLAGS)" | $EGREP -q -e '--silent' -e '^[[^ -]]*s' && echo --quiet)) EOT fi From bcfbb62a0c534ef9240de5d84093350c6ad3d586 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Mon, 31 Aug 2026 13:17:12 -0600 Subject: [PATCH 13/47] fix: share DD_TAGS map configuration --- components-rs/config.rs | 9 +- components-rs/config_codegen.rs | 32 ++++++- config.m4 | 1 + config.w32 | 1 + ext/configuration_shared.h | 7 +- ext/configuration_tags.c | 75 ++++++++++++++++ profiling/build.rs | 3 +- profiling/src/bindings/mod.rs | 16 +++- profiling/src/config.rs | 112 ++++++++++++++++++++++-- profiling/src/lib.rs | 4 +- profiling/src/php_ffi.c | 24 +++++- profiling/src/php_ffi.h | 8 ++ profiling/src/profile_tags.rs | 128 +++++++++++++++++----------- tests/Unit/ConfigurationTest.php | 6 ++ tracer/configuration_dependencies.h | 62 -------------- 15 files changed, 347 insertions(+), 141 deletions(-) create mode 100644 ext/configuration_tags.c diff --git a/components-rs/config.rs b/components-rs/config.rs index 62da12e0c07..8109def6d65 100644 --- a/components-rs/config.rs +++ b/components-rs/config.rs @@ -1,9 +1,8 @@ //! Rust IDs for the aggregate common/product ZAI configuration table. //! -//! Products keep their typed conversion helpers because validation, defaults, -//! and representation can be product-specific (for example, `DD_TAGS` is a -//! map in ddtrace and an encoded string in the standalone profiler table). -//! Generating only IDs and the table count avoids mirroring Zend/ZAI layouts -//! in this common Rust module. +//! IDs and storage-typed accessors are generated from the shared X-macro +//! declarations. Product-specific validation and composed defaults remain in +//! the product modules. Logical representations, including the unique-key +//! `DD_TAGS` map, are shared without mirroring Zend/ZAI layouts here. include!(concat!(env!("OUT_DIR"), "/generated_config.rs")); diff --git a/components-rs/config_codegen.rs b/components-rs/config_codegen.rs index 1b4de6c7037..5d75c0eb8a2 100644 --- a/components-rs/config_codegen.rs +++ b/components-rs/config_codegen.rs @@ -1,4 +1,5 @@ -//! Generates Rust configuration IDs from the same X-macro list used by C. +//! Generates Rust configuration IDs and storage-typed accessors from the same +//! X-macro list used by C. //! //! The C preprocessor reduces each CONFIG/CALIAS declaration to a type/name //! record. Keeping the records in preprocessor order makes the generated Rust @@ -60,23 +61,26 @@ pub fn build() { + MARKER.len(); let mut variants = String::new(); + let mut accessors = String::new(); let mut count = 0; for record in expanded[start..].split("|NEXT_CONFIG|") { let record = record.trim(); if record.is_empty() { continue; } - let (_, name) = record + let (ty, name) = record .split_once(',') .unwrap_or_else(|| panic!("malformed config record: {record:?}")); - variants.push_str(&format!(" {},\n", name.trim())); + let (ty, name) = (ty.trim(), name.trim()); + variants.push_str(&format!(" {name},\n")); + accessors.push_str(&render_accessors(ty, name)); count += 1; } fs::write( Path::new(&out_dir).join("generated_config.rs"), format!( - "pub const CONFIG_COUNT: usize = {count};\n\n#[repr(u16)]\n#[derive(Clone, Copy, Debug)]\n#[allow(non_camel_case_types)]\npub enum ConfigId {{\n{variants}}}\n" + "pub const CONFIG_COUNT: usize = {count};\n\n#[repr(u16)]\n#[derive(Clone, Copy, Debug)]\n#[allow(non_camel_case_types)]\npub enum ConfigId {{\n{variants}}}\n\n{accessors}" ), ) .expect("failed to write generated_config.rs"); @@ -92,6 +96,26 @@ pub fn build() { } } +fn render_accessors(ty: &str, name: &str) -> String { + let (return_type, current, memoized) = match ty { + "BOOL" => ("bool", "config_bool", "memoized_config_bool"), + "INT" => ("i64", "config_int", "memoized_config_int"), + "DOUBLE" => ("f64", "config_double", "memoized_config_double"), + "STRING" => ("String", "config_string", "memoized_config_string"), + "MAP" => ( + "crate::profiling::config::ConfigMap", + "config_map", + "memoized_config_map", + ), + other => panic!("unrecognized configuration type {other:?} for {name}"), + }; + + format!( + "#[cfg(feature = \"profiling\")]\n#[allow(dead_code, non_snake_case)]\npub(crate) unsafe fn get_{name}() -> {return_type} {{\n crate::profiling::config::{current}(ConfigId::{name})\n}}\n\ + #[cfg(feature = \"profiling\")]\n#[allow(dead_code, non_snake_case)]\npub(crate) unsafe fn get_global_{name}() -> {return_type} {{\n crate::profiling::config::{memoized}(ConfigId::{name})\n}}\n\n" + ) +} + fn configure_php_build(build: &mut cc::Build) { println!("cargo:rerun-if-env-changed=DDTRACE_PHP_INCLUDES"); println!("cargo:rerun-if-env-changed=DDTRACE_PHP_CFLAGS"); diff --git a/config.m4 b/config.m4 index ff3c8226298..619e2058e30 100644 --- a/config.m4 +++ b/config.m4 @@ -280,6 +280,7 @@ if test "$PHP_DDTRACE" != "no" && { test "$PHP_DDTRACE_PROFILING" = "no" || test ext/agent_info.c \ ext/compat_getrandom.c \ ext/configuration.c \ + ext/configuration_tags.c \ ext/crashtracking_frames.c \ ext/endpoints.c \ ext/excluded_modules.c \ diff --git a/config.w32 b/config.w32 index c6b8da0a680..53816ba8d39 100644 --- a/config.w32 +++ b/config.w32 @@ -23,6 +23,7 @@ if (PHP_DDTRACE != 'no') { var DDTRACE_EXT_SOURCES = "agent_info.c"; DDTRACE_EXT_SOURCES += " configuration.c"; + DDTRACE_EXT_SOURCES += " configuration_tags.c"; DDTRACE_EXT_SOURCES += " crashtracking_windows.c"; DDTRACE_EXT_SOURCES += " endpoints.c"; DDTRACE_EXT_SOURCES += " excluded_modules.c"; diff --git a/ext/configuration_shared.h b/ext/configuration_shared.h index d2094d22d79..04c6052ae85 100644 --- a/ext/configuration_shared.h +++ b/ext/configuration_shared.h @@ -10,6 +10,8 @@ * extension can preserve its historical ID ordering while standalone products * can compose just the shared subset in their own table. */ +bool datadog_config_parse_tags(zai_str value, zval *decoded_value, bool persistent); + #if defined(PROFILING) && !defined(TRACER) bool ddog_php_prof_config_parse_utf8_string(zai_str value, zval *decoded_value, bool persistent); @@ -20,7 +22,8 @@ bool ddog_php_prof_config_parse_utf8_string(zai_str value, zval *decoded_value, CONFIG(STRING, DD_ENV, "", .parser = ddog_php_prof_config_parse_utf8_string) #define DD_COMMON_SERVICE_CONFIGURATION \ CONFIG(STRING, DD_SERVICE, "", .parser = ddog_php_prof_config_parse_utf8_string) -#define DD_COMMON_TAGS_CONFIGURATION CONFIG(STRING, DD_TAGS, "") +#define DD_COMMON_TAGS_CONFIGURATION \ + CONFIG(CUSTOM(MAP), DD_TAGS, "", .parser = datadog_config_parse_tags) #define DD_COMMON_TRACE_AGENT_PORT_CONFIGURATION \ CONFIG(INT, DD_TRACE_AGENT_PORT, "0", .ini_change = zai_config_system_ini_change) #define DD_COMMON_TRACE_AGENT_URL_CONFIGURATION \ @@ -43,7 +46,7 @@ bool ddog_php_prof_config_parse_utf8_string(zai_str value, zval *decoded_value, .env_config_fallback = ddtrace_conf_otel_service_name) #define DD_COMMON_TAGS_CONFIGURATION \ CONFIG(CUSTOM(MAP), DD_TAGS, "", \ - .env_config_fallback = ddtrace_conf_otel_resource_attributes_tags, .parser = dd_parse_tags) + .env_config_fallback = ddtrace_conf_otel_resource_attributes_tags, .parser = datadog_config_parse_tags) #define DD_COMMON_TRACE_AGENT_PORT_CONFIGURATION \ CONFIG(INT, DD_TRACE_AGENT_PORT, "8126", .ini_change = zai_config_system_ini_change) #define DD_COMMON_TRACE_AGENT_URL_CONFIGURATION \ diff --git a/ext/configuration_tags.c b/ext/configuration_tags.c new file mode 100644 index 00000000000..8fec8270b74 --- /dev/null +++ b/ext/configuration_tags.c @@ -0,0 +1,75 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +#include "configuration_shared.h" + +#include + +bool datadog_config_parse_tags(zai_str value, zval *decoded_value, bool persistent) { + ZVAL_ARR(decoded_value, pemalloc(sizeof(HashTable), persistent)); + zend_hash_init(Z_ARR_P(decoded_value), 8, NULL, persistent ? ZVAL_INTERNAL_PTR_DTOR : ZVAL_PTR_DTOR, persistent); + + if (value.len == 0) { + return true; + } + + const char *str = value.ptr; + const char *end = str + value.len; + const char *current = str; + + /* Prefer comma-separated tags when the input contains a comma. */ + char separator = memchr(str, ',', value.len) ? ',' : ' '; + + while (current < end) { + while (current < end && (*current == ' ' || *current == separator)) { + current++; + } + if (current == end) { + break; + } + + const char *tag_end = memchr(current, separator, end - current); + if (!tag_end) { + tag_end = end; + } + + const char *key_start = current; + const char *key_end = tag_end; + const char *value_start = tag_end; + const char *value_end = tag_end; + const char *colon = memchr(current, ':', tag_end - current); + if (colon) { + key_end = colon; + value_start = colon + 1; + } + + while (key_start < key_end && *key_start == ' ') { + key_start++; + } + while (key_end > key_start && key_end[-1] == ' ') { + key_end--; + } + + if (key_start != key_end) { + while (value_start < value_end && *value_start == ' ') { + value_start++; + } + while (value_end > value_start && value_end[-1] == ' ') { + value_end--; + } + + zend_string *key = zend_string_init(key_start, key_end - key_start, persistent); + zval decoded_tag_value; + ZVAL_STR(&decoded_tag_value, + zend_string_init(value_start, value_end - value_start, persistent)); + /* OpenTelemetry attributes have unique keys. Retain the last value, + * matching the tracer's historical DD_TAGS behavior. */ + zend_hash_update(Z_ARRVAL_P(decoded_value), key, &decoded_tag_value); + zend_string_release(key); + } + + current = tag_end + 1; + } + + return true; +} diff --git a/profiling/build.rs b/profiling/build.rs index 84b0bcf3bab..205a97a3950 100644 --- a/profiling/build.rs +++ b/profiling/build.rs @@ -109,6 +109,7 @@ fn build_zend_php_ffis( println!("cargo:rerun-if-changed=profiling/src/configuration.c"); println!("cargo:rerun-if-changed=profiling/configuration.h"); println!("cargo:rerun-if-changed=ext/configuration_shared.h"); + println!("cargo:rerun-if-changed=ext/configuration_tags.c"); println!("cargo:rerun-if-changed=ext/handlers_api.c"); println!("cargo:rerun-if-changed=ext/handlers_api.h"); @@ -152,7 +153,7 @@ fn build_zend_php_ffis( .files(if combined { &[][..] } else { - &["profiling/src/configuration.c"][..] + &["profiling/src/configuration.c", "ext/configuration_tags.c"][..] }) .define("CFG_POST_STARTUP_CB", post_startup_cb) .define("CFG_PRELOAD", preload) diff --git a/profiling/src/bindings/mod.rs b/profiling/src/bindings/mod.rs index 3bd22ed7841..851f03d1f98 100644 --- a/profiling/src/bindings/mod.rs +++ b/profiling/src/bindings/mod.rs @@ -362,13 +362,10 @@ extern "C" { /// until mshutdown. pub(crate) fn ddog_php_prof_get_memoized_config(config_id: ConfigId) -> *mut zval; - /// Returns the current encoded common DD_TAGS value in combined mode. pub(crate) fn ddog_php_prof_config_count() -> u16; pub(crate) fn datadog_php_profiling_config_count_error(c_count: u16, rust_count: usize); #[cfg(all(feature = "profiling", not(feature = "tracer")))] pub(crate) fn ddog_php_prof_config_minit(module_number: libc::c_int) -> bool; - pub(crate) fn ddog_php_prof_get_common_tags() -> ZaiStr<'static>; - /// Returns true if the config value was explicitly set by the user (not /// just the compiled-in default), false otherwise. pub(crate) fn ddog_php_prof_config_is_set_by_user(config_id: ConfigId) -> bool; @@ -510,6 +507,19 @@ impl TryFrom<&mut zval> for zend_long { } } +impl TryFrom<&mut zval> for f64 { + type Error = u8; + + fn try_from(zval: &mut zval) -> Result { + let r#type = unsafe { zval.u1.v.type_ }; + if r#type == IS_DOUBLE { + Ok(unsafe { zval.value.dval }) + } else { + Err(r#type) + } + } +} + impl TryFrom<&mut zval> for u32 { type Error = u8; diff --git a/profiling/src/config.rs b/profiling/src/config.rs index 4089e25cdc6..c1fd82043c8 100644 --- a/profiling/src/config.rs +++ b/profiling/src/config.rs @@ -14,9 +14,10 @@ pub use http::Uri; use libc::c_int; use log::{debug, error, warn, LevelFilter}; use std::borrow::Cow; -use std::ffi::CString; +use std::ffi::{c_char, c_void, CString}; use std::num::NonZeroU32; use std::path::{Path, PathBuf}; +use std::{slice, str}; #[derive(Copy, Clone, Debug, Default)] pub enum SystemSettingsState { @@ -339,6 +340,46 @@ unsafe fn get_system_value(id: ConfigId) -> &'static mut zval { &mut *value } +pub(crate) unsafe fn config_bool(id: ConfigId) -> bool { + bool::try_from(get_value(id)).expect("generated BOOL accessor received a non-boolean zval") +} + +pub(crate) unsafe fn memoized_config_bool(id: ConfigId) -> bool { + bool::try_from(get_system_value(id)) + .expect("generated BOOL accessor received a non-boolean memoized zval") +} + +pub(crate) unsafe fn config_int(id: ConfigId) -> i64 { + zend_long::try_from(get_value(id)).expect("generated INT accessor received a non-integer zval") + as i64 +} + +pub(crate) unsafe fn memoized_config_int(id: ConfigId) -> i64 { + zend_long::try_from(get_system_value(id)) + .expect("generated INT accessor received a non-integer memoized zval") as i64 +} + +#[allow(dead_code)] +pub(crate) unsafe fn config_double(id: ConfigId) -> f64 { + f64::try_from(get_value(id)).expect("generated DOUBLE accessor received a non-double zval") +} + +#[allow(dead_code)] +pub(crate) unsafe fn memoized_config_double(id: ConfigId) -> f64 { + f64::try_from(get_system_value(id)) + .expect("generated DOUBLE accessor received a non-double memoized zval") +} + +pub(crate) unsafe fn config_string(id: ConfigId) -> String { + String::try_from(get_value(id)) + .unwrap_or_else(|_| panic!("generated STRING accessor received a non-string zval")) +} + +pub(crate) unsafe fn memoized_config_string(id: ConfigId) -> String { + String::try_from(get_system_value(id)) + .unwrap_or_else(|_| panic!("generated STRING accessor received a non-string memoized zval")) +} + pub(crate) use crate::config::ConfigId; use crate::config::ConfigId::{ DD_AGENT_HOST as AgentHost, DD_ENV as Env, DD_GIT_COMMIT_SHA as GitCommitSha, @@ -661,13 +702,68 @@ pub(crate) unsafe fn git_repository_url() -> Option { /// # Safety /// This function must only be called after config has been initialized in -/// rinit, and before it is uninitialized in mshutdown. -pub(crate) unsafe fn tags() -> Option { - let tags = unsafe { bindings::ddog_php_prof_get_common_tags() }; - tags.into_utf8() - .ok() - .filter(|tags| !tags.is_empty()) - .map(str::to_owned) +/// rinit, and before it is uninitialized in rshutdown. +pub(crate) unsafe fn tags() -> Option> { + crate::config::get_DD_TAGS().string_pairs() +} + +pub(crate) struct ConfigMap { + id: ConfigId, + memoized: bool, +} + +impl ConfigMap { + unsafe fn string_pairs(self) -> Option> { + read_config_map(self.id, self.memoized) + } +} + +pub(crate) unsafe fn config_map(id: ConfigId) -> ConfigMap { + ConfigMap { + id, + memoized: false, + } +} + +pub(crate) unsafe fn memoized_config_map(id: ConfigId) -> ConfigMap { + ConfigMap { id, memoized: true } +} + +unsafe fn read_config_map(id: ConfigId, memoized: bool) -> Option> { + struct Context { + tags: Vec<(String, String)>, + valid: bool, + } + + unsafe extern "C" fn visit( + context: *mut c_void, + key: *const c_char, + key_len: usize, + value: *const c_char, + value_len: usize, + ) -> bool { + let context = &mut *(context.cast::()); + let key = slice::from_raw_parts(key.cast::(), key_len); + let value = slice::from_raw_parts(value.cast::(), value_len); + let (Ok(key), Ok(value)) = (str::from_utf8(key), str::from_utf8(value)) else { + context.valid = false; + return false; + }; + context.tags.push((key.to_owned(), value.to_owned())); + true + } + + let mut context = Context { + tags: Vec::new(), + valid: true, + }; + let visited = bindings::ddog_php_prof_config_visit_map( + id as u16, + memoized, + (&mut context as *mut Context).cast(), + Some(visit), + ); + (visited && context.valid && !context.tags.is_empty()).then_some(context.tags) } /// # Safety diff --git a/profiling/src/lib.rs b/profiling/src/lib.rs index e6218ef51e1..ad94d0f371b 100644 --- a/profiling/src/lib.rs +++ b/profiling/src/lib.rs @@ -1,7 +1,7 @@ pub mod bindings; pub mod capi; mod clocks; -mod config; +pub(crate) mod config; mod logging; pub mod module_globals; pub mod profiler; @@ -700,7 +700,7 @@ extern "C" fn rinit(_type: c_int, _module_number: c_int) -> ZendResult { let mut custom = ProfileTagSegment::default(); if let Some(tags) = config::tags() { - if let Some(error) = custom.try_push_tags(&tags)? { + if let Some(error) = custom.try_push_kv_tags(&tags)? { // DD_TAGS can change on each request, so this warns on every // request. Maybe we should cache the error string and only // emit warnings for new ones? diff --git a/profiling/src/php_ffi.c b/profiling/src/php_ffi.c index 97b3f6c98a0..220dcdfcdad 100644 --- a/profiling/src/php_ffi.c +++ b/profiling/src/php_ffi.c @@ -193,9 +193,27 @@ zend_module_entry *datadog_get_module_entry(const char *str, uintptr_t len) { return zend_hash_str_find_ptr(&module_registry, str, len); } -zai_str ddog_php_prof_get_common_tags(void) { - const char *tags = zend_ini_string(ZEND_STRL("datadog.tags"), false); - return tags ? (zai_str)ZAI_STR_FROM_CSTR(tags) : (zai_str)ZAI_STR_EMPTY; +bool ddog_php_prof_config_visit_map(uint16_t config_id, bool memoized, + void *context, + ddog_php_prof_config_map_visitor visitor) { + zval *map = memoized + ? &zai_config_memoized_entries[config_id].decoded_value + : zai_config_get_value(config_id); + if (!map || Z_TYPE_P(map) != IS_ARRAY || !visitor) { + return false; + } + + zend_string *key; + zval *value; + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(map), key, value) { + if (!key || Z_TYPE_P(value) != IS_STRING || + !visitor(context, ZSTR_VAL(key), ZSTR_LEN(key), + Z_STRVAL_P(value), Z_STRLEN_P(value))) { + return false; + } + } ZEND_HASH_FOREACH_END(); + + return true; } #if defined(TRACER) && defined(PROFILING) diff --git a/profiling/src/php_ffi.h b/profiling/src/php_ffi.h index dc2672290d7..8926c4871fa 100644 --- a/profiling/src/php_ffi.h +++ b/profiling/src/php_ffi.h @@ -80,6 +80,14 @@ void *datadog_php_profiling_vm_interrupt_addr(void); /** Reports that standalone profiler and ddtrace were loaded together. */ void datadog_php_profiling_conflicting_extension_error(void); +/** Visits each string key/value in a decoded map configuration. */ +typedef bool (*ddog_php_prof_config_map_visitor)(void *context, + const char *key, size_t key_len, + const char *value, size_t value_len); +bool ddog_php_prof_config_visit_map(uint16_t config_id, bool memoized, + void *context, + ddog_php_prof_config_map_visitor visitor); + /** * For Code Hotspots, we need the tracer's local root span id and the current * span id. This is a cross-product struct, so keep it in sync with tracer's diff --git a/profiling/src/profile_tags.rs b/profiling/src/profile_tags.rs index 8ad6102323d..6ba53c486f2 100644 --- a/profiling/src/profile_tags.rs +++ b/profiling/src/profile_tags.rs @@ -3,7 +3,6 @@ use libdd_common::tag::{parse_tags, Tag, TagParser, TagValidationError}; use std::collections::TryReserveError; -use std::fmt::Write; use std::hash::{Hash, Hasher}; use std::ops::Range; use std::sync::Arc; @@ -76,42 +75,58 @@ impl ProfileTagSegment { Ok(()) } - pub(crate) fn try_push_tags(&mut self, input: &str) -> Result, ProfileTagError> { - let (tag_count, storage_len) = TagParser::new(input).try_fold( - (0usize, 0usize), - |(count, len), result| match result { - Ok(tag) => Ok::<_, ProfileTagError>(( - count - .checked_add(1) - .ok_or(ProfileTagError::CapacityOverflow)?, - len.checked_add(tag.len()) - .and_then(|len| len.checked_add(1)) - .ok_or(ProfileTagError::CapacityOverflow)?, - )), - Err(_) => Ok((count, len)), - }, - )?; - self.try_reserve(tag_count, storage_len)?; - + pub(crate) fn try_push_kv_tags( + &mut self, + tags: &[(String, String)], + ) -> Result, ProfileTagError> { + let mut valid_count = 0usize; + let mut storage_len = 0usize; let mut errors = String::new(); - for result in TagParser::new(input) { - match result { - Ok(tag) => self.push_serialized_validated(tag), - Err(error) => { - let additional = error - .value - .len() - .checked_add(64) - .ok_or(ProfileTagError::CapacityOverflow)?; - errors.try_reserve(additional)?; - if errors.is_empty() { - errors.push_str("Errors while parsing tags: "); - } else { - errors.push_str(", "); - } - // Writing to String only fails if its formatter does, which it does not. - let _ = write!(errors, "{error}"); + + for (key, value) in tags { + let error = if value.is_empty() { + TagParser::new(key) + .next() + .expect("configuration maps cannot contain empty keys") + .err() + .map(|error| error.to_string()) + } else { + Tag::validate(key, value) + .err() + .map(|error| error.to_string()) + }; + if let Some(error) = error { + errors.try_reserve(error.len().saturating_add(2))?; + if errors.is_empty() { + errors.push_str("Errors while parsing tags: "); + } else { + errors.push_str(", "); } + errors.push_str(&error); + continue; + } + + valid_count = valid_count + .checked_add(1) + .ok_or(ProfileTagError::CapacityOverflow)?; + storage_len = storage_len + .checked_add(key.len()) + .and_then(|len| len.checked_add(value.len())) + .and_then(|len| len.checked_add(if value.is_empty() { 1 } else { 2 })) + .ok_or(ProfileTagError::CapacityOverflow)?; + } + + self.try_reserve(valid_count, storage_len)?; + for (key, value) in tags { + let valid = if value.is_empty() { + TagParser::new(key) + .next() + .is_some_and(|result| result.is_ok()) + } else { + Tag::validate(key, value).is_ok() + }; + if valid { + self.push_validated(key, value); } } @@ -121,16 +136,10 @@ impl ProfileTagSegment { fn push_validated(&mut self, key: &str, value: &str) { let start = self.storage.len(); self.storage.push_str(key); - self.storage.push(':'); - self.storage.push_str(value); - let end = self.storage.len(); - self.storage.push(','); - self.tags.push(start..end); - } - - fn push_serialized_validated(&mut self, tag: &str) { - let start = self.storage.len(); - self.storage.push_str(tag); + if !value.is_empty() { + self.storage.push(':'); + self.storage.push_str(value); + } let end = self.storage.len(); self.storage.push(','); self.tags.push(start..end); @@ -306,6 +315,21 @@ impl ProfileTags { mod tests { use super::*; + #[test] + fn map_tags_are_materialized_as_key_value_tags() { + let tags = vec![ + ("location".to_owned(), "usa".to_owned()), + ("region".to_owned(), "west".to_owned()), + ("standalone".to_owned(), String::new()), + ]; + let mut segment = ProfileTagSegment::default(); + assert_eq!(segment.try_push_kv_tags(&tags).unwrap(), None); + assert_eq!( + segment.iter().collect::>(), + ["location:usa", "region:west", "standalone"] + ); + } + #[test] fn serialized_storage_defines_segment_identity() { use std::collections::hash_map::DefaultHasher; @@ -338,18 +362,20 @@ mod tests { #[test] fn segment_uses_one_comma_separated_string() { - let mut segment = - ProfileTagSegment::try_from_kv_slice(&[("service", "checkout"), ("env", "production")]) - .unwrap(); - segment.try_push_tags("standalone").unwrap(); + let segment = ProfileTagSegment::try_from_kv_slice(&[ + ("service", "checkout"), + ("env", "production"), + ("standalone", "enabled"), + ]) + .unwrap(); assert_eq!( segment.storage, - "service:checkout,env:production,standalone," + "service:checkout,env:production,standalone:enabled," ); assert_eq!( segment.iter().collect::>(), - ["service:checkout", "env:production", "standalone"] + ["service:checkout", "env:production", "standalone:enabled"] ); } } diff --git a/tests/Unit/ConfigurationTest.php b/tests/Unit/ConfigurationTest.php index f3129e5087b..b40be2a5d9d 100644 --- a/tests/Unit/ConfigurationTest.php +++ b/tests/Unit/ConfigurationTest.php @@ -256,6 +256,12 @@ public function testGlobalTagsCommaSeparated() $this->assertEquals(['key1' => 'value1', 'key2' => 'value2'], \dd_trace_env_config("DD_TAGS")); } + public function testGlobalTagsDuplicateKeysUseLastValue() + { + $this->putEnvAndReloadConfig(['DD_TAGS=location:utah,location:usa']); + $this->assertSame(['location' => 'usa'], \dd_trace_env_config("DD_TAGS")); + } + public function testGlobalTagsWhitespaceSeparated() { $this->putEnvAndReloadConfig(['DD_TAGS=key1:value1 key2:value2']); diff --git a/tracer/configuration_dependencies.h b/tracer/configuration_dependencies.h index 4518e2c5836..6125ee6c429 100644 --- a/tracer/configuration_dependencies.h +++ b/tracer/configuration_dependencies.h @@ -66,68 +66,6 @@ static bool dd_parse_sidecar_connection_mode(zai_str value, zval *decoded_value, return true; } -static bool dd_parse_tags(zai_str value, zval *decoded_value, bool persistent) { - ZVAL_ARR(decoded_value, pemalloc(sizeof(HashTable), persistent)); - zend_hash_init(Z_ARR_P(decoded_value), 8, NULL, persistent ? ZVAL_INTERNAL_PTR_DTOR : ZVAL_PTR_DTOR, persistent); - - if (value.len == 0) { - return true; - } - - const char *str = value.ptr; - const char *end = str + value.len; - const char *current = str; - - // Determine separator - prefer comma if present, otherwise use space - char sep = memchr(str, ',', value.len) ? ',': ' '; - - while (current < end) { - // Skip leading whitespace - while (current < end && (*current == ' ' || *current == sep)) current++; - if (current == end) { - // Abort if only separators are remaining - break; - } - - // Find next separator, this will be the end of the tag - const char *tag_end = memchr(current, sep, end - current); - if (!tag_end) { - tag_end = end; - } - // Prepare key and value - // Initialize key to be the entire tag and value to be empty - const char *key_start = current; - const char *key_end = tag_end; - const char *val_start = tag_end; - const char *val_end = tag_end; - // If the tag has a colon, use the index of the colon to split the tag into key and value - const char *colon = memchr(current, ':', tag_end - current); - if (colon) { - // Tag has a colon, use the index of the colon to split into key and value - key_end = colon; - val_start = colon + 1; - } - - // Strip whitespace from key - while (key_start < key_end && *key_start == ' ') key_start++; - while (key_end > key_start && key_end[-1] == ' ') key_end--; - // Only add if key is non-empty - if (key_start != key_end) { - // Strip whitespace from value - while (val_start < val_end && *val_start == ' ') val_start++; - while (val_end > val_start && val_end[-1] == ' ') val_end--; - - zval val; - ZVAL_STR(&val, zend_string_init(val_start, val_end - val_start, persistent)); - zend_hash_str_update(Z_ARRVAL_P(decoded_value), key_start, key_end - key_start, &val); - } - // Move to the start of the next tag - current = tag_end + 1; - } - - return true; -} - // Custom parser to ensure security-testing headers are always captured. static bool dd_parse_header_tags(zai_str value, zval *decoded_value, bool persistent) { if (!zai_config_decode_value(value, ZAI_CONFIG_TYPE_SET_OR_MAP_LOWERCASE, NULL, decoded_value, persistent)) { From e49d0b4487ac09ac3a9b0109b69117a728d6549a Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Mon, 31 Aug 2026 18:39:52 -0600 Subject: [PATCH 14/47] ci: build combined extension artifacts directly --- .gitlab/build-profiler.sh | 30 +++++++---- .gitlab/generate-package.php | 50 +++++++------------ dockerfiles/verify_packages/Makefile | 2 +- .../test_profiler_install_enabled_debug.sh | 20 ++++++++ ...test_profiler_install_unsupported_debug.sh | 30 ----------- profiling/README.md | 7 +-- tooling/bin/generate-final-artifact.sh | 31 +++++------- tooling/bin/generate-ssi-package.sh | 10 ++-- 8 files changed, 79 insertions(+), 101 deletions(-) create mode 100644 dockerfiles/verify_packages/installer/test_profiler_install_enabled_debug.sh delete mode 100644 dockerfiles/verify_packages/installer/test_profiler_install_unsupported_debug.sh diff --git a/.gitlab/build-profiler.sh b/.gitlab/build-profiler.sh index dae14a88ed6..900c9836d8b 100755 --- a/.gitlab/build-profiler.sh +++ b/.gitlab/build-profiler.sh @@ -15,7 +15,7 @@ fi set -u prefix="$1" -thread_safety="${2:-nts}" +build_variant="${2:-nts}" artifact_mode="${3:-standalone}" mkdir -vp "${prefix}" prefix="$(cd "${prefix}" && pwd)" @@ -28,16 +28,28 @@ else extension_name="datadog-profiling" fi -if [ "$thread_safety" = "zts" ]; then - switch-php "${PHP_VERSION}-zts" - output_file="${prefix}/${extension_name}-zts.so" -else - switch-php "${PHP_VERSION}" - output_file="${prefix}/${extension_name}.so" -fi +case "$build_variant" in + zts) + switch-php "${PHP_VERSION}-zts" + default_output_name="${extension_name}-zts.so" + ;; + debug) + switch-php "${PHP_VERSION}-debug" + default_output_name="${extension_name}-debug.so" + ;; + nts) + switch-php "${PHP_VERSION}" + default_output_name="${extension_name}.so" + ;; + *) + echo "Unsupported PHP build variant: ${build_variant}" >&2 + exit 1 + ;; +esac +output_file="${prefix}/${4:-${default_output_name}}" # Loadable profiling artifacts must go through the supported PHP build path. -build_dir="/tmp/ddtrace-build-profiler-${thread_safety}" +build_dir="/tmp/ddtrace-build-profiler-${build_variant}" rm -rf "${build_dir}" mkdir -p "${build_dir}/src" tar -cf - --exclude=.git --exclude=tmp . | tar -xf - -C "${build_dir}/src" diff --git a/.gitlab/generate-package.php b/.gitlab/generate-package.php index ab890c42564..058c751c93f 100644 --- a/.gitlab/generate-package.php +++ b/.gitlab/generate-package.php @@ -251,8 +251,11 @@ function appsec_image_from_tag_mapping(string $tag): string KUBERNETES_MEMORY_REQUEST: 4Gi KUBERNETES_MEMORY_LIMIT: 8Gi script: - - .gitlab/build-profiler.sh "combined-ddtrace/${TRIPLET}/lib/php/${ABI_NO}" "nts" "combined" - - .gitlab/build-profiler.sh "combined-ddtrace/${TRIPLET}/lib/php/${ABI_NO}" "zts" "combined" + - .gitlab/build-profiler.sh "extensions_" "nts" "combined" "ddtrace-${ABI_NO}.so" + + - .gitlab/build-profiler.sh "extensions_" "debug" "combined" "ddtrace-${ABI_NO}-debug.so" + + - .gitlab/build-profiler.sh "extensions_" "zts" "combined" "ddtrace-${ABI_NO}-zts.so" cache: - key: prefix: cargo-cache-${TRIPLET} @@ -263,7 +266,7 @@ function appsec_image_from_tag_mapping(string $tag): string policy: pull # `Cache Cargo Deps` is used to update/push the cache artifacts: paths: - - "combined-ddtrace" + - "extensions_*" $abi_no) { + if ($major_minor !== "7.0") { + continue; + } $image = sprintf($platform['image_template'], $major_minor); $suffix = ($platform['triplet'] === "x86_64-alpine-linux-musl" || $platform['triplet'] === "aarch64-alpine-linux-musl") ? "-alpine" : ""; - $catch_warnings = ($major_minor == "7.3" && $suffix != "-alpine") ? "0" : "1"; ?> "compile tracing extension: [, , ]": stage: tracing @@ -386,7 +391,7 @@ function appsec_image_from_tag_mapping(string $tag): string script: # Fix for $BASH_ENV not having a newline at the end of the file - echo "" >> "$BASH_ENV" - - ./.gitlab/build-tracing.sh "" "" + - ./.gitlab/build-tracing.sh "" artifacts: paths: - "extensions_*" @@ -410,12 +415,10 @@ function appsec_image_from_tag_mapping(string $tag): string - - job: "compile tracing extension: [, , ]" + - job: "link tracing extension: [, ]" artifacts: true @@ -487,10 +490,12 @@ function appsec_image_from_tag_mapping(string $tag): string needs: - job: "compile tracing sidecar: [, ]" artifacts: true + - job: "compile tracing extension: [7.0, , ]" + artifacts: true $abi_no) { +foreach ($profiler_minor_major_targets as $major_minor) { ?> - - job: "compile tracing extension: [, , ]" + - job: "compile combined extension: [, , ]" artifacts: true @@ -652,16 +659,6 @@ function appsec_image_from_tag_mapping(string $tag): string ARCH: "" TRIPLET: "" script: - # Overlay the PHP-version-specific combined artifacts onto the historical - # ddtrace paths consumed by package generation. PHP 7.0 and debug builds - # remain tracer-only because profiling does not support those builds. - - | - for module in combined-ddtrace/${TRIPLET}/lib/php/*/ddtrace*.so; do - api=$(basename "$(dirname "$module")") - variant=$(basename "$module" .so) - variant=${variant#ddtrace} - cp -v "$module" "extensions_/ddtrace-${api}${variant}.so" - done - make -j 4 - ./tooling/bin/generate-final-artifact.sh $( - # Combined tracer and profiler extension - - job: "compile combined extension: [, , ]" - artifacts: true - @@ -799,12 +789,6 @@ function appsec_image_from_tag_mapping(string $tag): string ?> - - job: "compile combined extension: [, , ]" - artifacts: true - Date: Tue, 1 Sep 2026 08:12:22 -0600 Subject: [PATCH 15/47] test: stabilize loader checks on Alpine --- loader/tests/functional/test_no_zombie.php | 88 ++++++++++++++-------- loader/tests/functional/test_phpinfo.php | 3 +- 2 files changed, 56 insertions(+), 35 deletions(-) diff --git a/loader/tests/functional/test_no_zombie.php b/loader/tests/functional/test_no_zombie.php index 8a70f77d7b9..2bd4a33b4c9 100644 --- a/loader/tests/functional/test_no_zombie.php +++ b/loader/tests/functional/test_no_zombie.php @@ -5,31 +5,33 @@ $telemetryLogPath = tempnam(sys_get_temp_dir(), 'test_loader_'); -// Build the command to run PHP with the loader +// Build the command to run PHP with the loader. "exec" makes the proc_open PID +// the PHP PID instead of the intermediate shell PID. $cmd = sprintf( - 'FAKE_FORWARDER_LOG_PATH=%s DD_TELEMETRY_FORWARDER_PATH=%s php -n -dzend_extension=%s -r "sleep(1);"', + 'exec env FAKE_FORWARDER_LOG_PATH=%s DD_TELEMETRY_FORWARDER_PATH=%s %s -n -dzend_extension=%s -r "sleep(1);"', escapeshellarg($telemetryLogPath), escapeshellarg(__DIR__.'/../../bin/fake_forwarder.sh'), + escapeshellarg(PHP_BINARY), escapeshellarg(getLoaderAbsolutePath()) ); +$process = null; try { - // Start the PHP process in background and get its PID + // Do not use pipes here. A telemetry descendant can inherit them and keep + // stream_get_contents() waiting for EOF after the PHP process has exited. $descriptors = [ - 0 => ['pipe', 'r'], - 1 => ['pipe', 'w'], - 2 => ['pipe', 'w'], + 0 => ['file', '/dev/null', 'r'], + 1 => ['file', '/dev/null', 'a'], + 2 => ['file', '/dev/null', 'a'], ]; - $process = proc_open($cmd . ' & echo $!', $descriptors, $pipes); + $process = proc_open($cmd, $descriptors, $pipes); if (!is_resource($process)) { throw new \Exception("Failed to start PHP process"); } - // Get the real PHP PID from the output - $firstLine = fgets($pipes[1]); - $phpPid = (int)trim($firstLine); - + $status = proc_get_status($process); + $phpPid = (int)$status['pid']; if ($phpPid <= 0) { throw new \Exception("Failed to get PHP PID"); } @@ -38,36 +40,56 @@ echo "[debug] PHP PID: $phpPid\n"; } - // Wait for the telemetry fork to happen and complete - usleep(300000); // 300ms - - // Check for zombie processes that are children of the PHP process - $zombieCheckCmd = sprintf('ps --ppid %d -o pid,state,comm --no-headers 2>/dev/null || echo "NO_CHILDREN"', $phpPid); - $zombieOutput = shell_exec($zombieCheckCmd); + // Wait for the telemetry fork to happen and complete. + usleep(300000); - if (debug()) { - echo "[debug] Children processes:\n" . $zombieOutput . "\n"; + $childrenPath = sprintf('/proc/%d/task/%d/children', $phpPid, $phpPid); + if (!is_readable($childrenPath)) { + throw new \Exception("Unable to inspect child processes for PHP PID $phpPid"); } - $zombieCount = substr_count($zombieOutput, ' Z '); - - // Wait for the PHP process to finish - $waitCmd = sprintf('wait %d 2>/dev/null; echo $?', $phpPid); - $phpExitCode = (int)trim(shell_exec($waitCmd)); + $zombies = []; + $children = preg_split('/\s+/', trim(file_get_contents($childrenPath)), -1, PREG_SPLIT_NO_EMPTY); + foreach ($children as $childPid) { + $stat = @file_get_contents('/proc/'.$childPid.'/stat'); + if ($stat === false) { + continue; // The child was reaped while it was being inspected. + } + + // /proc//stat begins with "pid (comm) state"; comm may contain spaces. + $stateOffset = strrpos($stat, ') '); + $state = $stateOffset === false ? null : substr($stat, $stateOffset + 2, 1); + if ($state === 'Z') { + $zombies[] = $childPid; + } + } - // Read the remaining output and close pipes - $output = stream_get_contents($pipes[1]); - $errors = stream_get_contents($pipes[2]); - fclose($pipes[0]); - fclose($pipes[1]); - fclose($pipes[2]); - proc_close($process); + if ($zombies) { + throw new \Exception('FAILED: Zombie process detected after telemetry fork: '.implode(', ', $zombies)); + } - if ($zombieCount > 0) { - throw new \Exception("FAILED: Zombie process detected after telemetry fork! Found $zombieCount zombie(s)"); + // Bound the test independently of inherited descriptors or descendants. + $deadline = microtime(true) + 5; + do { + $status = proc_get_status($process); + if (!$status['running']) { + break; + } + usleep(10000); + } while (microtime(true) < $deadline); + + if ($status['running']) { + proc_terminate($process); + throw new \Exception("PHP process did not exit within 5 seconds"); } + proc_close($process); + $process = null; echo "OK: No zombie processes detected\n"; } finally { + if (is_resource($process)) { + proc_terminate($process); + proc_close($process); + } @unlink($telemetryLogPath); } diff --git a/loader/tests/functional/test_phpinfo.php b/loader/tests/functional/test_phpinfo.php index ac7f2e57b8a..893aebaa0e8 100644 --- a/loader/tests/functional/test_phpinfo.php +++ b/loader/tests/functional/test_phpinfo.php @@ -72,8 +72,7 @@ Injection success => true Injection error => Extra config => provided by combined ddtrace -Logs => -%A +Logs =>%A => ddappsec Version => %s From 05bd9414c9de5c80aec59cd365ae9ddf08691e6c Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Wed, 2 Sep 2026 10:40:11 -0600 Subject: [PATCH 16/47] fix: weaken PHP symbols in Rust archives --- Cargo.lock | 1 + components-rs/php_sidecar_mockgen/Cargo.toml | 1 + .../src/bin/php_sidecar_mockgen.rs | 172 ++++++++++++++++-- config.m4 | 24 ++- 4 files changed, 182 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1e07843be61..44f773c7e5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4171,6 +4171,7 @@ dependencies = [ name = "php_sidecar_mockgen" version = "0.1.0" dependencies = [ + "object 0.31.1", "sidecar_mockgen", ] diff --git a/components-rs/php_sidecar_mockgen/Cargo.toml b/components-rs/php_sidecar_mockgen/Cargo.toml index ac3a03a172c..8478cebf93f 100644 --- a/components-rs/php_sidecar_mockgen/Cargo.toml +++ b/components-rs/php_sidecar_mockgen/Cargo.toml @@ -4,6 +4,7 @@ name = "php_sidecar_mockgen" version = "0.1.0" [dependencies] +object = { version = "0.31.0" } sidecar_mockgen = { path = "../../libdatadog/tools/sidecar_mockgen" } [[bin]] diff --git a/components-rs/php_sidecar_mockgen/src/bin/php_sidecar_mockgen.rs b/components-rs/php_sidecar_mockgen/src/bin/php_sidecar_mockgen.rs index d8f3ca315b5..a3239f476ba 100644 --- a/components-rs/php_sidecar_mockgen/src/bin/php_sidecar_mockgen.rs +++ b/components-rs/php_sidecar_mockgen/src/bin/php_sidecar_mockgen.rs @@ -1,26 +1,176 @@ // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2021-Present Datadog, Inc. -pub use sidecar_mockgen::weaken_object_symbols; +use object::macho::MachHeader64; +use object::read::archive::ArchiveFile; +use object::read::macho::{LoadCommandVariant, MachHeader}; +use object::{ + Endian, Endianness, File, FileKind, Object, ObjectSection, ObjectSymbol, SymbolFlags, +}; +use sidecar_mockgen::weaken_object_symbols; +use std::collections::HashSet; +use std::fs; use std::path::Path; -use std::{env, process}; +use std::process; + +fn symbol_name(name: &str) -> &str { + #[cfg(target_os = "macos")] + return name.strip_prefix('_').unwrap_or(name); + #[cfg(not(target_os = "macos"))] + return name; +} + +fn is_definition(symbol: &object::Symbol<'_, '_>) -> bool { + symbol.is_definition() + || matches!(symbol.flags(), SymbolFlags::Elf { st_info, .. } if st_info & 0xf == 10) +} + +fn php_symbols(binary: &Path) -> Result, String> { + let data = fs::read(binary).map_err(|e| format!("read {}: {e}", binary.display()))?; + let file = + File::parse(data.as_slice()).map_err(|e| format!("parse {}: {e}", binary.display()))?; + Ok(file + .dynamic_symbols() + .filter(is_definition) + .filter_map(|symbol| symbol.name().ok().map(symbol_name).map(str::to_owned)) + .collect()) +} + +fn referenced_symbol_indices(file: &File<'_>, binary_symbols: &HashSet) -> Vec { + file.symbols() + .filter(|symbol| { + symbol.is_undefined() + && !symbol.is_weak() + && symbol + .name() + .is_ok_and(|name| binary_symbols.contains(symbol_name(name))) + }) + .map(|symbol| symbol.index().0) + .collect() +} + +fn macho_symtab_offset(data: &[u8]) -> Result<(usize, bool), String> { + let header = MachHeader64::::parse(data, 0) + .map_err(|e| format!("parse Mach-O header: {e}"))?; + let endian = header + .endian() + .map_err(|e| format!("parse Mach-O endianness: {e}"))?; + let mut commands = header + .load_commands(endian, data, 0) + .map_err(|e| format!("parse Mach-O load commands: {e}"))?; + loop { + match commands.next() { + Ok(Some(command)) => { + if let Ok(LoadCommandVariant::Symtab(symtab)) = command.variant() { + return Ok((symtab.symoff.get(endian) as usize, endian.is_big_endian())); + } + } + Ok(None) => return Err("Mach-O archive member has no LC_SYMTAB".to_string()), + Err(e) => return Err(format!("parse Mach-O load command: {e}")), + } + } +} + +fn weaken_archive_symbols(target: &Path, binary: &Path) -> Result<(), String> { + let binary_symbols = php_symbols(binary)?; + let mut data = + fs::read(target).map_err(|e| format!("read archive {}: {e}", target.display()))?; + let mut byte_patches = Vec::new(); + let mut word_patches = Vec::new(); + + { + let archive = ArchiveFile::parse(data.as_slice()) + .map_err(|e| format!("parse archive {}: {e}", target.display()))?; + for member in archive.members() { + let member = member.map_err(|e| format!("parse archive member: {e}"))?; + let (member_offset, _) = member.file_range(); + let member_data = member + .data(data.as_slice()) + .map_err(|e| format!("read archive member: {e}"))?; + let Ok(kind) = FileKind::parse(member_data) else { + continue; + }; + let Ok(file) = File::parse(member_data) else { + continue; + }; + let indices = referenced_symbol_indices(&file, &binary_symbols); + if indices.is_empty() { + continue; + } + + match kind { + FileKind::Elf64 => { + let symtab = file + .section_by_name(".symtab") + .ok_or_else(|| "ELF archive member has no .symtab".to_string())?; + let (symtab_offset, _) = symtab.file_range().ok_or_else(|| { + "ELF archive member .symtab has no file range".to_string() + })?; + byte_patches.extend(indices.into_iter().map(|index| { + member_offset as usize + symtab_offset as usize + index * 24 + 4 + })); + } + FileKind::MachO64 => { + let (symtab_offset, big_endian) = macho_symtab_offset(member_data)?; + word_patches.extend(indices.into_iter().map(|index| { + ( + member_offset as usize + symtab_offset + index * 16 + 6, + big_endian, + ) + })); + } + _ => {} + } + } + } + + if byte_patches.is_empty() && word_patches.is_empty() { + return Ok(()); + } + for offset in byte_patches { + let old = data[offset]; + data[offset] = (2u8 << 4) | (old & 0xf); // ELF STB_WEAK = 2 + } + for (offset, big_endian) in word_patches { + let bytes: [u8; 2] = data[offset..offset + 2] + .try_into() + .map_err(|_| "truncated Mach-O symbol table".to_string())?; + let old = if big_endian { + u16::from_be_bytes(bytes) + } else { + u16::from_le_bytes(bytes) + }; + let new = old | 0x0040; // Mach-O N_WEAK_REF + let bytes = if big_endian { + new.to_be_bytes() + } else { + new.to_le_bytes() + }; + data[offset..offset + 2].copy_from_slice(&bytes); + } + fs::write(target, data).map_err(|e| format!("write archive {}: {e}", target.display())) +} + +fn weaken_target(target: &Path, binary: &Path) -> Result<(), String> { + let data = fs::read(target).map_err(|e| format!("read {}: {e}", target.display()))?; + match FileKind::parse(data.as_slice()) { + Ok(FileKind::Archive) => weaken_archive_symbols(target, binary), + _ => weaken_object_symbols(target, binary), + } +} fn main() { - let args: Vec<_> = env::args_os().collect(); + let args: Vec<_> = std::env::args_os().collect(); if args.get(1).and_then(|a| a.to_str()) != Some("weaken-dynsym") || args.len() < 4 { - eprintln!("Usage: php_sidecar_mockgen weaken-dynsym "); + eprintln!("Usage: php_sidecar_mockgen weaken-dynsym "); process::exit(1); } let php_binary = Path::new(args.last().unwrap()); - let targets: Vec<_> = args[2..args.len() - 1] - .iter() - .map(|a| Path::new(a.as_os_str())) - .collect(); - - for target in targets { - if let Err(e) = weaken_object_symbols(target, php_binary) { + for target in &args[2..args.len() - 1] { + let target = Path::new(target); + if let Err(e) = weaken_target(target, php_binary) { eprintln!("Warning: weaken-dynsym {}: {e}", target.display()); process::exit(1); } diff --git a/config.m4 b/config.m4 index 619e2058e30..854bed37a09 100644 --- a/config.m4 +++ b/config.m4 @@ -392,9 +392,18 @@ if test "$PHP_DDTRACE" != "no" && { test "$PHP_DDTRACE_PROFILING" = "no" || test PHP_NEW_EXTENSION(ddtrace, $ALL_DATADOG_SOURCES, $ext_shared,, $DATADOG_EXTENSION_FLAGS) PHP_ADD_BUILD_DIR($ext_builddir/ext, 1) - dnl sidecar requires us to be linked against libm for pow and powf and librt for shm_* functions + dnl sidecar requires us to be linked against libm for pow and powf and librt for shm_* functions. + dnl Keep libm as a DT_NEEDED dependency even though libtool places it before the Rust archive + dnl that references it; direct sidecar execution cannot inherit libm from the PHP executable. AC_CHECK_LIBM - EXTRA_LDFLAGS="$EXTRA_LDFLAGS $LIBM" + case $host_os in + linux*) + if test -n "$LIBM"; then + EXTRA_LDFLAGS="$EXTRA_LDFLAGS -Wl,--no-as-needed,$LIBM,--as-needed" + fi + ;; + *) EXTRA_LDFLAGS="$EXTRA_LDFLAGS $LIBM" ;; + esac dnl as well as explicitly for pthread_atfork PTHREADS_CHECK EXTRA_CFLAGS="$EXTRA_CFLAGS $ac_cv_pthreads_cflags" @@ -553,12 +562,17 @@ $ddtrace_rust_lib: $( (find "$ext_srcdir/components-rs" -name "*.rs"; find "$ext EOT fi - dnl Weaken PHP-origin symbols in all .o files before the link step so that - dnl the resulting .so/.a naturally has weak references. + dnl Weaken PHP-origin symbols before the link step so the resulting shared + dnl object can also be executed as the sidecar's main object. Combined builds + dnl have additional PHP references inside the Rust static archive. if test "$ext_shared" = "yes"; then + ddtrace_weaken_targets="$all_object_files_absolute" + if test "$PHP_DDTRACE_TRACER" != "no" && test "$PHP_DDTRACE_PROFILING" != "no"; then + ddtrace_weaken_targets="$ddtrace_weaken_targets $ddtrace_rust_lib" + fi _ddtrace_weaken_tmp=$(mktemp) cat > "$_ddtrace_weaken_tmp" << WEAKEN - ($ddtrace_mockgen_invocation weaken-dynsym $all_object_files_absolute $php_binary) + ($ddtrace_mockgen_invocation weaken-dynsym $ddtrace_weaken_targets $php_binary) WEAKEN sed -i $({ sed --version 2>&1 || echo ''; } | grep GNU >/dev/null || echo "''") -e "/\/ddtrace\.la:\ \\$/r $_ddtrace_weaken_tmp" Makefile.objects dnl run weaken only once, create a dependency on .la for .a From 44e6a29d493ee688f2a614b0097746bb2169fa81 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Wed, 2 Sep 2026 14:10:44 -0600 Subject: [PATCH 17/47] ci: report package artifact sizes --- .gitlab/generate-package.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.gitlab/generate-package.php b/.gitlab/generate-package.php index 058c751c93f..0dc3f47fcaf 100644 --- a/.gitlab/generate-package.php +++ b/.gitlab/generate-package.php @@ -646,6 +646,18 @@ function appsec_image_from_tag_mapping(string $tag): string stage: packaging image: registry.ddbuild.io/images/mirror/datadog/dd-trace-ci:php_fpm_packaging tags: [ "arch:amd64" ] + after_script: + - | + if [ -d packages ]; then + echo "Package artifact total size:" + du -sh packages + echo "Package artifact files, sorted by size:" + find packages -type f -exec du -h {} + | sort -h + fi + echo "Extension input total sizes:" + du -sh extensions_* + echo "Extension input files, sorted by size:" + find extensions_* -type f -exec du -h {} + | sort -h artifacts: paths: - "packages/" From 1a67c9658cf13bb4ca9dbfbeddb4cb271d88e61f Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Wed, 2 Sep 2026 14:10:48 -0600 Subject: [PATCH 18/47] test: expect combined loader configuration output --- loader/tests/functional/test_incompatibility_jit.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/loader/tests/functional/test_incompatibility_jit.php b/loader/tests/functional/test_incompatibility_jit.php index 957922088aa..c41e906e2a8 100644 --- a/loader/tests/functional/test_incompatibility_jit.php +++ b/loader/tests/functional/test_incompatibility_jit.php @@ -36,6 +36,7 @@ Injection success => true Injection error => Extra config => datadog.trace.sources_path=%s/trace/src +datadog.profiling.enabled=0 %A EOT @@ -66,6 +67,7 @@ Injection success => true Injection error => Extra config => datadog.trace.sources_path=%s/trace/src +datadog.profiling.enabled=0 %A EOT @@ -102,6 +104,7 @@ Injection success => false Injection error => Incompatible runtime Extra config => datadog.trace.sources_path=%s/trace/src +datadog.profiling.enabled=0 ddtrace.disable=1 %A @@ -169,6 +172,7 @@ Injection success => true Injection error => Extra config => datadog.trace.sources_path=%s/trace/src +datadog.profiling.enabled=0 %A OPcache JIT is enabled and may cause instability. Ignoring as DD_INJECT_FORCE is enabled @@ -206,6 +210,7 @@ Injection success => true Injection error => Extra config => datadog.trace.sources_path=%s/trace/src +datadog.profiling.enabled=0 %A OPcache JIT is enabled and may cause instability. Ignoring as DD_INJECT_FORCE is enabled From 24260eb9ce58a551f25e8f3a397d3b12204790ea Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Wed, 2 Sep 2026 16:22:36 -0600 Subject: [PATCH 19/47] build: remove intermediate extension archives --- .gitlab/link-tracing-extension.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitlab/link-tracing-extension.sh b/.gitlab/link-tracing-extension.sh index 971d6df7be7..0b4fa01446a 100755 --- a/.gitlab/link-tracing-extension.sh +++ b/.gitlab/link-tracing-extension.sh @@ -19,3 +19,7 @@ done for pid in "${pids[@]}"; do wait $pid done + +# The archives are intermediate inputs. Keeping them in extensions_* causes +# the legacy RPM, DEB, and tarball targets to ship them to users. +rm -f extensions_$(uname -m)/*.a From 04981fcc5745323ffb15140fb80d5131d362cd61 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Wed, 2 Sep 2026 21:03:42 -0600 Subject: [PATCH 20/47] build: discard unused extension sections --- config.m4 | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/config.m4 b/config.m4 index 854bed37a09..39ed2a52d9d 100644 --- a/config.m4 +++ b/config.m4 @@ -443,11 +443,12 @@ if test "$PHP_DDTRACE" != "no" && { test "$PHP_DDTRACE_PROFILING" = "no" || test fi EXTRA_LDFLAGS="$EXTRA_LDFLAGS -export-symbols $DDTRACE_EXPORT_SYMBOLS -flto -fuse-linker-plugin" - dnl On Linux: set the ELF entry point so ddtrace.so can be exec'd directly by ld.so - dnl for sidecar spawning (no trampoline binary, no memfd, no temp files). + dnl On Linux: discard unreachable function and data sections from the Rust static + dnl archive, and set the ELF entry point so ddtrace.so can be exec'd directly by + dnl ld.so for sidecar spawning (no trampoline binary, no memfd, no temp files). case $host_os in linux*) - EXTRA_LDFLAGS="$EXTRA_LDFLAGS -Wl,-e,ddog_spawn_direct_entry" + EXTRA_LDFLAGS="$EXTRA_LDFLAGS -Wl,--gc-sections -Wl,-e,ddog_spawn_direct_entry" ;; esac From 1a2aa1baed896e7b6e506213e64d36fc3edb9817 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Wed, 2 Sep 2026 22:01:52 -0600 Subject: [PATCH 21/47] test: match combined JIT configuration order --- loader/tests/functional/test_incompatibility_jit.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/loader/tests/functional/test_incompatibility_jit.php b/loader/tests/functional/test_incompatibility_jit.php index c41e906e2a8..66193ceccda 100644 --- a/loader/tests/functional/test_incompatibility_jit.php +++ b/loader/tests/functional/test_incompatibility_jit.php @@ -104,8 +104,8 @@ Injection success => false Injection error => Incompatible runtime Extra config => datadog.trace.sources_path=%s/trace/src -datadog.profiling.enabled=0 ddtrace.disable=1 +datadog.profiling.enabled=0 %A OPcache JIT is enabled and may cause instability. ddtrace will be disabled unless the environment DD_INJECT_FORCE is set to '1', 'true', 'yes' or 'on' From 71972a98e90f6947e141ddc3a2a44866d78d1178 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Thu, 3 Sep 2026 07:11:39 -0600 Subject: [PATCH 22/47] build: optimize packaged PHP debug artifacts --- .gitlab/build-profiler.sh | 5 +++++ Cargo.toml | 6 ++++++ config.m4 | 5 +++-- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.gitlab/build-profiler.sh b/.gitlab/build-profiler.sh index 900c9836d8b..11b154102fe 100755 --- a/.gitlab/build-profiler.sh +++ b/.gitlab/build-profiler.sh @@ -48,6 +48,11 @@ case "$build_variant" in esac output_file="${prefix}/${4:-${default_output_name}}" +# PHP debug ABI artifacts retain full Rust debug information while optimizing for size. +if [ "$build_variant" = "debug" ]; then + export DDTRACE_RUST_PROFILE=php-debug +fi + # Loadable profiling artifacts must go through the supported PHP build path. build_dir="/tmp/ddtrace-build-profiler-${build_variant}" rm -rf "${build_dir}" diff --git a/Cargo.toml b/Cargo.toml index da7e44f6e7c..195b45fdd0f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -123,6 +123,12 @@ required-features = ["profiling", "test"] [profile.dev] debug = 2 +[profile.php-debug] +inherits = "dev" +opt-level = "z" +debug = 2 +panic = "abort" + [profile.release] codegen-units = 1 debug = "line-tables-only" diff --git a/config.m4 b/config.m4 index 39ed2a52d9d..b1ca2b8455f 100644 --- a/config.m4 +++ b/config.m4 @@ -1,6 +1,7 @@ AC_ARG_VAR([DDTRACE_PROFILING_FEATURES], [additional Cargo features for standalone profiler test builds]) AC_ARG_VAR([DDTRACE_PROFILING_CARGO_BUILD_FLAGS], [additional Cargo build flags for the standalone profiler]) AC_ARG_VAR([DDTRACE_PROFILING_TARGET], [Rust target triple for the standalone profiler]) +AC_ARG_VAR([DDTRACE_RUST_PROFILE], [Cargo profile used to build the Rust extension library]) PHP_ARG_ENABLE(ddtrace, whether to enable Datadog support, [ --enable-ddtrace Enable Datadog tracing support]) @@ -69,7 +70,7 @@ if test "$PHP_DDTRACE_PROFILING" != "no"; then fi PHP_SUBST(DDTRACE_CARGO) - profiler_cargo_profile=$(test "$PHP_DDTRACE_RUST_DEBUG" != "no" && echo debug || echo profiler-release) + profiler_cargo_profile=${DDTRACE_RUST_PROFILE:-$(test "$PHP_DDTRACE_RUST_DEBUG" != "no" && echo debug || echo profiler-release)} profiler_target_dir="${CARGO_TARGET_DIR:-\$(builddir)/target-profiling}" if test "$PHP_DDTRACE_TRACER" = "no"; then @@ -538,7 +539,7 @@ EOT ddtrace_rust_lib="$PHP_DDTRACE_RUST_LIBRARY" else dnl consider it debug if -g is specified (but not -g0) - ddtrace_cargo_profile=$(test "$PHP_DDTRACE_RUST_DEBUG" != "no" && echo debug || echo tracer-release) + ddtrace_cargo_profile=${DDTRACE_RUST_PROFILE:-$(test "$PHP_DDTRACE_RUST_DEBUG" != "no" && echo debug || echo tracer-release)} ddtrace_target_dir="${CARGO_TARGET_DIR:-\$(builddir)/target-common}" ddtrace_target_path= ddtrace_target_args= From 37a72b8c32c7d4c2d4eb6ef62e9f2d469ed26eeb Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Thu, 3 Sep 2026 10:24:11 -0600 Subject: [PATCH 23/47] test: fix loader/profiler test gaps for combined ddtrace.so - loader: enable profiling in the JIT force-injection functional test cases so their assertions about profiler notification output match what the combined ddtrace.so actually does when DD_PROFILING_ENABLED is set. - profiling: gc_mem_caches_01.phpt assumed a fixed amount of incidental garbage is always reclaimable right after RINIT. That's not true once the tracer is active in the same process (the combined build), since the tracer's own request-lifetime allocations (e.g. the root span) can consume the small amount of cached/free memory the test relied on, making gc_mem_caches() legitimately return 0 for reasons unrelated to allocation profiling. The test now generates and frees its own garbage so the assertion is robust in both standalone and combined builds. - CI: the "profiling tests" job only ever built and exercised the standalone datadog-profiling.so, so gaps like the above were never caught. Added NTS and ZTS combined-mode (tracer + profiling in one ddtrace.so) build-and-test runs alongside the existing standalone runs. Verified locally against registry.ddbuild.io/ci/dd-trace-php/dd-trace-ci:php-8.5_bookworm-10 for both NTS and ZTS: built the real combined ddtrace.so and standalone datadog-profiling.so, reproduced the exact loader-test scenario (including the .ddtrace.profiling marker convention), and ran the full profiling/tests/phpt suite against both artifacts. --- .gitlab/generate-profiler.php | 21 ++++++++++++--- .../functional/test_incompatibility_jit.php | 3 ++- profiling/tests/phpt/gc_mem_caches_01.phpt | 27 ++++++++++++++++--- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/.gitlab/generate-profiler.php b/.gitlab/generate-profiler.php index c640cb00227..60c7eda702f 100644 --- a/.gitlab/generate-profiler.php +++ b/.gitlab/generate-profiler.php @@ -55,21 +55,34 @@ - unset DD_SERVICE; unset DD_ENV - mkdir -p "${CI_PROJECT_DIR}/artifacts/profiler-tests" - - '# NTS' + - '# NTS standalone' - '# Use if/then instead of `command -v switch-php && switch-php` — the && form exits 1 when switch-php is absent, which FF_ENABLE_BASH_EXIT_CODE_CHECK treats as a job failure' - if command -v switch-php > /dev/null 2>&1; then switch-php "${PHP_MAJOR_MINOR}"; fi - (cd ..; phpize && DDTRACE_PROFILING_FEATURES="debug_stats,stack_walking_tests,test,tracing,tracing-subscriber,trigger_time_sample" ./configure --disable-ddtrace-tracer --enable-ddtrace-profiling && make -j$(nproc)) - test -f "${CI_PROJECT_DIR}/modules/datadog-profiling.so" || { echo "ERROR standalone profiler build did not produce modules/datadog-profiling.so"; find "${CI_PROJECT_DIR}/modules" -maxdepth 1 -type f -print; exit 1; } - php -d "extension=${CI_PROJECT_DIR}/modules/datadog-profiling.so" -r 'if (!extension_loaded("datadog-profiling") || ini_get("datadog.profiling.enabled") === false) { exit(1); }' - - (cd ../; TEST_PHP_JUNIT="${CI_PROJECT_DIR}/artifacts/profiler-tests/nts-results.xml" php profiling/tests/run-tests.php -d "extension=${CI_PROJECT_DIR}/modules/datadog-profiling.so" --show-diff -g "FAIL,XFAIL,BORK,WARN,LEAK,XLEAK,SKIP" "profiling/tests/phpt") + - (cd ../; TEST_PHP_JUNIT="${CI_PROJECT_DIR}/artifacts/profiler-tests/nts-standalone-results.xml" php profiling/tests/run-tests.php -d "extension=${CI_PROJECT_DIR}/modules/datadog-profiling.so" --show-diff -g "FAIL,XFAIL,BORK,WARN,LEAK,XLEAK,SKIP" "profiling/tests/phpt") + - '# NTS combined (tracer + profiling in one ddtrace.so, as shipped)' + - if command -v switch-php > /dev/null 2>&1; then switch-php "${PHP_MAJOR_MINOR}"; fi + - (cd ..; make distclean || true; phpize && DDTRACE_PROFILING_FEATURES="debug_stats,stack_walking_tests,test,tracing,tracing-subscriber,trigger_time_sample" ./configure --enable-ddtrace-tracer --enable-ddtrace-profiling && make -j$(nproc)) + - test -f "${CI_PROJECT_DIR}/modules/ddtrace.so" || { echo "ERROR combined build did not produce modules/ddtrace.so"; find "${CI_PROJECT_DIR}/modules" -maxdepth 1 -type f -print; exit 1; } + - php -d "extension=${CI_PROJECT_DIR}/modules/ddtrace.so" -r 'if (!extension_loaded("ddtrace") || ini_get("datadog.profiling.enabled") === false) { exit(1); }' + - (cd ../; TEST_PHP_JUNIT="${CI_PROJECT_DIR}/artifacts/profiler-tests/nts-combined-results.xml" php profiling/tests/run-tests.php -d "extension=${CI_PROJECT_DIR}/modules/ddtrace.so" --show-diff -g "FAIL,XFAIL,BORK,WARN,LEAK,XLEAK,SKIP" "profiling/tests/phpt") - - '# ZTS' + - '# ZTS standalone' - if command -v switch-php > /dev/null 2>&1; then switch-php "${PHP_MAJOR_MINOR}-zts"; fi - (cd ..; make distclean || true; phpize && DDTRACE_PROFILING_FEATURES="debug_stats,stack_walking_tests,test,tracing,tracing-subscriber,trigger_time_sample" ./configure --disable-ddtrace-tracer --enable-ddtrace-profiling && make -j$(nproc)) - test -f "${CI_PROJECT_DIR}/modules/datadog-profiling.so" || { echo "ERROR standalone profiler ZTS build did not produce modules/datadog-profiling.so"; find "${CI_PROJECT_DIR}/modules" -maxdepth 1 -type f -print; exit 1; } - php -d "extension=${CI_PROJECT_DIR}/modules/datadog-profiling.so" -r 'if (!extension_loaded("datadog-profiling") || ini_get("datadog.profiling.enabled") === false) { exit(1); }' - - (cd ../; TEST_PHP_JUNIT="${CI_PROJECT_DIR}/artifacts/profiler-tests/zts-results.xml" php profiling/tests/run-tests.php -d "extension=${CI_PROJECT_DIR}/modules/datadog-profiling.so" --show-diff -g "FAIL,XFAIL,BORK,WARN,LEAK,XLEAK,SKIP" "profiling/tests/phpt") + - (cd ../; TEST_PHP_JUNIT="${CI_PROJECT_DIR}/artifacts/profiler-tests/zts-standalone-results.xml" php profiling/tests/run-tests.php -d "extension=${CI_PROJECT_DIR}/modules/datadog-profiling.so" --show-diff -g "FAIL,XFAIL,BORK,WARN,LEAK,XLEAK,SKIP" "profiling/tests/phpt") + + - '# ZTS combined (tracer + profiling in one ddtrace.so, as shipped)' + - if command -v switch-php > /dev/null 2>&1; then switch-php "${PHP_MAJOR_MINOR}-zts"; fi + - (cd ..; make distclean || true; phpize && DDTRACE_PROFILING_FEATURES="debug_stats,stack_walking_tests,test,tracing,tracing-subscriber,trigger_time_sample" ./configure --enable-ddtrace-tracer --enable-ddtrace-profiling && make -j$(nproc)) + - test -f "${CI_PROJECT_DIR}/modules/ddtrace.so" || { echo "ERROR combined ZTS build did not produce modules/ddtrace.so"; find "${CI_PROJECT_DIR}/modules" -maxdepth 1 -type f -print; exit 1; } + - php -d "extension=${CI_PROJECT_DIR}/modules/ddtrace.so" -r 'if (!extension_loaded("ddtrace") || ini_get("datadog.profiling.enabled") === false) { exit(1); }' + - (cd ../; TEST_PHP_JUNIT="${CI_PROJECT_DIR}/artifacts/profiler-tests/zts-combined-results.xml" php profiling/tests/run-tests.php -d "extension=${CI_PROJECT_DIR}/modules/ddtrace.so" --show-diff -g "FAIL,XFAIL,BORK,WARN,LEAK,XLEAK,SKIP" "profiling/tests/phpt") after_script: - | if [ "${IMAGE_SUFFIX}" != "_centos-7" ]; then diff --git a/loader/tests/functional/test_incompatibility_jit.php b/loader/tests/functional/test_incompatibility_jit.php index 66193ceccda..eb0648b807f 100644 --- a/loader/tests/functional/test_incompatibility_jit.php +++ b/loader/tests/functional/test_incompatibility_jit.php @@ -144,7 +144,7 @@ // JIT enabled + force injection via ENV [ "config" => "-dzend_extension=opcache -dopcache.enable_cli=1 -ddatadog.trace.cli_enabled=1 -dopcache.jit_buffer_size=32M -dopcache.jit=tracing", - "env" => ['DD_INJECT_FORCE=1'], + "env" => ['DD_INJECT_FORCE=1', 'DD_PROFILING_ENABLED=1'], "must_not_contain" => [], "must_contain" => [ $msg_forced, @@ -183,6 +183,7 @@ // JIT enabled + force injection via INI [ "config" => "-dzend_extension=opcache -dopcache.enable_cli=1 -ddatadog.trace.cli_enabled=1 -ddatadog.loader.force_inject=1 -dopcache.jit_buffer_size=32M -dopcache.jit=tracing", + "env" => ['DD_PROFILING_ENABLED=1'], "must_not_contain" => [], "must_contain" => [ $msg_forced, diff --git a/profiling/tests/phpt/gc_mem_caches_01.phpt b/profiling/tests/phpt/gc_mem_caches_01.phpt index b881b56d6b3..9b642299db0 100644 --- a/profiling/tests/phpt/gc_mem_caches_01.phpt +++ b/profiling/tests/phpt/gc_mem_caches_01.phpt @@ -1,10 +1,20 @@ --TEST-- [profiling] test allocation profiling not blocking `gc_mem_caches()` call --DESCRIPTION-- -Directly after rinit, the `gc_mem_caches()` function is able to cleanup some -60KB of data, but with a custom allocator installed in the ZendMM it won't do -anything and return `int(0)`. As we do prepare the heap for this call, the -function should actually cleanup some memory and return the amount in bytes. +With a custom allocator installed in the ZendMM, `gc_mem_caches()` won't do +anything on its own and would return `int(0)`. As we do prepare the heap for +this call, the function should actually cleanup some memory and return the +amount in bytes. + +Relying on incidental garbage left over from RINIT (e.g. directly after +startup) is not robust: when the tracer is active in the same process (as it +is in the combined ddtrace.so build), the tracer's own request-lifetime +allocations (e.g. the root span) can consume exactly the small amount of +cached/free memory that would otherwise be reclaimable, causing this test to +fail for reasons unrelated to allocation profiling. Instead, this test +allocates and frees enough memory itself to guarantee there is something for +`zend_mm_gc()` to reclaim, independent of what any other loaded extension +leaves behind. --SKIPIF-- Date: Thu, 3 Sep 2026 18:05:12 -0600 Subject: [PATCH 24/47] build: fix stale Rust archive reuse across NTS/ZTS configure runs Add the generated Makefile as a prerequisite of the Rust archive rules so make rebuilds when php-config (NTS vs ZTS) changes, instead of relinking a stale ABI-incompatible archive. Also drop the standalone profiler build phases from CI's profiling tests job since we only ship combined. --- .gitlab/generate-profiler.php | 34 ++++++++++++++++++---------------- config.m4 | 19 +++++++++++++++++-- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/.gitlab/generate-profiler.php b/.gitlab/generate-profiler.php index 60c7eda702f..2223c7f1eea 100644 --- a/.gitlab/generate-profiler.php +++ b/.gitlab/generate-profiler.php @@ -55,28 +55,30 @@ - unset DD_SERVICE; unset DD_ENV - mkdir -p "${CI_PROJECT_DIR}/artifacts/profiler-tests" - - '# NTS standalone' - - '# Use if/then instead of `command -v switch-php && switch-php` — the && form exits 1 when switch-php is absent, which FF_ENABLE_BASH_EXIT_CODE_CHECK treats as a job failure' - - if command -v switch-php > /dev/null 2>&1; then switch-php "${PHP_MAJOR_MINOR}"; fi - - (cd ..; phpize && DDTRACE_PROFILING_FEATURES="debug_stats,stack_walking_tests,test,tracing,tracing-subscriber,trigger_time_sample" ./configure --disable-ddtrace-tracer --enable-ddtrace-profiling && make -j$(nproc)) - - test -f "${CI_PROJECT_DIR}/modules/datadog-profiling.so" || { echo "ERROR standalone profiler build did not produce modules/datadog-profiling.so"; find "${CI_PROJECT_DIR}/modules" -maxdepth 1 -type f -print; exit 1; } - - php -d "extension=${CI_PROJECT_DIR}/modules/datadog-profiling.so" -r 'if (!extension_loaded("datadog-profiling") || ini_get("datadog.profiling.enabled") === false) { exit(1); }' - - (cd ../; TEST_PHP_JUNIT="${CI_PROJECT_DIR}/artifacts/profiler-tests/nts-standalone-results.xml" php profiling/tests/run-tests.php -d "extension=${CI_PROJECT_DIR}/modules/datadog-profiling.so" --show-diff -g "FAIL,XFAIL,BORK,WARN,LEAK,XLEAK,SKIP" "profiling/tests/phpt") - + # CI only builds and tests the combined ddtrace.so (tracer + profiling), + # since that's the only artifact we package and ship. The standalone + # datadog-profiling.so build path is intentionally not exercised here; + # the phpt suite itself remains compatible with a standalone build (see + # the `extension_loaded('datadog-profiling') || ini_get(...)` skip + # patterns throughout profiling/tests/phpt) so it still works if someone + # builds standalone locally, but CI has no need to spend time on it. - '# NTS combined (tracer + profiling in one ddtrace.so, as shipped)' + - '# Use if/then instead of `command -v switch-php && switch-php` — the && form exits 1 when switch-php is absent, which FF_ENABLE_BASH_EXIT_CODE_CHECK treats as a job failure' - if command -v switch-php > /dev/null 2>&1; then switch-php "${PHP_MAJOR_MINOR}"; fi - - (cd ..; make distclean || true; phpize && DDTRACE_PROFILING_FEATURES="debug_stats,stack_walking_tests,test,tracing,tracing-subscriber,trigger_time_sample" ./configure --enable-ddtrace-tracer --enable-ddtrace-profiling && make -j$(nproc)) + - (cd ..; phpize && DDTRACE_PROFILING_FEATURES="debug_stats,stack_walking_tests,test,tracing,tracing-subscriber,trigger_time_sample" ./configure --enable-ddtrace-tracer --enable-ddtrace-profiling && make -j$(nproc)) - test -f "${CI_PROJECT_DIR}/modules/ddtrace.so" || { echo "ERROR combined build did not produce modules/ddtrace.so"; find "${CI_PROJECT_DIR}/modules" -maxdepth 1 -type f -print; exit 1; } - php -d "extension=${CI_PROJECT_DIR}/modules/ddtrace.so" -r 'if (!extension_loaded("ddtrace") || ini_get("datadog.profiling.enabled") === false) { exit(1); }' - (cd ../; TEST_PHP_JUNIT="${CI_PROJECT_DIR}/artifacts/profiler-tests/nts-combined-results.xml" php profiling/tests/run-tests.php -d "extension=${CI_PROJECT_DIR}/modules/ddtrace.so" --show-diff -g "FAIL,XFAIL,BORK,WARN,LEAK,XLEAK,SKIP" "profiling/tests/phpt") - - '# ZTS standalone' - - if command -v switch-php > /dev/null 2>&1; then switch-php "${PHP_MAJOR_MINOR}-zts"; fi - - (cd ..; make distclean || true; phpize && DDTRACE_PROFILING_FEATURES="debug_stats,stack_walking_tests,test,tracing,tracing-subscriber,trigger_time_sample" ./configure --disable-ddtrace-tracer --enable-ddtrace-profiling && make -j$(nproc)) - - test -f "${CI_PROJECT_DIR}/modules/datadog-profiling.so" || { echo "ERROR standalone profiler ZTS build did not produce modules/datadog-profiling.so"; find "${CI_PROJECT_DIR}/modules" -maxdepth 1 -type f -print; exit 1; } - - php -d "extension=${CI_PROJECT_DIR}/modules/datadog-profiling.so" -r 'if (!extension_loaded("datadog-profiling") || ini_get("datadog.profiling.enabled") === false) { exit(1); }' - - (cd ../; TEST_PHP_JUNIT="${CI_PROJECT_DIR}/artifacts/profiler-tests/zts-standalone-results.xml" php profiling/tests/run-tests.php -d "extension=${CI_PROJECT_DIR}/modules/datadog-profiling.so" --show-diff -g "FAIL,XFAIL,BORK,WARN,LEAK,XLEAK,SKIP" "profiling/tests/phpt") - + # Re-running configure/make below switches from NTS to ZTS PHP headers + # while reusing the same checkout and CARGO_TARGET_DIR. `make`'s + # Rust-library rule now lists the generated top-level Makefile as a + # prerequisite (see config.m4), and that Makefile's `INCLUDES = ...` + # line changes between NTS and ZTS configure runs, so `make` correctly + # detects the change and rebuilds libdatadog_php.a instead of reusing the + # NTS-flavoured archive. Without that fix, this phase would silently + # relink a stale, ABI-incompatible archive into ddtrace.so, producing a + # combined ZTS build that segfaults immediately on load. - '# ZTS combined (tracer + profiling in one ddtrace.so, as shipped)' - if command -v switch-php > /dev/null 2>&1; then switch-php "${PHP_MAJOR_MINOR}-zts"; fi - (cd ..; make distclean || true; phpize && DDTRACE_PROFILING_FEATURES="debug_stats,stack_walking_tests,test,tracing,tracing-subscriber,trigger_time_sample" ./configure --enable-ddtrace-tracer --enable-ddtrace-profiling && make -j$(nproc)) diff --git a/config.m4 b/config.m4 index b1ca2b8455f..38b9bb8216f 100644 --- a/config.m4 +++ b/config.m4 @@ -93,7 +93,7 @@ if test "$PHP_DDTRACE_PROFILING" != "no"; then .PHONY: ddtrace-profiler-rust-build ddtrace-profiler-rust-build: -$profiler_rust_lib: ddtrace-profiler-rust-build \$(shell for dir in \$(srcdir)/components-rs \$(srcdir)/profiling \$(srcdir)/libdatadog \$(srcdir)/../../libdatadog; do test ! -d \$\$dir || find \$\$dir \( -type f -o -type l \) \( -name '*.rs' -o -name '*.c' -o -name '*.h' -o -name Cargo.toml \) -not -path '*/target/*' -not -path '*/.git/*'; done) \$(srcdir)/ext/configuration.h \$(srcdir)/ext/configuration_helpers.h \$(srcdir)/ext/configuration_shared.h \$(srcdir)/tracer/configuration.h \$(srcdir)/ext/handlers_api.c \$(srcdir)/ext/handlers_api.h \$(srcdir)/Cargo.toml \$(srcdir)/Cargo.lock \$(srcdir)/VERSION +$profiler_rust_lib: ddtrace-profiler-rust-build \$(shell for dir in \$(srcdir)/components-rs \$(srcdir)/profiling \$(srcdir)/libdatadog \$(srcdir)/../../libdatadog; do test ! -d \$\$dir || find \$\$dir \( -type f -o -type l \) \( -name '*.rs' -o -name '*.c' -o -name '*.h' -o -name Cargo.toml \) -not -path '*/target/*' -not -path '*/.git/*'; done) \$(srcdir)/ext/configuration.h \$(srcdir)/ext/configuration_helpers.h \$(srcdir)/ext/configuration_shared.h \$(srcdir)/tracer/configuration.h \$(srcdir)/ext/handlers_api.c \$(srcdir)/ext/handlers_api.h \$(srcdir)/Cargo.toml \$(srcdir)/Cargo.lock \$(srcdir)/VERSION \$(builddir)/Makefile (cd "\$(srcdir)"; DDTRACE_PHP_INCLUDES="\$(INCLUDES)" CARGO_TARGET_DIR="$profiler_target_dir" RUSTFLAGS="\$(RUSTFLAGS) --cfg tokio_unstable" "\$(DDTRACE_CARGO)" build $DDTRACE_PROFILING_CARGO_BUILD_FLAGS $profiler_target_arg --no-default-features --features "profiling${DDTRACE_PROFILING_FEATURES:+,$DDTRACE_PROFILING_FEATURES}" $(test "$profiler_cargo_profile" = debug || echo --profile "$profiler_cargo_profile") \$(shell echo "\$(MAKEFLAGS)" | $EGREP -o "[[-]]j[[0-9]]+") \$(shell echo "\$(MAKEFLAGS)" | $EGREP -q -e '--silent' -e '^[[^ -]]*s' && echo --quiet)) \$(phplibdir)/datadog-profiling.la: $profiler_rust_lib @@ -558,8 +558,23 @@ EOT fi ddtrace_cargo_build_flags="${ddtrace_cargo_build_flags:-}" + dnl `make` only reruns a rule when a listed prerequisite is newer than + dnl the target; it has no idea that the recipe's own command-line + dnl environment (DDTRACE_PHP_INCLUDES, CARGO_FEATURES, ...) can change + dnl between configure runs that reuse the same CARGO_TARGET_DIR and + dnl target file path (e.g. an NTS build followed by a ZTS build, or a + dnl tracer-only build followed by a combined one, sharing an externally + dnl set CARGO_TARGET_DIR). Without this, `make` would leave a stale, + dnl ABI-incompatible libdatadog_php.a in place and relink it as-is + dnl instead of rebuilding it. The generated top-level Makefile already + dnl captures this: its `INCLUDES = ...` line is derived from whichever + dnl php-config was on PATH when configure ran (NTS vs ZTS resolve to + dnl different php-config binaries and therefore different INCLUDES), + dnl and config.status rewrites Makefile with a fresh mtime on every + dnl configure run. Listing it as a prerequisite is enough to make `make` + dnl notice and rebuild. cat <> Makefile.fragments -$ddtrace_rust_lib: $( (find "$ext_srcdir/components-rs" -name "*.rs"; find "$ext_srcdir/profiling" \( -name "*.rs" -o -name "*.c" -o -name "*.h" \); find "$ext_srcdir/zend_abstract_interface" \( -name "*.c" -o -name "*.h" \); find "$ext_srcdir" -maxdepth 1 -name "Cargo.toml"; find "$ext_srcdir/../../libdatadog" -name "*.rs" -not -path "*/target/*"; find "$ext_srcdir/libdatadog" -name "*.rs" -not -path "*/target/*") 2>/dev/null | tr '\n' ' ' ) $ext_srcdir/ext/configuration.h $ext_srcdir/ext/configuration_helpers.h $ext_srcdir/ext/configuration_shared.h $ext_srcdir/tracer/configuration.h +$ddtrace_rust_lib: $( (find "$ext_srcdir/components-rs" -name "*.rs"; find "$ext_srcdir/profiling" \( -name "*.rs" -o -name "*.c" -o -name "*.h" \); find "$ext_srcdir/zend_abstract_interface" \( -name "*.c" -o -name "*.h" \); find "$ext_srcdir" -maxdepth 1 -name "Cargo.toml"; find "$ext_srcdir/../../libdatadog" -name "*.rs" -not -path "*/target/*"; find "$ext_srcdir/libdatadog" -name "*.rs" -not -path "*/target/*") 2>/dev/null | tr '\n' ' ' ) $ext_srcdir/ext/configuration.h $ext_srcdir/ext/configuration_helpers.h $ext_srcdir/ext/configuration_shared.h $ext_srcdir/tracer/configuration.h $ext_builddir/Makefile (cd "$ext_srcdir"; DDTRACE_PHP_INCLUDES="\$(INCLUDES)" CARGO_FEATURES="$ddtrace_cargo_features" CARGO_TARGET_DIR="$ddtrace_target_dir/" SHARED=$(test "$ext_shared" = "yes" && echo 1) PROFILE="$ddtrace_cargo_profile" host_os="$host_os" DDTRACE_CARGO="\$(DDTRACE_CARGO)" $(if test "$PHP_DDTRACE_SANITIZE" != "no"; then echo COMPILE_ASAN=1; fi) sh ./compile_rust.sh $ddtrace_cargo_build_flags \$(shell echo "\$(MAKEFLAGS)" | $EGREP -o "[[-]]j[[0-9]]+") \$(shell echo "\$(MAKEFLAGS)" | $EGREP -q -e '--silent' -e '^[[^ -]]*s' && echo --quiet)) EOT fi From a2d86db4e4e3bb84f1291f89afbc444ca6d04e2d Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Thu, 3 Sep 2026 21:33:46 -0600 Subject: [PATCH 25/47] profiling: init logger before other MINIT logging config::minit() now runs immediately after the module-conflict/config-count checks, since it's what installs the log crate's logger (gated by datadog.profiling.log_level). Previously it ran after the tracing-subscriber setup and PHP_VERSION detection, so any log/warn/error calls there were silent no-ops regardless of configured level, and debug builds separately hardcoded an early Trace-level logger that unconditionally leaked a MINIT trace line into every phpt test using the debug combined ddtrace.so (e.g. CI's min install tests). --- profiling/src/config.rs | 10 ++++++---- profiling/src/lib.rs | 27 +++++++++++++++------------ 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/profiling/src/config.rs b/profiling/src/config.rs index c1fd82043c8..09bcf8042b0 100644 --- a/profiling/src/config.rs +++ b/profiling/src/config.rs @@ -983,10 +983,12 @@ pub(crate) fn minit(_module_number: libc::c_int) { // table in combined mode; standalone initialized it above. let mut system_settings = SystemSettings::new(); - // Initialize logging before allocation's rinit, as it logs. - #[cfg(debug_assertions)] - log::set_max_level(system_settings.profiling_log_level); - #[cfg(not(debug_assertions))] + // Initialize logging before allocation's rinit, as it logs. This is + // also the earliest point any profiling module log message should + // be emitted: it's the first point at which datadog.profiling.log_level + // is resolvable (system-scoped: INI/env/stable-config), so there is + // no configured level to honor any earlier than this, in either + // debug or release builds. crate::profiling::logging::log_init(system_settings.profiling_log_level); SystemSettings::log_state( diff --git a/profiling/src/lib.rs b/profiling/src/lib.rs index ad94d0f371b..96d32d08475 100644 --- a/profiling/src/lib.rs +++ b/profiling/src/lib.rs @@ -280,16 +280,21 @@ extern "C" fn minit(_type: c_int, module_number: c_int) -> ZendResult { } } - // todo: merge these lifecycle things to tracing feature? - // When developing the extension, it's useful to see log messages that - // occur before the user can configure the log level. However, if we - // initialized the logger here unconditionally, then they'd have no way to - // hide these messages. That's why it's done only for debug builds. - #[cfg(debug_assertions)] - { - logging::log_init(log::LevelFilter::Trace); - trace!("MINIT({_type}, {module_number})"); - } + // config::minit() is called as early as possible in MINIT, immediately + // after the checks above, because it's what initializes the `log` crate's + // logger (see config::minit()'s call to logging::log_init(), gated by + // datadog.profiling.log_level). Every `log`-crate macro call (trace!, + // debug!, warn!, error!, ...) anywhere in this function is a silent + // no-op until a logger is installed and its level is raised above the + // crate-wide default of Off, so calling config::minit() any later would + // silently swallow genuine startup diagnostics -- such as the + // `error!` calls in the tracing-subscriber setup below, or the `warn!` + // in the PHP_VERSION detection further down -- regardless of what the + // user configured. System-scoped config (INI/env/stable-config) is + // genuinely resolvable this early: config::minit() calls + // zai_config_first_time_rinit(false) internally to make it so. + config::minit(module_number); + trace!("MINIT({_type}, {module_number})"); #[cfg(feature = "tracing-subscriber")] { @@ -356,8 +361,6 @@ extern "C" fn minit(_type: c_int, module_number: c_int) -> ZendResult { }; } - config::minit(module_number); - // Force early initialization of the HTTPS connector while we're still // single-threaded. This ensures rustls-native-certs reads SSL_CERT_FILE // and SSL_CERT_DIR environment variables safely before any threads are From 911a33c9255acce7681399fe11d71808aae3f33b Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Fri, 4 Sep 2026 16:36:51 -0600 Subject: [PATCH 26/47] ci: allow overriding system-tests repo/ref for system_tests jobs Temporarily point at DataDog/system-tests@levi/common-extension-2 to pick up the install_ddtrace.sh profiling-marker detection fix until it's merged upstream. --- .gitlab/generate-package.php | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/.gitlab/generate-package.php b/.gitlab/generate-package.php index 0dc3f47fcaf..8f7451ee5be 100644 --- a/.gitlab/generate-package.php +++ b/.gitlab/generate-package.php @@ -163,6 +163,28 @@ function appsec_image_from_tag_mapping(string $tag): string } ?> +# Local override: the shared one-pipeline.yml template only reports the +# final decomposed image size against LIB_INJECTION_IMAGE_MAX_SIZE_BYTES, not +# what's actually inside it. This prints a per-file size breakdown of both +# architectures' decomposed package contents (which the template's own +# script already produces at /scripts/lib-injection/decomposed-{amd64,arm64}/ +# contents, before the size check runs) so we can see what's driving the +# size without needing to reproduce the build locally. Runs unconditionally +# (not just on failure) so we always have a trend, not just a snapshot when +# it's already over threshold. +create-multiarch-lib-injection-image: + after_script: + - | + for arch in amd64 arm64; do + dir="/scripts/lib-injection/decomposed-${arch}/contents" + if [ -d "$dir" ]; then + echo "=== ${arch} decomposed package contents (total: $(du -sh "$dir" | cut -f1)) ===" + find "$dir" -type f -printf '%s\t%p\n' | sort -rn | awk 'BEGIN{FS="\t"} {printf "%10.1f MB %s\n", $1/1048576, $2}' | head -50 + else + echo "=== ${arch} decomposed package directory not found at ${dir} ===" + fi + done + requirements_json_test: rules: - when: on_success @@ -1284,6 +1306,12 @@ function appsec_image_from_tag_mapping(string $tag): string PIP_CACHE_DIR: $CI_PROJECT_DIR/.cache/pip APT_CACHE: $CI_PROJECT_DIR/.cache/apt DOCKER_DEFAULT_PLATFORM: linux/amd64 + # Override these to point at a fork/branch of system-tests (e.g. while a fix there + # is pending review/merge) without needing to touch this file. + SYSTEM_TESTS_REPO: "https://github.com/DataDog/system-tests.git" + # TODO: point back at "main" once DataDog/system-tests@levi/common-extension-2's + # install_ddtrace.sh profiling-marker fix has been merged upstream. + SYSTEM_TESTS_REF: "levi/common-extension-2" # TODO DD_API_KEY; SYSTEM_TESTS_AWS_ACCESS_KEY_ID; SYSTEM_TESTS_AWS_SECRET_ACCESS_KEY needs: - job: "package extension: [amd64, x86_64-unknown-linux-gnu]" @@ -1313,7 +1341,7 @@ function appsec_image_from_tag_mapping(string $tag): string pip install -U pip virtualenv - /tmp/vault kv get --format=json "kv/k8s/gitlab-runner/dd-trace-php/datadoghq-api-key" 2>/dev/null | python3 -c "import sys,json;print(json.load(sys.stdin)['data']['data']['key'])" > /tmp/.dd-api-key 2>/dev/null || true - - git clone https://github.com/DataDog/system-tests.git + - git clone --branch "$SYSTEM_TESTS_REF" --depth 1 "$SYSTEM_TESTS_REPO" system-tests - mv packages/{datadog-setup.php,dd-library-php-*x86_64-linux-gnu.tar.gz} system-tests/binaries - cd system-tests - ./build.sh $BUILD_SH_ARGS From 588d60754c5ca710e683fcc1ab18dd766161c898 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Fri, 4 Sep 2026 17:03:33 -0600 Subject: [PATCH 27/47] build: export DDTRACE_PHP_INCLUDES for SSI split-build Rust invocations The gradle SSI task and the CMake SSI build both invoke cargo/compile_rust.sh directly instead of going through the generated top-level Makefile, so they never got Make's INCLUDES. components-rs/config_codegen.rs now requires DDTRACE_PHP_INCLUDES to preprocess ext/configuration.h for the Rust config-ID codegen, so both call sites need to export it via php-config themselves. --- appsec/cmake/ddtrace.cmake | 13 +++++++++++-- appsec/tests/integration/build.gradle | 6 +++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/appsec/cmake/ddtrace.cmake b/appsec/cmake/ddtrace.cmake index 895f4c42f7a..e57ec8c4af8 100644 --- a/appsec/cmake/ddtrace.cmake +++ b/appsec/cmake/ddtrace.cmake @@ -7,14 +7,23 @@ else() # because it references symbols that are provided by the final ddtrace extension. set(CARGO_BUILD_CMD "cargo rustc --lib --crate-type staticlib") endif() -set(CARGO_BUILD_ENV "") # Initialize to empty +# components-rs/config_codegen.rs preprocesses ext/configuration.h (and, for combined +# tracer+profiling builds, profiling/configuration.h) at build time to generate the Rust +# configuration IDs, so it needs PHP's include path even though this is otherwise a plain +# `cargo build`/`cargo rustc` invocation with no other PHP awareness. +execute_process( + COMMAND ${PhpConfig_EXECUTABLE} --includes + RESULT_VARIABLE PhpConfig_INCLUDES_RESULT + OUTPUT_VARIABLE PhpConfig_INCLUDES + OUTPUT_STRIP_TRAILING_WHITESPACE COMMAND_ERROR_IS_FATAL ANY) +set(CARGO_BUILD_ENV "DDTRACE_PHP_INCLUDES='${PhpConfig_INCLUDES}'") if(CMAKE_BUILD_TYPE STREQUAL "Release") set(CARGO_BUILD_CMD "${CARGO_BUILD_CMD} --release") elseif(CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") set(CARGO_BUILD_CMD "${CARGO_BUILD_CMD} --release") - set(CARGO_BUILD_ENV RUSTFLAGS='-C\ debuginfo=2') + set(CARGO_BUILD_ENV "${CARGO_BUILD_ENV} RUSTFLAGS='-C\ debuginfo=2'") endif() set(LIBDATADOG_DIR "${CMAKE_SOURCE_DIR}/../libdatadog") diff --git a/appsec/tests/integration/build.gradle b/appsec/tests/integration/build.gradle index b86d21e7c7a..c2e8f65217d 100644 --- a/appsec/tests/integration/build.gradle +++ b/appsec/tests/integration/build.gradle @@ -451,8 +451,12 @@ def buildTracerSsiTask = { String version, String variant -> make /project/tmp/build_extension/modules/ddtrace.so cp /project/tmp/build_extension/modules/ddtrace.so /tracer-ssi/ddtrace.so - # Build libdatadog_php.so (cdylib) via compile_rust.sh. + # Build libdatadog_php.so (cdylib) via compile_rust.sh. Unlike the `make` + # call above, this invokes compile_rust.sh directly, so DDTRACE_PHP_INCLUDES + # (needed by components-rs/config_codegen.rs to preprocess ext/configuration.h) + # must be exported by hand instead of coming from Make's INCLUDES. CARGO_TARGET_DIR=/project/tmp/target \\ + DDTRACE_PHP_INCLUDES="\$(php-config --includes)" \\ SHARED=1 PROFILE=${cargoProfile} host_os=linux ./compile_rust.sh cp /project/tmp/target/${cargoProfile}/libdatadog_php.so /tracer-ssi/libdatadog_php.so From fb429bdab5611c8d1ebf60b6e13bb7f801f3644f Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Fri, 4 Sep 2026 18:45:45 -0600 Subject: [PATCH 28/47] Fix N -> 10 --- .claude/ci/diagnosing-failures.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/ci/diagnosing-failures.md b/.claude/ci/diagnosing-failures.md index 835bf994667..00febd82a60 100644 --- a/.claude/ci/diagnosing-failures.md +++ b/.claude/ci/diagnosing-failures.md @@ -28,7 +28,7 @@ non-fatal issues. If a test flips green→red **deterministically across every PHP version/SAPI** with no corresponding source/commit change, suspect a **mutable CI image tag -being republished** under the same tag (the `bookworm-10` images are rebuilt +being republished** under the same tag (the `bookworm-{N}` images are rebuilt in place with `docker buildx bake --no-cache --pull`), not a code regression. Verify the mechanism empirically before fixing: From 0a3ae8585756e3c04e099b9dcd8cc3f8c93490ce Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Fri, 4 Sep 2026 19:07:16 -0600 Subject: [PATCH 29/47] build: add compile_combined/install_combined Makefile targets Fixes local-repro docs that ran phpize/configure/make directly in the repo root, which overwrites the tracked top-level Makefile. Add compile_combined/install_combined next to the existing compile_profiler/install_profiler targets so combined tracer+profiler builds (what CI actually ships) go through an isolated tmp/build_combined/ copy instead, and point docs at these targets. --- .claude/ci/building-locally.md | 49 +++++++++++++++++++------ .claude/ci/github-actions-profiler.md | 51 +++++++++++++-------------- Makefile | 16 +++++++++ 3 files changed, 80 insertions(+), 36 deletions(-) diff --git a/.claude/ci/building-locally.md b/.claude/ci/building-locally.md index 211e53e53e1..be417b6f608 100644 --- a/.claude/ci/building-locally.md +++ b/.claude/ci/building-locally.md @@ -240,21 +240,50 @@ git submodule update --init \ ## Profiler Extension -### For correctness tests (bookworm) - -Build the loadable extension through phpize/configure/Make from the repository -root. Make owns the Cargo feature set, PHP headers, target directory, and final -module name: +**Do not run `phpize`/`configure` directly in the repo root** -- it overwrites +the tracked top-level `Makefile` (and `Makefile.fragments`/`Makefile.objects`) +with a generated one, corrupting the checkout for every other target. Always +go through the root `Makefile`'s own targets below, which copy sources into an +isolated `tmp/build_{combined,profiler}/` directory (via the `all` target's +`$(BUILD_DIR)/configure` prerequisite) before running `phpize`/`configure`/`make` +there -- the checked-out root `Makefile`/`configure` are never touched. + +### Combined tracer+profiler (preferred -- matches what CI ships) + +Most local profiler work should build the **combined** `ddtrace.so` (tracer + +profiling in one extension), since that's the only artifact CI packages and +tests as of the combined-extension milestone. Use `make compile_combined` +(or `make install_combined` to also copy it into the PHP extension dir and +register it via `ddtrace.ini`): ```bash dockerh --cache profiler-8.3-nts --php nts \ datadog/dd-trace-ci:php-8.3_bookworm-10 -- bash -c ' cd /project/dd-trace-php -phpize -DDTRACE_PROFILING_FEATURES=trigger_time_sample \ - ./configure --disable-ddtrace-tracer --enable-ddtrace-profiling --disable-ddtrace-rust-debug -make -j"$(nproc)" -php -n -d extension=modules/datadog-profiling.so --ri datadog-profiling +make compile_combined -j"$(nproc)" +php -n -d extension=tmp/build_combined/modules/ddtrace.so --ri ddtrace +' +``` + +`compile_combined` sets `EXTRA_CONFIGURE_OPTIONS="--enable-ddtrace-tracer +--enable-ddtrace-profiling"` and `DDTRACE_PROFILING_FEATURES=trigger_time_sample`, +and builds in `tmp/build_combined/` (`BUILD_SUFFIX=combined`) so it doesn't +collide with a plain tracer-only `tmp/build_extension/` build in the same +checkout. + +### Standalone profiler (only for testing the standalone artifact itself) + +The standalone `datadog-profiling.so` (no tracer) is not built or shipped by +CI anymore -- only use this when specifically testing standalone/combined +conflict behavior or other standalone-specific code paths, not as a general +substitute for the combined build above: + +```bash +dockerh --cache profiler-8.3-nts-standalone --php nts \ + datadog/dd-trace-ci:php-8.3_bookworm-10 -- bash -c ' +cd /project/dd-trace-php +make compile_profiler -j"$(nproc)" +php -n -d extension=tmp/build_profiler/modules/datadog-profiling.so --ri datadog-profiling ' ``` diff --git a/.claude/ci/github-actions-profiler.md b/.claude/ci/github-actions-profiler.md index eebd15c8c2f..a027430cab1 100644 --- a/.claude/ci/github-actions-profiler.md +++ b/.claude/ci/github-actions-profiler.md @@ -57,31 +57,29 @@ being tested. ### Build the profiler extension -Loadable artifacts must go through phpize/configure/Make from the repository root. +Loadable artifacts must go through the root `Makefile`'s `compile_combined` / +`compile_profiler` targets, not naked `phpize`/`configure`/`make` in the repo +root -- that would overwrite the tracked top-level `Makefile` itself. These +targets build in an isolated `tmp/build_{combined,profiler}/` copy instead. Do not load a Cargo target-directory cdylib. ```bash -# Standalone NTS example (PHP 8.3) -dockerh --cache profiler-8.3-nts --php nts datadog/dd-trace-ci:php-8.3_bookworm-10 -- bash -c ' +# Standalone NTS example (PHP 8.3) -- only for testing the standalone artifact +# itself; prefer the combined example below for general local testing. +dockerh --cache profiler-8.3-nts-standalone --php nts datadog/dd-trace-ci:php-8.3_bookworm-10 -- bash -c ' cd /project/dd-trace-php -phpize -DDTRACE_PROFILING_FEATURES=trigger_time_sample \ - ./configure --disable-ddtrace-tracer --enable-ddtrace-profiling --disable-ddtrace-rust-debug -make -j"$(nproc)" +make compile_profiler -j"$(nproc)" ' -# Combined ZTS example (PHP 8.5) +# Combined ZTS example (PHP 8.5) -- matches what CI ships/tests dockerh --cache profiler-8.5-zts --php zts datadog/dd-trace-ci:php-8.5_bookworm-10 -- bash -c ' cd /project/dd-trace-php -phpize -DDTRACE_PROFILING_FEATURES=trigger_time_sample \ - ./configure --enable-ddtrace-tracer --enable-ddtrace-profiling --disable-ddtrace-rust-debug -make -j"$(nproc)" +make compile_combined -j"$(nproc)" ' ``` -The supported outputs are `modules/datadog-profiling.so` and -`modules/ddtrace.so`, respectively. Use separate caches for PHP versions and NTS/ZTS +The supported outputs are `tmp/build_profiler/modules/datadog-profiling.so` and +`tmp/build_combined/modules/ddtrace.so`, respectively. Use separate caches for PHP versions and NTS/ZTS variants. ### Run a single test case @@ -102,7 +100,7 @@ TEST_CASE=allocations OUT=/project/dd-trace-php/tmp/correctness/$TEST_CASE mkdir -p $OUT DD_PROFILING_OUTPUT_PPROF=$OUT/test.pprof \ - php -d extension=/project/dd-trace-php/modules/datadog-profiling.so \ + php -d extension=/project/dd-trace-php/tmp/build_profiler/modules/datadog-profiling.so \ /project/dd-trace-php/profiling/tests/correctness/$TEST_CASE.php ls -la $OUT/ ' @@ -199,17 +197,17 @@ frame name formatting. The implementation is in `profiling/src/capi.rs` and ## Debug Build -Select Rust debug mode through configure, then consume the Make output: +Select Rust debug mode via `RUST_DEBUG_BUILD=1` (maps to +`--enable-ddtrace-rust-debug` in the root `Makefile`'s `$(BUILD_DIR)/Makefile` +rule), then consume the Make output. Do not run `phpize`/`configure` directly +in the repo root: ```bash -phpize -DDTRACE_PROFILING_FEATURES=trigger_time_sample \ - ./configure --disable-ddtrace-tracer --enable-ddtrace-profiling --enable-ddtrace-rust-debug -make -j"$(nproc)" +RUST_DEBUG_BUILD=1 make compile_profiler -j"$(nproc)" ``` -The artifact remains `modules/datadog-profiling.so`. Profiling PHPT and correctness -expectations are intended for optimized builds. +The artifact remains `tmp/build_profiler/modules/datadog-profiling.so`. Profiling +PHPT and correctness expectations are intended for optimized builds. ## ZTS tests -- parallel PECL extension @@ -241,10 +239,11 @@ rustup override set nightly-2025-06-13 export RUSTFLAGS="-Zsanitizer=address -C force-frame-pointers=yes" export DDTRACE_PROFILING_TARGET="$(uname -m)-unknown-linux-gnu" export DDTRACE_PROFILING_CARGO_BUILD_FLAGS="-Zbuild-std=std,panic_abort" -phpize -./configure --disable-ddtrace-tracer --enable-ddtrace-profiling --disable-ddtrace-rust-debug -make -j"$(nproc)" -cp -v modules/datadog-profiling.so \ +# CI runs phpize/configure/make directly since its checkout is ephemeral; do not +# do that here -- use the root Makefile target instead so a persistent local +# checkout is not left with a clobbered top-level Makefile. +make compile_profiler -j"$(nproc)" +cp -v tmp/build_profiler/modules/datadog-profiling.so \ "$(php-config --extension-dir)/datadog-profiling.so" # run-tests.php writes temp files next to .phpt files, so both must be in a writable dir. diff --git a/Makefile b/Makefile index d6b2ae0f8d7..34f3f28445f 100644 --- a/Makefile +++ b/Makefile @@ -400,6 +400,22 @@ prod: strict: $(eval CFLAGS=-Wall -Werror -Wextra) +# Combined tracer+profiling in a single ddtrace.so, as shipped to users. This is +# the target most people should use locally when they need profiling -- the +# standalone profiler below (compile_profiler/install_profiler) is only for +# testing the legacy standalone artifact itself, not a general substitute. +compile_combined: + DDTRACE_PROFILING_FEATURES=trigger_time_sample $(MAKE) BUILD_SUFFIX=combined EXTRA_CONFIGURE_OPTIONS="--enable-ddtrace-tracer --enable-ddtrace-profiling" all + +install_combined: compile_combined + $(SUDO) cp $(PROJECT_ROOT)/tmp/build_combined/modules/ddtrace.so $(PHP_EXTENSION_DIR)/ddtrace.so + +install_all_combined: install_combined install_ini + +# Standalone profiler only (no tracer). Not what CI ships or what most local +# testing needs -- prefer compile_combined/install_combined unless you're +# specifically testing the standalone artifact (e.g. standalone/combined +# conflict tests). compile_profiler: DDTRACE_PROFILING_FEATURES=trigger_time_sample $(MAKE) BUILD_SUFFIX=profiler PROFILING=1 EXTRA_CONFIGURE_OPTIONS="--disable-ddtrace-tracer --enable-ddtrace-profiling" all From 35dacd99a71ac905dc049221e00df29e0980c2d6 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Fri, 4 Sep 2026 21:04:55 -0600 Subject: [PATCH 30/47] ci: raise lib-injection image size threshold to match measured growth CI data (create-multiarch-lib-injection-image) shows 306M/292M vs the 210M ratchet set in April. Confirmed via generate-ssi-package.sh history that combining tracer+profiling into one ddtrace.so reduces per-API-version size (removes a second linked copy of the shared Rust runtime) rather than increasing it -- the growth is from unrelated upstream libdatadog additions since April (crashtracker, stats computation, FFE, multi-config). Raise the threshold instead of chasing a regression this branch didn't cause. --- .gitlab/generate-package.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.gitlab/generate-package.php b/.gitlab/generate-package.php index 8f7451ee5be..3b3cbe655d3 100644 --- a/.gitlab/generate-package.php +++ b/.gitlab/generate-package.php @@ -121,9 +121,18 @@ function appsec_image_from_tag_mapping(string $tag): string FF_USE_NEW_BASH_EVAL_STRATEGY: "true" CARGO_HOME: "${CI_PROJECT_DIR}/.cache/cargo" - # One pipeline injection package size ratchet + # One pipeline injection package size ratchet. + # LIB_INJECTION_IMAGE_MAX_SIZE_BYTES was set to 210M in April as a tightening + # of the template's 250M default. Since then, real measured size has grown + # to ~292-306M compressed (per-arch) purely from upstream libdatadog/Rust + # dependency growth (crashtracker, stats computation, FFE metrics, dynamic + # multi-config, etc.) -- NOT from combining tracer+profiling into one + # ddtrace.so (that change actually reduces per-PHP-API-version size, since + # it eliminates a second, separately-linked copy of the shared Rust runtime). + # Raised to give headroom over the current measured max; revisit if it + # keeps climbing. OCI_PACKAGE_MAX_SIZE_BYTES: 150_000_000 - LIB_INJECTION_IMAGE_MAX_SIZE_BYTES: 210_000_000 + LIB_INJECTION_IMAGE_MAX_SIZE_BYTES: 320_000_000 REPO_NOTIFICATION_CHANNEL: "#guild-dd-php" From 6df11ac9f7c54b8be669a18d23ff69d213eeba95 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Wed, 9 Sep 2026 15:02:19 -0600 Subject: [PATCH 31/47] ci(appsec): fix buildPortableLibdatadogPhp missing DDTRACE_PHP_INCLUDES config_codegen.rs (added by the combined-extension work) unconditionally preprocesses ext/configuration.h and needs real PHP headers, but this task's php-buildonly-rust image has no PHP under test to point at -- it builds libdatadog_php.so once and shares it across every PHP version in the integration matrix. Verified the macro list config_codegen.rs extracts doesn't vary by PHP version, so it doesn't matter which SDK's headers are used. Rather than pin a specific PHP version from php-buildonly-rust's image (which would silently go stale once that version drops out of testMatrix), discover whichever php-config happens to be present under /opt/php/*/bin, with an explicit failure if none exist. Fixes the failures in 'appsec integration tests (ssi): [test8.3-release-ssi]' and the PHP 8.5 variant: both failed with the same 'DDTRACE_PHP_INCLUDES is required' panic from config_codegen.rs. --- appsec/tests/integration/build.gradle | 39 +++++++++++++++------------ 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/appsec/tests/integration/build.gradle b/appsec/tests/integration/build.gradle index b7a5bd9d9d7..a7777dd6847 100644 --- a/appsec/tests/integration/build.gradle +++ b/appsec/tests/integration/build.gradle @@ -569,17 +569,11 @@ def buildTracerSsiTask = { String version, String variant -> grep -qx libdatadog_php.so cp "\$TRACER" /tracer-ssi/ddtrace.so - # NOTE: libdatadog_php.so now comes prebuilt from buildPortableLibdatadogPhp - # (see that task below), which runs in the php-buildonly-rust image -- a - # PHP-version-independent Rust build environment with no real PHP dev headers. - # components-rs/config_codegen.rs (which preprocesses ext/configuration.h and - # therefore needs real PHP headers via DDTRACE_PHP_INCLUDES) runs unconditionally - # from components-rs/build.rs, so this task is expected to break once - # config_codegen.rs exists in this tree -- needs a deliberate fix (e.g. a - # reference/synthetic PHP header set for preprocessing only, or excluding this - # portable build from needing generated config accessors) rather than just - # exporting DDTRACE_PHP_INCLUDES, since there is no single PHP version's real - # headers to point it at here. + # libdatadog_php.so comes prebuilt from buildPortableLibdatadogPhp (see that + # task below), which runs once in the php-buildonly-rust image and is shared + # across every PHP version in the integration matrix -- see the + # DDTRACE_PHP_INCLUDES comment on portableLibdatadogPhpCommand for why that is + # safe despite config_codegen.rs needing real PHP headers. cp /portable-rust/libdatadog_php.so \\ /tracer-ssi/libdatadog_php.so @@ -1148,16 +1142,27 @@ if (useHelperRustCoverage) { } def portableLibdatadogPhpBuildMode = useHelperRustCoverage ? 'helper-rust-coverage' : 'default' -// FIXME(combined-extension merge): this build has no real PHP dev headers (see -// php-buildonly-rust/Dockerfile), but components-rs/build.rs unconditionally calls -// config_codegen::build(), which preprocesses ext/configuration.h and needs -// DDTRACE_PHP_INCLUDES pointing at a real PHP include dir. Needs a deliberate fix, -// not a quick env var -- there is no single PHP version's headers to use here since -// this artifact is intentionally built once and shared across all PHP versions. +// components-rs/build.rs unconditionally calls config_codegen::build(), which +// preprocesses ext/configuration.h and needs DDTRACE_PHP_INCLUDES pointing at a +// real PHP include dir. This build has no PHP under test -- libdatadog_php.so is +// built once here and shared across every PHP version in the integration matrix +// -- but config_codegen.rs only extracts the CONFIG()/CALIAS() macro list, which +// does not vary by PHP version (see config.m4/config_codegen.rs). So rather than +// pin one SDK from php-buildonly-rust's image (which would silently go stale the +// day that version drops out of testMatrix), just discover whichever one happens +// to be present. def portableLibdatadogPhpCommand = ''' RUST_TARGET="$(uname -m)-unknown-linux-musl" RUST_TOOLCHAIN="nightly-$RUST_TARGET" export CARGO_TARGET_DIR=/portable-rust/cargo-target + for php_config in /opt/php/*/bin/php-config; do + export DDTRACE_PHP_INCLUDES="$("$php_config" --includes)" + break + done + if [ -z "${DDTRACE_PHP_INCLUDES:-}" ]; then + echo "No php-config found under /opt/php/*/bin -- cannot determine PHP headers for config_codegen.rs" >&2 + exit 1 + fi cd /project RUSTFLAGS='@RUSTFLAGS@' \ From 99174918896b678cc36e0c8b9297ddab50c06624 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Wed, 9 Sep 2026 17:58:25 -0600 Subject: [PATCH 32/47] Revert "ci(appsec): fix buildPortableLibdatadogPhp missing DDTRACE_PHP_INCLUDES" This reverts commit 6df11ac9f7c54b8be669a18d23ff69d213eeba95. --- appsec/tests/integration/build.gradle | 39 ++++++++++++--------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/appsec/tests/integration/build.gradle b/appsec/tests/integration/build.gradle index a7777dd6847..b7a5bd9d9d7 100644 --- a/appsec/tests/integration/build.gradle +++ b/appsec/tests/integration/build.gradle @@ -569,11 +569,17 @@ def buildTracerSsiTask = { String version, String variant -> grep -qx libdatadog_php.so cp "\$TRACER" /tracer-ssi/ddtrace.so - # libdatadog_php.so comes prebuilt from buildPortableLibdatadogPhp (see that - # task below), which runs once in the php-buildonly-rust image and is shared - # across every PHP version in the integration matrix -- see the - # DDTRACE_PHP_INCLUDES comment on portableLibdatadogPhpCommand for why that is - # safe despite config_codegen.rs needing real PHP headers. + # NOTE: libdatadog_php.so now comes prebuilt from buildPortableLibdatadogPhp + # (see that task below), which runs in the php-buildonly-rust image -- a + # PHP-version-independent Rust build environment with no real PHP dev headers. + # components-rs/config_codegen.rs (which preprocesses ext/configuration.h and + # therefore needs real PHP headers via DDTRACE_PHP_INCLUDES) runs unconditionally + # from components-rs/build.rs, so this task is expected to break once + # config_codegen.rs exists in this tree -- needs a deliberate fix (e.g. a + # reference/synthetic PHP header set for preprocessing only, or excluding this + # portable build from needing generated config accessors) rather than just + # exporting DDTRACE_PHP_INCLUDES, since there is no single PHP version's real + # headers to point it at here. cp /portable-rust/libdatadog_php.so \\ /tracer-ssi/libdatadog_php.so @@ -1142,27 +1148,16 @@ if (useHelperRustCoverage) { } def portableLibdatadogPhpBuildMode = useHelperRustCoverage ? 'helper-rust-coverage' : 'default' -// components-rs/build.rs unconditionally calls config_codegen::build(), which -// preprocesses ext/configuration.h and needs DDTRACE_PHP_INCLUDES pointing at a -// real PHP include dir. This build has no PHP under test -- libdatadog_php.so is -// built once here and shared across every PHP version in the integration matrix -// -- but config_codegen.rs only extracts the CONFIG()/CALIAS() macro list, which -// does not vary by PHP version (see config.m4/config_codegen.rs). So rather than -// pin one SDK from php-buildonly-rust's image (which would silently go stale the -// day that version drops out of testMatrix), just discover whichever one happens -// to be present. +// FIXME(combined-extension merge): this build has no real PHP dev headers (see +// php-buildonly-rust/Dockerfile), but components-rs/build.rs unconditionally calls +// config_codegen::build(), which preprocesses ext/configuration.h and needs +// DDTRACE_PHP_INCLUDES pointing at a real PHP include dir. Needs a deliberate fix, +// not a quick env var -- there is no single PHP version's headers to use here since +// this artifact is intentionally built once and shared across all PHP versions. def portableLibdatadogPhpCommand = ''' RUST_TARGET="$(uname -m)-unknown-linux-musl" RUST_TOOLCHAIN="nightly-$RUST_TARGET" export CARGO_TARGET_DIR=/portable-rust/cargo-target - for php_config in /opt/php/*/bin/php-config; do - export DDTRACE_PHP_INCLUDES="$("$php_config" --includes)" - break - done - if [ -z "${DDTRACE_PHP_INCLUDES:-}" ]; then - echo "No php-config found under /opt/php/*/bin -- cannot determine PHP headers for config_codegen.rs" >&2 - exit 1 - fi cd /project RUSTFLAGS='@RUSTFLAGS@' \ From e49ec8999f7878efe4799530aa91d7b43a6c6787 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Wed, 9 Sep 2026 19:40:38 -0600 Subject: [PATCH 33/47] fix: move profiling's config codegen out of components-rs/build.rs ConfigId/CONFIG_COUNT generation only serves profiling/src/config.rs, so relocate config_codegen.rs and its generated config.rs (now config_id.rs) into profiling/, and call config_codegen::build() from profiling/build.rs instead of unconditionally from components-rs/build.rs. Tracer-only builds (e.g. the SSI/portable-lib libdatadog_php.so, built once and shared across every PHP version) no longer even reference this codegen, so they never preprocess ext/configuration.h or need PHP headers -- not just gated off, but structurally absent from that build path. Also note in Cargo.toml that the "tracer" feature name is misleading. --- Cargo.toml | 8 ++ components-rs/build.rs | 4 - components-rs/lib.rs | 9 ++ profiling/build.rs | 12 ++ .../config_codegen.rs | 115 +++--------------- profiling/config_codegen_php_stubs/README.md | 25 ++++ .../config_codegen_php_stubs/Zend/zend.h | 0 .../Zend/zend_closures.h | 0 .../Zend/zend_portability.h | 0 .../Zend/zend_smart_str.h | 0 .../ext/standard/base64.h | 0 profiling/config_codegen_php_stubs/main/php.h | 0 .../config_codegen_php_stubs/main/snprintf.h | 0 profiling/config_codegen_php_stubs/php.h | 0 .../config.rs => profiling/config_id.rs | 0 15 files changed, 72 insertions(+), 101 deletions(-) rename {components-rs => profiling}/config_codegen.rs (57%) create mode 100644 profiling/config_codegen_php_stubs/README.md create mode 100644 profiling/config_codegen_php_stubs/Zend/zend.h create mode 100644 profiling/config_codegen_php_stubs/Zend/zend_closures.h create mode 100644 profiling/config_codegen_php_stubs/Zend/zend_portability.h create mode 100644 profiling/config_codegen_php_stubs/Zend/zend_smart_str.h create mode 100644 profiling/config_codegen_php_stubs/ext/standard/base64.h create mode 100644 profiling/config_codegen_php_stubs/main/php.h create mode 100644 profiling/config_codegen_php_stubs/main/snprintf.h create mode 100644 profiling/config_codegen_php_stubs/php.h rename components-rs/config.rs => profiling/config_id.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 6fea7e75652..8a3eeefa2e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,14 @@ bench = false [features] default = ["tracer"] +# TODO: "tracer" is a misleading name for what this actually gates: it pulls in +# the ddtrace-sidecar crate (sidecar entrypoint for direct-spawn + AppSec backend +# registration) and switches components-rs/build.rs to produce the shared +# "common native component" artifact shape (custom entrypoint, `libdatadog_php.so` +# soname) rather than the standalone-profiler-shaped one. It has nothing to do +# with tracer-specific logic per se -- both the combined ddtrace.so *and* the +# standalone SSI libdatadog_php.so need it, precisely because they're both this +# "common" artifact. Consider renaming to something like "sidecar" or "common". tracer = ["dep:ddtrace-sidecar"] helper-rust-coverage = ["ddtrace-sidecar/helper-rust-coverage"] profiling = [ diff --git a/components-rs/build.rs b/components-rs/build.rs index fe35fb7592d..acd4dd85876 100644 --- a/components-rs/build.rs +++ b/components-rs/build.rs @@ -1,5 +1,3 @@ -#[path = "config_codegen.rs"] -mod config_codegen; #[path = "../profiling/build.rs"] mod profiling_build; @@ -64,6 +62,4 @@ fn main() { if std::env::var_os("CARGO_FEATURE_PROFILING").is_some() { profiling_build::build(); } - - config_codegen::build(); } diff --git a/components-rs/lib.rs b/components-rs/lib.rs index ef92a0203fd..1a2b3583720 100644 --- a/components-rs/lib.rs +++ b/components-rs/lib.rs @@ -37,6 +37,15 @@ pub mod profiling; pub mod agent_info; #[cfg(not(all(feature = "profiling", not(feature = "tracer"))))] pub mod bytes; +// Only the profiler consumes ConfigId/CONFIG_COUNT/the generated accessors (see +// profiling/src/config.rs, profiling/src/lib.rs, profiling/src/bindings/mod.rs). +// The module itself, and the build.rs codegen step that generates its content, +// both live under profiling/ (see profiling/config_id.rs, profiling/build.rs, +// profiling/config_codegen.rs) rather than here, so a tracer-only build never +// even sees code that would preprocess ext/configuration.h through a C compiler +// -- not just gated off, but physically absent from this crate's shared build.rs. +#[cfg(feature = "profiling")] +#[path = "../profiling/config_id.rs"] pub mod config; #[cfg(not(all(feature = "profiling", not(feature = "tracer"))))] pub mod ffe; diff --git a/profiling/build.rs b/profiling/build.rs index 205a97a3950..d9ddf6a2c2a 100644 --- a/profiling/build.rs +++ b/profiling/build.rs @@ -4,7 +4,19 @@ use std::path::Path; use std::path::PathBuf; use std::{env, fs}; +#[path = "config_codegen.rs"] +mod config_codegen; + pub fn build() { + // ConfigId/CONFIG_COUNT/the generated accessors are consumed only by + // profiling/src/config.rs (see profiling/config_id.rs and this crate's + // cfg(feature = "profiling") gate in components-rs/lib.rs), so this codegen + // lives and runs here rather than in components-rs/build.rs -- a tracer-only + // build (e.g. libdatadog_php.so, shared across every PHP version in SSI/the + // portable-lib test harness) never needs to preprocess ext/configuration.h + // through a C compiler at all, and now structurally can't be made to. + config_codegen::build(); + // Make owns PHP toolchain selection and passes its generated include flags // into Cargo. Do not rediscover a potentially different PHP installation. println!("cargo:rerun-if-env-changed=DDTRACE_PHP_INCLUDES"); diff --git a/components-rs/config_codegen.rs b/profiling/config_codegen.rs similarity index 57% rename from components-rs/config_codegen.rs rename to profiling/config_codegen.rs index 5d75c0eb8a2..c9fc265da4b 100644 --- a/components-rs/config_codegen.rs +++ b/profiling/config_codegen.rs @@ -116,102 +116,23 @@ fn render_accessors(ty: &str, name: &str) -> String { ) } +// This probe only ever runs the C preprocessor (never a real compile), purely to +// expand the CONFIG()/CALIAS() X-macro list in ext/configuration.h into a flat, +// textual `type, name` record per entry. That expansion never depends on the +// *content* of any PHP header -- only on `#include` directives resolving to *some* +// file on disk (ext/configuration.h transitively includes real PHP headers for +// declarations this codegen step never looks at). So rather than require a real +// PHP installation -- which loadable extensions have via Make's INCLUDES, but +// which some build contexts genuinely don't have one "correct" instance of (e.g. +// buildPortableLibdatadogPhp in appsec/tests/integration/build.gradle, which +// builds one Rust artifact shared across every PHP version in the test matrix) -- +// point the preprocessor at a small set of bundled empty stub headers instead. +// See config_codegen_php_stubs/README.md for what to do if this ever needs a new +// stub (it will fail loudly with a `file not found` naming the missing path). fn configure_php_build(build: &mut cc::Build) { - println!("cargo:rerun-if-env-changed=DDTRACE_PHP_INCLUDES"); - println!("cargo:rerun-if-env-changed=DDTRACE_PHP_CFLAGS"); - - if env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") { - let flags = env::var("DDTRACE_PHP_CFLAGS") - .expect("DDTRACE_PHP_CFLAGS is required; config.w32 must pass PHP's CFLAGS to Cargo"); - apply_msvc_php_flags(build, &flags); - return; - } - - let includes = env::var("DDTRACE_PHP_INCLUDES").expect( - "DDTRACE_PHP_INCLUDES is required; loadable extensions must pass Make's INCLUDES to Cargo", - ); - for include in parse_include_flags(&includes) { - build.include(include); - } -} - -fn parse_include_flags(flags: &str) -> Vec { - flags - .split_whitespace() - .filter_map(|flag| flag.strip_prefix("-I")) - .map(str::to_string) - .collect() -} - -fn apply_msvc_php_flags(build: &mut cc::Build, flags: &str) { - let args = split_windows_command_line(flags); - let mut index = 0; - while index < args.len() { - let arg = &args[index]; - let upper = arg.to_ascii_uppercase(); - match upper.as_str() { - "/I" => { - index += 1; - build.include( - args.get(index) - .expect("/I is missing its include directory"), - ); - } - "/D" => { - index += 1; - apply_msvc_define( - build, - args.get(index).expect("/D is missing its definition"), - ); - } - "/FI" => { - index += 1; - build.flag("/FI"); - build.flag(args.get(index).expect("/FI is missing its header")); - } - "/EXPERIMENTAL:PREPROCESSOR" | "/ZC:PREPROCESSOR" => { - build.flag(arg); - } - _ if upper.starts_with("/I") => { - build.include(&arg[2..]); - } - _ if upper.starts_with("/D") => apply_msvc_define(build, &arg[2..]), - _ if upper.starts_with("/FI") => { - build.flag(arg); - } - _ => {} - } - index += 1; - } -} - -fn apply_msvc_define(build: &mut cc::Build, definition: &str) { - if let Some((name, value)) = definition.split_once('=') { - build.define(name, Some(value)); - } else { - build.define(definition, None); - } -} - -fn split_windows_command_line(command_line: &str) -> Vec { - let mut args = Vec::new(); - let mut current = String::new(); - let mut quoted = false; - - for ch in command_line.chars() { - match ch { - '"' => quoted = !quoted, - ch if ch.is_whitespace() && !quoted => { - if !current.is_empty() { - args.push(std::mem::take(&mut current)); - } - } - _ => current.push(ch), - } - } - assert!(!quoted, "unterminated quote in DDTRACE_PHP_CFLAGS"); - if !current.is_empty() { - args.push(current); - } - args + build.include("profiling/config_codegen_php_stubs"); + build.include("profiling/config_codegen_php_stubs/main"); + build.include("profiling/config_codegen_php_stubs/TSRM"); + build.include("profiling/config_codegen_php_stubs/Zend"); + build.include("profiling/config_codegen_php_stubs/ext"); } diff --git a/profiling/config_codegen_php_stubs/README.md b/profiling/config_codegen_php_stubs/README.md new file mode 100644 index 00000000000..7bb559ef75f --- /dev/null +++ b/profiling/config_codegen_php_stubs/README.md @@ -0,0 +1,25 @@ +# Stub PHP headers for `config_codegen.rs` + +These files are **intentionally empty**. Do not add real content to them, and do not +delete them as unused. + +`components-rs/config_codegen.rs` runs the C preprocessor over `ext/configuration.h` +purely to expand the `CONFIG()`/`CALIAS()` X-macro list into a flat, textual +`type, name` record per configuration entry -- it never compiles the result, and the +macro expansion does not depend on the *content* of any PHP header, only on the +`#include` directives resolving to *some* file on disk (`ext/configuration.h` +transitively includes real PHP headers like `Zend/zend_closures.h` for declarations +that this codegen step never looks at). + +This directory contains empty stand-ins for exactly the header paths that chain +needs, discovered by iteratively preprocessing with `clang -E` and creating an empty +stub for whatever `fatal error: '...' file not found` came back next, until the +whole file preprocessed cleanly. If a future change to `ext/configuration.h` (or +anything it includes) adds a new `#include` of a real PHP header, preprocessing will +fail the same way with a clear "file not found" error naming the missing path -- +add an empty file at that path here and it will resolve. + +This lets `config_codegen.rs` run without a real PHP installation, which matters for +build contexts that share one Rust artifact across many PHP versions/ABIs (see +`buildPortableLibdatadogPhp` in `appsec/tests/integration/build.gradle`) where there +is no one "correct" PHP install to point at. diff --git a/profiling/config_codegen_php_stubs/Zend/zend.h b/profiling/config_codegen_php_stubs/Zend/zend.h new file mode 100644 index 00000000000..e69de29bb2d diff --git a/profiling/config_codegen_php_stubs/Zend/zend_closures.h b/profiling/config_codegen_php_stubs/Zend/zend_closures.h new file mode 100644 index 00000000000..e69de29bb2d diff --git a/profiling/config_codegen_php_stubs/Zend/zend_portability.h b/profiling/config_codegen_php_stubs/Zend/zend_portability.h new file mode 100644 index 00000000000..e69de29bb2d diff --git a/profiling/config_codegen_php_stubs/Zend/zend_smart_str.h b/profiling/config_codegen_php_stubs/Zend/zend_smart_str.h new file mode 100644 index 00000000000..e69de29bb2d diff --git a/profiling/config_codegen_php_stubs/ext/standard/base64.h b/profiling/config_codegen_php_stubs/ext/standard/base64.h new file mode 100644 index 00000000000..e69de29bb2d diff --git a/profiling/config_codegen_php_stubs/main/php.h b/profiling/config_codegen_php_stubs/main/php.h new file mode 100644 index 00000000000..e69de29bb2d diff --git a/profiling/config_codegen_php_stubs/main/snprintf.h b/profiling/config_codegen_php_stubs/main/snprintf.h new file mode 100644 index 00000000000..e69de29bb2d diff --git a/profiling/config_codegen_php_stubs/php.h b/profiling/config_codegen_php_stubs/php.h new file mode 100644 index 00000000000..e69de29bb2d diff --git a/components-rs/config.rs b/profiling/config_id.rs similarity index 100% rename from components-rs/config.rs rename to profiling/config_id.rs From aa93c7e6155ace647e0c64b7a0e85c7d64d10e37 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Wed, 9 Sep 2026 19:57:38 -0600 Subject: [PATCH 34/47] fix: use real PHP headers for config codegen, drop stub headers config_codegen::build() now only ever runs from profiling::build(), which already requires and validates DDTRACE_PHP_INCLUDES before calling it. So the stub-header fallback (added when this codegen could still run from a tracer-only build with no real PHP headers available) is now both unnecessary and actively wrong: it silently shadowed real PHP headers even in combined builds, and broke real PHP headers whose macros expect __has_builtin to be defined by zend_portability.h (which the stub headers left empty), causing the CentOS 7 combined-extension CI job to fail with "missing binary operator before token '('" while preprocessing zai_assert.h. Pass profiling::build()'s already-validated DDTRACE_PHP_INCLUDES straight into config_codegen::build() instead, and delete the now-dead stub headers and configure_php_build(). --- profiling/build.rs | 21 +++++++----- profiling/config_codegen.rs | 34 ++++++------------- profiling/config_codegen_php_stubs/README.md | 25 -------------- .../config_codegen_php_stubs/Zend/zend.h | 0 .../Zend/zend_closures.h | 0 .../Zend/zend_portability.h | 0 .../Zend/zend_smart_str.h | 0 .../ext/standard/base64.h | 0 profiling/config_codegen_php_stubs/main/php.h | 0 .../config_codegen_php_stubs/main/snprintf.h | 0 profiling/config_codegen_php_stubs/php.h | 0 11 files changed, 23 insertions(+), 57 deletions(-) delete mode 100644 profiling/config_codegen_php_stubs/README.md delete mode 100644 profiling/config_codegen_php_stubs/Zend/zend.h delete mode 100644 profiling/config_codegen_php_stubs/Zend/zend_closures.h delete mode 100644 profiling/config_codegen_php_stubs/Zend/zend_portability.h delete mode 100644 profiling/config_codegen_php_stubs/Zend/zend_smart_str.h delete mode 100644 profiling/config_codegen_php_stubs/ext/standard/base64.h delete mode 100644 profiling/config_codegen_php_stubs/main/php.h delete mode 100644 profiling/config_codegen_php_stubs/main/snprintf.h delete mode 100644 profiling/config_codegen_php_stubs/php.h diff --git a/profiling/build.rs b/profiling/build.rs index d9ddf6a2c2a..a89b3a16f56 100644 --- a/profiling/build.rs +++ b/profiling/build.rs @@ -8,15 +8,6 @@ use std::{env, fs}; mod config_codegen; pub fn build() { - // ConfigId/CONFIG_COUNT/the generated accessors are consumed only by - // profiling/src/config.rs (see profiling/config_id.rs and this crate's - // cfg(feature = "profiling") gate in components-rs/lib.rs), so this codegen - // lives and runs here rather than in components-rs/build.rs -- a tracer-only - // build (e.g. libdatadog_php.so, shared across every PHP version in SSI/the - // portable-lib test harness) never needs to preprocess ext/configuration.h - // through a C compiler at all, and now structurally can't be made to. - config_codegen::build(); - // Make owns PHP toolchain selection and passes its generated include flags // into Cargo. Do not rediscover a potentially different PHP installation. println!("cargo:rerun-if-env-changed=DDTRACE_PHP_INCLUDES"); @@ -24,6 +15,18 @@ pub fn build() { .expect("DDTRACE_PHP_INCLUDES is required; build through phpize/configure/Make"); let php_include_root = php_include_root(&php_includes); + // ConfigId/CONFIG_COUNT/the generated accessors are consumed only by + // profiling/src/config.rs (see profiling/config_id.rs and this crate's + // cfg(feature = "profiling") gate in components-rs/lib.rs), so this codegen + // lives and runs here rather than in components-rs/build.rs -- a tracer-only + // build (e.g. libdatadog_php.so, shared across every PHP version in SSI/the + // portable-lib test harness) never needs to preprocess ext/configuration.h + // through a C compiler at all. This function only ever runs after the + // DDTRACE_PHP_INCLUDES check above has already succeeded, so it always has + // real, correct PHP headers for whichever PHP version Make is building + // against -- no stub headers needed. + config_codegen::build(&php_includes); + // Read the version from the VERSION file let version = fs::read_to_string("VERSION") .expect("Failed to read VERSION file") diff --git a/profiling/config_codegen.rs b/profiling/config_codegen.rs index c9fc265da4b..a510235d160 100644 --- a/profiling/config_codegen.rs +++ b/profiling/config_codegen.rs @@ -32,7 +32,7 @@ RUST_CONFIG_MARKER RUST_CONFIGURATIONS "#; -pub fn build() { +pub fn build(php_includes: &str) { let out_dir = env::var("OUT_DIR").expect("OUT_DIR not set"); let probe_path = Path::new(&out_dir).join("config_probe.c"); fs::write(&probe_path, PROBE_SOURCE).expect("failed to write config_probe.c"); @@ -51,7 +51,16 @@ pub fn build() { if env::var_os("CARGO_FEATURE_PROFILING").is_some() { cc_build.define("PROFILING", None); } - configure_php_build(&mut cc_build); + // php_includes is the same real, correct set of -I flags Make already + // validated for the PHP version this build is targeting (profiling/build.rs + // requires DDTRACE_PHP_INCLUDES before calling this function at all), so + // just reuse it rather than rediscovering or stubbing out PHP headers. + for include in php_includes + .split_whitespace() + .filter_map(|flag| flag.strip_prefix("-I")) + { + cc_build.include(include); + } let expanded = String::from_utf8(cc_build.expand()) .expect("config preprocessor output was not valid UTF-8"); @@ -115,24 +124,3 @@ fn render_accessors(ty: &str, name: &str) -> String { #[cfg(feature = \"profiling\")]\n#[allow(dead_code, non_snake_case)]\npub(crate) unsafe fn get_global_{name}() -> {return_type} {{\n crate::profiling::config::{memoized}(ConfigId::{name})\n}}\n\n" ) } - -// This probe only ever runs the C preprocessor (never a real compile), purely to -// expand the CONFIG()/CALIAS() X-macro list in ext/configuration.h into a flat, -// textual `type, name` record per entry. That expansion never depends on the -// *content* of any PHP header -- only on `#include` directives resolving to *some* -// file on disk (ext/configuration.h transitively includes real PHP headers for -// declarations this codegen step never looks at). So rather than require a real -// PHP installation -- which loadable extensions have via Make's INCLUDES, but -// which some build contexts genuinely don't have one "correct" instance of (e.g. -// buildPortableLibdatadogPhp in appsec/tests/integration/build.gradle, which -// builds one Rust artifact shared across every PHP version in the test matrix) -- -// point the preprocessor at a small set of bundled empty stub headers instead. -// See config_codegen_php_stubs/README.md for what to do if this ever needs a new -// stub (it will fail loudly with a `file not found` naming the missing path). -fn configure_php_build(build: &mut cc::Build) { - build.include("profiling/config_codegen_php_stubs"); - build.include("profiling/config_codegen_php_stubs/main"); - build.include("profiling/config_codegen_php_stubs/TSRM"); - build.include("profiling/config_codegen_php_stubs/Zend"); - build.include("profiling/config_codegen_php_stubs/ext"); -} diff --git a/profiling/config_codegen_php_stubs/README.md b/profiling/config_codegen_php_stubs/README.md deleted file mode 100644 index 7bb559ef75f..00000000000 --- a/profiling/config_codegen_php_stubs/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# Stub PHP headers for `config_codegen.rs` - -These files are **intentionally empty**. Do not add real content to them, and do not -delete them as unused. - -`components-rs/config_codegen.rs` runs the C preprocessor over `ext/configuration.h` -purely to expand the `CONFIG()`/`CALIAS()` X-macro list into a flat, textual -`type, name` record per configuration entry -- it never compiles the result, and the -macro expansion does not depend on the *content* of any PHP header, only on the -`#include` directives resolving to *some* file on disk (`ext/configuration.h` -transitively includes real PHP headers like `Zend/zend_closures.h` for declarations -that this codegen step never looks at). - -This directory contains empty stand-ins for exactly the header paths that chain -needs, discovered by iteratively preprocessing with `clang -E` and creating an empty -stub for whatever `fatal error: '...' file not found` came back next, until the -whole file preprocessed cleanly. If a future change to `ext/configuration.h` (or -anything it includes) adds a new `#include` of a real PHP header, preprocessing will -fail the same way with a clear "file not found" error naming the missing path -- -add an empty file at that path here and it will resolve. - -This lets `config_codegen.rs` run without a real PHP installation, which matters for -build contexts that share one Rust artifact across many PHP versions/ABIs (see -`buildPortableLibdatadogPhp` in `appsec/tests/integration/build.gradle`) where there -is no one "correct" PHP install to point at. diff --git a/profiling/config_codegen_php_stubs/Zend/zend.h b/profiling/config_codegen_php_stubs/Zend/zend.h deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/profiling/config_codegen_php_stubs/Zend/zend_closures.h b/profiling/config_codegen_php_stubs/Zend/zend_closures.h deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/profiling/config_codegen_php_stubs/Zend/zend_portability.h b/profiling/config_codegen_php_stubs/Zend/zend_portability.h deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/profiling/config_codegen_php_stubs/Zend/zend_smart_str.h b/profiling/config_codegen_php_stubs/Zend/zend_smart_str.h deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/profiling/config_codegen_php_stubs/ext/standard/base64.h b/profiling/config_codegen_php_stubs/ext/standard/base64.h deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/profiling/config_codegen_php_stubs/main/php.h b/profiling/config_codegen_php_stubs/main/php.h deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/profiling/config_codegen_php_stubs/main/snprintf.h b/profiling/config_codegen_php_stubs/main/snprintf.h deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/profiling/config_codegen_php_stubs/php.h b/profiling/config_codegen_php_stubs/php.h deleted file mode 100644 index e69de29bb2d..00000000000 From 8bc9941e407fbae273477e953a07fd0c7bb414a9 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Wed, 9 Sep 2026 20:30:48 -0600 Subject: [PATCH 35/47] test(prof): disable sidecar/telemetry in pcntl helper-thread phpt This test is about the profiler's own helper threads (ddprof_time/ ddprof_upload), not the tracer/sidecar/telemetry. Since CI only builds and tests the combined ddtrace.so, all of that machinery now starts up here too as a side effect, adding background threads and a sidecar connection attempt that add CPU contention unrelated to what this test verifies, making its timing-sensitive SIGCHLD-reaping loop flakier on constrained CI runners (e.g. "profiling tests: [7.3, amd64, ..._centos-7]" failing with "leftover children"). Disable every flag that gates datadog_sidecar_should_enable() so no sidecar connection is attempted, keeping the test's timing budget focused on the profiler. Verified locally that a real ddtrace-sidecar subprocess (datadog-ipc-helper) is spawned without these env vars and is not spawned with them. --- .../phpt/pcntl_async_signal_helper_thread.phpt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/profiling/tests/phpt/pcntl_async_signal_helper_thread.phpt b/profiling/tests/phpt/pcntl_async_signal_helper_thread.phpt index 8ca065c8406..bf5eeeab04d 100644 --- a/profiling/tests/phpt/pcntl_async_signal_helper_thread.phpt +++ b/profiling/tests/phpt/pcntl_async_signal_helper_thread.phpt @@ -42,6 +42,22 @@ if (getenv('SKIP_ASAN')) --ENV-- DD_PROFILING_ENABLED=yes DD_PROFILING_LOG_LEVEL=off +; This test is specifically about the profiler's own helper threads +; (ddprof_time/ddprof_upload) not eating async signals meant for a PHP +; thread -- it is not about the tracer/sidecar/telemetry. Since CI now only +; builds and tests the combined ddtrace.so, all of that machinery starts up +; here too purely as a side effect of the extension being combined, adding +; background threads/a sidecar connection attempt/CPU contention that has +; nothing to do with what this test verifies and makes its timing-sensitive +; SIGCHLD-reaping loop flakier on constrained CI runners. Disable every +; flag that gates datadog_sidecar_should_enable() (see ext/sidecar.c) so no +; sidecar connection is attempted at all, keeping this test's timing budget +; focused on the profiler. DD_TRACE_ENABLED is deliberately not part of +; that gate (see datadog_sidecar_should_enable()), so it is not listed here. +DD_INSTRUMENTATION_TELEMETRY_ENABLED=0 +DD_TRACE_SIDECAR_TRACE_SENDER=0 +DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED=0 +DD_METRICS_OTEL_ENABLED=0 --FILE-- Date: Thu, 10 Sep 2026 06:43:35 -0600 Subject: [PATCH 36/47] test(appsec): avoid sidecar restart race --- .../php/integration/SidecarFeaturesDisabledTests.groovy | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/SidecarFeaturesDisabledTests.groovy b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/SidecarFeaturesDisabledTests.groovy index 7deb59ce61b..8fe01f5b1c8 100644 --- a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/SidecarFeaturesDisabledTests.groovy +++ b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/SidecarFeaturesDisabledTests.groovy @@ -77,6 +77,13 @@ class SidecarFeaturesDisabledTests { 'sed', '-i', 's/^datadog.appsec.enabled.*$/datadog.appsec.enabled=false/', '/etc/php/php.ini') assert res.exitCode == 0 + // Stop the AppSec-enabled workers before killing the sidecar. Otherwise + // a worker can observe the broken transport and respawn the sidecar + // while Apache is restarting, leaving an orphan helper behind even + // though the replacement workers have AppSec disabled. + res = CONTAINER.execInContainer('service', 'apache2', 'stop') + assert res.exitCode == 0 + res = CONTAINER.execInContainer( '/bin/bash', '-c', ''' pid=`pgrep -f [d]atadog-ipc-helper`; @@ -84,7 +91,7 @@ class SidecarFeaturesDisabledTests { kill -9 $pid; fi''') assert res.exitCode == 0 - res = CONTAINER.execInContainer('service', 'apache2', 'restart') + res = CONTAINER.execInContainer('service', 'apache2', 'start') assert res.exitCode == 0 HttpRequest req = CONTAINER.buildReq('/hello.php') From c50cf70ff2ef76501f9af909958865373c995c6e Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Thu, 10 Sep 2026 06:47:48 -0600 Subject: [PATCH 37/47] test(installer): support legacy profiler versions --- .../verify_packages/installer/utils.sh | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/dockerfiles/verify_packages/installer/utils.sh b/dockerfiles/verify_packages/installer/utils.sh index 3c5c56fa6fc..38ac2ea54cf 100755 --- a/dockerfiles/verify_packages/installer/utils.sh +++ b/dockerfiles/verify_packages/installer/utils.sh @@ -32,7 +32,7 @@ assert_no_ddtrace() { } assert_no_profiler() { - if php -r 'exit(filter_var(ini_get("datadog.profiling.enabled"), FILTER_VALIDATE_BOOL) ? 0 : 1);'; then + if php -r 'exit(filter_var(ini_get("datadog.profiling.enabled"), FILTER_VALIDATE_BOOLEAN) ? 0 : 1);'; then echo "---\nError: profiler should not be enabled\n---\n${1}\n---\n" exit 1 fi @@ -71,8 +71,19 @@ assert_no_appsec() { } assert_profiler_version() { - assert_ddtrace_version "${1}" - assert_profiler_installed + expected_version=${1} + output="$(php -v)" + if [ -z "${output##*with datadog-profiling*}" ]; then + if [ -z "${output##*datadog-profiling v${expected_version}*}" ]; then + echo "---\nOk: datadog-profiling version '${expected_version}' is correctly installed\n---\n${output}\n---\n" + else + echo "---\nError: Wrong datadog-profiling version. Expected: ${expected_version}\n---\n${output}\n---\n" + exit 1 + fi + else + assert_ddtrace_version "${expected_version}" + assert_profiler_installed + fi } assert_tracer_installed() { @@ -88,7 +99,7 @@ assert_tracer_installed() { assert_profiler_installed() { php_bin=${1:-php} - if "$php_bin" -r '$value = ini_get("datadog.profiling.enabled"); exit($value !== false && filter_var($value, FILTER_VALIDATE_BOOL) ? 0 : 1);'; then + if "$php_bin" -r '$value = ini_get("datadog.profiling.enabled"); exit($value !== false && filter_var($value, FILTER_VALIDATE_BOOLEAN) ? 0 : 1);'; then echo "Ok: Profiling is available and enabled in ddtrace" else echo "Error: Profiling should be available and enabled in ddtrace" From 1fc782b5ce0f661d050c1400f87a886419116eb5 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Thu, 10 Sep 2026 11:55:30 -0600 Subject: [PATCH 38/47] ci: use HTTPS for Alpine package repositories --- .gitlab/generate-package.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitlab/generate-package.php b/.gitlab/generate-package.php index 02e4301282e..2f05987bd16 100644 --- a/.gitlab/generate-package.php +++ b/.gitlab/generate-package.php @@ -1150,6 +1150,10 @@ function package_extension_jobs(array $platform, string $kind = "all"): array - mkdir build - mv packages build + # Older Alpine images bake HTTP repository URLs into /etc/apk/repositories. + # Use HTTPS before the first apk invocation so package installation works + # when plaintext HTTP egress is blocked. + - sed -i 's|^http://|https://|' /etc/apk/repositories - apk add --no-cache ca-certificates || exit 75 # see https://support.circleci.com/hc/en-us/articles/360016505753-Resolve-Certificate-Signed-By-Unknown-Authority-error-in-Alpine-images?flash_digest=39b76521a337cecacac0cc10cb28f3747bb5fc6a - apk add curl ${INSTALL_PACKAGES:-} || exit 75 From 8418fdebd833a168f6964f345d18578fc3f75651 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Thu, 10 Sep 2026 13:29:00 -0600 Subject: [PATCH 39/47] fix: use one ASan runtime for Rust builds --- .github/workflows/prof_asan.yml | 2 +- config.m4 | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/prof_asan.yml b/.github/workflows/prof_asan.yml index e12941983d0..74881af7dfb 100644 --- a/.github/workflows/prof_asan.yml +++ b/.github/workflows/prof_asan.yml @@ -59,7 +59,7 @@ jobs: export LDFLAGS='-fsanitize=address -shared-libasan' export RUSTC_LINKER=lld-20 rustup override set "${RUST_TOOLCHAIN}" - export RUSTFLAGS='-Zsanitizer=address -C force-frame-pointers=yes -C link-arg=-fsanitize=address -C link-arg=-shared-libasan' + export RUSTFLAGS='-Zsanitizer=address -Zexternal-clangrt -C force-frame-pointers=yes -C link-arg=-fsanitize=address -C link-arg=-shared-libasan' export DDTRACE_PROFILING_TARGET="$(uname -m)-unknown-linux-gnu" export DDTRACE_PROFILING_CARGO_BUILD_FLAGS='-Zbuild-std=std,panic_abort' phpize diff --git a/config.m4 b/config.m4 index 019574c68bd..0d1d50bd538 100644 --- a/config.m4 +++ b/config.m4 @@ -559,7 +559,9 @@ EOT if test "$PHP_DDTRACE_SIDECAR_MOCKGEN" != "-"; then ddtrace_mockgen_invocation="HOST= TARGET= $PHP_DDTRACE_SIDECAR_MOCKGEN" else - ddtrace_mockgen_invocation="cd \"$ext_srcdir/components-rs/php_sidecar_mockgen\"; HOST= TARGET= CARGO_TARGET_DIR=\$(builddir)/target_mockgen/ \$(DDTRACE_CARGO) run \$(shell echo \"\$(MAKEFLAGS)\" | $EGREP -q -e '--silent' -e '^[[^ -]]*s' && echo --quiet)" + dnl The mock generator is a build-time host tool. Do not apply extension + dnl sanitizer flags to it or its build scripts and procedural macros. + ddtrace_mockgen_invocation="cd \"$ext_srcdir/components-rs/php_sidecar_mockgen\"; HOST= TARGET= CFLAGS= CXXFLAGS= CPPFLAGS= LDFLAGS= RUSTFLAGS= CARGO_TARGET_DIR=\$(builddir)/target_mockgen/ \$(DDTRACE_CARGO) run \$(shell echo \"\$(MAKEFLAGS)\" | $EGREP -q -e '--silent' -e '^[[^ -]]*s' && echo --quiet)" fi fi From cad341de9365812c0682df865513e296df5366f9 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Thu, 10 Sep 2026 13:37:55 -0600 Subject: [PATCH 40/47] ci: install libcurl for profiler correctness --- .github/workflows/prof_correctness.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prof_correctness.yml b/.github/workflows/prof_correctness.yml index 0fd49e1e6ce..3d5300cbf7e 100644 --- a/.github/workflows/prof_correctness.yml +++ b/.github/workflows/prof_correctness.yml @@ -72,7 +72,7 @@ jobs: sudo apt-get purge -y $llvm18_packages sudo apt-get autoremove -y fi - sudo apt-get install -y clang-${LLVM_VERSION} lld-${LLVM_VERSION} llvm-${LLVM_VERSION}-dev libclang-${LLVM_VERSION}-dev libclang-rt-${LLVM_VERSION}-dev + sudo apt-get install -y clang-${LLVM_VERSION} lld-${LLVM_VERSION} llvm-${LLVM_VERSION}-dev libclang-${LLVM_VERSION}-dev libclang-rt-${LLVM_VERSION}-dev libcurl4-openssl-dev sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-${LLVM_VERSION} 100 sudo update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-${LLVM_VERSION} 100 sudo update-alternatives --install /usr/bin/ld.lld ld.lld /usr/bin/ld.lld-${LLVM_VERSION} 100 From 2b2e8275d57cae727a068492a34372789a4fa404 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Thu, 10 Sep 2026 14:20:55 -0600 Subject: [PATCH 41/47] ci: preserve PHP runtime dependencies --- .github/workflows/prof_correctness.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/prof_correctness.yml b/.github/workflows/prof_correctness.yml index 3d5300cbf7e..bdc63f32c98 100644 --- a/.github/workflows/prof_correctness.yml +++ b/.github/workflows/prof_correctness.yml @@ -70,7 +70,6 @@ jobs: '*bolt*18*' '*polly*18*' 2>/dev/null || true)" if [ -n "$llvm18_packages" ]; then sudo apt-get purge -y $llvm18_packages - sudo apt-get autoremove -y fi sudo apt-get install -y clang-${LLVM_VERSION} lld-${LLVM_VERSION} llvm-${LLVM_VERSION}-dev libclang-${LLVM_VERSION}-dev libclang-rt-${LLVM_VERSION}-dev libcurl4-openssl-dev sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-${LLVM_VERSION} 100 From a3052bee83e5208d035ff015b5a1ab82e6ece214 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Thu, 10 Sep 2026 14:35:39 -0600 Subject: [PATCH 42/47] fix(installer): support legacy profiler packages --- datadog-setup.php | 48 ++++++++++++++----- .../test_install_custom_ext_dir_and_ini.sh | 1 - ...test_install_custom_extension_directory.sh | 1 - .../test_profiler_install_disabled.sh | 1 - .../test_profiler_install_enabled.sh | 1 - .../test_profiler_install_enabled_debug.sh | 11 ++++- .../verify_packages/installer/utils.sh | 4 +- 7 files changed, 48 insertions(+), 19 deletions(-) diff --git a/datadog-setup.php b/datadog-setup.php index 89372575e02..51188d29d6a 100644 --- a/datadog-setup.php +++ b/datadog-setup.php @@ -530,6 +530,7 @@ function install($options) } while (file_exists($tmpDir)); $tmpArchiveRoot = $tmpDir . '/dd-library-php'; $tmpArchiveTraceRoot = $tmpDir . '/dd-library-php/trace'; + $tmpArchiveProfilingRoot = $tmpDir . '/dd-library-php/profiling'; $tmpArchiveAppsecRoot = $tmpDir . '/dd-library-php/appsec'; $tmpArchiveAppsecEtc = "{$tmpArchiveAppsecRoot}/etc"; $tmpSrcDir = $tmpArchiveTraceRoot . '/src'; @@ -651,11 +652,20 @@ function install($options) $extensionDestination = $extDir . '/' . EXTENSION_PREFIX . 'ddtrace.' . EXTENSION_SUFFIX; safe_copy_extension($extensionRealPath, $extensionDestination); - // Profiling is compiled into supported ddtrace artifacts. The marker - // distinguishes combined builds from legacy tracer-only variants such - // as PHP 7.0 and PHP debug builds. + // Current packages compile profiling into ddtrace and identify those + // artifacts with a marker. Released packages may contain the legacy + // standalone profiler instead, which remains supported for upgrades. $profilingMarker = "$tmpArchiveTraceRoot/ext/$extensionVersion/.ddtrace$extensionSuffix.profiling"; - $shouldInstallProfiling = file_exists($profilingMarker); + $hasCombinedProfiling = file_exists($profilingMarker); + $profilingExtensionRealPath = "$tmpArchiveProfilingRoot/ext/$extensionVersion/" + . EXTENSION_PREFIX . "datadog-profiling$extensionSuffix." . EXTENSION_SUFFIX; + $hasStandaloneProfiling = file_exists($profilingExtensionRealPath); + $shouldInstallProfiling = $hasCombinedProfiling || $hasStandaloneProfiling; + + if ($hasStandaloneProfiling) { + $profilingExtensionDestination = $extDir . '/' . EXTENSION_PREFIX . 'datadog-profiling.' . EXTENSION_SUFFIX; + safe_copy_extension($profilingExtensionRealPath, $profilingExtensionDestination); + } // Appsec $appsecExtensionRealPath = "{$tmpArchiveAppsecRoot}/ext/{$extensionVersion}/" @@ -710,10 +720,16 @@ function install($options) ]; } - // The standalone profiler cannot coexist with combined ddtrace. - // Disable legacy entries while preserving them for users who - // intentionally install it separately. - $replacements['(^\s*(zend_)?extension\s*=\s*.*datadog-profiling.*)m'] = '; $0'; + $profilingExtensionPattern = '(^\s*;?\s*(zend_)?extension\s*=\s*.*datadog-profiling.*)m'; + if ($hasCombinedProfiling) { + // The standalone profiler cannot coexist with combined ddtrace. + $replacements[$profilingExtensionPattern] = '; $0'; + } elseif ($hasStandaloneProfiling && is_truthy($options[OPT_ENABLE_PROFILING])) { + $iniProfilingExtension = isset($options[OPT_EXTENSION_DIR]) + ? $profilingExtensionDestination + : 'datadog-profiling' . (IS_WINDOWS ? '' : '.' . EXTENSION_SUFFIX); + $replacements[$profilingExtensionPattern] = "extension = $iniProfilingExtension"; + } if (isset($options[OPT_EXTENSION_DIR])) { $replacements += [ @@ -770,7 +786,7 @@ function install($options) add_missing_ini_settings( $iniFilePath, - get_ini_settings($installDirSrcDir, $appSecRulesPath), + get_ini_settings($installDirSrcDir, $appSecRulesPath, $hasStandaloneProfiling), $replacements ); @@ -2204,12 +2220,13 @@ function map_env_to_ini($env) * * @param string $sourcesDir * @param string $appsecRulesPath + * @param bool $includeStandaloneProfiling * @return array */ -function get_ini_settings($sourcesDir, $appsecRulesPath) +function get_ini_settings($sourcesDir, $appsecRulesPath, $includeStandaloneProfiling = false) { // phpcs:disable Generic.Files.LineLength.TooLong - return [ + $settings = [ [ 'name' => 'extension', 'default' => 'ddtrace' . (IS_WINDOWS ? "" : "." . EXTENSION_SUFFIX), @@ -2619,7 +2636,16 @@ function get_ini_settings($sourcesDir, $appsecRulesPath) 'description' => 'Customises the JSON output provided on a blocked request', ], ]; + if ($includeStandaloneProfiling) { + array_splice($settings, 1, 0, [[ + 'name' => 'extension', + 'default' => 'datadog-profiling' . (IS_WINDOWS ? "" : "." . EXTENSION_SUFFIX), + 'commented' => true, + 'description' => 'Enables the standalone profiling module from legacy packages', + ]]); + } // phpcs:enable Generic.Files.LineLength.TooLong + return $settings; } /** diff --git a/dockerfiles/verify_packages/installer/test_install_custom_ext_dir_and_ini.sh b/dockerfiles/verify_packages/installer/test_install_custom_ext_dir_and_ini.sh index 99cc85a25f3..73b9d15e90b 100644 --- a/dockerfiles/verify_packages/installer/test_install_custom_ext_dir_and_ini.sh +++ b/dockerfiles/verify_packages/installer/test_install_custom_ext_dir_and_ini.sh @@ -13,7 +13,6 @@ custom_ini_file="$(get_php_conf_dir)/40-ddtrace.ini" php ./build/packages/datadog-setup.php --php-bin php --extension-dir /custom-ext-dir --enable-profiling --ini "$custom_ini_file" assert_file_exists /custom-ext-dir/ddtrace.so -assert_file_not_exists /custom-ext-dir/datadog-profiling.so assert_ddtrace_version "${trace_version}" assert_profiler_installed diff --git a/dockerfiles/verify_packages/installer/test_install_custom_extension_directory.sh b/dockerfiles/verify_packages/installer/test_install_custom_extension_directory.sh index 33ba9fc0e60..ed941dcf127 100644 --- a/dockerfiles/verify_packages/installer/test_install_custom_extension_directory.sh +++ b/dockerfiles/verify_packages/installer/test_install_custom_extension_directory.sh @@ -12,7 +12,6 @@ trace_version=$(cat VERSION) php ./build/packages/datadog-setup.php --php-bin php --extension-dir /custom-ext-dir --enable-profiling assert_file_exists /custom-ext-dir/ddtrace.so -assert_file_not_exists /custom-ext-dir/datadog-profiling.so assert_ddtrace_version "${trace_version}" assert_profiler_installed diff --git a/dockerfiles/verify_packages/installer/test_profiler_install_disabled.sh b/dockerfiles/verify_packages/installer/test_profiler_install_disabled.sh index c0dfa20a8d0..6c1f4dbea0d 100644 --- a/dockerfiles/verify_packages/installer/test_profiler_install_disabled.sh +++ b/dockerfiles/verify_packages/installer/test_profiler_install_disabled.sh @@ -13,6 +13,5 @@ php ./build/packages/datadog-setup.php --php-bin php assert_ddtrace_version "${version}" assert_file_exists "$(get_php_extension_dir)"/ddtrace.so -assert_file_not_exists "$(get_php_extension_dir)"/datadog-profiling.so assert_no_profiler assert_file_contains "$(get_php_conf_dir)/98-ddtrace.ini" "datadog.profiling.enabled = Off" diff --git a/dockerfiles/verify_packages/installer/test_profiler_install_enabled.sh b/dockerfiles/verify_packages/installer/test_profiler_install_enabled.sh index 0511d35e0a7..08cb36a4bb7 100644 --- a/dockerfiles/verify_packages/installer/test_profiler_install_enabled.sh +++ b/dockerfiles/verify_packages/installer/test_profiler_install_enabled.sh @@ -13,6 +13,5 @@ php ./build/packages/datadog-setup.php --php-bin php --enable-profiling assert_ddtrace_version "${version}" assert_file_exists "$(get_php_extension_dir)"/ddtrace.so -assert_file_not_exists "$(get_php_extension_dir)"/datadog-profiling.so assert_profiler_version "${version}" assert_file_contains "$(get_php_conf_dir)/98-ddtrace.ini" "datadog.profiling.enabled = On" diff --git a/dockerfiles/verify_packages/installer/test_profiler_install_enabled_debug.sh b/dockerfiles/verify_packages/installer/test_profiler_install_enabled_debug.sh index c755faec592..56f25f41988 100644 --- a/dockerfiles/verify_packages/installer/test_profiler_install_enabled_debug.sh +++ b/dockerfiles/verify_packages/installer/test_profiler_install_enabled_debug.sh @@ -9,9 +9,16 @@ switch-php debug # Initially no ddtrace assert_no_ddtrace -# Install the profiling-capable debug module using the PHP installer. +# Install the profiling-capable debug module from the package under test. version=$(cat VERSION) -php ./build/packages/datadog-setup.php --php-bin php --enable-profiling +uname=$(uname -a) +arch=$(if [ -z "${uname##*arm*}" ] || [ -z "${uname##*aarch*}" ]; then echo aarch64; else echo x86_64; fi) +bundle="./build/packages/dd-library-php-${version}-${arch}-linux-gnu.tar.gz" +if ! [ -f "${bundle}" ]; then + echo "SKIPPED: this test runs only in CI as it requires the .tar.gz at a specific path" + exit 0 +fi +php ./build/packages/datadog-setup.php --php-bin php --enable-profiling --file "${bundle}" assert_ddtrace_version "${version}" assert_file_exists "$(get_php_extension_dir)"/ddtrace.so diff --git a/dockerfiles/verify_packages/installer/utils.sh b/dockerfiles/verify_packages/installer/utils.sh index 38ac2ea54cf..ab04442ca5e 100755 --- a/dockerfiles/verify_packages/installer/utils.sh +++ b/dockerfiles/verify_packages/installer/utils.sh @@ -100,9 +100,9 @@ assert_tracer_installed() { assert_profiler_installed() { php_bin=${1:-php} if "$php_bin" -r '$value = ini_get("datadog.profiling.enabled"); exit($value !== false && filter_var($value, FILTER_VALIDATE_BOOLEAN) ? 0 : 1);'; then - echo "Ok: Profiling is available and enabled in ddtrace" + echo "Ok: Profiling is available and enabled" else - echo "Error: Profiling should be available and enabled in ddtrace" + echo "Error: Profiling should be available and enabled" exit 1 fi } From ee9816838cc002ab73f752a79fbbe5fe449aa418 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Thu, 10 Sep 2026 14:39:42 -0600 Subject: [PATCH 43/47] test(prof): accept combined module frames --- profiling/tests/correctness/strange_frames.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/profiling/tests/correctness/strange_frames.json b/profiling/tests/correctness/strange_frames.json index d0b83f649f8..092d269c214 100644 --- a/profiling/tests/correctness/strange_frames.json +++ b/profiling/tests/correctness/strange_frames.json @@ -6,7 +6,7 @@ "profile-type": "sample", "stack-content": [ { - "regular_expression": " Date: Thu, 10 Sep 2026 15:13:25 -0600 Subject: [PATCH 44/47] fix: block async signals on tracer writer --- .../pcntl_async_signal_helper_thread.phpt | 28 ++++++------------- tracer/coms.c | 27 ++++++++---------- 2 files changed, 20 insertions(+), 35 deletions(-) diff --git a/profiling/tests/phpt/pcntl_async_signal_helper_thread.phpt b/profiling/tests/phpt/pcntl_async_signal_helper_thread.phpt index bf5eeeab04d..a01511262b7 100644 --- a/profiling/tests/phpt/pcntl_async_signal_helper_thread.phpt +++ b/profiling/tests/phpt/pcntl_async_signal_helper_thread.phpt @@ -1,16 +1,16 @@ --TEST-- -[profiling] async PHP signal handlers must not run on profiler helper threads +[profiling] async PHP signal handlers must not run on extension helper threads --DESCRIPTION-- Regression test for a crash when a PHP script installs an async signal handler (pcntl_async_signals(true) + pcntl_signal()) for a signal the Zend Engine does not itself register, e.g. SIGCHLD. -The profiler's helper threads (`ddprof_time`, `ddprof_upload`) only masked the -fixed set of signals the Zend Engine uses, so the kernel could deliver SIGCHLD -to one of them. pcntl's handler then ran on a thread with no valid PHP/TSRM -context and dereferenced the thread-local PCNTL_G, segfaulting. The fix is to -block every (non-fault) signal on the helper threads so async signals are -delivered to a PHP thread. +Extension helper threads have no valid PHP/TSRM context. If they only mask the +fixed set of signals used by Zend, the kernel can deliver SIGCHLD to one of +them. pcntl's handler then dereferences the thread-local PCNTL_G and segfaults. +This originally affected profiler threads (`ddprof_time`, `ddprof_upload`) and, +in combined builds, also affects the tracer writer thread. Every helper thread +must block all non-fault signals so async signals are delivered to a PHP thread. Only crashes on ZTS, where PCNTL_G is thread-local. Observed on PHP 8.4 ZTS, reproduced by ext/pcntl/tests/waiting_on_sigchild_pcntl_wait.phpt: @@ -42,18 +42,8 @@ if (getenv('SKIP_ASAN')) --ENV-- DD_PROFILING_ENABLED=yes DD_PROFILING_LOG_LEVEL=off -; This test is specifically about the profiler's own helper threads -; (ddprof_time/ddprof_upload) not eating async signals meant for a PHP -; thread -- it is not about the tracer/sidecar/telemetry. Since CI now only -; builds and tests the combined ddtrace.so, all of that machinery starts up -; here too purely as a side effect of the extension being combined, adding -; background threads/a sidecar connection attempt/CPU contention that has -; nothing to do with what this test verifies and makes its timing-sensitive -; SIGCHLD-reaping loop flakier on constrained CI runners. Disable every -; flag that gates datadog_sidecar_should_enable() (see ext/sidecar.c) so no -; sidecar connection is attempted at all, keeping this test's timing budget -; focused on the profiler. DD_TRACE_ENABLED is deliberately not part of -; that gate (see datadog_sidecar_should_enable()), so it is not listed here. +; Keep unrelated sidecar/telemetry threads out of this test. The combined +; extension's tracer writer remains active so its signal mask is covered too. DD_INSTRUMENTATION_TELEMETRY_ENABLED=0 DD_TRACE_SIDECAR_TRACE_SENDER=0 DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED=0 diff --git a/tracer/coms.c b/tracer/coms.c index 28aa5a375dc..98f59757e22 100644 --- a/tracer/coms.c +++ b/tracer/coms.c @@ -1095,12 +1095,6 @@ static void dd_signal_data_processed(struct _writer_loop_data_t *writer) { } } -#ifdef __CYGWIN__ -#define TIMEOUT_SIG SIGALRM -#else -#define TIMEOUT_SIG SIGPROF -#endif - static void dd_writer_loop_cleanup(void *ctx) { dd_signal_writer_finished((struct _writer_loop_data_t *)ctx); #ifdef CXA_THREAD_ATEXIT_WRAPPER @@ -1110,18 +1104,19 @@ static void dd_writer_loop_cleanup(void *ctx) { static void *dd_writer_loop(void *_) { UNUSED(_); - /* This thread must not handle signals intended for the PHP threads. - * See Zend/zend_signal.c for which signals it registers. + /* This helper thread has no valid PHP/TSRM context, so it must not + * handle any asynchronous signal that a PHP script can register through + * pcntl_signal(). Keep synchronous fault signals unblocked so genuine + * faults in the writer are still reported. */ sigset_t sigset; - sigemptyset(&sigset); - sigaddset(&sigset, TIMEOUT_SIG); - sigaddset(&sigset, SIGHUP); - sigaddset(&sigset, SIGINT); - sigaddset(&sigset, SIGQUIT); - sigaddset(&sigset, SIGTERM); - sigaddset(&sigset, SIGUSR1); - sigaddset(&sigset, SIGUSR2); + sigfillset(&sigset); + sigdelset(&sigset, SIGSEGV); + sigdelset(&sigset, SIGBUS); + sigdelset(&sigset, SIGFPE); + sigdelset(&sigset, SIGILL); + sigdelset(&sigset, SIGABRT); + sigdelset(&sigset, SIGTRAP); pthread_sigmask(SIG_BLOCK, &sigset, NULL); struct _writer_loop_data_t *volatile writer = dd_get_writer(); From c1e8a8f2b7973df2d29b662a76919ecc8388a68a Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Thu, 10 Sep 2026 15:40:34 -0600 Subject: [PATCH 45/47] fix: preserve PHP 7.1 ZTS property names --- ext/compatibility.h | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/ext/compatibility.h b/ext/compatibility.h index 17ddd59c7b8..39e29cc44cf 100644 --- a/ext/compatibility.h +++ b/ext/compatibility.h @@ -389,7 +389,29 @@ static inline const zend_function *dd_zend_get_closure_method_def(zend_object *o #define ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(pass_by_ref, name, classname, allow_null, default_value) ZEND_ARG_OBJ_INFO(pass_by_ref, name, classname, allow_null) #define ZEND_ARG_OBJ_TYPE_MASK(pass_by_ref, name, class_name, type_mask, default_value) ZEND_ARG_INFO(pass_by_ref, name) -#define zend_declare_typed_property(ce, name, default, visibility, doc_comment, type) zend_declare_property_ex(ce, name, default, visibility, doc_comment); (void)type + +#if PHP_VERSION_ID >= 70100 && PHP_VERSION_ID < 70200 && defined(ZTS) +/* PHP 7.1 ZTS destroys its process-wide interned strings before destroying the + * main thread's copied class table. A typed-property stub may pass ZSTR_KNOWN() + * here, leaving the copied property info pointing at an already-freed string + * during tsrm_shutdown(). Store a regular persistent string instead. */ +static inline int datadog_declare_property_ex(zend_class_entry *ce, zend_string *name, zval *property, + int access_type, zend_string *doc_comment) { + if (!ZSTR_IS_INTERNED(name)) { + return zend_declare_property_ex(ce, name, property, access_type, doc_comment); + } + + zend_string *persistent_name = zend_string_init(ZSTR_VAL(name), ZSTR_LEN(name), true); + int result = zend_declare_property_ex(ce, persistent_name, property, access_type, doc_comment); + zend_string_release_ex(persistent_name, true); + return result; +} +#define zend_declare_typed_property(ce, name, default, visibility, doc_comment, type) \ + datadog_declare_property_ex(ce, name, default, visibility, doc_comment); (void)type +#else +#define zend_declare_typed_property(ce, name, default, visibility, doc_comment, type) \ + zend_declare_property_ex(ce, name, default, visibility, doc_comment); (void)type +#endif #define ZEND_TYPE_INIT_MASK(type) NULL #define ZEND_TYPE_INIT_CLASS(class_name, allow_null, extra_flags) NULL; zend_string_release(class_name) From 59915bc9bec333728977b82fbf85f58662d48897 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Fri, 11 Sep 2026 09:47:29 -0600 Subject: [PATCH 46/47] test: clear setup traces before filter checks --- .../client_side_stats_trace_filters.phpt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/ext/request-replayer/client_side_stats_trace_filters.phpt b/tests/ext/request-replayer/client_side_stats_trace_filters.phpt index a20f412cfa6..d4598ee4ca7 100644 --- a/tests/ext/request-replayer/client_side_stats_trace_filters.phpt +++ b/tests/ext/request-replayer/client_side_stats_trace_filters.phpt @@ -60,9 +60,14 @@ include __DIR__ . '/../includes/request_replayer.inc'; $rr = new RequestReplayer(); -// Block until the sidecar has received the agent's /info response before stats are computed +// Block until the sidecar has received the agent's /info response before stats are computed. dd_trace_internal_fn('await_agent_info'); +// SKIPIF configures the response through instrumented file_get_contents(), which can +// produce a trace under this test's session token. Remove that setup traffic before +// recording the traces whose filtering is asserted below. +$rr->clearDumpedData(); + // Each test case is a separate root span (= separate trace), because trace filters are // evaluated per trace (root span properties / tags). function makeSpan(string $name, string $resource, array $meta): void { @@ -123,10 +128,8 @@ makeSpan('GET /healthcheck', '', [ dd_trace_internal_fn('synchronous_flush'); -// Capture ALL trace requests from the second flush before consuming them. -// The first flush's data was already consumed by waitForDataAndReplay() above, so only -// second-flush requests remain. Poll until at least one trace request arrives, then -// collect everything that arrived in that batch. +// Capture all trace requests from the test flush. Setup traffic was cleared above, so +// poll until at least one trace request arrives, then collect everything in that batch. $secondFlushTraces = []; for ($i = 0; $i < 1000; $i++) { usleep(50000); // 50 ms (same interval as RequestReplayer::flushInterval) From 198d94b12d9ffecd94e411bb4dbee2d043c956cb Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Fri, 11 Sep 2026 09:47:37 -0600 Subject: [PATCH 47/47] Revert "ci: use HTTPS for Alpine package repositories" This reverts commit 1fc782b5ce0f661d050c1400f87a886419116eb5. --- .gitlab/generate-package.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.gitlab/generate-package.php b/.gitlab/generate-package.php index 2f05987bd16..02e4301282e 100644 --- a/.gitlab/generate-package.php +++ b/.gitlab/generate-package.php @@ -1150,10 +1150,6 @@ function package_extension_jobs(array $platform, string $kind = "all"): array - mkdir build - mv packages build - # Older Alpine images bake HTTP repository URLs into /etc/apk/repositories. - # Use HTTPS before the first apk invocation so package installation works - # when plaintext HTTP egress is blocked. - - sed -i 's|^http://|https://|' /etc/apk/repositories - apk add --no-cache ca-certificates || exit 75 # see https://support.circleci.com/hc/en-us/articles/360016505753-Resolve-Certificate-Signed-By-Unknown-Authority-error-in-Alpine-images?flash_digest=39b76521a337cecacac0cc10cb28f3747bb5fc6a - apk add curl ${INSTALL_PACKAGES:-} || exit 75