diff --git a/.claude/ci/appsec-native-tests.md b/.claude/ci/appsec-native-tests.md index edb960c249f..9d8c70d8b47 100644 --- a/.claude/ci/appsec-native-tests.md +++ b/.claude/ci/appsec-native-tests.md @@ -8,11 +8,11 @@ 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+) | -| `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) | +| `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+)} @@ -24,7 +24,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 @@ -42,7 +42,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 @@ -89,7 +89,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 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 f246dba38a6..9128a5cf34e 100644 --- a/.claude/ci/building-locally.md +++ b/.claude/ci/building-locally.md @@ -86,7 +86,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 @@ -132,7 +132,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 \ @@ -150,7 +150,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 @@ -210,17 +210,50 @@ helper artifact. ## Profiler Extension -### For correctness tests (bookworm) +**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. -`CARGO_TARGET_DIR` **must** be set explicitly (see -[github-actions-profiler.md](github-actions-profiler.md) for why): +### 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-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 +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 ' ``` @@ -228,23 +261,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) @@ -297,10 +330,16 @@ platforms and fails if artifacts are missing. compiled `.so` files: - `extensions_$(uname -m)/` — ddtrace extensions (`ddtrace-{API}[-zts|-debug|-debug-zts].so`) -- `appsec_$(uname -m)/` — appsec extensions (`ddappsec-{API}[-zts].so`) +- `appsec_$(uname -m)/` — appsec extensions (`ddappsec-{API}[-zts].so`); the + helper is now embedded in the sidecar (see `packaging-oci.md`), so there are + no separate `libddappsec-helper*.so` inputs anymore - `appsec/recommended.json` — bundled AppSec rules -- `datadog-profiling/{triplet}/lib/php/{API}/` — profiler - extensions +- `extensions_$(uname -m)/` also supplies the combined tracer+profiling + `ddtrace.so` artifacts directly for PHP versions >= 20160303 (profiling is + built into the same `ddtrace.so` rather than a separate + `combined-ddtrace/`/`datadog-profiling/` tree); the script writes a + `.ddtrace[-zts|-debug].profiling` marker file per API version alongside + each `ddtrace.so` so `datadog-setup.php` can detect profiling support 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 aa3a579fad5..b2798d2ea91 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 ``` @@ -41,8 +41,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); links `ddtrace.a` + `libdatadog_php.a` → `ddtrace.so` with the generated target export list and `-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); links `ddtrace.a` + `libdatadog_php.a` → `ddtrace.so` with the generated target export list and `-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}` @@ -70,17 +70,17 @@ 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 plus the embedded AppSec helper) 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}` | Links each per-version `.a` in `extensions_$(uname -m)/` against `libdatadog_php_$(uname -m)${suffix}.a` with `-whole-archive`, the generated target export list, and `-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_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 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+ @@ -130,7 +130,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) | | @@ -167,7 +167,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`). @@ -184,10 +184,11 @@ prepare code cache cargo deps: [{arch}, {triplet}] contains the embedded Rust exports. All other symbols are hidden via `--retain-symbols-file` and `-fvisibility=hidden`. -- `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 @@ -292,7 +293,7 @@ correct version string. | compile tracing extension (per version) | — | ~1.5 min | | compile tracing sidecar | — | ~3 min | | compile appsec extension (per version) | ~2 min | ~2 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 @@ -310,7 +311,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 \ @@ -318,7 +319,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 \ @@ -351,20 +352,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 \ @@ -382,14 +383,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/github-actions-profiler.md b/.claude/ci/github-actions-profiler.md index d362ef36646..a027430cab1 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,46 @@ 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 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 -# 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) -- 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 +make compile_profiler -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) -- 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 +make compile_combined -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 `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 @@ -88,8 +89,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 +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/target/profiler-release/libdatadog_php_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/ ' @@ -123,18 +123,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 +197,17 @@ frame name formatting. The implementation is in `profiling/src/capi.rs` and ## Debug Build -For a debug (unoptimized) build: +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 -cargo rustc --features=trigger_time_sample --profile dev --crate-type=cdylib +RUST_DEBUG_BUILD=1 make compile_profiler -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 `tmp/build_profiler/modules/datadog-profiling.so`. Profiling +PHPT and correctness expectations are intended for optimized builds. ## ZTS tests -- parallel PECL extension @@ -225,25 +223,27 @@ 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" +# 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. @@ -268,10 +268,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 5cb396d0c15..eedc36821df 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, @@ -255,7 +253,7 @@ this file instead of duplicating build commands. ### Group A — Native Linux unit and extension tests Runner: `arch:amd64` + `arch:arm64` -Image: `datadog/dd-trace-ci:php-{version}_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)** @@ -276,7 +274,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)** @@ -291,7 +289,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)** @@ -310,14 +308,15 @@ 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 profiler extension`, `compile extension windows`, `link tracing extension`, +`compile appsec extension` (the AppSec helper is embedded in the sidecar, so +there is no separate helper compile job any more), +`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 a8de7bd586f..ae05ad1c323 100644 --- a/.claude/ci/meta-job-group-doc.md +++ b/.claude/ci/meta-job-group-doc.md @@ -33,8 +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 | -| `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 38c17654335..938b87d65df 100644 --- a/.claude/ci/package-dind-verification.md +++ b/.claude/ci/package-dind-verification.md @@ -258,7 +258,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 c2d85516c22..a00a28158cc 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 | @@ -275,7 +275,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 @@ -294,7 +294,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 e2435cefabb..350bda6a84f 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 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 | @@ -58,14 +58,14 @@ Platform matrix for `package extension`: compile tracing extension ─┐ + link tracing extension │ compile appsec extension ─┤ → generate-final-artifact.sh → .tar.gz -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 @@ -129,8 +129,9 @@ shown above. [compile-artifacts.md](compile-artifacts.md) for details. - **`package loader` depends on many upstream compile jobs** — loader (glibc - and musl), tracing extension aggregates, sidecar, all - appsec and profiler extension versions. A single upstream failure blocks packaging. + and musl), tracing extension aggregates, sidecar (which embeds the AppSec + helper), all 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 0d5b1520af5..ccdd774c430 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 | | `PHP lint` | `dd-trace-ci:php-{latest}_bookworm-10` | Nearly-empty PHPCS + custom scripts over `src/`; see `tooling/php-lint/` | Runner: `arch:amd64` (all jobs in this pipeline are amd64-only) @@ -27,9 +27,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. - **PHP lint**: latest PHP version, NTS, single run. No extension build. Does not clone submodules. @@ -76,7 +76,7 @@ not compile the extension and is not a PSR-12 / formatter gate. ```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 @@ -99,7 +99,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" ' @@ -123,7 +123,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 \ @@ -144,7 +144,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 ' @@ -158,7 +158,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 @@ -178,7 +178,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 "" ' @@ -190,7 +190,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 \ @@ -246,7 +246,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 ' ``` @@ -296,7 +296,7 @@ PHPCS rules live in `tooling/php-lint/phpcs.xml`. Custom checks go in 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 439ef7773c5..74881af7dfb 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 @@ -58,13 +59,17 @@ 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 - ./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 9e9c2262335..bdc63f32c98 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 @@ -62,9 +70,8 @@ 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 + 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 @@ -79,7 +86,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 +102,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 +111,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" "socket_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 +130,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" "socket_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 +206,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 @@ -228,6 +292,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..11b154102fe 100755 --- a/.gitlab/build-profiler.sh +++ b/.gitlab/build-profiler.sh @@ -15,26 +15,52 @@ fi set -u prefix="$1" -thread_safety="${2:-nts}" +build_variant="${2:-nts}" +artifact_mode="${3:-standalone}" mkdir -vp "${prefix}" prefix="$(cd "${prefix}" && pwd)" -if [ "$thread_safety" = "zts" ]; then - switch-php "${PHP_VERSION}-zts" - output_file="${prefix}/datadog-profiling-zts.so" +if [ "$artifact_mode" = "combined" ]; then + configure_products="--enable-ddtrace-tracer --enable-ddtrace-profiling" + extension_name="ddtrace" else - switch-php "${PHP_VERSION}" - output_file="${prefix}/datadog-profiling.so" + configure_products="--disable-ddtrace-tracer --enable-ddtrace-profiling" + extension_name="datadog-profiling" fi -# Loadable profiler artifacts must go through the supported PHP build path. -build_dir="/tmp/ddtrace-build-profiler-${thread_safety}" +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}}" + +# 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}" 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/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}" diff --git a/.gitlab/compile_extension.sh b/.gitlab/compile_extension.sh index c57af326278..18ff139ea7f 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 cc -shared -Wl,-whole-archive "${MODULES_DIR}/ddtrace.a" \ diff --git a/.gitlab/generate-package.php b/.gitlab/generate-package.php index 1159f438493..02e4301282e 100644 --- a/.gitlab/generate-package.php +++ b/.gitlab/generate-package.php @@ -104,7 +104,7 @@ function package_extension_jobs(array $platform, string $kind = "all"): array stages: - prepare - - profiler + - combined - appsec - tracing - packaging @@ -129,9 +129,18 @@ function package_extension_jobs(array $platform, string $kind = "all"): array 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" @@ -171,6 +180,28 @@ function package_extension_jobs(array $platform, string $kind = "all"): array } ?> +# 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 @@ -240,8 +271,8 @@ function package_extension_jobs(array $platform, string $kind = "all"): array 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: @@ -259,8 +290,11 @@ function package_extension_jobs(array $platform, string $kind = "all"): array 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 "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} @@ -271,7 +305,7 @@ function package_extension_jobs(array $platform, string $kind = "all"): array policy: pull # `Cache Cargo Deps` is used to update/push the cache artifacts: paths: - - "datadog-profiling" + - "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 @@ -358,7 +394,7 @@ function package_extension_jobs(array $platform, string $kind = "all"): array 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_*" @@ -383,12 +419,10 @@ function package_extension_jobs(array $platform, string $kind = "all"): array - - job: "compile tracing extension: [, , ]" + - job: "link tracing extension: [, ]" artifacts: true @@ -461,10 +495,12 @@ function package_extension_jobs(array $platform, string $kind = "all"): array 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 @@ -613,6 +651,18 @@ function package_extension_jobs(array $platform, string $kind = "all"): array 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/" @@ -647,8 +697,8 @@ function package_extension_jobs(array $platform, string $kind = "all"): array - # Profiler extension - - job: "compile profiler extension: [, , ]" + # Combined tracer+profiling extension + - job: "compile combined extension: [, , ]" artifacts: true - - job: "compile profiler extension: [, , ]" - artifacts: true - - /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 diff --git a/.gitlab/generate-profiler.php b/.gitlab/generate-profiler.php index dac0c23de9a..2223c7f1eea 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 @@ -56,18 +55,36 @@ - unset DD_SERVICE; unset DD_ENV - mkdir -p "${CI_PROJECT_DIR}/artifacts/profiler-tests" - - '# NTS' + # 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 ..; 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/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 ..; 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' + # 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 - - 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") + - (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 @@ -96,13 +113,12 @@ 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 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 @@ -118,13 +134,11 @@ 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 - - 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 + - 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 - - 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 @@ -140,7 +154,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" diff --git a/.gitlab/generate-tracer.php b/.gitlab/generate-tracer.php index 5e7488158ec..1f9928af775 100644 --- a/.gitlab/generate-tracer.php +++ b/.gitlab/generate-tracer.php @@ -147,12 +147,14 @@ function windows_test_c_job($job_name, $thread_safety, $targets) { 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 - + # Build . 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 ; 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\\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\\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/.gitlab/link-tracing-extension.sh b/.gitlab/link-tracing-extension.sh index 6254ea1acba..6d4ba4452d7 100755 --- a/.gitlab/link-tracing-extension.sh +++ b/.gitlab/link-tracing-extension.sh @@ -18,3 +18,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 diff --git a/Cargo.lock b/Cargo.lock index a2ec5c0abf2..9e1b91fe7ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4282,6 +4282,7 @@ dependencies = [ name = "php_sidecar_mockgen" version = "0.1.0" dependencies = [ + "object 0.31.1", "sidecar_mockgen", ] diff --git a/Cargo.toml b/Cargo.toml index 535898d16f8..fb69d925cef 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 = [ @@ -135,6 +143,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/Makefile b/Makefile index ec877d56f95..9423c848ab3 100644 --- a/Makefile +++ b/Makefile @@ -408,6 +408,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 @@ -490,6 +506,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/appsec/cmake/ddtrace.cmake b/appsec/cmake/ddtrace.cmake index 8fbc01f2ca4..4c40fcc6132 100644 --- a/appsec/cmake/ddtrace.cmake +++ b/appsec/cmake/ddtrace.cmake @@ -12,14 +12,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 50e8ad3dfe7..b42932490bc 100644 --- a/appsec/tests/integration/build.gradle +++ b/appsec/tests/integration/build.gradle @@ -573,6 +573,17 @@ 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. cp /portable-rust/libdatadog_php.so \\ /tracer-ssi/libdatadog_php.so @@ -1173,6 +1184,12 @@ 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. def portableLibdatadogPhpCommand = ''' RUST_TARGET="$(uname -m)-unknown-linux-musl" RUST_TOOLCHAIN="nightly-$RUST_TARGET" 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/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/compile_rust.sh b/compile_rust.sh index d0a467a87c7..56571a3f221 100755 --- a/compile_rust.sh +++ b/compile_rust.sh @@ -20,7 +20,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/components-rs/lib.rs b/components-rs/lib.rs index b7f5a035b18..1a2b3583720 100644 --- a/components-rs/lib.rs +++ b/components-rs/lib.rs @@ -33,29 +33,39 @@ 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))] +// 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; -#[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)] @@ -591,5 +601,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/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 de877e18c06..0d1d50bd538 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]) @@ -8,8 +9,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) @@ -29,9 +30,6 @@ PHP_ARG_ENABLE(ddtrace-rust-debug, whether to compile rust in debug mode, PHP_ARG_ENABLE(ddtrace-rust-library-split, whether to keep the rust library separate from the extension, [ --enable-ddtrace-rust-library-split Do not build or statically link the rust code], no, no) -DDTRACE_PHP_CONFIG=$(command -v "$PHP_CONFIG" 2>/dev/null || echo "$PHP_CONFIG") -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], [ @@ -43,17 +41,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 +55,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 @@ -76,8 +70,10 @@ 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 profiler_target_path= profiler_target_arg= if test -n "$DDTRACE_PROFILING_TARGET"; then @@ -94,8 +90,11 @@ 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 - (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]]+")) +.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 \$(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 @mkdir -p \$(phplibdir)/.libs @@ -108,9 +107,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],[ @@ -281,6 +281,7 @@ if test "$PHP_DDTRACE" != "no" && test "$PHP_DDTRACE_PROFILING" = "no"; then 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 \ @@ -382,13 +383,19 @@ 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) 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 dnl Deliberately not in EXTRA_LDFLAGS: those land before libdatadog_php.a on dnl the link line, so they would not satisfy its undefined symbols. Note that @@ -462,8 +469,11 @@ if test "$PHP_DDTRACE" != "no" && test "$PHP_DDTRACE_PROFILING" = "no"; then case $host_os in linux*) dnl A fat Linux ddtrace.so is also the sidecar executable. When ld.so - dnl executes it directly, it jumps to this ELF entry point. - ddtrace_fat_ldflags="$ddtrace_fat_ldflags -Wl,-e,ddog_spawn_direct_entry" + dnl executes it directly, it jumps to this ELF entry point. Also discard + dnl unreachable function and data sections from the Rust static archive + dnl linked into the fat variant (the slim variant keeps the Rust library + dnl separate, so it has no archive to strip here). + ddtrace_fat_ldflags="$ddtrace_fat_ldflags -Wl,--gc-sections -Wl,-e,ddog_spawn_direct_entry" ;; esac @@ -549,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" + 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 @@ -575,26 +587,61 @@ EOT ddtrace_rust_lib="" 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_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= + 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:-}" + + 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/sidecar" -name "*.rs" -o -name "Cargo.toml"; find "$ext_srcdir/appsec/helper-rust" -name "*.rs" -o -name "*.c" -o -name "Cargo.toml" -o -name "build.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]]+")) +dnl File globs cover: our own Rust sources plus everything the combined +dnl (tracer+profiling) crate depends on -- the sidecar crate and the appsec +dnl Rust helper are real workspace dependencies of the `tracer` feature (see +dnl Cargo.toml), so changes there must also trigger a relink here. +$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/sidecar" -name "*.rs" -o -name "Cargo.toml"; find "$ext_srcdir/appsec/helper-rust" -name "*.rs" -o -name "*.c" -o -name "Cargo.toml" -o -name "build.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' ' ' ) $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 - 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 diff --git a/config.w32 b/config.w32 index 76b15612f4e..dc5668b5e75 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"; @@ -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"; @@ -111,7 +112,7 @@ if (PHP_DDTRACE != 'no') { ADD_FLAG("CFLAGS_DDTRACE", " /Zc:preprocessor "); } - ADD_FLAG("CFLAGS_DDTRACE", " /DDDTRACE "); + ADD_FLAG("CFLAGS_DDTRACE", " /DTRACER "); ADD_FLAG("CFLAGS_DDTRACE", " /DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 "); @@ -184,6 +185,10 @@ if (PHP_DDTRACE != 'no') { // Promote to env var frag_out.WriteLine("!if [set CARGO_TARGET_DIR=$(CARGO_TARGET_DIR)]"); frag_out.WriteLine("!endif"); + // 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 frag_out.WriteLine("!endif"); diff --git a/datadog-setup.php b/datadog-setup.php index eef80ebf95a..51188d29d6a 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 @@ -530,9 +530,9 @@ 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"; - $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)); @@ -652,12 +652,17 @@ function install($options) $extensionDestination = $extDir . '/' . EXTENSION_PREFIX . 'ddtrace.' . EXTENSION_SUFFIX; safe_copy_extension($extensionRealPath, $extensionDestination); - // Profiling + // 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"; + $hasCombinedProfiling = file_exists($profilingMarker); $profilingExtensionRealPath = "$tmpArchiveProfilingRoot/ext/$extensionVersion/" . EXTENSION_PREFIX . "datadog-profiling$extensionSuffix." . EXTENSION_SUFFIX; - $shouldInstallProfiling = file_exists($profilingExtensionRealPath); + $hasStandaloneProfiling = file_exists($profilingExtensionRealPath); + $shouldInstallProfiling = $hasCombinedProfiling || $hasStandaloneProfiling; - if ($shouldInstallProfiling) { + if ($hasStandaloneProfiling) { $profilingExtensionDestination = $extDir . '/' . EXTENSION_PREFIX . 'datadog-profiling.' . EXTENSION_SUFFIX; safe_copy_extension($profilingExtensionRealPath, $profilingExtensionDestination); } @@ -715,6 +720,17 @@ function install($options) ]; } + $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 += [ '(^\s*;?\s*extension\s*=\s*.*ddtrace.*)m' => "extension = $extensionDestination", @@ -726,28 +742,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 @@ -780,7 +786,7 @@ function install($options) add_missing_ini_settings( $iniFilePath, - get_ini_settings($installDirSrcDir, $appSecRulesPath), + get_ini_settings($installDirSrcDir, $appSecRulesPath, $hasStandaloneProfiling), $replacements ); @@ -2214,23 +2220,18 @@ 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), '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', @@ -2246,9 +2247,9 @@ function get_ini_settings($sourcesDir, $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', @@ -2635,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/Makefile b/dockerfiles/verify_packages/Makefile index ae87c58380c..a33d469020b 100644 --- a/dockerfiles/verify_packages/Makefile +++ b/dockerfiles/verify_packages/Makefile @@ -98,7 +98,7 @@ test_upgrade_from_php_installer.sh \ test_first_install_php_debug.sh \ test_first_install_php_debugzts.sh \ -test_profiler_install_unsupported_debug.sh \ +test_profiler_install_enabled_debug.sh \ : global_test_dependencies @echo "################### $(@) ###################" docker run --rm -i -v $(shell pwd)/../../:/app --env-file $(shell pwd)/.env -e CIRCLECI --workdir=/app datadog/dd-trace-ci:php-7.4_buster sh dockerfiles/verify_packages/installer/$(@) 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..6c1f4dbea0d 100644 --- a/dockerfiles/verify_packages/installer/test_profiler_install_disabled.sh +++ b/dockerfiles/verify_packages/installer/test_profiler_install_disabled.sh @@ -12,6 +12,6 @@ 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_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..08cb36a4bb7 100644 --- a/dockerfiles/verify_packages/installer/test_profiler_install_enabled.sh +++ b/dockerfiles/verify_packages/installer/test_profiler_install_enabled.sh @@ -12,6 +12,6 @@ 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_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 new file mode 100644 index 00000000000..56f25f41988 --- /dev/null +++ b/dockerfiles/verify_packages/installer/test_profiler_install_enabled_debug.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env sh + +set -e + +. "$(dirname ${0})/utils.sh" + +switch-php debug + +# Initially no ddtrace +assert_no_ddtrace + +# Install the profiling-capable debug module from the package under test. +version=$(cat VERSION) +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 +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_unsupported_debug.sh b/dockerfiles/verify_packages/installer/test_profiler_install_unsupported_debug.sh deleted file mode 100644 index b1ac48321ce..00000000000 --- a/dockerfiles/verify_packages/installer/test_profiler_install_unsupported_debug.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env sh - -set -e - -. "$(dirname ${0})/utils.sh" - -switch-php debug - -# Initially no ddtrace -assert_no_ddtrace - -# Install using the php installer -set +e -output=$(php ./build/packages/datadog-setup.php --php-bin php --enable-profiling) -exit_status=$? -set -e - -if [ "${exit_status}" = "1" ]; then - dashed_print "Ok: expected exit status 1." "${exit_status}" "${output}" -else - dashed_print "Error: Unexpected exit status. Should be 1." "${exit_status}" "${output}" - exit 1 -fi - -if [ -z "${output##*not supported*}" ]; then - dashed_print "Ok: Output contains text 'not supported'." "${output}" -else - dashed_print "Error: output did not contain 'not supported' as expected." "${output}" - exit 1 -fi diff --git a/dockerfiles/verify_packages/installer/utils.sh b/dockerfiles/verify_packages/installer/utils.sh index 6d87b8c2cf5..ab04442ca5e 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_BOOLEAN) ? 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,12 +71,18 @@ assert_no_appsec() { } assert_profiler_version() { + expected_version=${1} 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" + 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 - echo "---\nError: Wrong datadog-profiling version. Expected: ${1}\n---\n${output}\n---\n" - exit 1 + assert_ddtrace_version "${expected_version}" + assert_profiler_installed fi } @@ -94,11 +99,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_BOOLEAN) ? 0 : 1);'; then + echo "Ok: Profiling is available and enabled" else - echo "---\nError: Profiler should be installed\n---\n${output}\n---\n" + echo "Error: Profiling should be available and enabled" exit 1 fi } 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) 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 667c9c49766..f3e482a15ac 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, .sensitive = true) \ 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..04c6052ae85 --- /dev/null +++ b/ext/configuration_shared.h @@ -0,0 +1,72 @@ +#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. + */ +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); + +#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(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 \ + 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 = 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 \ + 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/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/ext/datadog.c b/ext/datadog.c index 7753ec46f21..9c761cd8976 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 @@ -139,19 +153,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) { @@ -168,13 +193,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, '/'); @@ -192,12 +218,13 @@ 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); +#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) { @@ -218,9 +245,15 @@ static void datadog_activate(void) { } } -#ifdef DDTRACE +#ifdef TRACER + ddtrace_activate_early(); ddtrace_activate_late(); #endif +#ifdef PROFILING + if (datadog_profiling_initialized) { + ddog_php_prof_zend_activate(); + } +#endif } static void datadog_deactivate(void) { @@ -242,7 +275,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, @@ -250,12 +283,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, @@ -275,11 +308,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. @@ -349,6 +394,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 @@ -368,10 +423,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); @@ -408,8 +459,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)}; @@ -431,7 +512,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(); @@ -450,7 +534,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)) { @@ -459,33 +543,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 @@ -496,10 +560,6 @@ static PHP_MINIT_FUNCTION(datadog) { datadog_sidecar_minit(); -#ifdef DDTRACE - ddtrace_minit_late(); -#endif - datadog_minit_remote_config(); #ifndef _WIN32 @@ -507,22 +567,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(); @@ -540,7 +622,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) { @@ -564,10 +650,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; @@ -594,18 +676,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; } @@ -634,6 +724,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 @@ -649,6 +743,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(); } @@ -657,10 +761,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; @@ -682,7 +782,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 @@ -690,14 +794,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) { @@ -706,6 +817,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) { @@ -717,7 +833,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); @@ -728,6 +844,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 ca9706133fd..1f18811ae9d 100644 --- a/ext/sidecar.c +++ b/ext/sidecar.c @@ -436,7 +436,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 @@ -692,7 +692,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; @@ -918,7 +918,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 f883727dadc..dbed175768b 100644 --- a/ext/signals.c +++ b/ext/signals.c @@ -66,7 +66,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 0525e42538e..0e595e9e14a 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 6d1261befcd..5280cb77177 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, @@ -140,7 +140,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', extension_loaded('Zend OPcache') ? 'yes' : 'no' ); PHP diff --git a/loader/dd_library_loader.c b/loader/dd_library_loader.c index 4bf6836a58f..822d8662f8e 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) { @@ -775,6 +785,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; } @@ -797,6 +814,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); @@ -1047,8 +1090,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]); } @@ -1093,6 +1147,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_incompatibility_jit.php b/loader/tests/functional/test_incompatibility_jit.php index 957922088aa..eb0648b807f 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 @@ -103,6 +105,7 @@ Injection error => Incompatible runtime Extra config => datadog.trace.sources_path=%s/trace/src 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' @@ -141,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, @@ -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 @@ -179,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, @@ -206,6 +211,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 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_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 a02f7180c1c..7ec8e136b62 100644 --- a/loader/tests/functional/test_phpinfo.php +++ b/loader/tests/functional/test_phpinfo.php @@ -59,6 +59,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 @@ -69,12 +70,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') -%A +Extra config => provided by combined ddtrace +Logs =>%A => ddappsec Version => %s diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index b36ce1f4782..951a77d3167 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -539,9 +539,9 @@ ], "DD_PROFILING_ENABLED": [ { - "implementation": "D", + "implementation": "A", "type": "boolean", - "default": "true" + "default": "false" } ], "DD_PROFILING_ENDPOINT_COLLECTION_ENABLED": [ @@ -614,7 +614,10 @@ { "implementation": "A", "type": "boolean", - "default": "true" + "default": "true", + "aliases": [ + "DD_PROFILING_EXPERIMENTAL_TIMELINE_ENABLED" + ] } ], "DD_PROFILING_WALLTIME_ENABLED": [ 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..69065e68e83 100644 --- a/profiling/README.md +++ b/profiling/README.md @@ -4,7 +4,7 @@ The profiler is implemented in Rust. To see the currently required Rust version, refer to the [rust-toolchain.toml](rust-toolchain.toml) file. The profiler -requires PHP 7.1+, and does not support debug builds. +requires PHP 7.1+ and supports both release and debug PHP builds. ## Time Profiling @@ -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. @@ -43,13 +49,20 @@ 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 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,10 +70,17 @@ 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 ``` -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 -tests. +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 +``` + +The `run-tests.php` version has to match the PHP version used to run the tests. ## Benchmarks @@ -69,6 +89,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 ``` @@ -80,6 +101,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..a89b3a16f56 100644 --- a/profiling/build.rs +++ b/profiling/build.rs @@ -2,30 +2,30 @@ 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()) -} +#[path = "config_codegen.rs"] +mod config_codegen; pub fn build() { - 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); + + // 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") @@ -35,10 +35,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); @@ -47,9 +44,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, @@ -61,32 +58,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] = &[ @@ -105,7 +110,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, @@ -116,6 +121,10 @@ 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/configuration_tags.c"); println!("cargo:rerun-if-changed=ext/handlers_api.c"); println!("cargo:rerun-if-changed=ext/handlers_api.h"); @@ -136,16 +145,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" }; @@ -161,9 +160,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", "ext/configuration_tags.c"][..] + }) .define("CFG_POST_STARTUP_CB", post_startup_cb) .define("CFG_PRELOAD", preload) .define("CFG_FIBERS", fibers) @@ -172,9 +178,10 @@ 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", "") + str::replace(php_includes, "-I", "") .split(' ') .map(Path::new) .chain([Path::new("zend_abstract_interface")]) @@ -182,6 +189,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"); @@ -214,7 +224,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( @@ -230,20 +240,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) @@ -293,7 +302,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 @@ -413,80 +422,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"); @@ -502,12 +447,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 +471,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/config_codegen.rs b/profiling/config_codegen.rs new file mode 100644 index 00000000000..a510235d160 --- /dev/null +++ b/profiling/config_codegen.rs @@ -0,0 +1,126 @@ +//! 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 +//! 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(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"); + + 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); + } + // 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"); + 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 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 (ty, name) = record + .split_once(',') + .unwrap_or_else(|| panic!("malformed config record: {record:?}")); + 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\n{accessors}" + ), + ) + .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 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" + ) +} diff --git a/profiling/config_id.rs b/profiling/config_id.rs new file mode 100644 index 00000000000..8109def6d65 --- /dev/null +++ b/profiling/config_id.rs @@ -0,0 +1,8 @@ +//! Rust IDs for the aggregate common/product ZAI configuration table. +//! +//! 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/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 59bf83c6909..99ed9957121 100644 --- a/profiling/src/bindings/mod.rs +++ b/profiling/src/bindings/mod.rs @@ -362,6 +362,10 @@ extern "C" { /// until mshutdown. pub(crate) fn ddog_php_prof_get_memoized_config(config_id: ConfigId) -> *mut zval; + 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; /// 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; @@ -503,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/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 4f582fc3940..09bcf8042b0 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,12 +11,13 @@ 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; +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 { @@ -324,71 +323,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,79 +340,68 @@ 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", - }; +pub(crate) unsafe fn config_bool(id: ConfigId) -> bool { + bool::try_from(get_value(id)).expect("generated BOOL accessor received a non-boolean zval") +} - // Safety: all these byte strings are [CStr::from_bytes_with_nul_unchecked] compatible. - unsafe { ZaiStr::literal(bytes) } - } +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, + 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 { state: SystemSettingsState::ConfigUnaware, @@ -499,7 +427,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 +623,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}"), @@ -774,9 +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 { - get_str(Tags) +/// 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 @@ -819,7 +806,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 +836,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 +870,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 +901,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 +945,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,382 +968,27 @@ 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, - sensitive: false, - }, - 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, - sensitive: false, - }, - 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, - sensitive: false, - }, - 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, - sensitive: false, - }, - 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, - sensitive: false, - }, - 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, - sensitive: false, - }, - 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, - sensitive: false, - }, - 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, - sensitive: false, - }, - 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, - sensitive: false, - }, - 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, - sensitive: false, - }, - 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, - sensitive: false, - }, - 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, - sensitive: false, - }, - 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, - sensitive: false, - }, - 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, - sensitive: false, - }, - // 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, - sensitive: false, - }, - 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, - sensitive: false, - }, - 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, - sensitive: false, - }, - 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, - sensitive: false, - }, - 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, - sensitive: false, - }, - 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, - sensitive: false, - }, - 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, - sensitive: false, - }, - 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, - sensitive: false, - }, - 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, - sensitive: false, - }, - 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, - sensitive: false, - }, - ] - }; - - 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. - #[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( @@ -1382,98 +1022,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 c0823195668..c6003d88dc2 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; @@ -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}; @@ -48,7 +49,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; @@ -56,6 +59,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"; /// Draws the exponential sampling distance assumed by libdatadog's upscaler, /// rounded up to whole units and clamped to `[1, 20 * mean]`. @@ -178,15 +183,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. @@ -197,8 +203,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(), @@ -229,6 +236,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); @@ -254,17 +262,55 @@ 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 { - // 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)] + #[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() { - logging::log_init(log::LevelFilter::Trace); - trace!("MINIT({_type}, {module_number})"); + 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; + } + } + + // 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")] { use std::fs::File; @@ -330,8 +376,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 @@ -345,12 +389,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 @@ -383,6 +425,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::(), @@ -400,7 +443,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(); @@ -426,7 +472,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(); @@ -545,11 +594,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() } @@ -591,11 +648,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. @@ -657,7 +718,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? @@ -1023,6 +1084,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); }); @@ -1122,10 +1184,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..220dcdfcdad 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,49 @@ 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); } +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) +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..8926c4871fa 100644 --- a/profiling/src/php_ffi.h +++ b/profiling/src/php_ffi.h @@ -77,6 +77,17 @@ 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); + +/** 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 @@ -88,15 +99,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 +114,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/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/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": " +--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-- @@ -26,6 +36,15 @@ DD_TRACE_AGENT_URL=http://datadog:8126 --FILE-- --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-- 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/tests/ext/profiling/runtime_id_01.phpt b/tests/ext/profiling/runtime_id_01.phpt index d883c870f78..9d185c44c6b 100644 --- a/tests/ext/profiling/runtime_id_01.phpt +++ b/tests/ext/profiling/runtime_id_01.phpt @@ -4,8 +4,8 @@ runtime-id exists in meta when profiling is enabled DD_PROFILING_ENABLED=true --SKIPIF-- --FILE-- --FILE-- 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) diff --git a/tests/randomized/config/inis.php b/tests/randomized/config/inis.php index 66a18637db9..abcd3d9d65d 100644 --- a/tests/randomized/config/inis.php +++ b/tests/randomized/config/inis.php @@ -6,5 +6,4 @@ const INIS = [ 'opcache.enable' => [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 3089fd66ef3..adb577b58cf 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 # Use a per-variant cmake build dir in the cache volume (nts and zts need separate builds). @@ -291,9 +287,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" fi @@ -321,11 +314,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 4e9d8463051..41216a72080 100755 --- a/tooling/bin/generate-final-artifact.sh +++ b/tooling/bin/generate-final-artifact.sh @@ -70,16 +70,10 @@ 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}/ + if [[ -z ${DDTRACE_MAKE_PACKAGES_ASAN:-} && $target != windows && ${php_api} -ge 20160303 ]]; 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} @@ -138,11 +132,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; + if [[ ${php_api} -ge 20160303 ]]; then + for profiling_config in "" -zts -debug; do + touch ${tmp_folder_final_gnu_trace}/ext/$php_api/.ddtrace${profiling_config}.profiling + done + fi 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; + if [[ ${php_api} -ge 20160303 ]]; then + for profiling_config in "" -zts; do + touch ${tmp_folder_final_musl_trace}/ext/$php_api/.ddtrace${profiling_config}.profiling + done + fi 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; @@ -164,11 +168,15 @@ 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}; + mkdir -p ${tmp_folder_final_gnu_trace}/profiling-licenses + cp ./profiling/LICENSE* ./profiling/NOTICE ${tmp_folder_final_gnu_trace}/profiling-licenses/ fi if [[ $target == "linux-musl" ]]; then mkdir -p ${tmp_folder_final_musl_trace} cp -r ./src ${tmp_folder_final_musl_trace}; + mkdir -p ${tmp_folder_final_musl_trace}/profiling-licenses + cp ./profiling/LICENSE* ./profiling/NOTICE ${tmp_folder_final_musl_trace}/profiling-licenses/ fi if [[ $target == "windows" && $architecture == "x86_64" ]]; then @@ -177,62 +185,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 dc9be393b98..84d37843a6c 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 are built as combined artifacts. + stripto ./extensions_${architecture}/ddtrace-${php_api}.so \ + ${gnu}/trace/ext/${php_api}/ddtrace.so + stripto ./extensions_${architecture}/ddtrace-${php_api}-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 ./extensions_${architecture}/ddtrace-${php_api}-alpine.so \ + ${musl}/trace/ext/${php_api}/ddtrace.so + stripto ./extensions_${architecture}/ddtrace-${php_api}-alpine-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/etc" diff --git a/tooling/generate-supported-configurations.sh b/tooling/generate-supported-configurations.sh index 0a70d2c2614..db431f6da75 100755 --- a/tooling/generate-supported-configurations.sh +++ b/tooling/generate-supported-configurations.sh @@ -9,18 +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[@]}" - "tracer/configuration.h" + "${CONFIG_DEPENDENCY_FILES[@]}" "${OTEL_CONFIG_FILES[@]}" - "${PROFILING_CONFIG_FILE}" "${GENERATOR_SCRIPT_FILE}" ) @@ -39,14 +42,15 @@ trap 'rm -f "$PHP_CODE_FILE"' EXIT cat >"$PHP_CODE_FILE" <<'ENDPHP' [ + '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; } } @@ -116,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]; @@ -149,154 +175,29 @@ function add_otel_entries(&$supported, $names, $metadata, $sensitiveNames = []) } } -// temporary solution until we merge configs -function map_rust_type($rawType, $parser) { - $map = [ - 'ZAI_CONFIG_TYPE_BOOL' => '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) < 4) { continue; } - [$type, $name, $default, $args] = array_map('trim', array_slice($config, 0, 4)); - $aliases = count($config) > 4 ? array_slice($config, 4) : []; - $mappedType = map_type($type); + [$type, $name, $default] = array_map('trim', array_slice($config, 0, 3)); + $aliases = count($config) > 3 ? array_slice($config, 3) : []; + $parser = preg_match('/\\.parser\\s*=\\s*([A-Za-z0-9_]+)/', $options, $parserMatch) + ? $parserMatch[1] : ''; + $mappedType = map_type($type, $parser); $entry = [ "implementation" => "A", "type" => $mappedType, "default" => normalize_default($default, $mappedType, $name), ]; - // $args is the stringized designated-initializer tail of the CONFIG/CALIAS - // macro invocation (e.g. ".ini_change = ..., .sensitive = 1"). - if (preg_match('/\.sensitive\s*=\s*(1|true)\b/', $args)) { + // $options is the stringized designated-initializer tail of the CONFIG/CALIAS + // macro invocation (e.g. ".ini_change = ..., .sensitive = 1"), captured above + // via the |CONFIG_OPTIONS| marker rather than as a CSV field, so it can't be + // confused by embedded commas. + if (preg_match('/\.sensitive\s*=\s*(1|true)\b/', $options)) { $entry["sensitive"] = true; } $norm = normalize_aliases($aliases, $name); @@ -367,9 +268,6 @@ preg_match_all('/\'(OTEL_[A-Z0-9_]+)\'/', $sensitiveMatch[1], $m); $otelSensitiveNames = $m[1]; add_otel_entries($supported, array_merge($otelNames, $otelSensitiveNames), $otelMetadata, array_fill_keys($otelSensitiveNames, true)); -$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); @@ -471,6 +369,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 - < 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)) { 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/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 74dcaea06f8..f7c7d4865e0 100644 --- a/zend_abstract_interface/components_rs.cmake +++ b/zend_abstract_interface/components_rs.cmake @@ -70,11 +70,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 diff --git a/zend_abstract_interface/config/config.h b/zend_abstract_interface/config/config.h index 41ea02e2fbb..cc9f14f706c 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"));