diff --git a/.claude/ci/building-locally.md b/.claude/ci/building-locally.md index 7abeb91adfc..c37b311b6c0 100644 --- a/.claude/ci/building-locally.md +++ b/.claude/ci/building-locally.md @@ -435,7 +435,7 @@ set -e Set `ARCHITECTURE=aarch64` for arm64 (still runs in the amd64 `php_fpm_packaging` image, using cross-tools). -### Slim package with debug binaries +### Slim package with debug binaries (preferred, if possible) `tooling/bin/build-debug-artifact` builds a tarball containing only the PHP version you need — no stubs, no `generate-final-artifact.sh`. It @@ -454,7 +454,7 @@ tooling/bin/build-debug-artifact gnu-x86_64-8.2-nts # Tracer + appsec (extension + both helpers) + profiler tooling/bin/build-debug-artifact gnu-x86_64-8.2-nts --appsec --profiler -# Musl/arm64 variant, custom output directory +# Musl/arm64 variant, custom output directory (preferred if the location is somewhere else) tooling/bin/build-debug-artifact musl-aarch64-8.2-nts /tmp/out ``` diff --git a/Cargo.lock b/Cargo.lock index ab175e31f3a..cd9f267b637 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1215,7 +1215,10 @@ dependencies = [ "io-lifetimes", "libc 0.2.177", "libdd-common", + "libdd-ddsketch", "libdd-tinybytes", + "libdd-trace-protobuf", + "libdd-trace-stats", "memfd", "nix 0.29.0", "page_size", @@ -1229,6 +1232,7 @@ dependencies = [ "tracing-subscriber", "winapi 0.3.9", "windows-sys 0.48.0", + "zwohash", ] [[package]] @@ -1367,6 +1371,7 @@ dependencies = [ "http-body-util", "httpmock", "libc 0.2.177", + "libdd-capabilities-impl", "libdd-common", "libdd-common-ffi", "libdd-crashtracker", @@ -1375,6 +1380,8 @@ dependencies = [ "libdd-dogstatsd-client", "libdd-telemetry", "libdd-tinybytes", + "libdd-trace-protobuf", + "libdd-trace-stats", "libdd-trace-utils", "manual_future", "memory-stats", @@ -1383,6 +1390,7 @@ dependencies = [ "prctl", "priority-queue", "rand 0.8.5", + "rmp-serde", "sendfd", "serde", "serde_json", @@ -1390,6 +1398,7 @@ dependencies = [ "sha2", "simd-json", "spawn_worker", + "sys-info", "tempfile", "tokio", "tokio-util", @@ -1462,6 +1471,7 @@ dependencies = [ "libdd-telemetry", "libdd-telemetry-ffi", "libdd-tinybytes", + "libdd-trace-stats", "libdd-trace-utils", "log", "paste", @@ -2850,7 +2860,6 @@ dependencies = [ "libdd-capabilities", "libdd-capabilities-impl", "libdd-common", - "libdd-ddsketch", "libdd-dogstatsd-client", "libdd-log", "libdd-shared-runtime", @@ -2899,13 +2908,13 @@ name = "libdd-library-config" version = "1.1.0" dependencies = [ "anyhow", + "libc 0.2.177", "libdd-trace-protobuf", "memfd", "prost", "rand 0.8.5", "rmp", "rmp-serde", - "rustix 1.1.3", "serde", "serde_yaml", "serial_test", @@ -2927,7 +2936,7 @@ dependencies = [ [[package]] name = "libdd-libunwind-sys" -version = "0.1.0" +version = "1.0.0" dependencies = [ "cc", "libc 0.2.177", @@ -2955,7 +2964,6 @@ dependencies = [ "bolero", "byteorder", "bytes", - "cc", "chrono", "criterion", "crossbeam-channel", @@ -3003,7 +3011,7 @@ dependencies = [ [[package]] name = "libdd-shared-runtime" -version = "1.0.0" +version = "0.1.0" dependencies = [ "async-trait", "futures", @@ -3099,12 +3107,25 @@ dependencies = [ name = "libdd-trace-stats" version = "2.0.0" dependencies = [ + "anyhow", + "async-trait", "criterion", "hashbrown 0.15.2", + "http", + "httpmock", + "libdd-capabilities", + "libdd-capabilities-impl", + "libdd-common", "libdd-ddsketch", + "libdd-shared-runtime", "libdd-trace-protobuf", "libdd-trace-utils", "rand 0.8.5", + "rmp-serde", + "serde", + "tokio", + "tokio-util", + "tracing", ] [[package]] diff --git a/Makefile b/Makefile index 6e22aadd9ef..88be1e4916e 100644 --- a/Makefile +++ b/Makefile @@ -48,7 +48,7 @@ RUN_TESTS_CMD := DD_SERVICE= DD_ENV= REPORT_EXIT_STATUS=1 TEST_PHP_SRCDIR=$(PROJ C_FILES = $(shell find components components-rs ext src/dogstatsd zend_abstract_interface -name '*.c' -o -name '*.h' | awk '{ printf "$(BUILD_DIR)/%s\n", $$1 }' ) TEST_FILES = $(shell find tests/ext -name '*.php*' -o -name '*.inc' -o -name '*.json' -o -name '*.yaml' -o -name 'CONFLICTS' | awk '{ printf "$(BUILD_DIR)/%s\n", $$1 }' ) -RUST_FILES = $(BUILD_DIR)/Cargo.toml $(BUILD_DIR)/Cargo.lock $(shell find components-rs -name '*.c' -o -name '*.rs' -o -name 'Cargo.toml' | awk '{ printf "$(BUILD_DIR)/%s\n", $$1 }' ) $(shell find libdatadog/{build-common,datadog-ffe,datadog-ipc,datadog-ipc-macros,datadog-live-debugger,datadog-live-debugger-ffi,datadog-remote-config,datadog-sidecar,datadog-sidecar-ffi,datadog-sidecar-macros,libdd-alloc,libdd-capabilities,libdd-capabilities-impl,libdd-common,libdd-common-ffi,libdd-crashtracker,libdd-crashtracker-ffi,libdd-data-pipeline,libdd-ddsketch,libdd-dogstatsd-client,libdd-library-config,libdd-library-config-ffi,libdd-log,libdd-libunwind-sys,libdd-shared-runtime,libdd-telemetry,libdd-telemetry-ffi,libdd-tinybytes,libdd-trace-*,spawn_worker,tools/{cc_utils,sidecar_mockgen},libdd-trace-*,Cargo.toml} \( -type l -o -type f \) \( -path "*/src*" -o -path "*/examples*" -o -path "*/libunwind*" -o -path "*Cargo.toml" -o -path "*/build.rs" -o -path "*/tests/dataservice.rs" -o -path "*/tests/service_functional.rs" \) -not -path "*/datadog-ipc/build.rs" -not -path "*/datadog-sidecar-ffi/build.rs") +RUST_FILES = $(BUILD_DIR)/Cargo.toml $(BUILD_DIR)/Cargo.lock $(shell find components-rs -name '*.c' -o -name '*.rs' -o -name 'Cargo.toml' | awk '{ printf "$(BUILD_DIR)/%s\n", $$1 }' ) $(shell find libdatadog/{build-common,datadog-ffe,datadog-ipc,datadog-ipc-macros,datadog-live-debugger,datadog-live-debugger-ffi,datadog-remote-config,datadog-sidecar,datadog-sidecar-ffi,datadog-sidecar-macros,libdd-alloc,libdd-capabilities,libdd-capabilities-impl,libdd-common,libdd-common-ffi,libdd-crashtracker,libdd-crashtracker-ffi,libdd-data-pipeline,libdd-ddsketch,libdd-dogstatsd-client,libdd-library-config,libdd-library-config-ffi,libdd-log,libdd-libunwind-sys,libdd-shared-runtime,libdd-telemetry,libdd-telemetry-ffi,libdd-tinybytes,libdd-trace-*,spawn_worker,tools/{cc_utils,sidecar_mockgen},libdd-trace-*,Cargo.toml} \( -type l -o -type f \) \( -path "*/src*" -o -path "*/examples*" -o -path "*/libdd-libunwind-sys*" -o -path "*Cargo.toml" -o -path "*/build.rs" -o -path "*/tests/dataservice.rs" -o -path "*/tests/service_functional.rs" \) -not -path "*/datadog-ipc/build.rs" -not -path "*/datadog-sidecar-ffi/build.rs") ALL_OBJECT_FILES = $(C_FILES) $(RUST_FILES) $(BUILD_DIR)/Makefile TEST_OPCACHE_FILES = $(shell find tests/opcache -name '*.php*' -o -name '.gitkeep' | awk '{ printf "$(BUILD_DIR)/%s\n", $$1 }' ) TEST_STUB_FILES = $(shell find tests/ext -type d -name 'stubs' -exec find '{}' -type f \; | awk '{ printf "$(BUILD_DIR)/%s\n", $$1 }' ) diff --git a/appsec/tests/extension/client_init_record_span_tags.phpt b/appsec/tests/extension/client_init_record_span_tags.phpt index 5b4a68db1f7..6571a4a17da 100644 --- a/appsec/tests/extension/client_init_record_span_tags.phpt +++ b/appsec/tests/extension/client_init_record_span_tags.phpt @@ -75,6 +75,7 @@ Array [runtime-id] => %s [http.url] => https://localhost:8888/foo [http.method] => GET + [span.kind] => server [http.useragent] => my user agent ) rinit @@ -102,6 +103,7 @@ Array [meta_1] => value_1 [meta_2] => value_2 [runtime-id] => %s + [span.kind] => server ) metrics: Array diff --git a/appsec/tests/extension/rinit_record_span_tags.phpt b/appsec/tests/extension/rinit_record_span_tags.phpt index f274d81ce4c..56d89c3cf3d 100644 --- a/appsec/tests/extension/rinit_record_span_tags.phpt +++ b/appsec/tests/extension/rinit_record_span_tags.phpt @@ -70,6 +70,7 @@ Array [runtime-id] => %s [http.url] => https://localhost:8888/foo [http.method] => GET + [span.kind] => server [http.useragent] => my user agent ) rinit @@ -96,6 +97,7 @@ Array [http.useragent] => my user agent [rshutdown_tag] => rshutdown_value [runtime-id] => %s + [span.kind] => server ) metrics: Array diff --git a/appsec/tests/extension/rinit_root_span_add_tag.phpt b/appsec/tests/extension/rinit_root_span_add_tag.phpt index b5b376b19d3..922a31b8705 100644 --- a/appsec/tests/extension/rinit_root_span_add_tag.phpt +++ b/appsec/tests/extension/rinit_root_span_add_tag.phpt @@ -68,5 +68,6 @@ Array [http.status_code] => 200 [http.url] => http://localhost:8888/my/ur%69/ [runtime-id] => %s + [span.kind] => server [version] => 0.42.69 ) diff --git a/components-rs/Cargo.toml b/components-rs/Cargo.toml index 90b6851e1e4..80ce2e21db7 100644 --- a/components-rs/Cargo.toml +++ b/components-rs/Cargo.toml @@ -20,6 +20,7 @@ datadog-sidecar = { path = "../libdatadog/datadog-sidecar" } datadog-sidecar-ffi = { path = "../libdatadog/datadog-sidecar-ffi" } libdd-tinybytes = { path = "../libdatadog/libdd-tinybytes" } libdd-trace-utils = { path = "../libdatadog/libdd-trace-utils" } +libdd-trace-stats = { path = "../libdatadog/libdd-trace-stats" } libdd-crashtracker-ffi = { path = "../libdatadog/libdd-crashtracker-ffi", default-features = false, features = ["collector"] } libdd-library-config-ffi = { path = "../libdatadog/libdd-library-config-ffi", default-features = false } spawn_worker = { path = "../libdatadog/spawn_worker" } diff --git a/components-rs/agent_info.rs b/components-rs/agent_info.rs new file mode 100644 index 00000000000..3d035729641 --- /dev/null +++ b/components-rs/agent_info.rs @@ -0,0 +1,92 @@ +// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! FFI wrappers that read from the agent /info SHM (`AgentInfoReader`) and propagate +//! the results to the concentrator and trace-filter subsystems. +//! +//! Both functions perform a single `reader.read()` call (advancing the internal SHM +//! position once) and use the returned `changed` flag to decide whether to rebuild +//! the concentrator and filter configuration. + +use crate::stats::apply_concentrator_config; +use datadog_sidecar::service::agent_info::AgentInfoReader; +use libdd_common_ffi::slice::CharSlice; +use std::ffi::c_char; + +/// Read all agent /info data in one SHM read and apply env, container-hash and concentrator +/// config atomically. +/// +/// Fills `env_out` with the agent's `config.default_env` (zero-length slice if absent). +/// Fills `container_hash_out` with `container_tags_hash` (zero-length slice if absent). +/// Both slices borrow from the reader's cached info — valid until the next `reader.read()`. +/// +/// Concentrator config (peer tags, span kinds, trace filters) is applied only when the +/// SHM has changed since the last read (`changed == true`). Calling this once at RINIT +/// ensures the config is always applied before the first span is processed, so the +/// per-span `ddog_apply_agent_info_concentrator_config` can safely rely on `changed` alone. +/// +/// # Safety +/// `reader` must be a valid pointer to an `AgentInfoReader`. +#[no_mangle] +pub unsafe extern "C" fn ddog_apply_agent_info( + reader: &mut AgentInfoReader, + env_out: &mut CharSlice<'static>, + container_hash_out: &mut CharSlice<'static>, +) { + let (changed, info) = reader.read(); + if let Some(info) = info { + if let Some(s) = info + .config + .as_ref() + .and_then(|c| c.default_env.as_deref()) + .filter(|s| !s.is_empty()) + { + *env_out = CharSlice::from_raw_parts(s.as_ptr() as *const c_char, s.len()); + } + if changed { + if let Some(s) = info.container_tags_hash.as_deref().filter(|s| !s.is_empty()) { + *container_hash_out = CharSlice::from_raw_parts(s.as_ptr() as *const c_char, s.len()); + } else { + *container_hash_out = CharSlice::empty(); + } + apply_concentrator_config( + info.peer_tags.as_deref().unwrap_or(&[]).to_owned(), + info.span_kinds_stats_computed.as_deref().unwrap_or(&[]).to_owned(), + info.filter_tags.as_ref().and_then(|f| f.require.as_deref()).unwrap_or(&[]).to_owned(), + info.filter_tags.as_ref().and_then(|f| f.reject.as_deref()).unwrap_or(&[]).to_owned(), + info.filter_tags_regex.as_ref().and_then(|f| f.require.as_deref()).unwrap_or(&[]).to_owned(), + info.filter_tags_regex.as_ref().and_then(|f| f.reject.as_deref()).unwrap_or(&[]).to_owned(), + info.ignore_resources.as_deref().unwrap_or(&[]).to_owned(), + ); + } + } +} + +/// Apply concentrator config changes from the agent /info SHM. +/// +/// Cheap no-op when the SHM has not changed (`changed == false`). Only applies when +/// new data has arrived mid-request — `ddog_apply_agent_info` at RINIT guarantees the +/// initial configuration is already in place, so `changed` alone is sufficient here. +/// +/// # Safety +/// `reader` must be a valid pointer to an `AgentInfoReader`. +#[no_mangle] +pub unsafe extern "C" fn ddog_apply_agent_info_concentrator_config( + reader: &mut AgentInfoReader, +) { + let (changed, info) = reader.read(); + if !changed { + return; + } + if let Some(info) = info { + apply_concentrator_config( + info.peer_tags.as_deref().unwrap_or(&[]).to_owned(), + info.span_kinds_stats_computed.as_deref().unwrap_or(&[]).to_owned(), + info.filter_tags.as_ref().and_then(|f| f.require.as_deref()).unwrap_or(&[]).to_owned(), + info.filter_tags.as_ref().and_then(|f| f.reject.as_deref()).unwrap_or(&[]).to_owned(), + info.filter_tags_regex.as_ref().and_then(|f| f.require.as_deref()).unwrap_or(&[]).to_owned(), + info.filter_tags_regex.as_ref().and_then(|f| f.reject.as_deref()).unwrap_or(&[]).to_owned(), + info.ignore_resources.as_deref().unwrap_or(&[]).to_owned(), + ); + } +} diff --git a/components-rs/common.h b/components-rs/common.h index 4ff8d1b9603..4dbc8ff0a78 100644 --- a/components-rs/common.h +++ b/components-rs/common.h @@ -268,6 +268,12 @@ typedef struct _zend_string _zend_string; #define ddog_LOG_ONCE (1 << 3) +/** + * Number of gRPC status-code keys checked by the stats aggregation (must match + * `GRPC_STATUS_CODE_FIELD` in libdd-trace-stats/src/span_concentrator/aggregation.rs). + */ +#define ddog_PHP_GRPC_KEY_COUNT 4 + #define ddog_MultiTargetFetcher_DEFAULT_CLIENTS_LIMIT 100 typedef enum ddog_ConfigurationOrigin { @@ -406,6 +412,7 @@ typedef enum ddog_RemoteConfigCapabilities { DDOG_REMOTE_CONFIG_CAPABILITIES_APM_TRACING_ENABLE_LIVE_DEBUGGING = 41, DDOG_REMOTE_CONFIG_CAPABILITIES_ASM_DD_MULTICONFIG = 42, DDOG_REMOTE_CONFIG_CAPABILITIES_ASM_TRACE_TAGGING_RULES = 43, + DDOG_REMOTE_CONFIG_CAPABILITIES_APM_TRACING_MULTICONFIG = 45, DDOG_REMOTE_CONFIG_CAPABILITIES_FFE_FLAG_CONFIGURATION_RULES = 46, } ddog_RemoteConfigCapabilities; @@ -426,6 +433,8 @@ typedef enum ddog_SpanProbeTarget { DDOG_SPAN_PROBE_TARGET_ROOT, } ddog_SpanProbeTarget; +typedef struct ddog_AgentInfoReader ddog_AgentInfoReader; + typedef struct ddog_DebuggerPayload ddog_DebuggerPayload; typedef struct ddog_DslString ddog_DslString; @@ -455,6 +464,20 @@ typedef struct ddog_SidecarActionsBuffer ddog_SidecarActionsBuffer; */ typedef struct ddog_SidecarTransport ddog_SidecarTransport; +/** + * Opaque shared-memory span stats concentrator exposed to C. + * + * Always heap-allocated (as a `Box`) — C holds a raw pointer and must pass it back to + * `ddog_span_concentrator_drop` to free. + * + * When `inner` is `None` this is a *virtual* concentrator: the SHM has not been created by the + * sidecar yet, but peer-tag keys and span-kinds from `DESIRED_CONFIG` are still available so the + * C callback can run eligibility checks and extract peer tags. A virtual concentrator is always + * considered stale (`needs_refresh` returns `true`) so it will be upgraded to a real one on the + * next call once the SHM becomes available. + */ +typedef struct ddog_SpanConcentrator ddog_SpanConcentrator; + /** * Holds the raw parts of a Rust Vec; it should only be created from Rust, * never from C. @@ -648,8 +671,96 @@ typedef struct ddog_Vec_DebuggerPayload { */ typedef uint64_t ddog_QueueId; +/** + * A (key, value) pair for peer-service tags, borrowed from PHP/concentrator memory. + */ +typedef struct ddog_PhpPeerTag { + /** + * Key string — borrows from the concentrator's `peer_tag_keys` Vec. + */ + ddog_CharSlice key; + /** + * Value string — borrows from PHP span meta memory. + */ + ddog_CharSlice value; +} ddog_PhpPeerTag; + +/** + * Flat representation of a PHP span's stats-relevant fields, filled by C code in one call. + * + * All `CharSlice` fields borrow from PHP memory (or from the concentrator for peer-tag keys) and + * must remain valid for the duration of `ddog_span_concentrator_add_php_span`. + * + * For absent optional strings pass an empty slice (ptr may be non-null with len == 0). + * For absent optional `f64` values pass `f64::NAN`. + */ +typedef struct ddog_PhpSpanStats { + ddog_CharSlice service; + ddog_CharSlice resource; + ddog_CharSlice name; + ddog_CharSlice type; + int64_t start; + int64_t duration; + bool is_error; + bool is_trace_root; + bool is_measured; + bool has_top_level; + bool is_partial_snapshot; + ddog_CharSlice span_kind; + ddog_CharSlice http_status_code; + ddog_CharSlice http_method; + ddog_CharSlice http_endpoint; + ddog_CharSlice http_route; + ddog_CharSlice origin; + /** + * Value of the `_dd.svc_src` meta tag; empty slice when absent. + */ + ddog_CharSlice service_source; + /** + * gRPC meta values in order: rpc.grpc.status_code, grpc.code, rpc.grpc.status.code, + * grpc.status.code. Empty slice = absent. + */ + ddog_CharSlice grpc_meta[ddog_PHP_GRPC_KEY_COUNT]; + /** + * Same gRPC keys but from metrics (NaN = absent). + */ + double grpc_metrics[ddog_PHP_GRPC_KEY_COUNT]; + /** + * Number of (key,value) pairs in `peer_tags`. + */ + uintptr_t peer_tags_count; + /** + * Pointer to an array of `peer_tags_count` `PhpPeerTag` pairs. + * May be null when `peer_tags_count == 0`. + */ + const struct ddog_PhpPeerTag *peer_tags; +} ddog_PhpSpanStats; + typedef struct ddog_HashMap_ShmCacheKey__ShmCache ddog_ShmCacheMap; +/** + * Fast path: exact-key lookup into a root span. Returns null when the key is absent. + */ +typedef const char *(*ddog_RootTagLookupFn)(const void *ctx, + const char *key, + uintptr_t key_len, + uintptr_t *out_len); + +/** + * Per-entry callback passed to `RootMetaIterFn`. Return `false` to stop iteration early. + */ +typedef bool (*ddog_MetaEntryCb)(void *iter_ctx, + const char *key, + uintptr_t key_len, + const char *val, + uintptr_t val_len); + +/** + * Slow-path meta iterator. `NULL` when no regex-key filter entries are present. + * Iterates all string meta entries, calling `cb` for each; stops when `cb` returns `false`. + */ +typedef void (*ddog_RootMetaIterFn)(const void *ctx, void *iter_ctx, ddog_MetaEntryCb cb); + /** * A 128-bit (16 byte) buffer containing the UUID. * @@ -1020,8 +1131,6 @@ typedef enum ddog_DynamicInstrumentationConfigState { DDOG_DYNAMIC_INSTRUMENTATION_CONFIG_STATE_NOT_SET, } ddog_DynamicInstrumentationConfigState; -typedef struct ddog_AgentInfoReader ddog_AgentInfoReader; - typedef struct ddog_AgentRemoteConfigReader ddog_AgentRemoteConfigReader; typedef struct ddog_AgentRemoteConfigWriter_ShmHandle ddog_AgentRemoteConfigWriter_ShmHandle; diff --git a/components-rs/ddtrace.h b/components-rs/ddtrace.h index c7494138857..a7371e47094 100644 --- a/components-rs/ddtrace.h +++ b/components-rs/ddtrace.h @@ -53,6 +53,45 @@ const char *ddog_normalize_process_tag_value(ddog_CharSlice tag_value); void ddog_free_normalized_tag_value(const char *ptr); +/** + * Read all agent /info data in one SHM read and apply env, container-hash and concentrator + * config atomically. + * + * Fills `env_out` with the agent's `config.default_env` (zero-length slice if absent). + * Fills `container_hash_out` with `container_tags_hash` (zero-length slice if absent). + * Both slices borrow from the reader's cached info — valid until the next `reader.read()`. + * + * Concentrator config (peer tags, span kinds, trace filters) is applied only when the + * SHM has changed since the last read (`changed == true`). Calling this once at RINIT + * ensures the config is always applied before the first span is processed, so the + * per-span `ddog_apply_agent_info_concentrator_config` can safely rely on `changed` alone. + * + * # Safety + * `reader` must be a valid pointer to an `AgentInfoReader`. + */ +void ddog_apply_agent_info(struct ddog_AgentInfoReader *reader, + ddog_CharSlice *env_out, + ddog_CharSlice *container_hash_out); + +/** + * Apply concentrator config changes from the agent /info SHM. + * + * Cheap no-op when the SHM has not changed (`changed == false`). Only applies when + * new data has arrived mid-request — `ddog_apply_agent_info` at RINIT guarantees the + * initial configuration is already in place, so `changed` alone is sufficient here. + * + * # Safety + * `reader` must be a valid pointer to an `AgentInfoReader`. + */ +void ddog_apply_agent_info_concentrator_config(struct ddog_AgentInfoReader *reader); + +/** + * Returns true once the sidecar has received and applied the agent /info response. + * Used by `dd_trace_internal_fn('await_agent_info')` to block until the concentrator + * peer-tag keys and span kinds are initialised. + */ +bool ddog_is_agent_info_ready(void); + bool ddog_shall_log(enum ddog_Log category); void ddog_set_error_log_level(bool once); @@ -135,6 +174,95 @@ bool ddog_exception_hash_limiter_inc(struct ddog_SidecarTransport *connection, uint64_t hash, uint32_t granularity_seconds); +/** + * Look up (or lazily create) the concentrator for `(env, version, service)` and invoke + * `callback` with a shared reference to it while holding the global read lock. + * + * The callback is **always** invoked — even before the sidecar has created the backing SHM. + * When the SHM is not yet available a *virtual* concentrator is used: peer-tag keys and + * span-kinds come from `DESIRED_CONFIG` so eligibility and peer-tag extraction still work + * correctly. The C callback should call `ddog_span_concentrator_has_shm` to decide whether to + * write to the SHM (real concentrator) or store the stats for the IPC path (virtual). + * + * A virtual concentrator is always considered stale so it will be transparently upgraded to a + * real one on the next call once the sidecar has created the SHM. + * + * Returns `true` after the callback returns, `false` only on an internal locking error. + * + * # Safety + * `env`, `version`, and `service` must be valid `CharSlice`s. `callback` must be a valid + * function pointer. `userdata` is forwarded to `callback` as-is. + */ +bool ddog_span_concentrator_with(ddog_CharSlice env, + ddog_CharSlice version, + ddog_CharSlice service, + void (*callback)(const struct ddog_SpanConcentrator*, void*), + void *userdata); + +/** + * Returns `true` when the concentrator is backed by a real SHM and + * `ddog_span_concentrator_add_php_span` will actually persist data. + * Returns `false` for virtual concentrators (SHM not yet available) — the C callback should + * store the stats for the IPC fallback path in that case. + */ +bool ddog_span_concentrator_has_shm(const struct ddog_SpanConcentrator *c); + +/** + * Return a pointer to the concentrator's peer-tag-key array and write the count to `*out_count`. + * + * The returned pointer is valid for the lifetime of the guard passed to this call. + * May return null when there are no peer tag keys. + */ +const ddog_CharSlice *ddog_span_concentrator_peer_tag_keys(const struct ddog_SpanConcentrator *c, + uintptr_t *out_count); + +/** + * Add a PHP span to the concentrator for stats computation. + * + * Fast eligibility pre-check: returns true if a span with these attributes would be accepted + * by `ddog_span_concentrator_add_php_span`. + * + * Call this before constructing the full `PhpSpanStats`. If it returns false, skip the span + * entirely. If it returns true, fill the remaining fields and call `add_php_span`. + */ +bool ddog_span_concentrator_is_eligible(const struct ddog_SpanConcentrator *c, + bool has_top_level, + bool is_measured, + ddog_CharSlice span_kind, + bool is_partial_snapshot); + +/** + * Write a PHP span to the concentrator's backing SHM. + * + * Only valid when `ddog_span_concentrator_has_shm` returns `true`. For virtual concentrators + * (no SHM) the caller should use the IPC path instead. + * + * All `CharSlice` fields in `span` (and in the `peer_tags` array it points to) must remain valid + * for the duration of this call. + * + * # Safety + * `span` must point to a valid `PhpSpanStats`. The concentrator must have a backing SHM + * (`ddog_span_concentrator_has_shm` returns `true`). + */ +void ddog_span_concentrator_add_php_span(const struct ddog_SpanConcentrator *c, + const struct ddog_PhpSpanStats *span); + +/** + * IPC fallback: send a PHP span directly to the sidecar's SHM concentrator for (env, version). + * + * Called when the SHM is not yet available. The sidecar processes IPC messages sequentially, + * and `set_universal_service_tags` is always sent before this message, so the concentrator + * is guaranteed to exist when the sidecar handles this call. The sidecar resolves the service + * dimension from the session's `DD_SERVICE` config. + * + * # Safety + * All pointers must be valid. + */ +void ddog_sidecar_add_php_span_to_concentrator(struct ddog_SidecarTransport **transport, + ddog_CharSlice env, + ddog_CharSlice version, + const struct ddog_PhpSpanStats *span); + bool ddtrace_detect_composer_installed_json(struct ddog_SidecarTransport **transport, const struct ddog_InstanceId *instance_id, const ddog_QueueId *queue_id, @@ -207,6 +335,25 @@ bool ddog_sidecar_telemetry_are_endpoints_collected(ddog_ShmCacheMap *cache, ddog_CharSlice service, ddog_CharSlice env); +/** + * Check whether the trace rooted at `resource` / `root_span` passes all configured trace + * filters (filter_tags, filter_tags_regex, ignore_resources from agent /info). + * + * Returns `true` to include in the pipeline, `false` to drop the entire trace (no sending, + * no stats). Filters are evaluated against the root span — the decision applies uniformly + * to all spans of the trace. + * + * * **Common case**: `filter_tags` and literal-key `filter_tags_regex` entries — one O(1) + * `lookup_fn` call per filter entry. + * * **Rare case**: `filter_tags_regex` entries with regex key patterns — `iter_fn` is invoked + * to scan all meta entries for those filters. Pass `NULL` when not needed. + * * **Fast path**: returns `true` immediately when no filters are configured. + */ +bool ddog_check_stats_trace_filter(ddog_CharSlice resource, + const void *root_span, + ddog_RootTagLookupFn lookup_fn, + ddog_RootMetaIterFn iter_fn); + void ddog_init_span_func(void (*free_func)(ddog_OwnedZendString), void (*addref_func)(struct _zend_string*), ddog_OwnedZendString (*init_func)(ddog_CharSlice)); diff --git a/components-rs/lib.rs b/components-rs/lib.rs index 7c539641036..49281dc7f2d 100644 --- a/components-rs/lib.rs +++ b/components-rs/lib.rs @@ -3,10 +3,13 @@ #![feature(linkage)] #![allow(static_mut_refs)] // remove with move to Rust 2024 edition +pub mod agent_info; pub mod log; pub mod remote_config; pub mod sidecar; +pub mod stats; pub mod telemetry; +pub mod trace_filter; pub mod bytes; use libdd_common::entity_id::{get_container_id, set_cgroup_file}; diff --git a/components-rs/sidecar.h b/components-rs/sidecar.h index 5b0934c7922..10a501d124f 100644 --- a/components-rs/sidecar.h +++ b/components-rs/sidecar.h @@ -211,7 +211,9 @@ ddog_MaybeError ddog_sidecar_session_set_config(struct ddog_SidecarTransport **t uintptr_t remote_config_capabilities_count, bool remote_config_enabled, bool is_fork, - const struct ddog_Vec_Tag *process_tags); + const struct ddog_Vec_Tag *process_tags, + ddog_CharSlice hostname, + ddog_CharSlice root_service); /** * Updates the process_tags for an existing session. diff --git a/components-rs/stats.rs b/components-rs/stats.rs new file mode 100644 index 00000000000..a12c63c0672 --- /dev/null +++ b/components-rs/stats.rs @@ -0,0 +1,496 @@ +// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! FFI wrapper for the local SpanConcentrator used to compute span stats on the PHP tracer side. +//! +//! The C extraction side fills a `PhpSpanStats` struct (one call per span), then passes it to +//! `ddog_span_concentrator_add_php_span`. All string slices borrow from PHP memory and are only +//! valid for the duration of that call. + +use crate::trace_filter; +use datadog_ipc::shm_stats::{OwnedShmSpanInput, ShmSpanConcentrator, ShmSpanInput, MAX_PEER_TAGS}; +use datadog_sidecar::service::blocking::{add_span_to_concentrator, SidecarTransport}; +use libdd_trace_stats::span_concentrator::FixedAggregationKey; +use libdd_common_ffi::slice::{AsBytes, CharSlice}; +use std::collections::HashMap; +use std::ffi::{c_char, c_void}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{LazyLock, RwLock}; +use tracing::trace; + +/// Number of gRPC status-code keys checked by the stats aggregation (must match +/// `GRPC_STATUS_CODE_FIELD` in libdd-trace-stats/src/span_concentrator/aggregation.rs). +pub const PHP_GRPC_KEY_COUNT: usize = 4; + +/// A (key, value) pair for peer-service tags, borrowed from PHP/concentrator memory. +#[repr(C)] +#[derive(Copy, Clone)] +pub struct PhpPeerTag<'a> { + /// Key string — borrows from the concentrator's `peer_tag_keys` Vec. + pub key: CharSlice<'a>, + /// Value string — borrows from PHP span meta memory. + pub value: CharSlice<'a>, +} + +/// Flat representation of a PHP span's stats-relevant fields, filled by C code in one call. +/// +/// All `CharSlice` fields borrow from PHP memory (or from the concentrator for peer-tag keys) and +/// must remain valid for the duration of `ddog_span_concentrator_add_php_span`. +/// +/// For absent optional strings pass an empty slice (ptr may be non-null with len == 0). +/// For absent optional `f64` values pass `f64::NAN`. +#[repr(C)] +pub struct PhpSpanStats<'a> { + pub service: CharSlice<'a>, + pub resource: CharSlice<'a>, + pub name: CharSlice<'a>, + pub r#type: CharSlice<'a>, + + pub start: i64, + pub duration: i64, + + pub is_error: bool, + pub is_trace_root: bool, + pub is_measured: bool, + pub has_top_level: bool, + pub is_partial_snapshot: bool, + + pub span_kind: CharSlice<'a>, + pub http_status_code: CharSlice<'a>, + pub http_method: CharSlice<'a>, + pub http_endpoint: CharSlice<'a>, + pub http_route: CharSlice<'a>, + pub origin: CharSlice<'a>, + /// Value of the `_dd.svc_src` meta tag; empty slice when absent. + pub service_source: CharSlice<'a>, + + + /// gRPC meta values in order: rpc.grpc.status_code, grpc.code, rpc.grpc.status.code, + /// grpc.status.code. Empty slice = absent. + pub grpc_meta: [CharSlice<'a>; PHP_GRPC_KEY_COUNT], + /// Same gRPC keys but from metrics (NaN = absent). + pub grpc_metrics: [f64; PHP_GRPC_KEY_COUNT], + + /// Number of (key,value) pairs in `peer_tags`. + pub peer_tags_count: usize, + /// Pointer to an array of `peer_tags_count` `PhpPeerTag` pairs. + /// May be null when `peer_tags_count == 0`. + pub peer_tags: *const PhpPeerTag<'a>, +} + +#[inline] +fn char_slice_str(s: CharSlice) -> &str { + s.try_to_utf8().unwrap_or("") +} + +/// Prefer string meta, fall back to metrics f64, default 0. +#[inline] +fn extract_http_status_code(span: &PhpSpanStats<'_>) -> u32 { + let s = char_slice_str(span.http_status_code); + if !s.is_empty() { + s.parse::().unwrap_or(0) + } else { + 0 + } +} + +/// First non-absent value across gRPC meta keys then gRPC metric keys. +#[inline] +fn extract_grpc_status_code(span: &PhpSpanStats<'_>) -> Option { + span.grpc_meta + .iter() + .find_map(|m| { + let s = char_slice_str(*m); + if s.is_empty() { None } else { s.parse::().ok() } + }) + .or_else(|| { + span.grpc_metrics + .iter() + .find_map(|&v| if v.is_nan() { None } else { Some(v as u8) }) + }) +} + +/// Prefer http.endpoint, fall back to http.route. +#[inline] +fn extract_http_endpoint<'a>(span: &'a PhpSpanStats<'a>) -> &'a str { + let ep = char_slice_str(span.http_endpoint); + if !ep.is_empty() { ep } else { char_slice_str(span.http_route) } +} + +/// Decode the raw peer-tags pointer into a slice. +/// +/// # Safety +/// `span.peer_tags` must be valid for `span.peer_tags_count` elements when non-null. +#[inline] +unsafe fn peer_tags_slice<'a>(span: &'a PhpSpanStats<'a>) -> &'a [PhpPeerTag<'a>] { + if span.peer_tags_count > 0 && !span.peer_tags.is_null() { + std::slice::from_raw_parts(span.peer_tags, span.peer_tags_count) + } else { + &[] + } +} + +#[inline] +fn is_synthetics_request(span: &PhpSpanStats<'_>) -> bool { + char_slice_str(span.origin).starts_with("synthetics") +} + +/// Build the `FixedAggregationKey` from borrowed `PhpSpanStats` fields. +#[inline] +fn build_fixed_key<'a>(span: &'a PhpSpanStats<'a>) -> FixedAggregationKey<&'a str> { + FixedAggregationKey { + service_name: char_slice_str(span.service), + resource_name: char_slice_str(span.resource), + operation_name: char_slice_str(span.name), + span_type: char_slice_str(span.r#type), + span_kind: char_slice_str(span.span_kind), + http_method: char_slice_str(span.http_method), + http_endpoint: extract_http_endpoint(span), + http_status_code: extract_http_status_code(span), + is_synthetics_request: is_synthetics_request(span), + is_trace_root: span.is_trace_root, + grpc_status_code: extract_grpc_status_code(span), + service_source: char_slice_str(span.service_source), + } +} + +/// Extract a `ShmSpanInput` from a `PhpSpanStats`. +/// +/// `peer_tag_buf` must be a caller-allocated buffer of at least `MAX_PEER_TAGS` entries; it is +/// filled in-place so that `ShmSpanInput::peer_tags` can borrow from it. +#[inline] +fn php_span_to_shm_input<'a>( + span: &'a PhpSpanStats<'a>, + peer_tag_buf: &'a mut [(&'a str, &'a str); MAX_PEER_TAGS], +) -> ShmSpanInput<'a> { + let mut peer_tag_count = 0usize; + // Safety: caller guarantees PhpSpanStats validity (see ddog_span_concentrator_add_php_span). + for pt in unsafe { peer_tags_slice(span) }.iter().take(MAX_PEER_TAGS) { + peer_tag_buf[peer_tag_count] = (char_slice_str(pt.key), char_slice_str(pt.value)); + peer_tag_count += 1; + } + + ShmSpanInput { + fixed: build_fixed_key(span), + peer_tags: &peer_tag_buf[..peer_tag_count], + duration_ns: span.duration, + is_error: span.is_error, + is_top_level: span.has_top_level, + } +} + +/// Opaque shared-memory span stats concentrator exposed to C. +/// +/// Always heap-allocated (as a `Box`) — C holds a raw pointer and must pass it back to +/// `ddog_span_concentrator_drop` to free. +/// +/// When `inner` is `None` this is a *virtual* concentrator: the SHM has not been created by the +/// sidecar yet, but peer-tag keys and span-kinds from `DESIRED_CONFIG` are still available so the +/// C callback can run eligibility checks and extract peer tags. A virtual concentrator is always +/// considered stale (`needs_refresh` returns `true`) so it will be upgraded to a real one on the +/// next call once the SHM becomes available. +pub struct SpanConcentrator { + /// `Some` when the backing SHM is open; `None` for virtual concentrators. + inner: Option, + /// Whether the backing SHM is available. False for virtual concentrators. + pub has_shm: bool, + peer_tag_keys: Vec, + /// Contiguous array of `CharSlice<'static>` views into `peer_tag_keys`. + /// Rebuilt whenever `set_peer_tags` is called so that C can get a stable pointer. + peer_tag_key_slices: Vec>, + span_kinds: Vec, +} + +// SAFETY: `peer_tag_key_slices` borrows from the owned `peer_tag_keys` Vec that lives +// alongside it in the same struct, which is always protected by the global RwLock. +unsafe impl Send for SpanConcentrator {} +unsafe impl Sync for SpanConcentrator {} + +impl SpanConcentrator { + fn rebuild_key_slices(&mut self) { + self.peer_tag_key_slices = self + .peer_tag_keys + .iter() + .map(|s| unsafe { CharSlice::from_raw_parts(s.as_ptr() as *const c_char, s.len()) }) + .collect(); + } + + /// Returns `true` when the entry should be replaced. + /// Virtual concentrators (no SHM) always request a refresh so the caller can upgrade them + /// to real concentrators once the SHM becomes available. + fn needs_refresh(&self) -> bool { + match &self.inner { + Some(shm) => shm.needs_reload(), + None => true, + } + } +} + +static SPAN_CONCENTRATORS: LazyLock>> = LazyLock::new(|| RwLock::default()); + +/// Set to true once `apply_concentrator_config` has been called at least once, +/// i.e. the sidecar has received and applied the agent's /info response. +static AGENT_INFO_RECEIVED: AtomicBool = AtomicBool::new(false); + +/// Returns true once the agent /info has been received and applied. +/// Used by the PHP extension to skip stats computation until the concentrator +/// has been properly initialised with peer-tag keys and span kinds. +#[no_mangle] +pub extern "C" fn ddog_is_agent_info_ready() -> bool { + AGENT_INFO_RECEIVED.load(Ordering::Acquire) +} + +/// Desired concentrator configuration sourced from the agent's /info endpoint. +/// Populated via `ddog_apply_agent_info`; applied to every concentrator +/// at creation time and when the config changes. +#[derive(Default)] +struct DesiredConfig { + peer_tag_keys: Vec, + span_kinds: Vec, +} + +static DESIRED_CONFIG: LazyLock> = LazyLock::new(|| RwLock::default()); + +/// Apply updated concentrator config (peer-tag keys, span kinds, trace filters) to +/// `DESIRED_CONFIG` and propagate peer-tag / span-kind changes to all open concentrators. +pub(crate) fn apply_concentrator_config( + peer_tag_keys: Vec, + span_kinds: Vec, + tags_require: Vec, + tags_reject: Vec, + regex_require: Vec, + regex_reject: Vec, + ignore_resources: Vec, +) { + let compiled = trace_filter::compile_trace_filter( + &tags_require, &tags_reject, ®ex_require, ®ex_reject, &ignore_resources, + ); + trace_filter::set_trace_filter(compiled); + AGENT_INFO_RECEIVED.store(true, Ordering::Release); + { + let mut dc = DESIRED_CONFIG.write().unwrap(); + dc.peer_tag_keys = peer_tag_keys.clone(); + dc.span_kinds = span_kinds.clone(); + } + let mut wg = SPAN_CONCENTRATORS.write().unwrap(); + for c in wg.values_mut() { + c.peer_tag_keys = peer_tag_keys.clone(); + c.span_kinds = span_kinds.clone(); + c.rebuild_key_slices(); + } +} + +/// Look up (or lazily create) the concentrator for `(env, version, service)` and invoke +/// `callback` with a shared reference to it while holding the global read lock. +/// +/// The callback is **always** invoked — even before the sidecar has created the backing SHM. +/// When the SHM is not yet available a *virtual* concentrator is used: peer-tag keys and +/// span-kinds come from `DESIRED_CONFIG` so eligibility and peer-tag extraction still work +/// correctly. The C callback should call `ddog_span_concentrator_has_shm` to decide whether to +/// write to the SHM (real concentrator) or store the stats for the IPC path (virtual). +/// +/// A virtual concentrator is always considered stale so it will be transparently upgraded to a +/// real one on the next call once the sidecar has created the SHM. +/// +/// Returns `true` after the callback returns, `false` only on an internal locking error. +/// +/// # Safety +/// `env`, `version`, and `service` must be valid `CharSlice`s. `callback` must be a valid +/// function pointer. `userdata` is forwarded to `callback` as-is. +#[no_mangle] +pub unsafe extern "C" fn ddog_span_concentrator_with( + env: CharSlice<'_>, + version: CharSlice<'_>, + service: CharSlice<'_>, + callback: unsafe extern "C" fn(*const SpanConcentrator, *mut c_void), + userdata: *mut c_void, +) -> bool { + let env_key = char_slice_str(env).to_owned(); + let version_key = char_slice_str(version).to_owned(); + let service_key = char_slice_str(service).to_owned(); + let map_key = format!("{env_key}\0{version_key}\0{service_key}"); + let map = &SPAN_CONCENTRATORS; + + // Fast path: read lock — entry present and up-to-date (real or virtual). + { + let guard = map.read().unwrap(); + if let Some(c) = guard.get(&map_key) { + if !c.needs_refresh() { + callback(c as *const SpanConcentrator, userdata); + return true; + } + } + } + + // Slow path: need to create or refresh — acquire write lock. + { + let mut wg = map.write().unwrap(); + let refresh = wg.get(&map_key).map_or(true, |c| c.needs_refresh()); + if refresh { + wg.remove(&map_key); + let path = datadog_sidecar::service::stats_flusher::env_stats_shm_path(&env_key, &version_key, &service_key); + let (shm, has_shm) = match ShmSpanConcentrator::open(path.as_c_str()) { + Ok(s) => (Some(s), true), + Err(e) => { + trace!("SHM for env={env_key} version={version_key} service={service_key} not yet available ({e}); using virtual concentrator"); + (None, false) + } + }; + let (peer_tag_keys, span_kinds) = { + let dc = DESIRED_CONFIG.read().unwrap(); + (dc.peer_tag_keys.clone(), dc.span_kinds.clone()) + }; + let mut c = SpanConcentrator { + inner: shm, + has_shm, + peer_tag_keys, + peer_tag_key_slices: vec![], + span_kinds, + }; + c.rebuild_key_slices(); + wg.insert(map_key.clone(), c); + } + } // write lock dropped + + // Re-acquire read lock after write. + let guard = map.read().unwrap(); + match guard.get(&map_key) { + Some(c) => { + callback(c as *const SpanConcentrator, userdata); + true + } + None => false, + } +} + +/// Returns `true` when the concentrator is backed by a real SHM and +/// `ddog_span_concentrator_add_php_span` will actually persist data. +/// Returns `false` for virtual concentrators (SHM not yet available) — the C callback should +/// store the stats for the IPC fallback path in that case. +#[no_mangle] +pub extern "C" fn ddog_span_concentrator_has_shm(c: &SpanConcentrator) -> bool { + c.has_shm +} + +/// Return a pointer to the concentrator's peer-tag-key array and write the count to `*out_count`. +/// +/// The returned pointer is valid for the lifetime of the guard passed to this call. +/// May return null when there are no peer tag keys. +#[no_mangle] +pub extern "C" fn ddog_span_concentrator_peer_tag_keys<'a>( + c: &'a SpanConcentrator, + out_count: &mut usize, +) -> *const CharSlice<'a> { + let slices = &c.peer_tag_key_slices; + *out_count = slices.len(); + if slices.is_empty() { + std::ptr::null() + } else { + slices.as_ptr() + } +} + +/// Add a PHP span to the concentrator for stats computation. +/// +/// Fast eligibility pre-check: returns true if a span with these attributes would be accepted +/// by `ddog_span_concentrator_add_php_span`. +/// +/// Call this before constructing the full `PhpSpanStats`. If it returns false, skip the span +/// entirely. If it returns true, fill the remaining fields and call `add_php_span`. +#[no_mangle] +pub extern "C" fn ddog_span_concentrator_is_eligible( + c: &SpanConcentrator, + has_top_level: bool, + is_measured: bool, + span_kind: CharSlice<'_>, + is_partial_snapshot: bool, +) -> bool { + if is_partial_snapshot { + return false; + } + if has_top_level || is_measured { + return true; + } + let kind = span_kind.try_to_utf8().unwrap_or(""); + if kind.is_empty() { + return false; + } + c.span_kinds.iter().any(|k| k == kind) +} + +/// Write a PHP span to the concentrator's backing SHM. +/// +/// Only valid when `ddog_span_concentrator_has_shm` returns `true`. For virtual concentrators +/// (no SHM) the caller should use the IPC path instead. +/// +/// All `CharSlice` fields in `span` (and in the `peer_tags` array it points to) must remain valid +/// for the duration of this call. +/// +/// # Safety +/// `span` must point to a valid `PhpSpanStats`. The concentrator must have a backing SHM +/// (`ddog_span_concentrator_has_shm` returns `true`). +#[no_mangle] +pub unsafe extern "C" fn ddog_span_concentrator_add_php_span( + c: &SpanConcentrator, + span: &PhpSpanStats<'_>, +) { + if let Some(shm) = &c.inner { + let mut peer_tag_buf = [("", ""); MAX_PEER_TAGS]; + let input = php_span_to_shm_input(span, &mut peer_tag_buf); + shm.add_span(&input); + } +} + +/// Convert a `PhpSpanStats` to `OwnedShmSpanInput` for IPC transport. +unsafe fn php_span_to_owned_input(span: &PhpSpanStats<'_>) -> OwnedShmSpanInput { + let peer_tags = peer_tags_slice(span) + .iter() + .take(MAX_PEER_TAGS) + .map(|pt| (char_slice_str(pt.key).to_owned(), char_slice_str(pt.value).to_owned())) + .collect(); + let fixed = build_fixed_key(span); + + OwnedShmSpanInput { + fixed: FixedAggregationKey { + service_name: fixed.service_name.to_owned(), + resource_name: fixed.resource_name.to_owned(), + operation_name: fixed.operation_name.to_owned(), + span_type: fixed.span_type.to_owned(), + span_kind: fixed.span_kind.to_owned(), + http_method: fixed.http_method.to_owned(), + http_endpoint: fixed.http_endpoint.to_owned(), + http_status_code: fixed.http_status_code, + grpc_status_code: fixed.grpc_status_code, + is_synthetics_request: fixed.is_synthetics_request, + is_trace_root: fixed.is_trace_root, + service_source: fixed.service_source.to_owned(), + }, + peer_tags, + duration_ns: span.duration, + is_error: span.is_error, + is_top_level: span.has_top_level, + } +} + +/// IPC fallback: send a PHP span directly to the sidecar's SHM concentrator for (env, version). +/// +/// Called when the SHM is not yet available. The sidecar processes IPC messages sequentially, +/// and `set_universal_service_tags` is always sent before this message, so the concentrator +/// is guaranteed to exist when the sidecar handles this call. The sidecar resolves the service +/// dimension from the session's `DD_SERVICE` config. +/// +/// # Safety +/// All pointers must be valid. +#[no_mangle] +pub unsafe extern "C" fn ddog_sidecar_add_php_span_to_concentrator( + transport: &mut Box, + env: CharSlice<'_>, + version: CharSlice<'_>, + span: &PhpSpanStats<'_>, +) { + let env_str = char_slice_str(env).to_owned(); + let version_str = char_slice_str(version).to_owned(); + let owned_span = php_span_to_owned_input(span); + let _ = add_span_to_concentrator(transport, env_str, version_str, owned_span); +} + diff --git a/components-rs/trace_filter.rs b/components-rs/trace_filter.rs new file mode 100644 index 00000000000..2fefd0543b6 --- /dev/null +++ b/components-rs/trace_filter.rs @@ -0,0 +1,304 @@ +// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Trace-level filter logic for client-side stats (filter_tags, filter_tags_regex, +//! ignore_resources as published by the agent's /info endpoint). +//! +//! The filter is a **full-pipeline gate**: when a trace fails, it is dropped from both +//! serialisation (no trace send) and stats computation. Individual spans are never +//! filtered in isolation — the root span's tags are evaluated and the decision applies +//! to the entire trace. +//! +//! # Fast path +//! `TRACE_FILTER` holds `None` when no filters are configured (the overwhelmingly common +//! case). `ddog_check_stats_trace_filter` returns `true` immediately in that case. + +use libdd_common_ffi::slice::{AsBytes, CharSlice}; +use regex::Regex; +use std::ffi::{c_char, c_void}; +use std::sync::{LazyLock, RwLock}; +use tracing::info; + +/// An exact-match tag filter: `"key"` (key-presence only) or `"key:value"`. +struct TagFilter { + key: String, + value: Option, +} + +/// A `filter_tags_regex` entry whose key is a plain literal — direct O(1) lookup. +struct TagRegexFilter { + key: String, + value: Option, +} + +/// A `filter_tags_regex` entry whose key is itself a regex pattern — requires meta iteration. +struct TagRegexKeyFilter { + key_re: Regex, + value: Option, +} + +/// Compiled trace filter rules, ready for fast evaluation. +pub(crate) struct TraceFilterConfig { + filter_tags_require: Vec, + filter_tags_reject: Vec, + /// Literal-key regex filters: O(1) lookup per entry via the C lookup callback. + filter_tags_regex_require: Vec, + filter_tags_regex_reject: Vec, + /// Regex-key filters (rare): require meta iteration via the C iterator callback. + filter_tags_regex_key_require: Vec, + filter_tags_regex_key_reject: Vec, + ignore_resources: Vec, +} + +fn parse_tag_filter(s: &str) -> TagFilter { + match s.find(':') { + Some(i) => TagFilter { key: s[..i].to_owned(), value: Some(s[i + 1..].to_owned()) }, + None => TagFilter { key: s.to_owned(), value: None }, + } +} + +/// Compile a regex anchored to the full string. +fn compile_anchored(pattern: &str) -> Option { + Regex::new(&format!("^(?:{pattern})$")).ok() +} + +/// Returns `true` when `key` contains no regex metacharacters and can be used for a direct +/// O(1) lookup. `.` is intentionally treated as a literal (not a wildcard) in key patterns. +fn is_literal_key(key: &str) -> bool { + !key.contains(|c: char| matches!(c, '*' | '+' | '?' | '[' | ']' | '(' | ')' | '{' | '}' | '^' | '$' | '|' | '\\')) +} + +/// Compile all `filter_tags_regex` entries, splitting into literal-key (fast) and +/// regex-key (slow) lists based on whether the key portion contains metacharacters. +fn compile_regex_filters(entries: &[String]) -> (Vec, Vec) { + let mut literal = Vec::new(); + let mut regex_key = Vec::new(); + for s in entries { + let (key_str, value_str) = match s.find(':') { + Some(i) => (&s[..i], Some(&s[i + 1..])), + None => (s.as_str(), None), + }; + let value = value_str.and_then(|v| compile_anchored(v)); + if is_literal_key(key_str) { + literal.push(TagRegexFilter { key: key_str.to_owned(), value }); + } else if let Some(key_re) = compile_anchored(key_str) { + regex_key.push(TagRegexKeyFilter { key_re, value }); + } else { + info!("'{key_str}' regex tag filter is not a valid regex"); + } + } + (literal, regex_key) +} + +/// Compile all filter arguments into a `TraceFilterConfig`, or return `None` when all lists +/// are empty (no filtering needed). +pub(crate) fn compile_trace_filter( + tags_require: &[String], + tags_reject: &[String], + regex_require: &[String], + regex_reject: &[String], + ignore_resources: &[String], +) -> Option { + if tags_require.is_empty() + && tags_reject.is_empty() + && regex_require.is_empty() + && regex_reject.is_empty() + && ignore_resources.is_empty() + { + return None; + } + let (regex_require_literal, regex_require_key) = compile_regex_filters(regex_require); + let (regex_reject_literal, regex_reject_key) = compile_regex_filters(regex_reject); + Some(TraceFilterConfig { + filter_tags_require: tags_require.iter().map(|s| parse_tag_filter(s)).collect(), + filter_tags_reject: tags_reject.iter().map(|s| parse_tag_filter(s)).collect(), + filter_tags_regex_require: regex_require_literal, + filter_tags_regex_reject: regex_reject_literal, + filter_tags_regex_key_require: regex_require_key, + filter_tags_regex_key_reject: regex_reject_key, + ignore_resources: ignore_resources.iter().filter_map(|s| compile_anchored(s)).collect(), + }) +} + +/// Currently active compiled trace filter. `None` when no filters are configured. +static TRACE_FILTER: LazyLock>> = + LazyLock::new(|| RwLock::new(None)); + +/// Replace the active trace filter with a freshly compiled one. +/// +/// Called from `apply_concentrator_config` in `stats.rs` whenever the agent /info SHM +/// is updated. +pub(crate) fn set_trace_filter(compiled: Option) { + *TRACE_FILTER.write().unwrap() = compiled; +} + +/// Fast path: exact-key lookup into a root span. Returns null when the key is absent. +pub type RootTagLookupFn = unsafe extern "C" fn( + ctx: *const c_void, + key: *const c_char, + key_len: usize, + out_len: *mut usize, +) -> *const c_char; + +/// Per-entry callback passed to `RootMetaIterFn`. Return `false` to stop iteration early. +pub type MetaEntryCb = unsafe extern "C" fn( + iter_ctx: *mut c_void, + key: *const c_char, + key_len: usize, + val: *const c_char, + val_len: usize, +) -> bool; + +/// Slow-path meta iterator. `NULL` when no regex-key filter entries are present. +/// Iterates all string meta entries, calling `cb` for each; stops when `cb` returns `false`. +pub type RootMetaIterFn = Option< + unsafe extern "C" fn(ctx: *const c_void, iter_ctx: *mut c_void, cb: MetaEntryCb), +>; + +#[inline] +unsafe fn lookup_tag<'a>( + lookup: RootTagLookupFn, + ctx: *const c_void, + key: &str, +) -> Option<&'a str> { + let mut vlen: usize = 0; + let vptr = lookup(ctx, key.as_ptr() as *const c_char, key.len(), &mut vlen); + if vptr.is_null() { + None + } else { + Some(std::str::from_utf8_unchecked(std::slice::from_raw_parts( + vptr as *const u8, + vlen, + ))) + } +} + +/// State threaded through the opaque `iter_ctx` pointer during regex-key meta scans. +struct IterState<'a> { + key_re: &'a Regex, + value: &'a Option, + found: bool, +} + +/// Trampoline called by C for each meta entry. Sets `found = true` and stops on a match. +unsafe extern "C" fn meta_entry_cb( + iter_ctx: *mut c_void, + key: *const c_char, + klen: usize, + val: *const c_char, + vlen: usize, +) -> bool { + let state = &mut *(iter_ctx as *mut IterState); + let k = std::str::from_utf8_unchecked(std::slice::from_raw_parts(key as *const u8, klen)); + if state.key_re.is_match(k) { + let v = std::str::from_utf8_unchecked(std::slice::from_raw_parts(val as *const u8, vlen)); + if state.value.as_ref().map_or(true, |re| re.is_match(v)) { + state.found = true; + return false; // stop iteration + } + } + true // continue +} + +/// Use the iterator to check whether any meta entry matches a `TagRegexKeyFilter`. +#[inline] +unsafe fn regex_key_matches( + iter: unsafe extern "C" fn(*const c_void, *mut c_void, MetaEntryCb), + ctx: *const c_void, + filter: &TagRegexKeyFilter, +) -> bool { + let mut state = IterState { key_re: &filter.key_re, value: &filter.value, found: false }; + iter(ctx, &mut state as *mut IterState as *mut c_void, meta_entry_cb); + state.found +} + +/// Check whether the trace rooted at `resource` / `root_span` passes all configured trace +/// filters (filter_tags, filter_tags_regex, ignore_resources from agent /info). +/// +/// Returns `true` to include in the pipeline, `false` to drop the entire trace (no sending, +/// no stats). Filters are evaluated against the root span — the decision applies uniformly +/// to all spans of the trace. +/// +/// * **Common case**: `filter_tags` and literal-key `filter_tags_regex` entries — one O(1) +/// `lookup_fn` call per filter entry. +/// * **Rare case**: `filter_tags_regex` entries with regex key patterns — `iter_fn` is invoked +/// to scan all meta entries for those filters. Pass `NULL` when not needed. +/// * **Fast path**: returns `true` immediately when no filters are configured. +#[no_mangle] +pub unsafe extern "C" fn ddog_check_stats_trace_filter( + resource: CharSlice<'_>, + root_span: *const c_void, + lookup_fn: RootTagLookupFn, + iter_fn: RootMetaIterFn, +) -> bool { + let guard = TRACE_FILTER.read().unwrap(); + // Fast path: None means no filters configured (overwhelmingly common). + let Some(f) = guard.as_ref() else { + return true; + }; + + let resource_str = resource.try_to_utf8().unwrap_or(""); + + // 1. ignore_resources: reject if root resource matches any pattern. + for re in &f.ignore_resources { + if re.is_match(resource_str) { + return false; + } + } + // 2. filter_tags.reject: reject if the root span has a matching tag. + for filter in &f.filter_tags_reject { + if let Some(val) = lookup_tag(lookup_fn, root_span, &filter.key) { + if filter.value.as_deref().map_or(true, |v| v == val) { + return false; + } + } + } + // 3a. filter_tags_regex.reject (literal key): reject if value matches. + for filter in &f.filter_tags_regex_reject { + if let Some(val) = lookup_tag(lookup_fn, root_span, &filter.key) { + if filter.value.as_ref().map_or(true, |re| re.is_match(val)) { + return false; + } + } + } + // 3b. filter_tags_regex.reject (regex key): slow path — iterate all meta. + if let Some(iter) = iter_fn { + for filter in &f.filter_tags_regex_key_reject { + if regex_key_matches(iter, root_span, filter) { + return false; + } + } + } + // 4. filter_tags.require: reject unless every required tag is present and matches. + for filter in &f.filter_tags_require { + match lookup_tag(lookup_fn, root_span, &filter.key) { + None => return false, + Some(val) => { + if filter.value.as_deref().is_some_and(|v| v != val) { + return false; + } + } + } + } + // 5a. filter_tags_regex.require (literal key): reject unless matched. + for filter in &f.filter_tags_regex_require { + match lookup_tag(lookup_fn, root_span, &filter.key) { + None => return false, + Some(val) => { + if filter.value.as_ref().is_some_and(|re| !re.is_match(val)) { + return false; + } + } + } + } + // 5b. filter_tags_regex.require (regex key): slow path — every required pattern must match. + if let Some(iter) = iter_fn { + for filter in &f.filter_tags_regex_key_require { + if !regex_key_matches(iter, root_span, filter) { + return false; + } + } + } + + true +} diff --git a/config.m4 b/config.m4 index 6202b96db96..3b8f92d6125 100644 --- a/config.m4 +++ b/config.m4 @@ -225,6 +225,8 @@ if test "$PHP_DDTRACE" != "no"; then ext/sidecar.c \ ext/signals.c \ ext/span.c \ + ext/span_stats.c \ + ext/trace_filter.c \ ext/startup_logging.c \ ext/telemetry.c \ ext/threads.c \ diff --git a/config.w32 b/config.w32 index 06b1a0ff048..dbf504c3320 100644 --- a/config.w32 +++ b/config.w32 @@ -56,9 +56,11 @@ if (PHP_DDTRACE != 'no') { DDTRACE_EXT_SOURCES += " serializer.c"; DDTRACE_EXT_SOURCES += " sidecar.c"; DDTRACE_EXT_SOURCES += " span.c"; + DDTRACE_EXT_SOURCES += " span_stats.c"; DDTRACE_EXT_SOURCES += " startup_logging.c"; DDTRACE_EXT_SOURCES += " telemetry.c"; DDTRACE_EXT_SOURCES += " trace_source.c"; + DDTRACE_EXT_SOURCES += " trace_filter.c"; DDTRACE_EXT_SOURCES += " threads.c"; DDTRACE_EXT_SOURCES += " user_request.c"; DDTRACE_EXT_SOURCES += " weak_resources.c"; diff --git a/dockerfiles/services/request-replayer/src/index.php b/dockerfiles/services/request-replayer/src/index.php index c40105a0b97..ff7de4a3adc 100644 --- a/dockerfiles/services/request-replayer/src/index.php +++ b/dockerfiles/services/request-replayer/src/index.php @@ -79,6 +79,7 @@ function decodeDogStatsDMetrics($metrics) define('REQUEST_RC_REQUESTS_FILE', getenv('REQUEST_RC_REQUESTS_FILE') ?: ("$temp_location/rc_requests.json")); define('REQUEST_METRICS_FILE', getenv('REQUEST_METRICS_FILE') ?: ("$temp_location/metrics.json")); define('REQUEST_METRICS_LOG_FILE', getenv('REQUEST_METRICS_LOG_FILE') ?: ("$temp_location/metrics-log.txt")); +define('REQUEST_STATS_FILE', getenv('REQUEST_STATS_FILE') ?: ("$temp_location/stats.json")); define('REQUEST_AGENT_INFO_FILE', getenv('REQUEST_AGENT_INFO_FILE') ?: ("$temp_location/agent-info.txt")); function logRequest($message, $data = '') @@ -125,6 +126,16 @@ function logRequest($message, $data = '') unlink(REQUEST_METRICS_LOG_FILE); logRequest('Returned last metrics and deleted metrics log', $request); break; + case '/replay-stats': + if (!file_exists(REQUEST_STATS_FILE)) { + logRequest('Cannot replay stats; stats log does not exist'); + break; + } + $request = file_get_contents(REQUEST_STATS_FILE); + echo $request; + unlink(REQUEST_STATS_FILE); + logRequest('Returned stats and deleted stats log', $request); + break; case '/replay-rc-requests': if (!file_exists(REQUEST_RC_REQUESTS_FILE)) { logRequest('Cannot replay RC requests; RC requests log does not exist'); @@ -151,6 +162,9 @@ function logRequest($message, $data = '') unlink(REQUEST_METRICS_FILE); unlink(REQUEST_METRICS_LOG_FILE); } + if (file_exists(REQUEST_STATS_FILE)) { + unlink(REQUEST_STATS_FILE); + } if (file_exists(REQUEST_NEXT_RESPONSE_FILE)) { unlink(REQUEST_NEXT_RESPONSE_FILE); } @@ -251,6 +265,38 @@ function logRequest($message, $data = '') header("datadog-agent-state: " . sha1($file)); echo $file; break; + case '/v0.6/stats': + $raw = file_get_contents('php://input'); + $unpacker = new BufferUnpacker($raw, UnpackOptions::BIGINT_AS_STR); + $decoded = $unpacker->unpack(); + // OkSummary and ErrorSummary are binary DDSketch protobuf bytes; hex-encode them so + // json_encode does not fail on non-UTF-8 data. + $hexEncodeBinaryStats = function (&$arr) use (&$hexEncodeBinaryStats) { + if (!is_array($arr)) return; + foreach ($arr as $key => &$value) { + if (($key === 'OkSummary' || $key === 'ErrorSummary') && is_string($value)) { + $value = bin2hex($value); + } elseif (is_array($value)) { + $hexEncodeBinaryStats($value); + } + } + }; + $hexEncodeBinaryStats($decoded); + $body = json_encode($decoded); + $newStatsRequest = [ + 'uri' => $_SERVER['REQUEST_URI'], + 'headers' => getallheaders(), + 'body' => $body, + ]; + if (file_exists(REQUEST_STATS_FILE)) { + $statsStack = json_decode(file_get_contents(REQUEST_STATS_FILE), true); + } else { + $statsStack = []; + } + $statsStack[] = $newStatsRequest; + file_put_contents(REQUEST_STATS_FILE, json_encode($statsStack)); + logRequest('Logged stats request', $body); + break; default: $headers = getallheaders(); if (isset($headers['X-Datadog-Diagnostic-Check']) || isset($headers['x-datadog-diagnostic-check'])) { diff --git a/ext/agent_info.c b/ext/agent_info.c index 645dabf8bde..4d32b83c4d3 100644 --- a/ext/agent_info.c +++ b/ext/agent_info.c @@ -6,33 +6,26 @@ ZEND_EXTERN_MODULE_GLOBALS(ddtrace); -void ddtrace_check_agent_info_env() { - if (DDTRACE_G(agent_info_reader) && ZSTR_LEN(get_DD_ENV()) == 0) { - bool changed; - ddog_CharSlice env = ddog_get_agent_info_env(DDTRACE_G(agent_info_reader), &changed); - if (env.len) { - zend_alter_ini_entry_chars(zai_config_memoized_entries[DDTRACE_CONFIG_DD_ENV].ini_entries[0]->name, env.ptr, env.len, ZEND_INI_USER, ZEND_INI_STAGE_RUNTIME); - } - } -} - void ddtrace_agent_info_rinit() { - if (ddtrace_endpoint && !DDTRACE_G(agent_info_reader) && !ZSTR_LEN(get_global_DD_ENV())) { + if (ddtrace_endpoint && !DDTRACE_G(agent_info_reader)) { DDTRACE_G(agent_info_reader) = ddog_get_agent_info_reader(ddtrace_endpoint); } } -void ddtrace_get_container_tags_hash(void) { - if (DDTRACE_G(agent_info_reader)) { - bool changed; - ddog_CharSlice hash = ddog_get_agent_info_container_tags_hash( - DDTRACE_G(agent_info_reader), - &changed - ); - if (hash.len > 0) { - zend_string *hash_str = zend_string_init(hash.ptr, hash.len, 1); - ddtrace_process_tags_set_container_tags_hash(hash_str); - zend_string_release(hash_str); - } +void ddtrace_apply_agent_info(void) { + if (!DDTRACE_G(agent_info_reader)) { + return; + } + ddog_CharSlice env = {0}, hash = {0}; + ddog_apply_agent_info(DDTRACE_G(agent_info_reader), &env, &hash); + if (env.len && ZSTR_LEN(get_DD_ENV()) == 0) { + zend_alter_ini_entry_chars( + zai_config_memoized_entries[DDTRACE_CONFIG_DD_ENV].ini_entries[0]->name, + env.ptr, env.len, ZEND_INI_USER, ZEND_INI_STAGE_RUNTIME); + } + if (hash.len) { + zend_string *hash_str = zend_string_init(hash.ptr, hash.len, 1); + ddtrace_process_tags_set_container_tags_hash(hash_str); + zend_string_release(hash_str); } } diff --git a/ext/agent_info.h b/ext/agent_info.h index 19c1ee397fb..62e4c4dc1f6 100644 --- a/ext/agent_info.h +++ b/ext/agent_info.h @@ -4,8 +4,7 @@ #include #include "Zend/zend_types.h" -void ddtrace_check_agent_info_env(void); void ddtrace_agent_info_rinit(void); -void ddtrace_get_container_tags_hash(void); +void ddtrace_apply_agent_info(void); #endif // DD_AGENT_INFO_H diff --git a/ext/auto_flush.c b/ext/auto_flush.c index 0da8f6864c4..90b1912d893 100644 --- a/ext/auto_flush.c +++ b/ext/auto_flush.c @@ -56,8 +56,8 @@ ZEND_RESULT_CODE ddtrace_flush_tracer(bool force_on_startup, bool collect_cycles .lang_vendor = DDOG_CHARSLICE_C_BARE(""), .tracer_version = DDOG_CHARSLICE_C_BARE(PHP_DDTRACE_VERSION), .lang_version = php_version_rt, - .client_computed_top_level = false, - .client_computed_stats = !get_global_DD_APM_TRACING_ENABLED(), + .client_computed_top_level = get_DD_TRACE_STATS_COMPUTATION_ENABLED(), + .client_computed_stats = !get_global_DD_APM_TRACING_ENABLED() || get_DD_TRACE_STATS_COMPUTATION_ENABLED(), }, .transport = ddtrace_sidecar, .instance_id = ddtrace_sidecar_instance_id, diff --git a/ext/coms.c b/ext/coms.c index 1b0185aba40..9b8add58e19 100644 --- a/ext/coms.c +++ b/ext/coms.c @@ -771,9 +771,12 @@ static struct curl_slist *dd_agent_headers_alloc(void) { dd_append_header(&list, "Datadog-Meta-Lang-Interpreter", sapi_module.name, strlen(sapi_module.name)); dd_append_header(&list, "Datadog-Meta-Lang-Version", php_version_rt.ptr, php_version_rt.len); dd_append_header(&list, "Datadog-Meta-Tracer-Version", ZEND_STRL(PHP_DDTRACE_VERSION)); - if (!get_global_DD_APM_TRACING_ENABLED()) { + if (!get_global_DD_APM_TRACING_ENABLED() || (ddtrace_sidecar && get_global_DD_TRACE_STATS_COMPUTATION_ENABLED())) { dd_append_header(&list, "Datadog-Client-Computed-Stats", ZEND_STRL("true")); } + if (ddtrace_sidecar && get_global_DD_TRACE_STATS_COMPUTATION_ENABLED()) { + dd_append_header(&list, "Datadog-Client-Computed-Top-Level", ZEND_STRL("true")); + } ddog_CharSlice id = ddtrace_get_container_id(); if (id.len) { diff --git a/ext/configuration.h b/ext/configuration.h index 599d2ed4eb8..97f2ff82b94 100644 --- a/ext/configuration.h +++ b/ext/configuration.h @@ -274,6 +274,7 @@ enum ddtrace_sidecar_connection_mode { CONFIG(BOOL, DD_TRACE_RESOURCE_RENAMING_ENABLED, "false") \ CONFIG(BOOL, DD_TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT, "false") \ CONFIG(BOOL, DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED, "true") \ + CONFIG(BOOL, DD_TRACE_STATS_COMPUTATION_ENABLED, "false") \ DD_INTEGRATIONS #ifndef _WIN32 diff --git a/ext/ddtrace.c b/ext/ddtrace.c index 14451a111c7..6134c252392 100644 --- a/ext/ddtrace.c +++ b/ext/ddtrace.c @@ -1671,9 +1671,6 @@ static void dd_initialize_request(void) { zend_hash_init(&DDTRACE_G(tracestate_unknown_dd_keys), 8, unused, ZVAL_PTR_DTOR, 0); zend_hash_init(&DDTRACE_G(baggage), 8, unused, ZVAL_PTR_DTOR, 0); - // Check for the env first, before the first RC - ddtrace_check_agent_info_env(); - // Do after env check, so that RC data is not updated before RC init DDTRACE_G(request_initialized) = true; @@ -1720,7 +1717,8 @@ static void dd_initialize_request(void) { ddtrace_agent_info_rinit(); - ddtrace_get_container_tags_hash(); + // Single combined read: applies env, container-hash, and concentrator config. + ddtrace_apply_agent_info(); // Reset compile time after request init hook has compiled ddtrace_compile_time_reset(); @@ -3124,6 +3122,24 @@ PHP_FUNCTION(dd_trace_internal_fn) { ddog_logf(DDOG_LOG_WARN, false, "bar"); ddog_logf(DDOG_LOG_ERROR, false, "Boum"); RETVAL_TRUE; + } else if (FUNCTION_NAME_MATCHES("await_agent_info")) { + // Block until the sidecar has received and applied the agent /info response. + // This ensures peer-tag keys and span kinds are initialised before the caller + // makes requests that produce stats. Times out after 5 seconds. + uint32_t timeout_ms = 5000; + if (params_count == 1) { + timeout_ms = (uint32_t)Z_LVAL_P(ZVAL_VARARG_PARAM(params, 0)); + } + uint32_t waited = 0; + while (!ddog_is_agent_info_ready() && waited < timeout_ms) { + // Actively read the SHM so we pick up the update the sidecar wrote. + if (DDTRACE_G(agent_info_reader)) { + ddog_apply_agent_info_concentrator_config(DDTRACE_G(agent_info_reader)); + } + usleep(10000); // 10ms + waited += 10; + } + RETVAL_BOOL(ddog_is_agent_info_ready()); } } } diff --git a/ext/priority_sampling/priority_sampling.c b/ext/priority_sampling/priority_sampling.c index d92cf1d9425..8fb373bf0ca 100644 --- a/ext/priority_sampling/priority_sampling.c +++ b/ext/priority_sampling/priority_sampling.c @@ -267,7 +267,7 @@ static void dd_decide_on_sampling(ddtrace_root_span_data *span) { if (is_trace_root || zval_get_long(&span->property_propagated_sampling_priority) == DDTRACE_PRIORITY_SAMPLING_UNKNOWN) { // when we sample, we need to fetch the env first - ddtrace_check_agent_info_env(); + ddtrace_apply_agent_info(); double default_sample_rate = get_DD_TRACE_SAMPLE_RATE(); sample_rate = default_sample_rate >= 0 ? default_sample_rate : 1; diff --git a/ext/process_tags.c b/ext/process_tags.c index bc24fcc3c26..106203a1446 100644 --- a/ext/process_tags.c +++ b/ext/process_tags.c @@ -2,6 +2,11 @@ #include #include #include +#ifndef _WIN32 +#include +#else +#include +#endif #include #include #include "process_tags.h" @@ -33,8 +38,8 @@ typedef struct { size_t count; size_t capacity; zend_string *serialized; - zend_string *base_hash; - zend_string *container_tags_hash; + _Atomic(zend_string *) base_hash; + _Atomic(zend_string *) container_tags_hash; ddog_Vec_Tag vec; } process_tags_t; @@ -204,18 +209,14 @@ static void recompute_base_hash(void) { return; } - if (process_tags.base_hash) { - zend_string_release(process_tags.base_hash); - process_tags.base_hash = NULL; - } - uint64_t hash_value; - if (process_tags.container_tags_hash) { - size_t total_len = ZSTR_LEN(process_tags.serialized) + ZSTR_LEN(process_tags.container_tags_hash); + zend_string *cth = atomic_load(&process_tags.container_tags_hash); + if (cth) { + size_t total_len = ZSTR_LEN(process_tags.serialized) + ZSTR_LEN(cth); unsigned char *combined = emalloc(total_len); memcpy(combined, ZSTR_VAL(process_tags.serialized), ZSTR_LEN(process_tags.serialized)); - memcpy(combined + ZSTR_LEN(process_tags.serialized), ZSTR_VAL(process_tags.container_tags_hash), ZSTR_LEN(process_tags.container_tags_hash)); + memcpy(combined + ZSTR_LEN(process_tags.serialized), ZSTR_VAL(cth), ZSTR_LEN(cth)); hash_value = dd_fnv1a_64(combined, total_len); efree(combined); @@ -227,7 +228,11 @@ static void recompute_base_hash(void) { smart_str_alloc(&hash_buf, 21, 1); smart_str_append_printf(&hash_buf, "%" PRIu64, hash_value); smart_str_0(&hash_buf); - process_tags.base_hash = hash_buf.s; + + zend_string *old_hash = atomic_exchange(&process_tags.base_hash, hash_buf.s); + if (old_hash) { + zend_string_release(old_hash); + } } static void serialize_process_tags(void) { @@ -285,10 +290,11 @@ void ddtrace_process_tags_set_container_tags_hash(zend_string *container_tags_ha return; } - if (process_tags.container_tags_hash) { - zend_string_release(process_tags.container_tags_hash); + zend_string *new_hash = zend_string_copy(container_tags_hash); + zend_string *old_hash = atomic_exchange(&process_tags.container_tags_hash, new_hash); + if (old_hash) { + zend_string_release(old_hash); } - process_tags.container_tags_hash = zend_string_copy(container_tags_hash); recompute_base_hash(); } diff --git a/ext/serializer.c b/ext/serializer.c index 70ce1f1f1fd..28ab4c9521d 100644 --- a/ext/serializer.c +++ b/ext/serializer.c @@ -51,6 +51,8 @@ #include "trace_source.h" #include "exception_serialize.h" #include "sidecar.h" +#include "span_stats.h" +#include "trace_filter.h" ZEND_EXTERN_MODULE_GLOBALS(ddtrace); @@ -636,6 +638,14 @@ static void dd_set_entrypoint_root_span_props(struct superglob_equiv *data, ddtr ZVAL_STR(&http_method, zend_string_init(method, strlen(method), 0)); zend_hash_str_add_new(meta, ZEND_STRL("http.method"), &http_method); + // Mark HTTP server entry spans with span.kind=server for client-side stats aggregation. + // Only add if not already set (e.g. by an OTel or framework integration). + zval span_kind_server; + ZVAL_STRING(&span_kind_server, "server"); + if (!zend_hash_str_add(meta, ZEND_STRL("span.kind"), &span_kind_server)) { + zval_ptr_dtor(&span_kind_server); + } + if (get_DD_TRACE_URL_AS_RESOURCE_NAMES_ENABLED()) { const char *uri = dd_get_req_uri(data->server); zval *prop_resource = &span->property_resource; @@ -1171,7 +1181,7 @@ static bool dd_is_http_error(int status) { return false; } -static void dd_set_entrypoint_root_span_props_end(zend_array *meta, int status, struct iter *headers, bool ignore_error) { +static void dd_set_http_error(zend_array *meta, int status, bool ignore_error) { if (status) { zend_string *status_str = zend_long_to_str((long)status); zval status_zv; @@ -1186,6 +1196,10 @@ static void dd_set_entrypoint_root_span_props_end(zend_array *meta, int status, } } } +} + +static void dd_set_entrypoint_root_span_props_end(zend_array *meta, int status, struct iter *headers, bool ignore_error) { + dd_set_http_error(meta, status, ignore_error); for (zend_string *lowerheader, *headerval; headers->next(headers, &lowerheader, &headerval);) { dd_add_header_to_meta(meta, "response", lowerheader, headerval); @@ -1194,19 +1208,7 @@ static void dd_set_entrypoint_root_span_props_end(zend_array *meta, int status, } } -static void dd_set_entrypoint_root_rust_span_props_end(ddog_SpanBytes *span, int status, struct iter *headers, bool ignore_error) { - if (status) { - zend_string *status_str = zend_long_to_str((long)status); - ddog_add_str_span_meta_zstr(span, "http.status_code", status_str); - zend_string_release(status_str); - - if (!ignore_error && dd_is_http_error(status)) { - if (!ddog_has_span_meta_str(span, "error.type")) { - ddog_add_str_span_meta_str(span, "error.type", "HttpError"); - } - } - } - +static void dd_set_entrypoint_root_rust_span_props_end(ddog_SpanBytes *span, struct iter *headers) { for (zend_string *lowerheader, *headerval; headers->next(headers, &lowerheader, &headerval);) { dd_add_header_to_rust_span(span, "response", lowerheader, headerval); zend_string_release(lowerheader); @@ -1232,220 +1234,6 @@ static bool should_track_error(zend_object *exception, ddtrace_span_data *span) return true; } -static void _serialize_meta(ddog_SpanBytes *rust_span, ddtrace_span_data *span, zend_string *service_name) { - bool is_root_span = span->std.ce == ddtrace_ce_root_span_data; - bool is_inferred_span = span->std.ce == ddtrace_ce_inferred_span_data; - bool ignore_error = false; - - ddtrace_span_data *inferred_span = NULL; - if (is_root_span) { - inferred_span = ddtrace_get_inferred_span(ROOTSPANDATA(&span->std)); - } - - zval *meta = &span->property_meta; - ZVAL_DEREF(meta); - - if (Z_TYPE_P(meta) == IS_ARRAY) { - zend_string *str_key; - zval *orig_val; - ZEND_HASH_FOREACH_STR_KEY_VAL_IND(Z_ARRVAL_P(meta), str_key, orig_val) { - if (str_key) { - if (zend_string_equals_literal_ci(str_key, "error.ignored")) { - ignore_error = zend_is_true(orig_val); - continue; - } - - if (!ddog_has_span_meta_zstr(rust_span, str_key)) { - dd_serialize_array_meta_recursively(rust_span, str_key, orig_val); - } - } - } - ZEND_HASH_FOREACH_END(); - } - - zval *existing_env, new_env; - if ((existing_env = zend_hash_str_find(Z_ARRVAL_P(meta), ZEND_STRL("env")))) { - LOG(DEPRECATED, "Using \"env\" in meta is deprecated. Instead specify the env property directly on the span."); - } else { - ddtrace_convert_to_string(&new_env, &span->property_env); - if (Z_STRLEN(new_env)) { - ddog_add_str_span_meta_zstr(rust_span, "env", Z_STR_P(&new_env)); - } else { - zval_ptr_dtor(&new_env); - ZVAL_EMPTY_STRING(&new_env); - } - - existing_env = &new_env; - } - - if (existing_env == &new_env) { - zval_ptr_dtor(&new_env); - ZVAL_UNDEF(&new_env); - } - - zval new_version; - if (zend_hash_str_exists(Z_ARRVAL_P(meta), ZEND_STRL("version"))) { - LOG(DEPRECATED, "Using \"version\" in meta is deprecated. Instead specify the version property directly on the span."); - } else { - ddtrace_convert_to_string(&new_version, &span->property_version); - if (Z_STRLEN(new_version)) { - ddog_add_str_span_meta_zstr(rust_span, "version", Z_STR_P(&new_version)); - } - zval_ptr_dtor(&new_version); - } - - zval *exception_zv = &span->property_exception; - bool has_exception = Z_TYPE_P(exception_zv) == IS_OBJECT && instanceof_function(Z_OBJCE_P(exception_zv), zend_ce_throwable); - if (has_exception && !ignore_error) { - enum dd_exception exception_type = DD_EXCEPTION_THROWN; - if (is_root_span) { - exception_type = Z_PROP_FLAG_P(exception_zv) == 2 ? DD_EXCEPTION_CAUGHT : DD_EXCEPTION_UNCAUGHT; - } - ddtrace_exception_to_meta(Z_OBJ_P(exception_zv), service_name, span->start, rust_span, exception_type); - } - - zend_array *span_links = ddtrace_property_array(&span->property_links); - if (zend_hash_num_elements(span_links) > 0) { - // Save the current exception, if any, and clear it for php_json_encode_serializable_object not to fail - // and zend_call_function to actually call the jsonSerialize method - // Restored after span links are serialized - zend_object* current_exception = EG(exception); - EG(exception) = NULL; - - smart_str buf = {0}; - _dd_serialize_json(span_links, &buf, 0); - ddog_add_str_span_meta_zstr(rust_span, "_dd.span_links", buf.s); - - smart_str_free(&buf); - - // Restore the exception - EG(exception) = current_exception; - } - - zend_array *span_events = ddtrace_property_array(&span->property_events); - if (zend_hash_num_elements(span_events) > 0) { - // Save the current exception, if any, and clear it for php_json_encode_serializable_object not to fail - // and zend_call_function to actually call the jsonSerialize method - // Restored after span events are serialized - zend_object* current_exception = EG(exception); - EG(exception) = NULL; - - smart_str buf = {0}; - _dd_serialize_json(span_events, &buf, 0); - ddog_add_str_span_meta_zstr(rust_span, "events", buf.s); - - smart_str_free(&buf); - - // Restore the exception - EG(exception) = current_exception; - } - - zval *git_metadata = &span->root->property_git_metadata; - if (git_metadata && Z_TYPE_P(git_metadata) == IS_OBJECT) { - ddtrace_git_metadata *metadata = (ddtrace_git_metadata *)Z_OBJ_P(git_metadata); - if (is_root_span) { - if (Z_TYPE(metadata->property_commit) == IS_STRING) { - zend_string *commit_sha = ddtrace_convert_to_str(&metadata->property_commit); - ddog_add_str_span_meta_zstr(rust_span, "_dd.git.commit.sha", commit_sha); - zend_string_release(commit_sha); - } - if (Z_TYPE(metadata->property_repository) == IS_STRING) { - zend_string *repository_url = ddtrace_convert_to_str(&metadata->property_repository); - ddog_add_str_span_meta_zstr(rust_span, "_dd.git.repository_url", repository_url); - zend_string_release(repository_url); - - } - } - } - - if (get_DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED()) { // opt-in - zend_array *peer_service_sources = ddtrace_property_array(&span->property_peer_service_sources); - zval *peer_service = zend_hash_str_find(Z_ARRVAL_P(meta), ZEND_STRL("peer.service")); - if (peer_service && Z_TYPE_P(peer_service) == IS_STRING) { // peer.service is already set by the user, honor it - ddog_add_str_span_meta_str(rust_span, "_dd.peer.service.source", "peer.service"); - dd_set_mapped_peer_service(rust_span, Z_STR_P(peer_service)); - } else if (zend_hash_num_elements(peer_service_sources) > 0) { - zval *tag; - ZEND_HASH_FOREACH_VAL(peer_service_sources, tag) { - if (Z_TYPE_P(tag) == IS_STRING) { // Use the first tag that is found in the span, if any - zval *peer_service = zend_hash_find(Z_ARRVAL_P(meta), Z_STR_P(tag)); - if (peer_service && Z_TYPE_P(peer_service) == IS_STRING) { - ddog_add_str_span_meta_zstr(rust_span, "_dd.peer.service.source", Z_STR_P(tag)); - - zend_string *peer = zval_get_string(peer_service); - if (!dd_set_mapped_peer_service(rust_span, peer)) { - ddog_add_str_span_meta_zstr(rust_span, "peer.service", peer); - } - zend_string_release(peer); - break; - } - } - } ZEND_HASH_FOREACH_END(); - } - } - - if (ddtrace_span_is_entrypoint_root(span) || is_inferred_span) { - int status = SG(sapi_headers).http_response_code; - if (ddtrace_active_sapi == DATADOG_PHP_SAPI_FRANKENPHP && !status) { - status = has_exception ? 500 : 200; - } - struct iter *headers = dd_iterate_sapi_headers(); - dd_set_entrypoint_root_rust_span_props_end(rust_span, status, headers, ignore_error); - efree(headers); - } - - zval *origin = &span->root->property_origin; - if (Z_TYPE_P(origin) > IS_NULL && (Z_TYPE_P(origin) != IS_STRING || Z_STRLEN_P(origin))) { - ddog_add_str_span_meta_zstr(rust_span, "_dd.origin", Z_STR_P(origin)); - } - - bool error = ddog_has_span_meta_str(rust_span, "error.message") || - ddog_has_span_meta_str(rust_span, "error.type"); - if (error && !ignore_error) { - ddog_set_span_error(rust_span, 1); - - if (!ignore_error && Z_TYPE(span->property_exception) == IS_OBJECT) { - zend_object *exception = Z_OBJ(span->property_exception); - ddtrace_span_data *current = span; - bool should_track; - - do { - should_track = should_track_error(exception, current); - if (!should_track) { - ddog_add_str_span_meta_str(rust_span, "track_error", "false"); - break; - } - current = current->parent ? SPANDATA(current->parent) : NULL; - } while (current); - } - } - - if (is_inferred_span || (span->root->trace_id.high && is_root_span && !inferred_span)) { - zend_string *trace_id_str = zend_strpprintf(0, "%" PRIx64, span->root->trace_id.high); - ddog_add_str_span_meta_zstr(rust_span, "_dd.p.tid", trace_id_str); - zend_string_release(trace_id_str); - } - - // Add _dd.base_service if service name differs from mapped root service name - zval prop_service_as_string; - ddtrace_convert_to_string(&prop_service_as_string, &span->property_service); - zval prop_root_service_as_string; - ddtrace_convert_to_string(&prop_root_service_as_string, &span->root->property_service); - - zend_array *service_mappings = get_DD_SERVICE_MAPPING(); - zval *new_root_name = zend_hash_find(service_mappings, Z_STR(prop_root_service_as_string)); - if (new_root_name) { - zend_string_release(Z_STR(prop_root_service_as_string)); - ZVAL_COPY(&prop_root_service_as_string, new_root_name); - } - - if (!is_inferred_span && !zend_string_equals_ci(Z_STR(prop_service_as_string), Z_STR(prop_root_service_as_string))) { - ddog_add_str_span_meta_zstr(rust_span, "_dd.base_service", Z_STR_P(&prop_root_service_as_string)); - } - - zend_string_release(Z_STR(prop_root_service_as_string)); - zend_string_release(Z_STR(prop_service_as_string)); -} static HashTable dd_span_sampling_limiters; #if ZTS @@ -1546,190 +1334,82 @@ void transfer_metrics_data(ddog_SpanBytes *source, ddog_SpanBytes *destination, } ddog_SpanBytes *ddtrace_serialize_span_to_rust_span(ddtrace_span_data *span, ddog_TraceBytes *trace) { - bool is_first_span = ddog_get_trace_size(trace) == 0; - ddog_SpanBytes *rust_span = ddog_trace_new_span(trace); - - bool is_root_span = span->std.ce == ddtrace_ce_root_span_data; - bool is_inferred_span = span->std.ce == ddtrace_ce_inferred_span_data; - - ddtrace_span_data *inferred_span = NULL; - if (is_root_span) { - ddtrace_root_span_data *root_span = ROOTSPANDATA(&span->std); - inferred_span = ddtrace_get_inferred_span(root_span); - if (inferred_span) { - inferred_span->root = root_span; - } - } - - ddog_set_span_trace_id(rust_span, span->root->trace_id.low); - ddog_set_span_id(rust_span, span->span_id); - - if (inferred_span) { - ddog_set_span_parent_id(rust_span, inferred_span->span_id); - } else if (span->parent) { // handle dropped spans - ddtrace_span_data *parent = SPANDATA(span->parent); - // Ensure the parent id is the root span if everything else was dropped - while (parent->parent && ddtrace_span_is_dropped(parent)) { - parent = SPANDATA(parent->parent); - } - if (parent) { - ddog_set_span_parent_id(rust_span, parent->span_id); - } - } else if (is_root_span) { - ddog_set_span_parent_id(rust_span, ROOTSPANDATA(&span->std)->parent_id); - } else if (is_inferred_span) { - ddog_set_span_parent_id(rust_span, span->root->parent_id); - } - - ddog_set_span_start(rust_span, span->start); - ddog_set_span_duration(rust_span, span->duration); - zend_array *meta = ddtrace_property_array(&span->property_meta); zend_array *metrics = ddtrace_property_array(&span->property_metrics); + // Remap OTel's status code (metric, http.status_code) to DD's status code (meta, http.status_code) + // OTel HTTP semantic conventions < 1.21.0 + zval *http_status_code = zend_hash_str_find(metrics, ZEND_STRL("http.status_code")); + if (http_status_code) { + zval status_code_as_string; + ddtrace_convert_to_string(&status_code_as_string, http_status_code); + zend_hash_str_update(meta, ZEND_STRL("http.status_code"), &status_code_as_string); + zend_hash_str_del(metrics, ZEND_STRL("http.status_code")); + } + // Remap OTel's status code (metric, http.response.status_code) to DD's status code (meta, http.status_code) // OTel HTTP semantic conventions >= 1.21.0 zval *http_response_status_code = zend_hash_str_find(metrics, ZEND_STRL("http.response.status_code")); if (http_response_status_code) { zval status_code_as_string; ddtrace_convert_to_string(&status_code_as_string, http_response_status_code); - ddog_add_str_span_meta_zstr(rust_span, "http.status_code", Z_STR_P(&status_code_as_string)); + zend_hash_str_update(meta, ZEND_STRL("http.status_code"), &status_code_as_string); zend_hash_str_del(metrics, ZEND_STRL("http.response.status_code")); - zval_ptr_dtor(&status_code_as_string); } - // Remap OTel's status code (metric, http.status_code) to DD's status code (meta, http.status_code) - // OTel HTTP semantic conventions < 1.21.0 - zval *http_status_code = zend_hash_str_find(metrics, ZEND_STRL("http.status_code")); - if (http_status_code) { - if (!ddog_has_span_meta_str(rust_span, "http.status_code")) { - zval status_code_as_string; - ddtrace_convert_to_string(&status_code_as_string, http_status_code); - ddog_add_str_span_meta_zstr(rust_span, "http.status_code", Z_STR_P(&status_code_as_string)); - zval_ptr_dtor(&status_code_as_string); - } - zend_hash_str_del(metrics, ZEND_STRL("http.status_code")); - } + ddtrace_span_precomputed pre; + ddtrace_precompute_span(span, &pre); - if (is_first_span) { - zend_string *process_tags = ddtrace_process_tags_get_serialized(); - if (ZSTR_LEN(process_tags)) { - ddog_add_str_span_meta_zstr(rust_span, "_dd.tags.process", process_tags); + // Trace-level filter: when stats computation is enabled, drop the span from the + // entire pipeline (trace sending + stats) if its trace is filtered. + if (ddtrace_sidecar && get_DD_TRACE_STATS_COMPUTATION_ENABLED()) { + if (DDTRACE_G(agent_info_reader)) { + ddog_apply_agent_info_concentrator_config(DDTRACE_G(agent_info_reader)); } - } - - // SpanData::$name defaults to fully qualified called name (set at span close) - zval *operation_name = zend_hash_str_find(meta, ZEND_STRL("operation.name")); - zval *prop_name = &span->property_name; - - if (operation_name) { - zend_string *lcname = zend_string_tolower(Z_STR_P(operation_name)); - ddog_set_span_name_zstr(rust_span, lcname); - zend_string_release(lcname); - } else { - ZVAL_DEREF(prop_name); - if (Z_TYPE_P(prop_name) > IS_NULL) { - zval prop_name_as_string; - ddtrace_convert_to_string(&prop_name_as_string, prop_name); - ddog_set_span_name_zstr(rust_span, Z_STR_P(&prop_name_as_string)); - zval_ptr_dtor(&prop_name_as_string); + if (!ddtrace_trace_passes_filter(span)) { + ddtrace_free_span_precomputed(&pre); + return NULL; } } - // SpanData::$resource defaults to SpanData::$name - zval *resource_name = zend_hash_str_find(meta, ZEND_STRL("resource.name")); - zval *prop_resource = resource_name ? resource_name : &span->property_resource; - ZVAL_DEREF(prop_resource); - zval prop_resource_as_string; - ZVAL_UNDEF(&prop_resource_as_string); - - if (Z_TYPE_P(prop_resource) > IS_FALSE && (Z_TYPE_P(prop_resource) != IS_STRING || Z_STRLEN_P(prop_resource) > 0)) { - ddtrace_convert_to_string(&prop_resource_as_string, prop_resource); - } else if (Z_TYPE_P(prop_name) > IS_NULL) { - ZVAL_COPY(&prop_resource_as_string, prop_name); - } - - if (Z_TYPE(prop_resource_as_string) == IS_STRING) { - ddog_set_span_resource_zstr(rust_span, Z_STR_P(&prop_resource_as_string)); - } - - if (resource_name) { - zend_hash_str_del(meta, ZEND_STRL("resource.name")); - } - - // TODO: SpanData::$service defaults to parent SpanData::$service or DD_SERVICE if root span - zval *service_name = zend_hash_str_find(meta, ZEND_STRL("service.name")); - zval *prop_service = &span->property_service; - ZVAL_DEREF(prop_service); - zval prop_service_as_string; - ZVAL_UNDEF(&prop_service_as_string); - - if (service_name) { - ddtrace_convert_to_string(&prop_service_as_string, service_name); - } else if (Z_TYPE_P(prop_service) > IS_NULL) { - ddtrace_convert_to_string(&prop_service_as_string, prop_service); - } + bool is_root_span = span->std.ce == ddtrace_ce_root_span_data; + bool is_inferred_span = span->std.ce == ddtrace_ce_inferred_span_data; - if (Z_TYPE(prop_service_as_string) == IS_STRING) { - zend_array *service_mappings = get_DD_SERVICE_MAPPING(); - zval *new_name = zend_hash_find(service_mappings, Z_STR(prop_service_as_string)); - if (new_name) { - zend_string_release(Z_STR(prop_service_as_string)); - ZVAL_COPY(&prop_service_as_string, new_name); + if (ddtrace_span_is_entrypoint_root(span) || is_inferred_span) { + int status = SG(sapi_headers).http_response_code; + if (ddtrace_active_sapi == DATADOG_PHP_SAPI_FRANKENPHP && !status) { + status = pre.has_exception ? 500 : 200; } - - ddog_set_span_service_zstr(rust_span, Z_STR_P(&prop_service_as_string)); - zval_ptr_dtor(&prop_service_as_string); - } - - if (service_name) { - zend_hash_str_del(meta, ZEND_STRL("service.name")); - } - - // SpanData::$type is optional and defaults to 'custom' at the Agent level - zval *span_type = zend_hash_str_find(meta, ZEND_STRL("span.type")); - zval *prop_type = span_type ? span_type : &span->property_type; - ZVAL_DEREF(prop_type); - zval prop_type_as_string; - ZVAL_UNDEF(&prop_type_as_string); - - if (Z_TYPE_P(prop_type) > IS_NULL) { - ddtrace_convert_to_string(&prop_type_as_string, prop_type); - ddog_set_span_type_zstr(rust_span, Z_STR_P(&prop_type_as_string)); - } - - if (span_type) { - zend_hash_str_del(meta, ZEND_STRL("span.type")); + dd_set_http_error(meta, status, pre.ignore_error); } - zval *analytics_event = zend_hash_str_find(meta, ZEND_STRL("analytics.event")); - if (analytics_event) { - if (Z_TYPE_P(analytics_event) == IS_STRING) { - double parsed_analytics_event = strconv_parse_bool(Z_STR_P(analytics_event)); - if (parsed_analytics_event >= 0) { - ddog_add_span_metrics_str(rust_span, "_dd1.sr.eausr", parsed_analytics_event); - } - } else { - ddog_add_span_metrics_str(rust_span, "_dd1.sr.eausr", zval_get_double(analytics_event)); + ddtrace_span_data *inferred_span = NULL; + if (is_root_span) { + ddtrace_root_span_data *root_span = ROOTSPANDATA(&span->std); + inferred_span = ddtrace_get_inferred_span(root_span); + if (inferred_span) { + inferred_span->root = root_span; } - zend_hash_str_del(meta, ZEND_STRL("analytics.event")); } // Notify profiling for Endpoint Profiling. - if (profiling_notify_trace_finished && ddtrace_span_is_entrypoint_root(span) && Z_TYPE(prop_resource_as_string) == IS_STRING) { + if (profiling_notify_trace_finished && ddtrace_span_is_entrypoint_root(span) && pre.resource) { zai_str type = ZAI_STRL("custom"); - if (Z_TYPE(prop_type_as_string) == IS_STRING) { - type = (zai_str) ZAI_STR_FROM_ZSTR(Z_STR(prop_type_as_string)); + if (pre.type) { + type = (zai_str) ZAI_STR_FROM_ZSTR(pre.type); } - zai_str resource = (zai_str)ZAI_STR_FROM_ZSTR(Z_STR(prop_resource_as_string)); + zai_str resource = (zai_str)ZAI_STR_FROM_ZSTR(pre.resource); LOG(DEBUG, "Notifying profiler of finished local root span."); profiling_notify_trace_finished(span->span_id, type, resource); } - zval_ptr_dtor(&prop_type_as_string); - zval_ptr_dtor(&prop_resource_as_string); + // Determine sampling before allocating the rust span to avoid unnecessary work. + bool span_sampling_applied = false; + double span_sampling_rate = 1.0; + double span_sampling_max_per_second = 0.0; + bool span_sampling_has_max = false; - if (zend_hash_num_elements(get_DD_SPAN_SAMPLING_RULES()) && ddtrace_fetch_priority_sampling_from_span(span->root) <= 0) { + if (!is_inferred_span && zend_hash_num_elements(get_DD_SPAN_SAMPLING_RULES()) && ddtrace_fetch_priority_sampling_from_span(span->root) <= 0) { zval *rule; ZEND_HASH_FOREACH_VAL(get_DD_SPAN_SAMPLING_RULES(), rule) { if (Z_TYPE_P(rule) != IS_ARRAY) { @@ -1740,16 +1420,16 @@ ddog_SpanBytes *ddtrace_serialize_span_to_rust_span(ddtrace_span_data *span, ddo zval *rule_service; if ((rule_service = zend_hash_str_find(Z_ARR_P(rule), ZEND_STRL("service")))) { - if (Z_TYPE_P(prop_service) > IS_NULL) { - rule_matches &= dd_glob_rule_matches(rule_service, Z_STR(prop_service_as_string)); + if (pre.service) { + rule_matches &= dd_glob_rule_matches(rule_service, pre.service); } else { - rule_matches &= false; + rule_matches = false; } } zval *rule_name; if ((rule_name = zend_hash_str_find(Z_ARR_P(rule), ZEND_STRL("name")))) { - if (Z_TYPE_P(prop_name) > IS_NULL) { - rule_matches &= dd_glob_rule_matches(rule_name, Z_STR_P(prop_name)); + if (pre.name) { + rule_matches &= dd_glob_rule_matches(rule_name, pre.name); } else { rule_matches = false; } @@ -1845,23 +1525,263 @@ ddog_SpanBytes *ddtrace_serialize_span_to_rust_span(ddtrace_span_data *span, ddo } } - ddog_add_span_metrics_str(rust_span, "_dd.span_sampling.mechanism", 8.0); - ddog_add_span_metrics_str(rust_span, "_dd.span_sampling.rule_rate", sample_rate); + span_sampling_rate = sample_rate; + span_sampling_has_max = max_per_second_zv != NULL; + span_sampling_max_per_second = max_per_second; + span_sampling_applied = true; + break; + } + ZEND_HASH_FOREACH_END(); + - if (max_per_second_zv) { - ddog_add_span_metrics_str(rust_span, "_dd.span_sampling.max_per_second", max_per_second); + if (!span_sampling_applied && ddtrace_sidecar && get_DD_TRACE_STATS_COMPUTATION_ENABLED() && ddog_is_agent_info_ready()) { + if (inferred_span) { + // Inferred span won't be serialized, so feed it to the concentrator here. + ddtrace_span_precomputed inferred_pre; + ddtrace_precompute_span(inferred_span, &inferred_pre); + ddtrace_feed_span_to_concentrator(inferred_span, &inferred_pre); + ddtrace_free_span_precomputed(&inferred_pre); } + ddtrace_feed_span_to_concentrator(span, &pre); + ddtrace_free_span_precomputed(&pre); + return NULL; + } + } - break; + bool is_first_span = ddog_get_trace_size(trace) == 0; + ddog_SpanBytes *rust_span = ddog_trace_new_span(trace); + + ddog_set_span_trace_id(rust_span, span->root->trace_id.low); + ddog_set_span_id(rust_span, span->span_id); + + if (inferred_span) { + ddog_set_span_parent_id(rust_span, inferred_span->span_id); + } else if (span->parent) { // handle dropped spans + ddtrace_span_data *parent = SPANDATA(span->parent); + // Ensure the parent id is the root span if everything else was dropped + while (parent->parent && ddtrace_span_is_dropped(parent)) { + parent = SPANDATA(parent->parent); } - ZEND_HASH_FOREACH_END(); + if (parent) { + ddog_set_span_parent_id(rust_span, parent->span_id); + } + } else if (is_root_span) { + ddog_set_span_parent_id(rust_span, ROOTSPANDATA(&span->std)->parent_id); + } else if (is_inferred_span) { + ddog_set_span_parent_id(rust_span, span->root->parent_id); } - if (operation_name) { + ddog_set_span_start(rust_span, span->start); + ddog_set_span_duration(rust_span, span->duration); + + if (is_first_span) { + zend_string *process_tags = ddtrace_process_tags_get_serialized(); + if (ZSTR_LEN(process_tags)) { + ddog_add_str_span_meta_zstr(rust_span, "_dd.tags.process", process_tags); + } + } + + // SpanData::$name defaults to fully qualified called name (set at span close) + if (pre.name) { + ddog_set_span_name_zstr(rust_span, pre.name); + } + if (pre.name_from_meta) { zend_hash_str_del(meta, ZEND_STRL("operation.name")); } - _serialize_meta(rust_span, span, Z_TYPE_P(prop_service) > IS_NULL ? Z_STR(prop_service_as_string) : ZSTR_EMPTY_ALLOC()); + // SpanData::$resource defaults to SpanData::$name + if (pre.resource) { + ddog_set_span_resource_zstr(rust_span, pre.resource); + } + if (pre.resource_from_meta) { + zend_hash_str_del(meta, ZEND_STRL("resource.name")); + } + + // TODO: SpanData::$service defaults to parent SpanData::$service or DD_SERVICE if root span + if (pre.service) { + ddog_set_span_service_zstr(rust_span, pre.service); + } + if (pre.service_from_meta) { + zend_hash_str_del(meta, ZEND_STRL("service.name")); + } + + // SpanData::$type is optional and defaults to 'custom' at the Agent level + if (pre.type) { + ddog_set_span_type_zstr(rust_span, pre.type); + } + if (pre.type_from_meta) { + zend_hash_str_del(meta, ZEND_STRL("span.type")); + } + + zval *analytics_event = zend_hash_str_find(meta, ZEND_STRL("analytics.event")); + if (analytics_event) { + if (Z_TYPE_P(analytics_event) == IS_STRING) { + double parsed_analytics_event = strconv_parse_bool(Z_STR_P(analytics_event)); + if (parsed_analytics_event >= 0) { + ddog_add_span_metrics_str(rust_span, "_dd1.sr.eausr", parsed_analytics_event); + } + } else { + ddog_add_span_metrics_str(rust_span, "_dd1.sr.eausr", zval_get_double(analytics_event)); + } + zend_hash_str_del(meta, ZEND_STRL("analytics.event")); + } + + if (span_sampling_applied) { + ddog_add_span_metrics_str(rust_span, "_dd.span_sampling.mechanism", 8.0); + ddog_add_span_metrics_str(rust_span, "_dd.span_sampling.rule_rate", span_sampling_rate); + if (span_sampling_has_max) { + ddog_add_span_metrics_str(rust_span, "_dd.span_sampling.max_per_second", span_sampling_max_per_second); + } + } + + if (meta) { + zend_string *meta_str_key; + zval *orig_val; + ZEND_HASH_FOREACH_STR_KEY_VAL_IND(meta, meta_str_key, orig_val) { + if (meta_str_key) { + if (!ddog_has_span_meta_zstr(rust_span, meta_str_key)) { + dd_serialize_array_meta_recursively(rust_span, meta_str_key, orig_val); + } + } + } + ZEND_HASH_FOREACH_END(); + } + + // Avoid adding it twice to meta + if (!pre.env_deprecated && pre.env) { + ddog_add_str_span_meta_zstr(rust_span, "env", pre.env); + } + if (!pre.version_deprecated && pre.version) { + ddog_add_str_span_meta_zstr(rust_span, "version", pre.version); + } + + zval *exception_zv = &span->property_exception; + if (pre.has_exception && !pre.ignore_error) { + enum dd_exception exception_type = DD_EXCEPTION_THROWN; + if (is_root_span) { + exception_type = Z_PROP_FLAG_P(exception_zv) == 2 ? DD_EXCEPTION_CAUGHT : DD_EXCEPTION_UNCAUGHT; + } + ddtrace_exception_to_meta(Z_OBJ_P(exception_zv), pre.service ? pre.service : ZSTR_EMPTY_ALLOC(), span->start, rust_span, exception_type); + } + + zend_array *span_links = ddtrace_property_array(&span->property_links); + if (zend_hash_num_elements(span_links) > 0) { + zend_object *current_exception = EG(exception); + EG(exception) = NULL; + smart_str buf = {0}; + _dd_serialize_json(span_links, &buf, 0); + ddog_add_str_span_meta_zstr(rust_span, "_dd.span_links", buf.s); + smart_str_free(&buf); + EG(exception) = current_exception; + } + + zend_array *span_events = ddtrace_property_array(&span->property_events); + if (zend_hash_num_elements(span_events) > 0) { + zend_object *current_exception = EG(exception); + EG(exception) = NULL; + smart_str buf = {0}; + _dd_serialize_json(span_events, &buf, 0); + ddog_add_str_span_meta_zstr(rust_span, "events", buf.s); + smart_str_free(&buf); + EG(exception) = current_exception; + } + + zval *git_metadata = &span->root->property_git_metadata; + if (git_metadata && Z_TYPE_P(git_metadata) == IS_OBJECT) { + ddtrace_git_metadata *metadata = (ddtrace_git_metadata *)Z_OBJ_P(git_metadata); + if (is_root_span) { + if (Z_TYPE(metadata->property_commit) == IS_STRING) { + zend_string *commit_sha = ddtrace_convert_to_str(&metadata->property_commit); + ddog_add_str_span_meta_zstr(rust_span, "_dd.git.commit.sha", commit_sha); + zend_string_release(commit_sha); + } + if (Z_TYPE(metadata->property_repository) == IS_STRING) { + zend_string *repository_url = ddtrace_convert_to_str(&metadata->property_repository); + ddog_add_str_span_meta_zstr(rust_span, "_dd.git.repository_url", repository_url); + zend_string_release(repository_url); + } + } + } + + if (get_DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED()) { + zend_array *peer_service_sources = ddtrace_property_array(&span->property_peer_service_sources); + zval *peer_service_tag = meta ? zend_hash_str_find(meta, ZEND_STRL("peer.service")) : NULL; + if (peer_service_tag && Z_TYPE_P(peer_service_tag) == IS_STRING) { + ddog_add_str_span_meta_str(rust_span, "_dd.peer.service.source", "peer.service"); + dd_set_mapped_peer_service(rust_span, Z_STR_P(peer_service_tag)); + } else if (zend_hash_num_elements(peer_service_sources) > 0) { + zval *tag; + ZEND_HASH_FOREACH_VAL(peer_service_sources, tag) { + if (Z_TYPE_P(tag) == IS_STRING) { + zval *found_peer_service = meta ? zend_hash_find(meta, Z_STR_P(tag)) : NULL; + if (found_peer_service && Z_TYPE_P(found_peer_service) == IS_STRING) { + ddog_add_str_span_meta_zstr(rust_span, "_dd.peer.service.source", Z_STR_P(tag)); + zend_string *peer = zval_get_string(found_peer_service); + if (!dd_set_mapped_peer_service(rust_span, peer)) { + ddog_add_str_span_meta_zstr(rust_span, "peer.service", peer); + } + zend_string_release(peer); + break; + } + } + } ZEND_HASH_FOREACH_END(); + } + } + + if (ddtrace_span_is_entrypoint_root(span) || is_inferred_span) { + struct iter *headers = dd_iterate_sapi_headers(); + dd_set_entrypoint_root_rust_span_props_end(rust_span, headers); + efree(headers); + } + + zval *origin = &span->root->property_origin; + if (Z_TYPE_P(origin) > IS_NULL && (Z_TYPE_P(origin) != IS_STRING || Z_STRLEN_P(origin))) { + ddog_add_str_span_meta_zstr(rust_span, "_dd.origin", Z_STR_P(origin)); + } + + bool error = dd_compute_span_is_error(&pre); + if (error) { + ddog_set_span_error(rust_span, 1); + if (Z_TYPE(span->property_exception) == IS_OBJECT) { + zend_object *exception = Z_OBJ(span->property_exception); + ddtrace_span_data *current = span; + bool should_track; + do { + should_track = should_track_error(exception, current); + if (!should_track) { + ddog_add_str_span_meta_str(rust_span, "track_error", "false"); + break; + } + current = current->parent ? SPANDATA(current->parent) : NULL; + } while (current); + } + } + + if (is_inferred_span || (span->root->trace_id.high && is_root_span && !inferred_span)) { + zend_string *trace_id_str = zend_strpprintf(0, "%" PRIx64, span->root->trace_id.high); + ddog_add_str_span_meta_zstr(rust_span, "_dd.p.tid", trace_id_str); + zend_string_release(trace_id_str); + } + + // Add _dd.base_service if service name differs from mapped root service name. + zval prop_service_as_string; + ddtrace_convert_to_string(&prop_service_as_string, &span->property_service); + zval prop_root_service_as_string; + ddtrace_convert_to_string(&prop_root_service_as_string, &span->root->property_service); + zval *new_root_name = zend_hash_find(get_DD_SERVICE_MAPPING(), Z_STR(prop_root_service_as_string)); + if (new_root_name) { + zend_string_release(Z_STR(prop_root_service_as_string)); + ZVAL_COPY(&prop_root_service_as_string, new_root_name); + } + if (!is_inferred_span && !zend_string_equals_ci(Z_STR(prop_service_as_string), Z_STR(prop_root_service_as_string))) { + ddog_add_str_span_meta_zstr(rust_span, "_dd.base_service", Z_STR_P(&prop_root_service_as_string)); + } + zend_string_release(Z_STR(prop_root_service_as_string)); + zend_string_release(Z_STR(prop_service_as_string)); + + if (ddtrace_sidecar && get_DD_TRACE_STATS_COMPUTATION_ENABLED() && ddog_is_agent_info_ready()) { + ddtrace_feed_span_to_concentrator(span, &pre); + } zend_string *str_key; zval *val; @@ -1884,6 +1804,24 @@ ddog_SpanBytes *ddtrace_serialize_span_to_rust_span(ddtrace_span_data *span, ddo } } + if (ddtrace_sidecar && get_DD_TRACE_STATS_COMPUTATION_ENABLED() && !is_inferred_span) { + bool is_top_level_span = !span->parent; + if (span->parent) { + zval *parent_service = &SPANDATA(span->parent)->property_service; + zval *span_service = &span->property_service; + ZVAL_DEREF(parent_service); + ZVAL_DEREF(span_service); + if (Z_TYPE_P(span_service) == IS_STRING && Z_TYPE_P(parent_service) == IS_STRING) { + is_top_level_span = !zend_string_equals(Z_STR_P(span_service), Z_STR_P(parent_service)); + } else if (Z_TYPE_P(span_service) != Z_TYPE_P(parent_service)) { + is_top_level_span = true; + } + } + if (is_top_level_span) { + ddog_add_span_metrics_str(rust_span, "_dd.top_level", 1); + } + } + if (ddtrace_span_is_entrypoint_root(span)) { if (get_DD_TRACE_MEASURE_COMPILE_TIME()) { ddog_add_span_metrics_str(rust_span, "php.compilation.total_time_ms", ddtrace_compile_time_get() / 1000.); @@ -1928,6 +1866,9 @@ ddog_SpanBytes *ddtrace_serialize_span_to_rust_span(ddtrace_span_data *span, ddo } ZEND_HASH_FOREACH_END(); + ddog_del_span_meta_str(rust_span, "error.ignored"); + + ddtrace_free_span_precomputed(&pre); return rust_span; } diff --git a/ext/sidecar.c b/ext/sidecar.c index 2246e0c81e7..eb4dc299b59 100644 --- a/ext/sidecar.c +++ b/ext/sidecar.c @@ -118,7 +118,9 @@ static void dd_sidecar_post_connect(ddog_SidecarTransport **transport, bool is_f DDTRACE_REMOTE_CONFIG_CAPABILITIES.len, get_global_DD_REMOTE_CONFIG_ENABLED(), is_fork, - process_tags + process_tags, + dd_zend_string_to_CharSlice(get_global_DD_HOSTNAME()), + dd_zend_string_to_CharSlice(get_global_DD_SERVICE()) ); if (get_global_DD_INSTRUMENTATION_TELEMETRY_ENABLED()) { diff --git a/ext/span_stats.c b/ext/span_stats.c new file mode 100644 index 00000000000..0344992d41c --- /dev/null +++ b/ext/span_stats.c @@ -0,0 +1,403 @@ +// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +#include "span_stats.h" + +#include // NAN +#include +#include +#include + +#include +#include + +#include "compat_string.h" +#include "configuration.h" +#include "ddtrace.h" +#include "sidecar.h" +#include "span.h" + +ZEND_EXTERN_MODULE_GLOBALS(ddtrace); + +// gRPC status-code meta keys in the same order as PHP_GRPC_KEY_COUNT / grpc_meta[] in stats.rs. +static const char *const GRPC_META_KEYS[] = { + "rpc.grpc.status_code", + "grpc.code", + "rpc.grpc.status.code", + "grpc.status.code", +}; +static const size_t GRPC_META_KEY_LENS[] = { + sizeof("rpc.grpc.status_code") - 1, + sizeof("grpc.code") - 1, + sizeof("rpc.grpc.status.code") - 1, + sizeof("grpc.status.code") - 1, +}; + +// Maximum number of peer tags we handle per span (a hard cap to bound stack usage). +#define DDTRACE_MAX_PEER_TAGS 32 + +void ddtrace_precompute_span(ddtrace_span_data *span, ddtrace_span_precomputed *pre) { + pre->meta = ddtrace_property_array(&span->property_meta); + pre->metrics = ddtrace_property_array(&span->property_metrics); + + // Service: meta["service.name"] override then span property, then apply DD_SERVICE_MAPPING. + zval *service_name_meta = pre->meta ? zend_hash_str_find(pre->meta, ZEND_STRL("service.name")) : NULL; + pre->service_from_meta = (service_name_meta != NULL); + pre->service = NULL; + if (service_name_meta) { + pre->service = ddtrace_convert_to_str(service_name_meta); + } else { + zval *prop_service = &span->property_service; + ZVAL_DEREF(prop_service); + if (Z_TYPE_P(prop_service) > IS_NULL) { + pre->service = ddtrace_convert_to_str(prop_service); + } + } + if (pre->service) { + zval *mapped = zend_hash_find(get_DD_SERVICE_MAPPING(), pre->service); + if (mapped) { + zend_string_release(pre->service); + pre->service = zend_string_copy(Z_STR_P(mapped)); + } + } + + // Name: meta["operation.name"] (lowercased!) or span property. + zval *operation_name = pre->meta ? zend_hash_str_find(pre->meta, ZEND_STRL("operation.name")) : NULL; + pre->name = NULL; + if (operation_name && Z_TYPE_P(operation_name) == IS_STRING) { + pre->name = zend_string_tolower(Z_STR_P(operation_name)); + pre->name_from_meta = true; + } else { + zval *prop_name = &span->property_name; + ZVAL_DEREF(prop_name); + if (Z_TYPE_P(prop_name) > IS_NULL) { + pre->name = ddtrace_convert_to_str(prop_name); + } + pre->name_from_meta = false; + } + + // Resource: meta["resource.name"] or span property, falling back to name. + zval *resource_name = pre->meta ? zend_hash_str_find(pre->meta, ZEND_STRL("resource.name")) : NULL; + pre->resource_from_meta = (resource_name != NULL); + pre->resource = NULL; + if (resource_name) { + pre->resource = ddtrace_convert_to_str(resource_name); + } else { + zval *prop_resource = &span->property_resource; + ZVAL_DEREF(prop_resource); + if (Z_TYPE_P(prop_resource) > IS_FALSE && + (Z_TYPE_P(prop_resource) != IS_STRING || Z_STRLEN_P(prop_resource) > 0)) { + pre->resource = ddtrace_convert_to_str(prop_resource); + } + } + if (!pre->resource && pre->name) { + pre->resource = zend_string_copy(pre->name); + } + + // Type: meta["span.type"] or span property. + zval *span_type = pre->meta ? zend_hash_str_find(pre->meta, ZEND_STRL("span.type")) : NULL; + pre->type_from_meta = (span_type != NULL); + pre->type = NULL; + zval *prop_type = span_type ? span_type : &span->property_type; + ZVAL_DEREF(prop_type); + if (Z_TYPE_P(prop_type) > IS_NULL) { + pre->type = ddtrace_convert_to_str(prop_type); + } + + // Env: prefer deprecated meta["env"] (with a warning), else span property. + pre->env = NULL; + zval *meta_env = pre->meta ? zend_hash_str_find(pre->meta, ZEND_STRL("env")) : NULL; + if (meta_env) { + pre->env_deprecated = true; + LOG(DEPRECATED, "Using \"env\" in meta is deprecated. Instead specify the env property directly on the span."); + zend_string *str = ddtrace_convert_to_str(meta_env); + if (ZSTR_LEN(str) > 0) { + pre->env = str; + } else { + zend_string_release(str); + } + } else { + pre->env_deprecated = false; + zval *prop_env = &span->property_env; + ZVAL_DEREF(prop_env); + if (Z_TYPE_P(prop_env) > IS_NULL) { + zend_string *str = ddtrace_convert_to_str(prop_env); + if (ZSTR_LEN(str) > 0) { + pre->env = str; + } else { + zend_string_release(str); + } + } + } + + // Version: prefer deprecated meta["version"] (with a warning), else the span's own property. + pre->version = NULL; + zval *meta_version = pre->meta ? zend_hash_str_find(pre->meta, ZEND_STRL("version")) : NULL; + if (meta_version) { + pre->version_deprecated = true; + LOG(DEPRECATED, "Using \"version\" in meta is deprecated. Instead specify the version property directly on the span."); + zend_string *str = ddtrace_convert_to_str(meta_version); + if (ZSTR_LEN(str) > 0) { + pre->version = str; + } else { + zend_string_release(str); + } + } else { + pre->version_deprecated = false; + zval *prop_version = &span->property_version; + ZVAL_DEREF(prop_version); + if (Z_TYPE_P(prop_version) > IS_NULL) { + zend_string *str = ddtrace_convert_to_str(prop_version); + if (ZSTR_LEN(str) > 0) { + pre->version = str; + } else { + zend_string_release(str); + } + } + } + + // has_exception: used by dd_compute_span_is_error() for exception-based error detection. + zval *exception_zv = &span->property_exception; + pre->has_exception = Z_TYPE_P(exception_zv) == IS_OBJECT && + instanceof_function(Z_OBJCE_P(exception_zv), zend_ce_throwable); + + zval *error_ignored_zv = pre->meta ? zend_hash_str_find(pre->meta, ZEND_STRL("error.ignored")) : NULL; + pre->ignore_error = error_ignored_zv && zend_is_true(error_ignored_zv); + + // Stats eligibility fields — fetched once here to avoid duplicate lookups in the two + // call sites (ddtrace_span_concentrator_feed_cb and ddtrace_feed_span_to_concentrator). + pre->has_top_level = ddtrace_span_is_entrypoint_root(span); + zval *is_measured = pre->metrics ? zend_hash_str_find(pre->metrics, ZEND_STRL("_dd.measured")) : NULL; + pre->is_measured = is_measured && zval_get_double(is_measured) != 0.0; + pre->is_partial_snapshot = false; + zval *span_kind_zv = pre->meta ? zend_hash_str_find(pre->meta, ZEND_STRL("span.kind")) : NULL; + pre->span_kind = (span_kind_zv && Z_TYPE_P(span_kind_zv) == IS_STRING) ? Z_STR_P(span_kind_zv) : NULL; +} + +bool dd_compute_span_is_error(const ddtrace_span_precomputed *pre) { + if (pre->ignore_error) { + return false; + } + if (pre->meta && (zend_hash_str_find(pre->meta, ZEND_STRL("error.message")) != NULL || + zend_hash_str_find(pre->meta, ZEND_STRL("error.type")) != NULL)) { + return true; + } + return pre->has_exception; +} + +void ddtrace_free_span_precomputed(ddtrace_span_precomputed *pre) { + if (pre->service) { + zend_string_release(pre->service); + } + if (pre->name) { + zend_string_release(pre->name); + } + if (pre->resource) { + zend_string_release(pre->resource); + } + if (pre->type) { + zend_string_release(pre->type); + } + if (pre->env) { + zend_string_release(pre->env); + } + if (pre->version) { + zend_string_release(pre->version); + } +} + +typedef struct { + ddtrace_span_data *span; + const ddtrace_span_precomputed *pre; + // Set by the callback when the concentrator has no backing SHM (virtual concentrator). + // The caller should then forward ipc_stats to the sidecar via IPC. + bool needs_ipc; + ddog_PhpSpanStats ipc_stats; + // Owned storage for peer_tags when going through the IPC path. + // ipc_stats.peer_tags points into this array. + ddog_PhpPeerTag ipc_peer_tags[DDTRACE_MAX_PEER_TAGS]; +} ddtrace_concentrator_cb_data; + +// Build the stats fields for a span (all except peer_tags). +// All CharSlice fields in the returned struct borrow from PHP memory valid for this call. +// peer_tags_count = 0 and peer_tags = NULL in the returned struct; fill them in separately. +// span_kind_slice is passed in to avoid recomputing it when the caller already has it (e.g. +// for the eligibility check that precedes this call). +static ddog_PhpSpanStats ddtrace_build_span_stats_core( + ddtrace_span_data *span, const ddtrace_span_precomputed *pre, ddog_CharSlice span_kind_slice +) { + zend_array *meta = pre->meta; + zend_array *metrics = pre->metrics; + + ddog_CharSlice service_slice = dd_zend_string_to_CharSlice(pre->service); + ddog_CharSlice name_slice = dd_zend_string_to_CharSlice(pre->name); + ddog_CharSlice resource_slice = dd_zend_string_to_CharSlice(pre->resource); + ddog_CharSlice type_slice = dd_zend_string_to_CharSlice(pre->type); + + bool is_root_span = span->std.ce == ddtrace_ce_root_span_data; + bool is_trace_root = is_root_span && (span->root->parent_id == 0); + bool is_error = dd_compute_span_is_error(pre); + + // HTTP and gRPC fields only appear on service entry spans, which are always stats-eligible. + // They are fetched here (after the eligibility check) rather than in ddtrace_precompute_span. + zval *http_status_str_zv = meta ? zend_hash_str_find(meta, ZEND_STRL("http.status_code")) : NULL; + ddog_CharSlice http_status_str_slice = http_status_str_zv && Z_TYPE_P(http_status_str_zv) == IS_STRING + ? dd_zend_string_to_CharSlice(Z_STR_P(http_status_str_zv)) + : DDOG_CHARSLICE_C(""); + + zval *http_method_zv = meta ? zend_hash_str_find(meta, ZEND_STRL("http.method")) : NULL; + ddog_CharSlice http_method_slice = http_method_zv && Z_TYPE_P(http_method_zv) == IS_STRING + ? dd_zend_string_to_CharSlice(Z_STR_P(http_method_zv)) + : DDOG_CHARSLICE_C(""); + + zval *http_endpoint_zv = meta ? zend_hash_str_find(meta, ZEND_STRL("http.endpoint")) : NULL; + ddog_CharSlice http_endpoint_slice = http_endpoint_zv && Z_TYPE_P(http_endpoint_zv) == IS_STRING + ? dd_zend_string_to_CharSlice(Z_STR_P(http_endpoint_zv)) + : DDOG_CHARSLICE_C(""); + + zval *http_route_zv = meta ? zend_hash_str_find(meta, ZEND_STRL("http.route")) : NULL; + ddog_CharSlice http_route_slice = http_route_zv && Z_TYPE_P(http_route_zv) == IS_STRING + ? dd_zend_string_to_CharSlice(Z_STR_P(http_route_zv)) + : DDOG_CHARSLICE_C(""); + + zval *origin_zv = &span->root->property_origin; + ZVAL_DEREF(origin_zv); + ddog_CharSlice origin_slice = Z_TYPE_P(origin_zv) == IS_STRING && ZSTR_LEN(Z_STR_P(origin_zv)) > 0 + ? dd_zend_string_to_CharSlice(Z_STR_P(origin_zv)) + : DDOG_CHARSLICE_C(""); + + zval *service_source_zv = meta ? zend_hash_str_find(meta, ZEND_STRL("_dd.svc_src")) : NULL; + ddog_CharSlice service_source_slice = service_source_zv && Z_TYPE_P(service_source_zv) == IS_STRING + ? dd_zend_string_to_CharSlice(Z_STR_P(service_source_zv)) + : DDOG_CHARSLICE_C(""); + + ddog_CharSlice grpc_meta[ddog_PHP_GRPC_KEY_COUNT]; + double grpc_metrics[ddog_PHP_GRPC_KEY_COUNT]; + for (int i = 0; i < ddog_PHP_GRPC_KEY_COUNT; i++) { + grpc_meta[i] = DDOG_CHARSLICE_C(""); + grpc_metrics[i] = NAN; + if (meta) { + zval *gm = zend_hash_str_find(meta, GRPC_META_KEYS[i], GRPC_META_KEY_LENS[i]); + if (gm && Z_TYPE_P(gm) == IS_STRING) { + grpc_meta[i] = dd_zend_string_to_CharSlice(Z_STR_P(gm)); + } + } + if (metrics) { + zval *gv = zend_hash_str_find(metrics, GRPC_META_KEYS[i], GRPC_META_KEY_LENS[i]); + if (gv) { + grpc_metrics[i] = zval_get_double(gv); + } + } + } + + ddog_PhpSpanStats stats = { + .service = service_slice, + .resource = resource_slice, + .name = name_slice, + .type = type_slice, + + .start = (int64_t)span->start, + .duration = (int64_t)span->duration, + + .is_error = is_error, + .is_trace_root = is_trace_root, + .is_measured = pre->is_measured, + .has_top_level = pre->has_top_level, + .is_partial_snapshot = pre->is_partial_snapshot, + + .span_kind = span_kind_slice, + .http_status_code = http_status_str_slice, + .http_method = http_method_slice, + .http_endpoint = http_endpoint_slice, + .http_route = http_route_slice, + .origin = origin_slice, + .service_source = service_source_slice, + + .grpc_meta = {grpc_meta[0], grpc_meta[1], grpc_meta[2], grpc_meta[3]}, + .grpc_metrics = {grpc_metrics[0], grpc_metrics[1], grpc_metrics[2], grpc_metrics[3]}, + + .peer_tags_count = 0, + .peer_tags = NULL, + }; + return stats; +} + +static void ddtrace_span_concentrator_feed_cb(const ddog_SpanConcentrator *c, void *data_ptr) { + ddtrace_concentrator_cb_data *data = data_ptr; + ddtrace_span_data *span = data->span; + const ddtrace_span_precomputed *pre = data->pre; + + ddog_CharSlice span_kind_slice = dd_zend_string_to_CharSlice(pre->span_kind); + + if (!ddog_span_concentrator_is_eligible(c, pre->has_top_level, pre->is_measured, span_kind_slice, pre->is_partial_snapshot)) { + return; + } + + ddog_PhpSpanStats stats = ddtrace_build_span_stats_core(span, pre, span_kind_slice); + + // Peer tag extraction rules by span kind (spec: Peer Tags in Aggregation): + // client/producer/consumer → all configured peer tag keys + // server → no peer tags + // internal / no span.kind → only _dd.base_service if a service override is present + ddog_PhpPeerTag peer_tags[DDTRACE_MAX_PEER_TAGS]; + size_t actual_peer_tags = 0; + + if (pre->span_kind && (zend_string_equals_literal(pre->span_kind, "client") || zend_string_equals_literal(pre->span_kind, "producer") || zend_string_equals_literal(pre->span_kind, "consumer"))) { + size_t peer_tag_keys_count = 0; + const ddog_CharSlice *peer_tag_keys = ddog_span_concentrator_peer_tag_keys(c, &peer_tag_keys_count); + if (peer_tag_keys_count > DDTRACE_MAX_PEER_TAGS) { + peer_tag_keys_count = DDTRACE_MAX_PEER_TAGS; + } + if (peer_tag_keys_count > 0 && peer_tag_keys) { + for (size_t i = 0; i < peer_tag_keys_count; i++) { + const ddog_CharSlice *k = &peer_tag_keys[i]; + zval *val = zend_hash_str_find(pre->meta, k->ptr, k->len); + if (val && Z_TYPE_P(val) == IS_STRING) { + peer_tags[actual_peer_tags].key = *k; + peer_tags[actual_peer_tags].value = dd_zend_string_to_CharSlice(Z_STR_P(val)); + actual_peer_tags++; + } + } + } + } else if (!pre->span_kind || !zend_string_equals_literal(pre->span_kind, "server")) { + // internal or no span.kind: use _dd.base_service only if it marks a service override + static const ddog_CharSlice BASE_SERVICE_KEY = DDOG_CHARSLICE_C_BARE("_dd.base_service"); + zval *base_svc_zv = zend_hash_str_find(pre->meta, ZEND_STRL("_dd.base_service")); + if (base_svc_zv && Z_TYPE_P(base_svc_zv) == IS_STRING) { + peer_tags[0].key = BASE_SERVICE_KEY; + peer_tags[0].value = dd_zend_string_to_CharSlice(Z_STR_P(base_svc_zv)); + actual_peer_tags = 1; + } + } + // "server" spans have no special handling + stats.peer_tags_count = actual_peer_tags; + stats.peer_tags = actual_peer_tags > 0 ? peer_tags : NULL; + + if (ddog_span_concentrator_has_shm(c)) { + ddog_span_concentrator_add_php_span(c, &stats); + } else { + // No backing SHM yet; submit via sidecar IPC instead. + for (size_t i = 0; i < actual_peer_tags; i++) { + data->ipc_peer_tags[i] = peer_tags[i]; + } + data->ipc_stats = stats; + data->ipc_stats.peer_tags_count = actual_peer_tags; + data->ipc_stats.peer_tags = actual_peer_tags > 0 ? data->ipc_peer_tags : NULL; + data->needs_ipc = true; + } +} + +void ddtrace_feed_span_to_concentrator(ddtrace_span_data *span, const ddtrace_span_precomputed *pre) { + ddog_CharSlice env_slice = dd_zend_string_to_CharSlice(pre->env); + ddog_CharSlice version_slice = dd_zend_string_to_CharSlice(pre->version); + // Use the process-level DD_SERVICE as the concentrator key so all spans from this PHP + // process share one SHM concentrator regardless of per-request service overrides. + ddog_CharSlice service_slice = dd_zend_string_to_CharSlice(get_global_DD_SERVICE()); + + ddtrace_concentrator_cb_data data = { .span = span, .pre = pre, .needs_ipc = false }; + ddog_span_concentrator_with(env_slice, version_slice, service_slice, ddtrace_span_concentrator_feed_cb, &data); + + if (data.needs_ipc && ddtrace_sidecar) { + ddog_sidecar_add_php_span_to_concentrator(&ddtrace_sidecar, env_slice, version_slice, &data.ipc_stats); + } +} diff --git a/ext/span_stats.h b/ext/span_stats.h new file mode 100644 index 00000000000..b33e068672c --- /dev/null +++ b/ext/span_stats.h @@ -0,0 +1,74 @@ +#ifndef DD_SPAN_STATS_H +#define DD_SPAN_STATS_H + +#include "span.h" + +/** + * Fields precomputed by the serializer and shared with the span stats subsystem. + * + * The serializer computes service (with mapping), name, resource, and type once for its own + * purposes. This struct carries those results so the concentrator callback can use them directly + * instead of re-fetching from meta/metrics. + * + * Owned strings (service, name, resource, type) are zend_string* that must be released with + * ddtrace_free_span_precomputed() when no longer needed. NULL means the field is absent. + * + * meta and metrics are borrowed pointers (no ownership transfer). + * zval* fields below are also borrowed from meta/metrics and share the same lifetime. + */ +typedef struct { + zend_array *meta; + zend_array *metrics; + + /* Owned strings — release with ddtrace_free_span_precomputed() */ + zend_string *service; /* mapped service name */ + zend_string *name; /* resolved operation name, lowercased when from operation.name meta */ + zend_string *resource; /* resolved resource; falls back to name when property_resource is empty */ + zend_string *type; /* resolved span type */ + zend_string *env; /* from span->property_env; NULL when empty */ + zend_string *version; /* from span->root->property_version; NULL when empty */ + zend_string *span_kind; /* meta["span.kind"], NULL if absent or not a string */ + + /* True when the value came from a meta override (serializer must delete that meta key) */ + bool service_from_meta; + bool name_from_meta; + bool resource_from_meta; + bool type_from_meta; + + /* True when the span's meta hash contains a deprecated "env"/"version" key */ + bool env_deprecated; + bool version_deprecated; + + bool has_exception; /* when span->property_exception holds a Throwable */ + bool ignore_error; + + /* Stats-specific fields — precomputed to avoid repeated lookups across two call sites. */ + bool has_top_level; /* ddtrace_span_is_entrypoint_root() */ + bool is_measured; /* metrics["_dd.measured"] != 0 */ + bool is_partial_snapshot; /* always false until partial-flush is implemented */ +} ddtrace_span_precomputed; + +/** + * Fill *pre from span's properties and meta hash tables. + * Must be called before any meta modifications (OTel remapping, meta key deletion). + */ +void ddtrace_precompute_span(ddtrace_span_data *span, ddtrace_span_precomputed *pre); + +/** Release the owned strings inside *pre (does not free pre itself). */ +void ddtrace_free_span_precomputed(ddtrace_span_precomputed *pre); + +/** + * Compute whether a span should be marked as an error. + * Checks meta["error.message"], meta["error.type"], pre->has_exception, and meta["error.ignored"]. + * Accurate for both sampled and non-sampled spans (HTTP errors are in PHP meta via + * dd_ser_response_committed before the span is closed). + */ +bool dd_compute_span_is_error(const ddtrace_span_precomputed *pre); + +/** + * Feed a closed PHP span into the appropriate per-(env,version) concentrator. + * pre must have been filled by ddtrace_precompute_span(). + */ +void ddtrace_feed_span_to_concentrator(ddtrace_span_data *span, const ddtrace_span_precomputed *pre); + +#endif // DD_SPAN_STATS_H diff --git a/ext/trace_filter.c b/ext/trace_filter.c new file mode 100644 index 00000000000..c2659a8cc8d --- /dev/null +++ b/ext/trace_filter.c @@ -0,0 +1,105 @@ +// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +#include "trace_filter.h" + +#include +#include + +#include + +#include "ddtrace.h" +#include "sidecar.h" +#include "span.h" + +#ifndef _WIN32 +#include +#endif + +ZEND_EXTERN_MODULE_GLOBALS(ddtrace); + +// Lookup callback for ddog_check_stats_trace_filter. +// Returns null when the key is not found anywhere on the span. +static const char *ddtrace_root_tag_value(const void *ctx, const char *key, uintptr_t key_len, uintptr_t *out_len) { + ddtrace_span_data *root = (ddtrace_span_data *)ctx; + + // Check well-known span struct properties first (not in meta). +#define CHECK_PROP(name_lit, prop) \ + if (key_len == sizeof(name_lit) - 1 \ + && memcmp(key, name_lit, sizeof(name_lit) - 1) == 0) { \ + zval *_pv = &root->prop; \ + ZVAL_DEREF(_pv); \ + if (Z_TYPE_P(_pv) == IS_STRING) { \ + *out_len = Z_STRLEN_P(_pv); \ + return Z_STRVAL_P(_pv); \ + } \ + } + + CHECK_PROP("name", property_name) + CHECK_PROP("type", property_type) + CHECK_PROP("env", property_env) + CHECK_PROP("version", property_version) + CHECK_PROP("service", property_service) + CHECK_PROP("resource", property_resource) +#undef CHECK_PROP + + // Meta hash: string tags. + zend_array *meta = ddtrace_property_array(&root->property_meta); + if (meta) { + zval *val = zend_hash_str_find(meta, key, key_len); + if (val && Z_TYPE_P(val) == IS_STRING) { + *out_len = Z_STRLEN_P(val); + return Z_STRVAL_P(val); + } + } + + // Metrics hash: numeric tags returned as stringified float. + zend_array *metrics = ddtrace_property_array(&root->property_metrics); + if (metrics) { + zval *val = zend_hash_str_find(metrics, key, key_len); + if (val) { + ZEND_TLS char metric_buf[32]; + double d = zval_get_double(val); + int len = snprintf(metric_buf, sizeof(metric_buf), "%g", d); + *out_len = (uintptr_t)(len > 0 ? len : 0); + return metric_buf; + } + } + + // Meta struct: key-presence only (value is unrepresentable as a string). + zend_array *meta_struct = ddtrace_property_array(&root->property_meta_struct); + if (meta_struct && zend_hash_str_exists(meta_struct, key, key_len)) { + *out_len = 0; + return ""; + } + + return NULL; +} + +// Slow-path iterator for regex-key filter entries: walks every string meta entry. +static void ddtrace_meta_iter(const void *ctx, void *iter_ctx, + bool (*cb)(void *, const char *, uintptr_t, const char *, uintptr_t)) { + ddtrace_span_data *root = (ddtrace_span_data *)ctx; + zend_array *meta = ddtrace_property_array(&root->property_meta); + if (!meta) { + return; + } + zend_string *key; + zval *val; + ZEND_HASH_FOREACH_STR_KEY_VAL(meta, key, val) { + if (key && Z_TYPE_P(val) == IS_STRING) { + if (!cb(iter_ctx, ZSTR_VAL(key), ZSTR_LEN(key), Z_STRVAL_P(val), Z_STRLEN_P(val))) { + break; + } + } + } ZEND_HASH_FOREACH_END(); +} + +bool ddtrace_trace_passes_filter(ddtrace_span_data *span) { + zval *root_resource_zv = &span->root->property_resource; + ZVAL_DEREF(root_resource_zv); + ddog_CharSlice resource = Z_TYPE_P(root_resource_zv) == IS_STRING + ? dd_zend_string_to_CharSlice(Z_STR_P(root_resource_zv)) + : DDOG_CHARSLICE_C(""); + return ddog_check_stats_trace_filter(resource, span, ddtrace_root_tag_value, ddtrace_meta_iter); +} diff --git a/ext/trace_filter.h b/ext/trace_filter.h new file mode 100644 index 00000000000..b50bfcea7f6 --- /dev/null +++ b/ext/trace_filter.h @@ -0,0 +1,18 @@ +// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +#ifndef DD_TRACE_FILTER_H +#define DD_TRACE_FILTER_H + +#include "span.h" + +/** + * Check whether the trace should be processed at all (serialized + sent + stats). + * + * Applies ignore_resources, filter_tags, and filter_tags_regex configured via the + * agent /info endpoint against the root span of the trace. Returns true to keep + * the trace, false to drop it entirely from the pipeline. + */ +bool ddtrace_trace_passes_filter(ddtrace_span_data *span); + +#endif // DD_TRACE_FILTER_H diff --git a/libdatadog b/libdatadog index fc869988ed4..ff8e9120c7f 160000 --- a/libdatadog +++ b/libdatadog @@ -1 +1 @@ -Subproject commit fc869988ed4f3dc04a081c08d1fda352d4ee2650 +Subproject commit ff8e9120c7fe1746f3b0cad5b5e7c1cefa4d99ef diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index f4a3c4d0644..729898465e7 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -2291,6 +2291,13 @@ "default": "true" } ], + "DD_TRACE_STATS_COMPUTATION_ENABLED": [ + { + "implementation": "A", + "type": "boolean", + "default": "false" + } + ], "DD_TRACE_STRIPE_ANALYTICS_ENABLED": [ { "implementation": "B", diff --git a/tests/Composer/ComposerInteroperabilityTest.php b/tests/Composer/ComposerInteroperabilityTest.php index 4a73d1b577d..c47aec8146e 100644 --- a/tests/Composer/ComposerInteroperabilityTest.php +++ b/tests/Composer/ComposerInteroperabilityTest.php @@ -96,6 +96,7 @@ function ($execute) { 'http.method' => 'GET', 'http.url' => 'http://127.0.0.1:' . self::$webserverPort . '/no-manual-tracing', 'http.status_code' => '200', + 'span.kind' => 'server', ]), ]); } @@ -130,6 +131,7 @@ function ($execute) { 'http.method' => 'GET', 'http.url' => 'http://127.0.0.1:' . self::$webserverPort . '/manual-tracing', 'http.status_code' => '200', + 'span.kind' => 'server', ]) ->withChildren([ SpanAssertion::build('my_operation', 'web.request', 'memcached', 'my_resource') @@ -170,6 +172,7 @@ function ($execute) { 'http.method' => 'GET', 'http.url' => 'http://127.0.0.1:' . self::$webserverPort . '/manual-tracing', 'http.status_code' => '200', + 'span.kind' => 'server', ]) ->withChildren([ SpanAssertion::build('my_operation', 'web.request', 'memcached', 'my_resource') @@ -210,6 +213,7 @@ function ($execute) { 'http.method' => 'GET', 'http.url' => 'http://127.0.0.1:' . self::$webserverPort . '/no-manual-tracing', 'http.status_code' => '200', + 'span.kind' => 'server', ]), ]); } @@ -244,6 +248,7 @@ function ($execute) { 'http.method' => 'GET', 'http.url' => 'http://127.0.0.1:' . self::$webserverPort . '/manual-tracing', 'http.status_code' => '200', + 'span.kind' => 'server', ]) ->withChildren([ SpanAssertion::build('my_operation', 'web.request', 'memcached', 'my_resource') @@ -278,6 +283,7 @@ function ($execute) { 'http.method' => 'GET', 'http.url' => 'http://127.0.0.1:' . self::$webserverPort . '/no-manual-tracing', 'http.status_code' => '200', + 'span.kind' => 'server', ]), ]); } @@ -306,6 +312,7 @@ function ($execute) { 'http.method' => 'GET', 'http.url' => 'http://127.0.0.1:' . self::$webserverPort . '/no-composer', 'http.status_code' => '200', + 'span.kind' => 'server', ]), ]); } @@ -339,6 +346,7 @@ function ($execute) { 'http.method' => 'GET', 'http.url' => 'http://127.0.0.1:' . self::$webserverPort . '/no-composer', 'http.status_code' => '200', + 'span.kind' => 'server', ]), ]); } @@ -374,6 +382,7 @@ function ($execute) { 'http.method' => 'GET', 'http.url' => 'http://127.0.0.1:' . self::$webserverPort . '/no-composer-autoload-fails', 'http.status_code' => '200', + 'span.kind' => 'server', ]), ]); } @@ -409,6 +418,7 @@ function ($execute) { 'http.method' => 'GET', 'http.url' => 'http://127.0.0.1:' . self::$webserverPort . '/composer-autoload-fails', 'http.status_code' => '200', + 'span.kind' => 'server', ]), ]); } diff --git a/tests/DistributedTracing/SyntheticsTest.php b/tests/DistributedTracing/SyntheticsTest.php index acb20e7e018..995f4e9bef1 100644 --- a/tests/DistributedTracing/SyntheticsTest.php +++ b/tests/DistributedTracing/SyntheticsTest.php @@ -53,6 +53,7 @@ public function testSyntheticsRequest() 'http.method' => 'GET', 'http.url' => 'http://localhost/index.php', 'http.status_code' => '200', + 'span.kind' => 'server', '_dd.origin' => 'synthetics-browser', ])->withExactMetrics([ '_sampling_priority_v1' => 1, diff --git a/tests/Integration/ResponseStatusCodeTest.php b/tests/Integration/ResponseStatusCodeTest.php index ee1a949a72a..58896f2e061 100644 --- a/tests/Integration/ResponseStatusCodeTest.php +++ b/tests/Integration/ResponseStatusCodeTest.php @@ -39,6 +39,7 @@ function () { 'http.method' => 'GET', 'http.url' => 'http://localhost/success', 'http.status_code' => '200', + 'span.kind' => 'server', ]), ] ); @@ -64,6 +65,7 @@ function () { 'http.method' => 'GET', 'http.url' => 'http://localhost/error', 'http.status_code' => '500', + 'span.kind' => 'server', ] )->setError(), ] diff --git a/tests/Integrations/Curl/CurlIntegrationTest.php b/tests/Integrations/Curl/CurlIntegrationTest.php index 91fbc3dbdb4..b366b5c0bcb 100644 --- a/tests/Integrations/Curl/CurlIntegrationTest.php +++ b/tests/Integrations/Curl/CurlIntegrationTest.php @@ -189,7 +189,7 @@ function ($execute) { $this->assertFlameGraph($traces, [ SpanAssertion::build('web.request', 'top_level_app', 'web', 'GET /curl_in_web_request.php') - ->withExistingTagsNames(['http.method', 'http.url', 'http.status_code']) + ->withExistingTagsNames(['http.method', 'http.url', 'http.status_code', 'span.kind']) ->withChildren([ SpanAssertion::build('curl_exec', 'curl', 'http', 'http://' . HTTPBIN_INTEGRATION . '/status/?') ->withExactTags([ @@ -620,7 +620,7 @@ function ($execute) { $this->assertFlameGraph($traces, [ SpanAssertion::build('web.request', 'top_level_app', 'web', 'GET /curl_in_web_request.php') - ->withExistingTagsNames(['http.method', 'http.url', 'http.status_code']) + ->withExistingTagsNames(['http.method', 'http.url', 'http.status_code', 'span.kind']) ->withExactMetrics(['_sampling_priority_v1' => 1, '_dd.agent_psr' => 1, 'process_id' => getmypid()]) ->withChildren([ SpanAssertion::build('curl_exec', 'curl', 'http', 'http://' . HTTPBIN_INTEGRATION . '/status/?') diff --git a/tests/Integrations/Custom/Autoloaded/CommonScenariosTest.php b/tests/Integrations/Custom/Autoloaded/CommonScenariosTest.php index 3dd0676ffe3..d53be6143fe 100644 --- a/tests/Integrations/Custom/Autoloaded/CommonScenariosTest.php +++ b/tests/Integrations/Custom/Autoloaded/CommonScenariosTest.php @@ -49,6 +49,7 @@ public function provideSpecs() 'http.method' => 'GET', 'http.url' => 'http://localhost/simple?key=value&', 'http.status_code' => '200', + 'span.kind' => 'server', ]), ], 'A simple GET request with a view' => [ @@ -61,6 +62,7 @@ public function provideSpecs() 'http.method' => 'GET', 'http.url' => 'http://localhost/simple_view?key=value&', 'http.status_code' => '200', + 'span.kind' => 'server', ]), ], 'A GET request with an exception' => [ @@ -73,6 +75,7 @@ public function provideSpecs() 'http.method' => 'GET', 'http.url' => 'http://localhost/error?key=value&', 'http.status_code' => '500', + 'span.kind' => 'server', ])->setError(), ], ] diff --git a/tests/Integrations/Custom/Autoloaded/FatalErrorTest.php b/tests/Integrations/Custom/Autoloaded/FatalErrorTest.php index 25030afd51f..cad7a21dca9 100644 --- a/tests/Integrations/Custom/Autoloaded/FatalErrorTest.php +++ b/tests/Integrations/Custom/Autoloaded/FatalErrorTest.php @@ -46,6 +46,7 @@ public function testScenario() 'http.method' => 'GET', 'http.url' => 'http://localhost/fatal', 'http.status_code' => '200', + 'span.kind' => 'server', ]) ->setError("E_ERROR", "Intentional E_ERROR") ->withExistingTagsNames(['error.stack']), diff --git a/tests/Integrations/Custom/Autoloaded/TraceSearchConfigTest.php b/tests/Integrations/Custom/Autoloaded/TraceSearchConfigTest.php index de1aab575c7..717b6ddbc26 100644 --- a/tests/Integrations/Custom/Autoloaded/TraceSearchConfigTest.php +++ b/tests/Integrations/Custom/Autoloaded/TraceSearchConfigTest.php @@ -42,6 +42,7 @@ public function testScenario() 'http.method' => 'GET', 'http.url' => 'http://localhost/simple', 'http.status_code' => '200', + 'span.kind' => 'server', ])->withExactMetrics([ '_dd1.sr.eausr' => 0.3, '_sampling_priority_v1' => 1, diff --git a/tests/Integrations/Custom/NotAutoloaded/HttpHeadersConfiguredTest.php b/tests/Integrations/Custom/NotAutoloaded/HttpHeadersConfiguredTest.php index 46c825e2196..4e95479b97f 100644 --- a/tests/Integrations/Custom/NotAutoloaded/HttpHeadersConfiguredTest.php +++ b/tests/Integrations/Custom/NotAutoloaded/HttpHeadersConfiguredTest.php @@ -43,6 +43,7 @@ public function testSelectedHeadersAreIncluded() 'http.request.headers.first-header' => 'some value: with colon', 'http.request.headers.forth-header' => '123', 'http.response.headers.third-header' => 'separated: with : colon', + 'span.kind' => 'server', ]; if (\getenv('DD_TRACE_TEST_SAPI') != 'apache2handler') { $tags['http.request.headers.w__rd-header'] = 'foo'; diff --git a/tests/Integrations/Custom/NotAutoloaded/HttpHeadersNotConfiguredTest.php b/tests/Integrations/Custom/NotAutoloaded/HttpHeadersNotConfiguredTest.php index aaedb955863..135b17939c9 100644 --- a/tests/Integrations/Custom/NotAutoloaded/HttpHeadersNotConfiguredTest.php +++ b/tests/Integrations/Custom/NotAutoloaded/HttpHeadersNotConfiguredTest.php @@ -46,6 +46,7 @@ public function testSelectedHeadersAreIncluded() 'http.method' => 'GET', 'http.url' => 'http://localhost/', 'http.status_code' => 200, + 'span.kind' => 'server', ]), ] ); diff --git a/tests/Integrations/Custom/NotAutoloaded/IncomingUserInfoTest.php b/tests/Integrations/Custom/NotAutoloaded/IncomingUserInfoTest.php index 983ce9d668e..b9684547b88 100644 --- a/tests/Integrations/Custom/NotAutoloaded/IncomingUserInfoTest.php +++ b/tests/Integrations/Custom/NotAutoloaded/IncomingUserInfoTest.php @@ -37,6 +37,7 @@ public function testSelectedHeadersAreIncluded() 'http.method' => 'GET', 'http.url' => 'http://localhost/', 'http.status_code' => 200, + 'span.kind' => 'server', ]), ] ); diff --git a/tests/Integrations/Guzzle/V5/GuzzleIntegrationTest.php b/tests/Integrations/Guzzle/V5/GuzzleIntegrationTest.php index 980aeb2c156..d8e6a0448b5 100644 --- a/tests/Integrations/Guzzle/V5/GuzzleIntegrationTest.php +++ b/tests/Integrations/Guzzle/V5/GuzzleIntegrationTest.php @@ -367,7 +367,7 @@ function ($execute) { $this->assertFlameGraph($traces, [ SpanAssertion::build('web.request', 'top_level_app', 'web', 'GET /guzzle_in_web_request.php') - ->withExistingTagsNames(['http.method', 'http.url', 'http.status_code']) + ->withExistingTagsNames(['http.method', 'http.url', 'http.status_code', 'span.kind']) ->withChildren([ SpanAssertion::build('GuzzleHttp\Client.send', 'guzzle', 'http', 'send') ->withExactTags([ diff --git a/tests/Integrations/Guzzle/V6/GuzzleIntegrationTest.php b/tests/Integrations/Guzzle/V6/GuzzleIntegrationTest.php index a5bd2398bc4..3cc84180173 100644 --- a/tests/Integrations/Guzzle/V6/GuzzleIntegrationTest.php +++ b/tests/Integrations/Guzzle/V6/GuzzleIntegrationTest.php @@ -388,7 +388,7 @@ function ($execute) { $this->assertFlameGraph($traces, [ SpanAssertion::build('web.request', 'top_level_app', 'web', 'GET /guzzle_in_web_request.php') - ->withExistingTagsNames(['http.method', 'http.url', 'http.status_code']) + ->withExistingTagsNames(['http.method', 'http.url', 'http.status_code', 'span.kind']) ->withChildren([ SpanAssertion::build('GuzzleHttp\Client.send', 'guzzle', 'http', 'send') ->withExactTags([ diff --git a/tests/ext/client_side_stats_config.phpt b/tests/ext/client_side_stats_config.phpt new file mode 100644 index 00000000000..07663ffbe51 --- /dev/null +++ b/tests/ext/client_side_stats_config.phpt @@ -0,0 +1,16 @@ +--TEST-- +Client-side stats: DD_TRACE_STATS_COMPUTATION_ENABLED config can be read +--ENV-- +DD_TRACE_STATS_COMPUTATION_ENABLED=true +DD_TRACE_AUTO_FLUSH_ENABLED=0 +DD_TRACE_GENERATE_ROOT_SPAN=0 +--FILE-- + +--EXPECT-- +DD_TRACE_STATS_COMPUTATION_ENABLED=true diff --git a/tests/ext/client_side_stats_disabled_by_default.phpt b/tests/ext/client_side_stats_disabled_by_default.phpt new file mode 100644 index 00000000000..618b754bfe4 --- /dev/null +++ b/tests/ext/client_side_stats_disabled_by_default.phpt @@ -0,0 +1,24 @@ +--TEST-- +Client-side stats: _dd.top_level metric is not set when stats disabled (default) +--ENV-- +DD_TRACE_AUTO_FLUSH_ENABLED=0 +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_CODE_ORIGIN_FOR_SPANS_ENABLED=0 +--FILE-- +name = "root"; +$root->service = "my-service"; +\DDTrace\close_span(); + +$spans = dd_trace_serialize_closed_spans(); + +foreach ($spans as $span) { + $has_top_level = isset($span["metrics"]["_dd.top_level"]); + echo $span["name"] . ": _dd.top_level=" . ($has_top_level ? "1" : "not set") . "\n"; +} + +?> +--EXPECT-- +root: _dd.top_level=not set diff --git a/tests/ext/client_side_stats_top_level.phpt b/tests/ext/client_side_stats_top_level.phpt new file mode 100644 index 00000000000..36f4b38ba60 --- /dev/null +++ b/tests/ext/client_side_stats_top_level.phpt @@ -0,0 +1,45 @@ +--TEST-- +Client-side stats: _dd.top_level metric is set on top-level spans +--ENV-- +DD_TRACE_STATS_COMPUTATION_ENABLED=true +DD_TRACE_AUTO_FLUSH_ENABLED=0 +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_CODE_ORIGIN_FOR_SPANS_ENABLED=0 +--FILE-- +name = "root"; +$root->service = "my-service"; + +$child_same = \DDTrace\start_span(); +$child_same->name = "child_same_service"; +$child_same->service = "my-service"; + +$grandchild_diff = \DDTrace\start_span(); +$grandchild_diff->name = "grandchild_diff_service"; +$grandchild_diff->service = "other-service"; +\DDTrace\close_span(); + +\DDTrace\close_span(); + +$child_diff = \DDTrace\start_span(); +$child_diff->name = "child_diff_service"; +$child_diff->service = "other-service"; +\DDTrace\close_span(); + +\DDTrace\close_span(); + +$spans = dd_trace_serialize_closed_spans(); + +foreach ($spans as $span) { + $has_top_level = isset($span["metrics"]["_dd.top_level"]); + echo $span["name"] . ": _dd.top_level=" . ($has_top_level ? "1" : "not set") . "\n"; +} + +?> +--EXPECT-- +root: _dd.top_level=1 +child_diff_service: _dd.top_level=1 +child_same_service: _dd.top_level=not set +grandchild_diff_service: _dd.top_level=1 diff --git a/tests/ext/includes/request_replayer.inc b/tests/ext/includes/request_replayer.inc index a8fc12f2414..764bb9d98de 100644 --- a/tests/ext/includes/request_replayer.inc +++ b/tests/ext/includes/request_replayer.inc @@ -124,6 +124,34 @@ class RequestReplayer ])), true); } + public function replayAllStats() + { + return json_decode(file_get_contents($this->endpoint . '/replay-stats', false, stream_context_create([ + "http" => [ + "header" => "X-Datadog-Test-Session-Token: " . ini_get("datadog.trace.agent_test_session_token"), + ], + ])), true); + } + + public function waitForStats($matcher = null) + { + $i = 0; + do { + if ($i++ == $this->maxIteration) { + throw new Exception("wait for stats timeout"); + } + usleep($this->flushInterval); + $requests = $this->replayAllStats(); + if (is_array($requests)) { + foreach ($requests as $request) { + if ($matcher === null || $matcher($request)) { + return $request; + } + } + } + } while (true); + } + public function replayAllRcRequests() { return json_decode(file_get_contents($this->endpoint . '/replay-rc-requests', false, stream_context_create([ diff --git a/tests/ext/inferred_proxy/alter_service.phpt b/tests/ext/inferred_proxy/alter_service.phpt index 0f939ab1e61..ac671bb417d 100644 --- a/tests/ext/inferred_proxy/alter_service.phpt +++ b/tests/ext/inferred_proxy/alter_service.phpt @@ -56,6 +56,7 @@ echo json_encode(dd_trace_serialize_closed_spans(), JSON_PRETTY_PRINT); "http.status_code": "200", "http.url": "http:\/\/localhost:8888\/foo", "runtime-id": "%s", + "span.kind": "server", "version": "1.0" }, "metrics": { diff --git a/tests/ext/inferred_proxy/basic_test.phpt b/tests/ext/inferred_proxy/basic_test.phpt index b5e0c5fb1da..d269de3f483 100644 --- a/tests/ext/inferred_proxy/basic_test.phpt +++ b/tests/ext/inferred_proxy/basic_test.phpt @@ -74,6 +74,7 @@ if ($percentageDifference > 0.01) { // 0.01% difference for the sake of the test "http.status_code": "200", "http.url": "http:\/\/localhost:8888\/foo", "runtime-id": "%s", + "span.kind": "server", "version": "1.0" }, "metrics": { diff --git a/tests/ext/inferred_proxy/distributed_tracing.phpt b/tests/ext/inferred_proxy/distributed_tracing.phpt index 1b4e44ad153..b5ec3bd0e6d 100644 --- a/tests/ext/inferred_proxy/distributed_tracing.phpt +++ b/tests/ext/inferred_proxy/distributed_tracing.phpt @@ -65,6 +65,7 @@ echo json_encode(dd_trace_serialize_closed_spans(), JSON_PRETTY_PRINT); "http.status_code": "200", "http.url": "http:\/\/localhost:8888\/foo", "runtime-id": "%s", + "span.kind": "server", "version": "1.0" }, "metrics": { diff --git a/tests/ext/inferred_proxy/error_propagated.phpt b/tests/ext/inferred_proxy/error_propagated.phpt index 7a464ff808d..3db1322edc7 100644 --- a/tests/ext/inferred_proxy/error_propagated.phpt +++ b/tests/ext/inferred_proxy/error_propagated.phpt @@ -69,6 +69,7 @@ echo json_encode(dd_trace_serialize_closed_spans(), JSON_PRETTY_PRINT); "http.status_code": "500", "http.url": "http:\/\/localhost:8888\/foo", "runtime-id": "%s", + "span.kind": "server", "version": "1.0" }, "metrics": { diff --git a/tests/ext/inferred_proxy/fallback_service_name.phpt b/tests/ext/inferred_proxy/fallback_service_name.phpt index 21ebaa6265f..d36f99d9742 100644 --- a/tests/ext/inferred_proxy/fallback_service_name.phpt +++ b/tests/ext/inferred_proxy/fallback_service_name.phpt @@ -58,6 +58,7 @@ echo json_encode(dd_trace_serialize_closed_spans(), JSON_PRETTY_PRINT); "http.status_code": "200", "http.url": "http:\/\/localhost:8888\/foo", "runtime-id": "%s", + "span.kind": "server", "version": "1.0" }, "metrics": { diff --git a/tests/ext/inferred_proxy/incomplete_headers.phpt b/tests/ext/inferred_proxy/incomplete_headers.phpt index a1806eda79a..bcd9b52e8e8 100644 --- a/tests/ext/inferred_proxy/incomplete_headers.phpt +++ b/tests/ext/inferred_proxy/incomplete_headers.phpt @@ -54,6 +54,7 @@ echo json_encode(dd_trace_serialize_closed_spans(), JSON_PRETTY_PRINT); "http.status_code": "200", "http.url": "http:\/\/localhost:8888\/foo", "runtime-id": "%s", + "span.kind": "server", "version": "1.0" }, "metrics": { diff --git a/tests/ext/inferred_proxy/multiple_traces.phpt b/tests/ext/inferred_proxy/multiple_traces.phpt index 63325a93152..3caef98da3f 100644 --- a/tests/ext/inferred_proxy/multiple_traces.phpt +++ b/tests/ext/inferred_proxy/multiple_traces.phpt @@ -61,6 +61,7 @@ echo json_encode(dd_trace_serialize_closed_spans(), JSON_PRETTY_PRINT); "http.status_code": "200", "http.url": "http:\/\/localhost:8888\/foo", "runtime-id": "%s", + "span.kind": "server", "version": "1.0" }, "metrics": { @@ -88,6 +89,7 @@ echo json_encode(dd_trace_serialize_closed_spans(), JSON_PRETTY_PRINT); "http.status_code": "200", "http.url": "http:\/\/localhost:8888\/foo", "runtime-id": "%s", + "span.kind": "server", "version": "1.0" }, "metrics": { diff --git a/tests/ext/inferred_proxy/propagated_tags_after_span_start.phpt b/tests/ext/inferred_proxy/propagated_tags_after_span_start.phpt index 73a963f77e3..3f31afbb704 100644 --- a/tests/ext/inferred_proxy/propagated_tags_after_span_start.phpt +++ b/tests/ext/inferred_proxy/propagated_tags_after_span_start.phpt @@ -63,6 +63,7 @@ echo json_encode(dd_trace_serialize_closed_spans(), JSON_PRETTY_PRINT); "http.status_code": "200", "http.url": "http:\/\/localhost:8888\/foo", "runtime-id": "%s", + "span.kind": "server", "version": "1.0" }, "metrics": { diff --git a/tests/ext/inferred_proxy/propagated_tags_before_span_start.phpt b/tests/ext/inferred_proxy/propagated_tags_before_span_start.phpt index 0477d22aa97..e1f5f815d17 100644 --- a/tests/ext/inferred_proxy/propagated_tags_before_span_start.phpt +++ b/tests/ext/inferred_proxy/propagated_tags_before_span_start.phpt @@ -62,6 +62,7 @@ echo json_encode(dd_trace_serialize_closed_spans(), JSON_PRETTY_PRINT); "http.status_code": "200", "http.url": "http:\/\/localhost:8888\/foo", "runtime-id": "%s", + "span.kind": "server", "version": "1.0" }, "metrics": { diff --git a/tests/ext/inferred_proxy/sampling_rules.phpt b/tests/ext/inferred_proxy/sampling_rules.phpt index eec6a5a1092..03c64317136 100644 --- a/tests/ext/inferred_proxy/sampling_rules.phpt +++ b/tests/ext/inferred_proxy/sampling_rules.phpt @@ -52,7 +52,8 @@ echo json_encode(dd_trace_serialize_closed_spans(), JSON_PRETTY_PRINT); "http.method": "GET", "http.status_code": "200", "http.url": "http:\/\/localhost:8888\/foo", - "runtime-id": "%s" + "runtime-id": "%s", + "span.kind": "server" }, "metrics": { "php.compilation.total_time_ms": %f, diff --git a/tests/ext/referrer_extraction_01.phpt b/tests/ext/referrer_extraction_01.phpt index 4abbb50e23d..53202402416 100644 --- a/tests/ext/referrer_extraction_01.phpt +++ b/tests/ext/referrer_extraction_01.phpt @@ -18,7 +18,7 @@ $spans = dd_trace_serialize_closed_spans(); var_dump($spans[0]['meta']); ?> --EXPECTF-- -array(6) { +array(7) { ["_dd.p.dm"]=> string(2) "-0" ["_dd.p.tid"]=> @@ -31,4 +31,6 @@ array(6) { string(25) "http://localhost:8888/foo" ["runtime-id"]=> string(36) "%s" + ["span.kind"]=> + string(6) "server" } \ No newline at end of file diff --git a/tests/ext/referrer_extraction_02.phpt b/tests/ext/referrer_extraction_02.phpt index 3fe3ff3280e..e2f6e052442 100644 --- a/tests/ext/referrer_extraction_02.phpt +++ b/tests/ext/referrer_extraction_02.phpt @@ -18,7 +18,7 @@ $spans = dd_trace_serialize_closed_spans(); var_dump($spans[0]['meta']); ?> --EXPECTF-- -array(7) { +array(8) { ["_dd.p.dm"]=> string(2) "-0" ["_dd.p.tid"]=> @@ -33,4 +33,6 @@ array(7) { string(25) "http://localhost:8888/foo" ["runtime-id"]=> string(36) "%s" + ["span.kind"]=> + string(6) "server" } \ No newline at end of file diff --git a/tests/ext/referrer_extraction_03.phpt b/tests/ext/referrer_extraction_03.phpt index f4c5e8e059f..dad8c17ad0e 100644 --- a/tests/ext/referrer_extraction_03.phpt +++ b/tests/ext/referrer_extraction_03.phpt @@ -18,7 +18,7 @@ $spans = dd_trace_serialize_closed_spans(); var_dump($spans[0]['meta']); ?> --EXPECTF-- -array(6) { +array(7) { ["_dd.p.dm"]=> string(2) "-0" ["_dd.p.tid"]=> @@ -31,4 +31,6 @@ array(6) { string(25) "http://localhost:8888/foo" ["runtime-id"]=> string(36) "%s" + ["span.kind"]=> + string(6) "server" } \ No newline at end of file diff --git a/tests/ext/referrer_extraction_04.phpt b/tests/ext/referrer_extraction_04.phpt index de36eb81538..46f3e0969dc 100644 --- a/tests/ext/referrer_extraction_04.phpt +++ b/tests/ext/referrer_extraction_04.phpt @@ -18,7 +18,7 @@ $spans = dd_trace_serialize_closed_spans(); var_dump($spans[0]['meta']); ?> --EXPECTF-- -array(6) { +array(7) { ["_dd.p.dm"]=> string(2) "-0" ["_dd.p.tid"]=> @@ -31,4 +31,6 @@ array(6) { string(25) "http://localhost:8888/foo" ["runtime-id"]=> string(36) "%s" + ["span.kind"]=> + string(6) "server" } \ No newline at end of file diff --git a/tests/ext/referrer_extraction_05.phpt b/tests/ext/referrer_extraction_05.phpt index 9a4237c8e53..6190a4e4087 100644 --- a/tests/ext/referrer_extraction_05.phpt +++ b/tests/ext/referrer_extraction_05.phpt @@ -18,7 +18,7 @@ $spans = dd_trace_serialize_closed_spans(); var_dump($spans[0]['meta']); ?> --EXPECTF-- -array(7) { +array(8) { ["_dd.p.dm"]=> string(2) "-0" ["_dd.p.tid"]=> @@ -33,4 +33,6 @@ array(7) { string(25) "http://localhost:8888/foo" ["runtime-id"]=> string(36) "%s" + ["span.kind"]=> + string(6) "server" } \ No newline at end of file diff --git a/tests/ext/request-replayer/client_side_stats.phpt b/tests/ext/request-replayer/client_side_stats.phpt new file mode 100644 index 00000000000..7eea5c62ce7 --- /dev/null +++ b/tests/ext/request-replayer/client_side_stats.phpt @@ -0,0 +1,73 @@ +--TEST-- +Client-side SHM span stats are computed and flushed to the agent on trace flush +--SKIPIF-- + +--ENV-- +DD_AGENT_HOST=request-replayer +DD_TRACE_AGENT_PORT=80 +DD_TRACE_AGENT_FLUSH_INTERVAL=333 +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_INSTRUMENTATION_TELEMETRY_ENABLED=0 +DD_TRACE_SIDECAR_TRACE_SENDER=1 +DD_TRACE_STATS_COMPUTATION_ENABLED=1 +--INI-- +datadog.env=test-env +datadog.version=1.2.3-basic +datadog.trace.agent_test_session_token=client_side_stats +--FILE-- +name = "web.request"; +$root->resource = "GET /test"; +$root->service = "stats-test-service"; +\DDTrace\close_span(); + +dd_trace_internal_fn('synchronous_flush'); +$rr->waitForDataAndReplay(); + +// The request-replayer stores the msgpack-decoded body as JSON, with OkSummary/ErrorSummary +// (binary DDSketch fields) hex-encoded. We json_decode it to get the payload. +$statsRequest = $rr->waitForStats(); +$payload = json_decode($statsRequest['body'], true); + +echo "env: " . $payload['Env'] . "\n"; +echo "version: " . $payload['Version'] . "\n"; + +$buckets = $payload['Stats']; +$found = false; +foreach ($buckets as $bucket) { + foreach ($bucket['Stats'] as $group) { + if ($group['Service'] === 'stats-test-service' && $group['Name'] === 'web.request') { + echo "service: " . $group['Service'] . "\n"; + echo "name: " . $group['Name'] . "\n"; + echo "resource: " . $group['Resource'] . "\n"; + echo "hits >= 1: " . ($group['Hits'] >= 1 ? "true" : "false") . "\n"; + echo "errors: " . $group['Errors'] . "\n"; + $found = true; + break 2; + } + } +} +if (!$found) { + echo "ERROR: no matching stats group found\n"; + var_dump($payload); +} + +?> +--EXPECT-- +env: test-env +version: 1.2.3-basic +service: stats-test-service +name: web.request +resource: GET /test +hits >= 1: true +errors: 0 diff --git a/tests/ext/request-replayer/client_side_stats_peer_tags.phpt b/tests/ext/request-replayer/client_side_stats_peer_tags.phpt new file mode 100644 index 00000000000..243e2964508 --- /dev/null +++ b/tests/ext/request-replayer/client_side_stats_peer_tags.phpt @@ -0,0 +1,101 @@ +--TEST-- +Client-side SHM span stats include peer tags configured via agent info +--SKIPIF-- + += 80100) { + echo "nocache\n"; +} +$ctx = stream_context_create([ + 'http' => [ + 'method' => 'PUT', + 'header' => [ + 'Content-Type: application/json', + 'X-Datadog-Test-Session-Token: client_side_stats_peer_tags', + ], + 'content' => '{"peer_tags":["db.hostname"]}', + ] +]); +file_get_contents('http://request-replayer/set-agent-info', false, $ctx); +?> +--ENV-- +DD_AGENT_HOST=request-replayer +DD_TRACE_AGENT_PORT=80 +DD_TRACE_AGENT_FLUSH_INTERVAL=333 +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_INSTRUMENTATION_TELEMETRY_ENABLED=0 +DD_TRACE_SIDECAR_TRACE_SENDER=1 +DD_TRACE_STATS_COMPUTATION_ENABLED=1 +DD_TRACE_LOG_LEVEL=off +--INI-- +datadog.env=test-env +datadog.version=1.2.3-peer +datadog.trace.agent_test_session_token=client_side_stats_peer_tags +--FILE-- +name = "dummy"; +$dummy->service = "dummy-service"; +\DDTrace\close_span(); +dd_trace_internal_fn('synchronous_flush'); +$rr->waitForDataAndReplay(); + +// Now create the span whose stats we want to inspect. When this span is fed to the +// concentrator, ddog_apply_agent_info_concentrator_config() is called first, picks up +// the peer_tags update from the SHM, and the concentrator extracts db.hostname from meta. +$root = \DDTrace\start_trace_span(); +$root->name = "web.request"; +$root->resource = "GET /db"; +$root->service = "stats-test-service"; +$root->meta['span.kind'] = 'client'; +$root->meta['db.hostname'] = 'my-db-host'; +\DDTrace\close_span(); + +dd_trace_internal_fn('synchronous_flush'); +$rr->waitForDataAndReplay(); + +// SKIPIF also generates stats (file_get_contents span) under this token; use a matcher +// so we find the stats payload that actually contains our service. +$statsRequest = $rr->waitForStats(function ($request) { + $payload = json_decode($request['body'], true); + foreach ($payload['Stats'] ?? [] as $bucket) { + foreach ($bucket['Stats'] ?? [] as $group) { + if ($group['Service'] === 'stats-test-service' && $group['Name'] === 'web.request') { + return true; + } + } + } + return false; +}); + +$payload = json_decode($statsRequest['body'], true); +$found = false; +foreach ($payload['Stats'] as $bucket) { + foreach ($bucket['Stats'] as $group) { + if ($group['Service'] === 'stats-test-service' && $group['Name'] === 'web.request') { + $peerTags = $group['PeerTags'] ?? []; + sort($peerTags); + echo "peer_tags: " . json_encode($peerTags) . "\n"; + $found = true; + break 2; + } + } +} +if (!$found) { + echo "ERROR: no matching stats group found\n"; + var_dump($payload); +} + +?> +--EXPECT-- +peer_tags: ["db.hostname:my-db-host"] diff --git a/tests/ext/request-replayer/client_side_stats_trace_filters.phpt b/tests/ext/request-replayer/client_side_stats_trace_filters.phpt new file mode 100644 index 00000000000..e99af3a161e --- /dev/null +++ b/tests/ext/request-replayer/client_side_stats_trace_filters.phpt @@ -0,0 +1,210 @@ +--TEST-- +Client-side stats respect trace filters (filter_tags, filter_tags_regex, ignore_resources) from agent info +--SKIPIF-- + += 80100) { + echo "nocache\n"; +} +// Configure the request-replayer to return these filter rules from the /info endpoint. +// The sidecar will pick them up on its next poll cycle (triggered by the dummy flush below). +// +// Filters configured: +// filter_tags.require: filter_required:yes +// filter_tags.reject: filter_reject:yes +// filter_tags_regex.require: http.method matching G.* (GET passes, DELETE fails) +// filter_tags_regex.reject: http.url matching .*\.internal\..* +// ignore_resources: GET /healthcheck (exact resource match) +$ctx = stream_context_create([ + 'http' => [ + 'method' => 'PUT', + 'header' => [ + 'Content-Type: application/json', + 'X-Datadog-Test-Session-Token: client_side_stats_trace_filters', + ], + 'content' => json_encode([ + 'filter_tags' => [ + 'require' => ['filter_required:yes'], + 'reject' => ['filter_reject:yes'], + ], + 'filter_tags_regex' => [ + 'require' => ['http.method:G.*'], + 'reject' => ['http.url:.*\\.internal\\..*'], + ], + 'ignore_resources' => ['GET /healthcheck'], + ]), + ] +]); +file_get_contents('http://request-replayer/set-agent-info', false, $ctx); +?> +--ENV-- +DD_AGENT_HOST=request-replayer +DD_TRACE_AGENT_PORT=80 +DD_TRACE_AGENT_FLUSH_INTERVAL=333 +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_INSTRUMENTATION_TELEMETRY_ENABLED=0 +DD_TRACE_SIDECAR_TRACE_SENDER=1 +DD_TRACE_STATS_COMPUTATION_ENABLED=1 +DD_TRACE_LOG_LEVEL=off +--INI-- +datadog.env=test-env +datadog.version=1.2.3-filters +datadog.trace.agent_test_session_token=client_side_stats_trace_filters +--FILE-- +name = 'dummy'; +$dummy->service = 'dummy-service'; +\DDTrace\close_span(); +dd_trace_internal_fn('synchronous_flush'); +$rr->waitForDataAndReplay(); + +// 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 { + $s = \DDTrace\start_trace_span(); + $s->name = $name; + $s->resource = $resource; + $s->service = 'filter-test-service'; + foreach ($meta as $k => $v) { + $s->meta[$k] = $v; + } + \DDTrace\close_span(); +} + +// 1. PASS — satisfies every filter. +makeSpan('op.pass', 'GET /api', [ + 'filter_required' => 'yes', + 'http.method' => 'GET', +]); + +// 2. BLOCKED by ignore_resources — resource "GET /healthcheck" matches the pattern. +makeSpan('op.blocked.resource', 'GET /healthcheck', [ + 'filter_required' => 'yes', + 'http.method' => 'GET', +]); + +// 3. BLOCKED by filter_tags.require — missing required tag "filter_required:yes". +makeSpan('op.blocked.missing_require', 'GET /other', [ + 'http.method' => 'GET', +]); + +// 4. BLOCKED by filter_tags.reject — tag "filter_reject:yes" triggers exact rejection. +makeSpan('op.blocked.reject_tag', 'GET /other2', [ + 'filter_required' => 'yes', + 'filter_reject' => 'yes', + 'http.method' => 'GET', +]); + +// 5. BLOCKED by filter_tags_regex.reject — http.url matches ".*\.internal\..*". +makeSpan('op.blocked.regex_reject', 'GET /other3', [ + 'filter_required' => 'yes', + 'http.method' => 'GET', + 'http.url' => 'http://my.internal.service/path', +]); + +// 6. BLOCKED by filter_tags_regex.require — http.method is "DELETE" which does not +// match "G.*" (anchored, so "GET" passes but "DELETE" fails). +makeSpan('op.blocked.regex_require', 'GET /other4', [ + 'filter_required' => 'yes', + 'http.method' => 'DELETE', +]); + +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. +$secondFlushTraces = []; +for ($i = 0; $i < 1000; $i++) { + usleep(50000); // 50 ms (same interval as RequestReplayer::flushInterval) + $reqs = $rr->replayAllRequests() ?? []; + $traces = array_values(array_filter($reqs, function ($r) { + return strpos($r['uri'] ?? '', 'traces') !== false; + })); + if (!empty($traces)) { + $secondFlushTraces = $traces; + break; + } +} + +// Extract span names from every trace request in this flush. +$namesInTraces = []; +foreach ($secondFlushTraces as $req) { + $body = json_decode($req['body'] ?? '', true); + if (!is_array($body)) continue; + if (isset($body['chunks'])) { + // v0.7 / sidecar format + foreach ($body['chunks'] as $chunk) { + foreach ($chunk['spans'] ?? [] as $span) { + $n = $span['name'] ?? ''; + if ($n !== '') $namesInTraces[$n] = true; + } + } + } else { + // v0.4 format: array of traces, each trace is an array of spans + foreach ($body as $trace) { + if (!is_array($trace)) continue; + foreach ($trace as $span) { + $n = $span['name'] ?? ''; + if ($n !== '') $namesInTraces[$n] = true; + } + } + } +} +ksort($namesInTraces); +foreach (array_keys($namesInTraces) as $n) { + echo "in traces: $n\n"; +} + +// Wait for a stats payload that contains our service. +// Stats from the second flush arrive a few seconds after waitForDataAndReplay() returns; +// use a matcher so we wait for the right payload rather than returning the first one +// (which contains the dummy span from the first flush). +$statsRequest = $rr->waitForStats(function ($request) { + $payload = json_decode($request['body'], true); + foreach ($payload['Stats'] ?? [] as $bucket) { + foreach ($bucket['Stats'] ?? [] as $group) { + if ($group['Service'] === 'filter-test-service') { + return true; + } + } + } + return false; +}); + +// Print which operation names appear in stats (sorted for determinism). +// Only op.pass should survive all filters. +$payload = json_decode($statsRequest['body'], true); +$ops = []; +foreach ($payload['Stats'] as $bucket) { + foreach ($bucket['Stats'] as $group) { + if ($group['Service'] === 'filter-test-service') { + $ops[] = $group['Name']; + } + } +} +sort($ops); + +if (empty($ops)) { + echo "ERROR: no filter-test-service stats groups found\n"; + var_dump($payload); +} else { + foreach ($ops as $op) { + echo "in stats: $op\n"; + } +} +?> +--EXPECT-- +in traces: op.pass +in stats: op.pass diff --git a/tests/ext/root_span_url_as_resource_names.phpt b/tests/ext/root_span_url_as_resource_names.phpt index 9788766b5a3..b00af24a0e2 100644 --- a/tests/ext/root_span_url_as_resource_names.phpt +++ b/tests/ext/root_span_url_as_resource_names.phpt @@ -20,7 +20,7 @@ $spans = dd_trace_serialize_closed_spans(); var_dump($spans[0]['meta']); ?> --EXPECTF-- -array(6) { +array(7) { ["_dd.p.dm"]=> string(2) "-0" ["_dd.p.tid"]=> @@ -33,4 +33,6 @@ array(6) { string(26) "https://localhost:9999/foo" ["runtime-id"]=> string(36) "%s" + ["span.kind"]=> + string(6) "server" } diff --git a/tests/ext/root_span_url_as_resource_names_no_host.phpt b/tests/ext/root_span_url_as_resource_names_no_host.phpt index b2cc73abf12..c3ab97e6ae8 100644 --- a/tests/ext/root_span_url_as_resource_names_no_host.phpt +++ b/tests/ext/root_span_url_as_resource_names_no_host.phpt @@ -19,7 +19,7 @@ $spans = dd_trace_serialize_closed_spans(); var_dump($spans[0]['meta']); ?> --EXPECTF-- -array(6) { +array(7) { ["_dd.p.dm"]=> string(2) "-0" ["_dd.p.tid"]=> @@ -32,4 +32,6 @@ array(6) { string(25) "http://localhost:8888/foo" ["runtime-id"]=> string(36) "%s" + ["span.kind"]=> + string(6) "server" } diff --git a/tests/ext/sandbox/safe_to_string_properties.phpt b/tests/ext/sandbox/safe_to_string_properties.phpt index 796506a3c49..a8592fa29f6 100644 --- a/tests/ext/sandbox/safe_to_string_properties.phpt +++ b/tests/ext/sandbox/safe_to_string_properties.phpt @@ -117,7 +117,7 @@ NULL bool(false) string(5) "false" -'resource' dropped +string(5) "false" string(5) "false" string(5) "false" diff --git a/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_dynamic_route.json b/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_dynamic_route.json index aacb2d2f807..a259ccb31a7 100644 --- a/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_dynamic_route.json +++ b/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_dynamic_route.json @@ -14,7 +14,8 @@ "http.method": "GET", "http.status_code": "404", "http.url": "http://localhost/dynamic_route/dynamic01/static/dynamic02", - "runtime-id": "19a12c46-a841-41eb-9a2f-013107929bdc" + "runtime-id": "19a12c46-a841-41eb-9a2f-013107929bdc", + "span.kind": "server" }, "metrics": { "_sampling_priority_v1": 1.0 diff --git a/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_to_missing_route.json b/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_to_missing_route.json index 0ffee505693..cba33f84cdc 100644 --- a/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_to_missing_route.json +++ b/tests/snapshots/tests.integrations.laravel.latest.common_scenarios_test.test_scenario_get_to_missing_route.json @@ -14,7 +14,8 @@ "http.method": "GET", "http.status_code": "404", "http.url": "http://localhost/does_not_exist?key=value&", - "runtime-id": "19a12c46-a841-41eb-9a2f-013107929bdc" + "runtime-id": "19a12c46-a841-41eb-9a2f-013107929bdc", + "span.kind": "server" }, "metrics": { "_sampling_priority_v1": 1.0 diff --git a/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_dynamic_route.json b/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_dynamic_route.json index 89275103cf2..61f9f360c1c 100644 --- a/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_dynamic_route.json +++ b/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_dynamic_route.json @@ -14,7 +14,8 @@ "http.method": "GET", "http.status_code": "404", "http.url": "http://localhost/dynamic_route/dynamic01/static/dynamic02", - "runtime-id": "b32e6f79-c5ef-4d17-bae1-33f912b0a03d" + "runtime-id": "b32e6f79-c5ef-4d17-bae1-33f912b0a03d", + "span.kind": "server" }, "metrics": { "_sampling_priority_v1": 1.0 diff --git a/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_to_missing_route.json b/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_to_missing_route.json index bf078718fa8..078498389ad 100644 --- a/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_to_missing_route.json +++ b/tests/snapshots/tests.integrations.laravel.v10_x.common_scenarios_test.test_scenario_get_to_missing_route.json @@ -14,7 +14,8 @@ "http.method": "GET", "http.status_code": "404", "http.url": "http://localhost/does_not_exist?key=value&", - "runtime-id": "d74a50e5-32ec-4668-ad83-9a267ef93418" + "runtime-id": "d74a50e5-32ec-4668-ad83-9a267ef93418", + "span.kind": "server" }, "metrics": { "_sampling_priority_v1": 1.0 diff --git a/tests/snapshots/tests.integrations.laravel.v11_x.common_scenarios_test.test_scenario_get_dynamic_route.json b/tests/snapshots/tests.integrations.laravel.v11_x.common_scenarios_test.test_scenario_get_dynamic_route.json index 33032069c5a..173019f7ad9 100644 --- a/tests/snapshots/tests.integrations.laravel.v11_x.common_scenarios_test.test_scenario_get_dynamic_route.json +++ b/tests/snapshots/tests.integrations.laravel.v11_x.common_scenarios_test.test_scenario_get_dynamic_route.json @@ -14,7 +14,8 @@ "http.method": "GET", "http.status_code": "404", "http.url": "http://localhost/dynamic_route/dynamic01/static/dynamic02", - "runtime-id": "e719c2fb-81c9-4ae3-b1f9-e1a2843d4845" + "runtime-id": "e719c2fb-81c9-4ae3-b1f9-e1a2843d4845", + "span.kind": "server" }, "metrics": { "_sampling_priority_v1": 1.0 diff --git a/tests/snapshots/tests.integrations.laravel.v11_x.common_scenarios_test.test_scenario_get_to_missing_route.json b/tests/snapshots/tests.integrations.laravel.v11_x.common_scenarios_test.test_scenario_get_to_missing_route.json index 3d61374a577..75ca1fa3f67 100644 --- a/tests/snapshots/tests.integrations.laravel.v11_x.common_scenarios_test.test_scenario_get_to_missing_route.json +++ b/tests/snapshots/tests.integrations.laravel.v11_x.common_scenarios_test.test_scenario_get_to_missing_route.json @@ -14,7 +14,8 @@ "http.method": "GET", "http.status_code": "404", "http.url": "http://localhost/does_not_exist?key=value&", - "runtime-id": "e719c2fb-81c9-4ae3-b1f9-e1a2843d4845" + "runtime-id": "e719c2fb-81c9-4ae3-b1f9-e1a2843d4845", + "span.kind": "server" }, "metrics": { "_sampling_priority_v1": 1.0 diff --git a/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_to_missing_route.json b/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_to_missing_route.json index 401cf3101e1..e311fa2b009 100644 --- a/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_to_missing_route.json +++ b/tests/snapshots/tests.integrations.laravel.v5_7.common_scenarios_test.test_scenario_get_to_missing_route.json @@ -14,7 +14,8 @@ "http.method": "GET", "http.status_code": "404", "http.url": "http://localhost/does_not_exist?key=value&", - "runtime-id": "4878b25b-9211-4ac8-8906-d7721b2fa3e5" + "runtime-id": "4878b25b-9211-4ac8-8906-d7721b2fa3e5", + "span.kind": "server" }, "metrics": { "_sampling_priority_v1": 1.0 diff --git a/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_to_missing_route.json b/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_to_missing_route.json index 9338355bbeb..f8a3c1d0e0f 100644 --- a/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_to_missing_route.json +++ b/tests/snapshots/tests.integrations.laravel.v5_8.common_scenarios_test.test_scenario_get_to_missing_route.json @@ -14,7 +14,8 @@ "http.method": "GET", "http.status_code": "404", "http.url": "http://localhost/does_not_exist?key=value&", - "runtime-id": "5db5654d-5a9e-4cf0-908b-a51f7ced2ff2" + "runtime-id": "5db5654d-5a9e-4cf0-908b-a51f7ced2ff2", + "span.kind": "server" }, "metrics": { "_sampling_priority_v1": 1.0 diff --git a/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_to_missing_route.json b/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_to_missing_route.json index f45fcafb81b..884e59bdbc7 100644 --- a/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_to_missing_route.json +++ b/tests/snapshots/tests.integrations.laravel.v8_x.common_scenarios_test.test_scenario_get_to_missing_route.json @@ -14,7 +14,8 @@ "http.method": "GET", "http.status_code": "404", "http.url": "http://localhost/does_not_exist?key=value&", - "runtime-id": "143d2b36-2082-43af-b61f-f60b9133ac91" + "runtime-id": "143d2b36-2082-43af-b61f-f60b9133ac91", + "span.kind": "server" }, "metrics": { "_sampling_priority_v1": 1.0 diff --git a/tests/snapshots/tests.integrations.laravel.v9_x.common_scenarios_test.test_scenario_get_dynamic_route.json b/tests/snapshots/tests.integrations.laravel.v9_x.common_scenarios_test.test_scenario_get_dynamic_route.json index 1ad2cf0d802..6c2bed6035b 100644 --- a/tests/snapshots/tests.integrations.laravel.v9_x.common_scenarios_test.test_scenario_get_dynamic_route.json +++ b/tests/snapshots/tests.integrations.laravel.v9_x.common_scenarios_test.test_scenario_get_dynamic_route.json @@ -14,7 +14,8 @@ "http.method": "GET", "http.status_code": "404", "http.url": "http://localhost/dynamic_route/dynamic01/static/dynamic02", - "runtime-id": "5c9f7cdc-4833-4e56-a8af-852b6607f988" + "runtime-id": "5c9f7cdc-4833-4e56-a8af-852b6607f988", + "span.kind": "server" }, "metrics": { "_sampling_priority_v1": 1.0 diff --git a/tests/snapshots/tests.integrations.laravel.v9_x.common_scenarios_test.test_scenario_get_to_missing_route.json b/tests/snapshots/tests.integrations.laravel.v9_x.common_scenarios_test.test_scenario_get_to_missing_route.json index 41a084a9bf1..eea5cc8a4bb 100644 --- a/tests/snapshots/tests.integrations.laravel.v9_x.common_scenarios_test.test_scenario_get_to_missing_route.json +++ b/tests/snapshots/tests.integrations.laravel.v9_x.common_scenarios_test.test_scenario_get_to_missing_route.json @@ -14,7 +14,8 @@ "http.method": "GET", "http.status_code": "404", "http.url": "http://localhost/does_not_exist?key=value&", - "runtime-id": "614cba3e-708f-4797-8da5-40fa71e9c272" + "runtime-id": "614cba3e-708f-4797-8da5-40fa71e9c272", + "span.kind": "server" }, "metrics": { "_sampling_priority_v1": 1.0 diff --git a/tooling/bin/build-debug-artifact b/tooling/bin/build-debug-artifact index f79f2b7239c..135f5392aac 100755 --- a/tooling/bin/build-debug-artifact +++ b/tooling/bin/build-debug-artifact @@ -164,7 +164,7 @@ if [[ "$libc" == "musl" ]]; then HOME_DIR="/app" else DOCKER_SHELL="bash" - HOME_DIR="/home/builduser/app" + HOME_DIR="/home/circleci/app" fi # ─── Generate PHP bridge files ─────────────────────────────────────────────── @@ -227,11 +227,11 @@ _preamble() { # $1 = shell commands to run as the host user "echo 'builduser:x:${HOST_UID}:${HOST_GID}::/tmp:/bin/bash' >> /etc/passwd" \ "echo 'buildgroup:x:${HOST_GID}:builduser' >> /etc/group" \ "switch-php ${_switch_php_variant}" \ - "chown -R ${HOST_UID}:${HOST_GID} ${HOME_DIR}/tmp /output /rust/cargo 2>/dev/null || true" \ + "chown -R ${HOST_UID}:${HOST_GID} ${HOME_DIR}/tmp /output 2>/dev/null || true" \ 'chown '"${HOST_UID}:${HOST_GID}"' "$(php-config --extension-dir)" "$(php -r '"'"'echo PHP_CONFIG_FILE_SCAN_DIR;'"'"')" 2>/dev/null || true' \ "cat > /tmp/build.sh <<'BUILDEOF'" \ "set -e" \ - "export CARGO_HOME=/rust/cargo" \ + "export CARGO_HOME=${HOME_DIR}/tmp/cargo_home" \ "cd ${HOME_DIR}" \ "$1" \ "BUILDEOF" \ @@ -298,7 +298,7 @@ _common_volumes=( --platform "$DOCKER_PLATFORM" -v "${REPO_ROOT}:${HOME_DIR}" -v "${CACHE_VOLUME}:${HOME_DIR}/tmp" - -v "ddtrace-cargo-registry-$libc:/rust/cargo/registry" + -v "ddtrace-cargo-registry-$libc:${HOME_DIR}/tmp/cargo_home/registry" -v "${TMP_OUT}:/output" )