diff --git a/Cargo.lock b/Cargo.lock index 72bf6449d2a..81513ade5fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1326,6 +1326,7 @@ dependencies = [ "libdd-crashtracker-ffi", "libdd-data-pipeline", "libdd-library-config-ffi", + "libdd-otel-thread-ctx-ffi", "libdd-remote-config", "libdd-telemetry", "libdd-telemetry-ffi", @@ -3011,6 +3012,23 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "libdd-otel-thread-ctx" +version = "1.0.0" +dependencies = [ + "build_common", + "cc", +] + +[[package]] +name = "libdd-otel-thread-ctx-ffi" +version = "1.0.0" +dependencies = [ + "build_common", + "libdd-common-ffi", + "libdd-otel-thread-ctx", +] + [[package]] name = "libdd-profiling" version = "1.0.0" diff --git a/Makefile b/Makefile index fb02702cc05..e38e577ba9f 100644 --- a/Makefile +++ b/Makefile @@ -48,7 +48,7 @@ RUN_TESTS_CMD := DD_SERVICE= DD_ENV= DD_TRACE_RETRY_INTERVAL=1 REPORT_EXIT_STATU C_FILES = $(shell find components components-rs ext src/dogstatsd tracer 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,libdd-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-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 "*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,libdd-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-otel-thread-ctx*,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 "*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 }' ) @@ -423,9 +423,9 @@ clang_format_fix: cbindgen: remove_cbindgen generate_cbindgen remove_cbindgen: - rm -f components-rs/datadog.h components-rs/live-debugger.h components-rs/telemetry.h components-rs/sidecar.h components-rs/common.h components-rs/crashtracker.h components-rs/library-config.h + rm -f components-rs/datadog.h components-rs/live-debugger.h components-rs/telemetry.h components-rs/sidecar.h components-rs/common.h components-rs/crashtracker.h components-rs/library-config.h components-rs/otel-thread-ctx.h -generate_cbindgen: cbindgen_binary # Regenerate components-rs/datadog.h components-rs/live-debugger.h components-rs/telemetry.h components-rs/sidecar.h components-rs/common.h components-rs/crashtracker.h components-rs/library-config.h +generate_cbindgen: cbindgen_binary # Regenerate components-rs/datadog.h components-rs/live-debugger.h components-rs/telemetry.h components-rs/sidecar.h components-rs/common.h components-rs/crashtracker.h components-rs/library-config.h components-rs/otel-thread-ctx.h ( \ $(command rustup && echo run nightly --) cbindgen --crate datadog-php \ --config cbindgen.toml \ @@ -449,11 +449,14 @@ generate_cbindgen: cbindgen_binary # Regenerate components-rs/datadog.h componen $(command rustup && echo run nightly --) cbindgen --crate libdd-library-config-ffi \ --config libdd-library-config-ffi/cbindgen.toml \ --output $(PROJECT_ROOT)/components-rs/library-config.h; \ + $(command rustup && echo run nightly --) cbindgen --crate libdd-otel-thread-ctx-ffi \ + --config libdd-otel-thread-ctx-ffi/cbindgen.toml \ + --output $(PROJECT_ROOT)/components-rs/otel-thread-ctx.h; \ if test -d $(PROJECT_ROOT)/tmp; then \ mkdir -pv "$(BUILD_DIR)"; \ export CARGO_TARGET_DIR="$(BUILD_DIR)/target"; \ fi; \ - cargo run -p tools --bin dedup_headers -- $(PROJECT_ROOT)/components-rs/common.h $(PROJECT_ROOT)/components-rs/datadog.h $(PROJECT_ROOT)/components-rs/live-debugger.h $(PROJECT_ROOT)/components-rs/telemetry.h $(PROJECT_ROOT)/components-rs/sidecar.h $(PROJECT_ROOT)/components-rs/crashtracker.h $(PROJECT_ROOT)/components-rs/library-config.h \ + cargo run -p tools --bin dedup_headers -- $(PROJECT_ROOT)/components-rs/common.h $(PROJECT_ROOT)/components-rs/datadog.h $(PROJECT_ROOT)/components-rs/live-debugger.h $(PROJECT_ROOT)/components-rs/telemetry.h $(PROJECT_ROOT)/components-rs/sidecar.h $(PROJECT_ROOT)/components-rs/crashtracker.h $(PROJECT_ROOT)/components-rs/library-config.h $(PROJECT_ROOT)/components-rs/otel-thread-ctx.h \ ) cbindgen_binary: diff --git a/components-rs/Cargo.toml b/components-rs/Cargo.toml index c4b8de1c341..9ba399dfaf3 100644 --- a/components-rs/Cargo.toml +++ b/components-rs/Cargo.toml @@ -54,6 +54,9 @@ libc = "0.2" bincode = { version = "1.3.3" } hashbrown = "0.15" +[target.'cfg(target_os = "linux")'.dependencies] +libdd-otel-thread-ctx-ffi = { path = "../libdatadog/libdd-otel-thread-ctx-ffi", default-features = false } + [build-dependencies] cbindgen = "0.27" diff --git a/components-rs/lib.rs b/components-rs/lib.rs index f73beec72d2..0471186f509 100644 --- a/components-rs/lib.rs +++ b/components-rs/lib.rs @@ -13,25 +13,29 @@ pub mod telemetry; pub mod trace_filter; pub mod bytes; -use libdd_common::entity_id::{get_container_id, set_cgroup_file}; +pub use datadog_sidecar_ffi::*; +pub use libdd_crashtracker_ffi::*; +pub use libdd_common_ffi::*; +pub use libdd_library_config_ffi::*; +pub use libdd_telemetry_ffi::*; + use http::uri::{PathAndQuery, Scheme}; use http::Uri; +use libdd_common::entity_id::{get_container_id, set_cgroup_file}; +use libdd_common::{parse_uri, Endpoint}; +use libdd_common_ffi::slice::AsBytes; use std::borrow::Cow; use std::ffi::{c_char, OsStr}; -#[cfg(unix)] -use std::path::Path; use std::ptr::null_mut; use uuid::Uuid; -pub use libdd_crashtracker_ffi::*; -pub use libdd_library_config_ffi::*; -pub use datadog_sidecar_ffi::*; -use libdd_common::{parse_uri, Endpoint}; #[cfg(unix)] use libdd_common::connector::uds::socket_path_to_uri; -use libdd_common_ffi::slice::AsBytes; -pub use libdd_common_ffi::*; -pub use libdd_telemetry_ffi::*; +#[cfg(unix)] +use std::path::Path; + +#[cfg(target_os = "linux")] +pub use libdd_otel_thread_ctx_ffi::*; #[no_mangle] #[allow(non_upper_case_globals)] diff --git a/components-rs/otel-thread-ctx.h b/components-rs/otel-thread-ctx.h new file mode 100644 index 00000000000..0799390d5b4 --- /dev/null +++ b/components-rs/otel-thread-ctx.h @@ -0,0 +1,35 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +#ifndef DDOG_OTEL_THREAD_CTX_H +#define DDOG_OTEL_THREAD_CTX_H + +#pragma once + +#include +#include +#include + +#ifdef __linux__ + +#define ddog_MAX_ATTRS_DATA_SIZE 612 + +typedef struct ddog_ThreadContextHandle ddog_ThreadContextHandle; + +struct ddog_ThreadContextHandle *ddog_otel_thread_ctx_new(const uint8_t (*trace_id)[16], + const uint8_t (*span_id)[8], + const uint8_t (*local_root_span_id)[8]); + +void ddog_otel_thread_ctx_free(struct ddog_ThreadContextHandle *ctx); + +struct ddog_ThreadContextHandle *ddog_otel_thread_ctx_attach(struct ddog_ThreadContextHandle *ctx); + +struct ddog_ThreadContextHandle *ddog_otel_thread_ctx_detach(void); + +void ddog_otel_thread_ctx_update(const uint8_t (*trace_id)[16], + const uint8_t (*span_id)[8], + const uint8_t (*local_root_span_id)[8]); + +#endif + +#endif /* DDOG_OTEL_THREAD_CTX_H */ diff --git a/datadog.sym b/datadog.sym index 510519066f2..9e86ca34e33 100644 --- a/datadog.sym +++ b/datadog.sym @@ -1,6 +1,7 @@ ddtrace_close_all_spans_and_flush datadog_get_formatted_session_id ddtrace_get_profiling_context +otel_thread_ctx_v1 ddtrace_get_root_span datadog_process_tags_get_serialized datadog_get_sidecar_queue_id diff --git a/profiling/build.rs b/profiling/build.rs index 42d0bc2ac34..0e3036b2f7e 100644 --- a/profiling/build.rs +++ b/profiling/build.rs @@ -275,6 +275,8 @@ fn generate_bindings(php_config_includes: &str, fibers: bool, zend_error_observe .raw_line("pub type zend_vm_opcode_handler_func_t = *const ::std::ffi::c_void;") // Block a few of functions that we'll provide defs for manually .blocklist_item("datadog_php_profiling_vm_interrupt_addr") + .blocklist_item("datadog_php_profiling_rinit") + .blocklist_item("datadog_php_profiling_context_api_name") // I had to block these for some reason *shrug* .blocklist_item("FP_INFINITE") .blocklist_item("FP_INT_DOWNWARD") diff --git a/profiling/src/bindings/mod.rs b/profiling/src/bindings/mod.rs index 220be2cd123..b8cedeff4d8 100644 --- a/profiling/src/bindings/mod.rs +++ b/profiling/src/bindings/mod.rs @@ -340,6 +340,14 @@ extern "C" { /// Must be called from a PHP thread during a request. pub fn datadog_php_profiling_vm_interrupt_addr() -> *const AtomicBool; + /// Initializes per-thread profiler FFI state. + /// # Safety + /// Must be called from a PHP thread during a request. + pub fn datadog_php_profiling_rinit(); + + /// Returns the profiling context API selected for this request. + pub fn datadog_php_profiling_context_api_name() -> ZaiStr<'static>; + /// Registers the extension. Note that it's kept in a zend_llist and gets /// pemalloc'd + memcpy'd into place. The engine says this is a mutable /// pointer, but in practice it's const. diff --git a/profiling/src/lib.rs b/profiling/src/lib.rs index 4e7e23bb1ff..08c940f3f0a 100644 --- a/profiling/src/lib.rs +++ b/profiling/src/lib.rs @@ -575,6 +575,8 @@ extern "C" fn rinit(_type: c_int, _module_number: c_int) -> ZendResult { let result = REQUEST_LOCALS.try_with_borrow_mut(|locals| { // SAFETY: we are in rinit on a PHP thread. locals.vm_interrupt_addr = unsafe { zend::datadog_php_profiling_vm_interrupt_addr() }; + // SAFETY: we are in rinit on a PHP thread. + unsafe { zend::datadog_php_profiling_rinit() }; // SAFETY: We are after first rinit and before mshutdown. unsafe { @@ -615,8 +617,9 @@ extern "C" fn rinit(_type: c_int, _module_number: c_int) -> ZendResult { warn!("{err}"); } locals.tags = tags; - locals.profiling_experimental_heap_live_enabled = - system_settings.as_ref().profiling_experimental_heap_live_enabled + locals.profiling_experimental_heap_live_enabled = system_settings + .as_ref() + .profiling_experimental_heap_live_enabled && config::profiling_experimental_heap_live_enabled_current(); } locals.system_settings = system_settings; @@ -653,6 +656,13 @@ extern "C" fn rinit(_type: c_int, _module_number: c_int) -> ZendResult { let once = unsafe { &*ptr::addr_of!(RINIT_ONCE) }; once.call_once(|| { if system_settings.profiling_enabled { + // SAFETY: this returns a view of a static string owned by php_ffi.c. + let context_api = unsafe { bindings::datadog_php_profiling_context_api_name() }; + info!( + "Profiling context API selected: {}.", + context_api.to_string_lossy() + ); + // SAFETY: sapi_module is initialized by rinit and shouldn't be // modified at this point (safe to read values). let sapi_module = unsafe { &*ptr::addr_of!(zend::sapi_module) }; diff --git a/profiling/src/php_ffi.c b/profiling/src/php_ffi.c index 1e906f3cbd3..4dac4903771 100644 --- a/profiling/src/php_ffi.c +++ b/profiling/src/php_ffi.c @@ -7,14 +7,172 @@ #include #include "SAPI.h" -#if CFG_STACK_WALKING_TESTS +#if CFG_STACK_WALKING_TESTS || defined(__linux__) #include // for dlsym #endif +#ifdef __linux__ +#include +#include +#endif const char *datadog_extension_build_id(void) { return ZEND_EXTENSION_BUILD_ID; } const char *datadog_module_build_id(void) { return ZEND_MODULE_BUILD_ID; } uint8_t *datadog_runtime_id = NULL; +static const zai_str datadog_php_profiling_context_api_none = ZAI_STRL("none"); +static const zai_str datadog_php_profiling_context_api_otel = ZAI_STRL("otel_thread_ctx_v1"); +static const zai_str datadog_php_profiling_context_api_legacy = ZAI_STRL("ddtrace_get_profiling_context"); + +static ddtrace_profiling_context noop_get_profiling_context(void) { + return (ddtrace_profiling_context){0, 0}; +} + +static ddtrace_profiling_context datadog_php_profiling_get_context(void); + +static ddtrace_profiling_context (*datadog_php_profiling_get_legacy_context)(void) = + noop_get_profiling_context; + +#ifdef __linux__ +#define DATADOG_PHP_PROFILING_OTEL_ATTRS_DATA_SIZE 612 +#define DATADOG_PHP_PROFILING_OTEL_TLS_SYMBOL "otel_thread_ctx_v1" + +typedef struct datadog_php_profiling_otel_thread_context_record { + uint8_t trace_id[16]; + uint8_t span_id[8]; + _Atomic uint8_t valid; + uint8_t reserved; + uint16_t attrs_data_size; + uint8_t attrs_data[DATADOG_PHP_PROFILING_OTEL_ATTRS_DATA_SIZE]; +} datadog_php_profiling_otel_thread_context_record; + +_Static_assert(sizeof(datadog_php_profiling_otel_thread_context_record) == 640, + "unexpected OTel thread context record size"); +_Static_assert(offsetof(datadog_php_profiling_otel_thread_context_record, trace_id) == 0, + "unexpected OTel thread context trace_id offset"); +_Static_assert(offsetof(datadog_php_profiling_otel_thread_context_record, span_id) == 16, + "unexpected OTel thread context span_id offset"); +_Static_assert(offsetof(datadog_php_profiling_otel_thread_context_record, valid) == 24, + "unexpected OTel thread context valid offset"); +_Static_assert(offsetof(datadog_php_profiling_otel_thread_context_record, reserved) == 25, + "unexpected OTel thread context reserved offset"); +_Static_assert(offsetof(datadog_php_profiling_otel_thread_context_record, attrs_data_size) == 26, + "unexpected OTel thread context attrs_data_size offset"); +_Static_assert(offsetof(datadog_php_profiling_otel_thread_context_record, attrs_data) == 28, + "unexpected OTel thread context attrs_data offset"); + +static __thread void **datadog_php_profiling_otel_thread_ctx_slot = NULL; + +static inline uint64_t datadog_php_profiling_read_u64_be(const uint8_t src[8]) { + uint64_t be_value; + memcpy(&be_value, src, sizeof(be_value)); + +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + return __builtin_bswap64(be_value); +#elif __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ + return be_value; +#else +#error "Unsupported byte order" +#endif +} + +static inline uint8_t datadog_php_profiling_hex_to_u4(uint8_t hex) { + if (hex >= '0' && hex <= '9') { + return (uint8_t)(hex - '0'); + } + if (hex >= 'a' && hex <= 'f') { + return (uint8_t)(hex - 'a' + 10); + } + if (hex >= 'A' && hex <= 'F') { + return (uint8_t)(hex - 'A' + 10); + } + return UINT8_MAX; +} + +static bool datadog_php_profiling_parse_u64_hex(const uint8_t hex[16], uint64_t *value) { + uint64_t result = 0; + + for (size_t i = 0; i < 16; ++i) { + uint8_t nibble = datadog_php_profiling_hex_to_u4(hex[i]); + if (nibble == UINT8_MAX) { + return false; + } + result = (result << 4) | nibble; + } + + *value = result; + return true; +} + +static uint64_t datadog_php_profiling_otel_context_local_root_span_id( + const datadog_php_profiling_otel_thread_context_record *record) { + if (record->attrs_data_size < 18 || record->attrs_data[0] != 0 || record->attrs_data[1] != 16) { + return 0; + } + + uint64_t local_root_span_id = 0; + if (!datadog_php_profiling_parse_u64_hex(record->attrs_data + 2, &local_root_span_id)) { + return 0; + } + + return local_root_span_id; +} + +static ddtrace_profiling_context datadog_php_profiling_read_otel_context(void) { + ddtrace_profiling_context context = {0, 0}; + if (!datadog_php_profiling_otel_thread_ctx_slot) { + return context; + } + + datadog_php_profiling_otel_thread_context_record *record = + (datadog_php_profiling_otel_thread_context_record *)*datadog_php_profiling_otel_thread_ctx_slot; + if (!record || atomic_load_explicit(&record->valid, memory_order_relaxed) != 1) { + return context; + } + + atomic_signal_fence(memory_order_acquire); + + context.span_id = datadog_php_profiling_read_u64_be(record->span_id); + context.local_root_span_id = datadog_php_profiling_otel_context_local_root_span_id(record); + + atomic_signal_fence(memory_order_acquire); + + if (atomic_load_explicit(&record->valid, memory_order_relaxed) != 1) { + return (ddtrace_profiling_context){0, 0}; + } + + return context; +} + +static void *datadog_php_profiling_find_otel_thread_ctx_symbol(void) { + void *tls_symbol = dlsym(RTLD_DEFAULT, DATADOG_PHP_PROFILING_OTEL_TLS_SYMBOL); + if (tls_symbol) { + return tls_symbol; + } + + const zend_llist *extensions = &zend_extensions; + for (const zend_llist_element *item = extensions->head; item; item = item->next) { + const zend_extension *extension = (zend_extension *)item->data; + if (extension && extension->handle) { + tls_symbol = DL_FETCH_SYMBOL(extension->handle, DATADOG_PHP_PROFILING_OTEL_TLS_SYMBOL); + if (tls_symbol) { + return tls_symbol; + } + } + } + + zend_module_entry *module; + ZEND_HASH_FOREACH_PTR(&module_registry, module) { + if (module && module->handle) { + tls_symbol = DL_FETCH_SYMBOL(module->handle, DATADOG_PHP_PROFILING_OTEL_TLS_SYMBOL); + if (tls_symbol) { + return tls_symbol; + } + } + } ZEND_HASH_FOREACH_END(); + + return NULL; +} +#endif static void locate_datadog_runtime_id(const zend_extension *extension) { datadog_runtime_id = DL_FETCH_SYMBOL(extension->handle, "datadog_runtime_id"); @@ -24,7 +182,7 @@ static void locate_ddtrace_get_profiling_context(const zend_extension *extension ddtrace_profiling_context (*get_profiling)(void) = DL_FETCH_SYMBOL(extension->handle, "ddtrace_get_profiling_context"); if (EXPECTED(get_profiling)) { - datadog_php_profiling_get_profiling_context = get_profiling; + datadog_php_profiling_get_legacy_context = get_profiling; } } @@ -40,10 +198,6 @@ static bool is_ddtrace_extension(const zend_extension *ext) { return ext && ext->name && strcmp(ext->name, "ddtrace") == 0; } -static ddtrace_profiling_context noop_get_profiling_context(void) { - return (ddtrace_profiling_context){0, 0}; -} - static zend_string *noop_get_process_tags_serialized(void) { return NULL; } @@ -155,7 +309,8 @@ void datadog_php_profiling_startup(zend_extension *extension) { _ignore_run_time_cache = strcmp(sapi_module.name, "cli") == 0; #endif - datadog_php_profiling_get_profiling_context = noop_get_profiling_context; + datadog_php_profiling_get_profiling_context = datadog_php_profiling_get_context; + datadog_php_profiling_get_legacy_context = noop_get_profiling_context; datadog_php_profiling_get_process_tags_serialized = noop_get_process_tags_serialized; /* Due to the optional dependency on ddtrace, the profiling module will be @@ -181,14 +336,43 @@ void datadog_php_profiling_startup(zend_extension *extension) { #endif } +void datadog_php_profiling_rinit(void) { +#ifdef __linux__ + datadog_php_profiling_otel_thread_ctx_slot = + (void **)datadog_php_profiling_find_otel_thread_ctx_symbol(); +#endif +} + +zai_str datadog_php_profiling_context_api_name(void) { +#ifdef __linux__ + if (datadog_php_profiling_otel_thread_ctx_slot) { + return datadog_php_profiling_context_api_otel; + } +#endif + if (datadog_php_profiling_get_legacy_context != noop_get_profiling_context) { + return datadog_php_profiling_context_api_legacy; + } + return datadog_php_profiling_context_api_none; +} + void *datadog_php_profiling_vm_interrupt_addr(void) { return &EG(vm_interrupt); } zend_module_entry *datadog_get_module_entry(const char *str, uintptr_t len) { return zend_hash_str_find_ptr(&module_registry, str, len); } +static ddtrace_profiling_context datadog_php_profiling_get_context(void) { +#ifdef __linux__ + ddtrace_profiling_context otel_context = datadog_php_profiling_read_otel_context(); + if (otel_context.local_root_span_id || otel_context.span_id) { + return otel_context; + } +#endif + return datadog_php_profiling_get_legacy_context(); +} + ddtrace_profiling_context (*datadog_php_profiling_get_profiling_context)(void) = - noop_get_profiling_context; + datadog_php_profiling_get_context; zend_string *(*datadog_php_profiling_get_process_tags_serialized)(void) = noop_get_process_tags_serialized; diff --git a/profiling/src/php_ffi.h b/profiling/src/php_ffi.h index 558c3de4413..6771f900244 100644 --- a/profiling/src/php_ffi.h +++ b/profiling/src/php_ffi.h @@ -73,9 +73,9 @@ zend_module_entry *datadog_get_module_entry(const char *str, uintptr_t len); void *datadog_php_profiling_vm_interrupt_addr(void); /** - * For Code Hotspots, we need the tracer's local root span id and the current - * span id. This is a cross-product struct, so keep it in sync with tracer's - * version of this struct. + * For Code Hotspots, we need the local root span id and the current span id. + * The legacy ddtrace_get_profiling_context ABI also uses this struct, so keep + * it in sync with tracer's version. * todo: re-use the tracer's header? */ typedef struct ddtrace_profiling_context_s { @@ -83,8 +83,10 @@ typedef struct ddtrace_profiling_context_s { } ddtrace_profiling_context; /** - * A pointer to the tracer's ddtrace_get_profiling_context function if it was - * found, otherwise points to a function which just returns {0, 0}. + * A pointer to the profiling-context function. On Linux it first reads the + * OTel thread-context ABI directly when available, then falls back to the + * tracer's legacy ddtrace_get_profiling_context function if it was found. + * Otherwise it returns {0, 0}. */ extern ddtrace_profiling_context (*datadog_php_profiling_get_profiling_context)(void); @@ -101,6 +103,18 @@ extern zend_string *(*datadog_php_profiling_get_process_tags_serialized)(void); */ void datadog_php_profiling_startup(zend_extension *extension); +/** + * Called by this zend_extension's .activate handler to initialize per-thread + * profiler FFI state. + */ +void datadog_php_profiling_rinit(void); + +/** + * Returns the profiling context API selected for this request, or "none" when + * no provider was found. + */ +zai_str datadog_php_profiling_context_api_name(void); + /** * Used to hold information for overwriting the internal function handler * pointer in the Zend Engine. diff --git a/profiling/src/profiling/mod.rs b/profiling/src/profiling/mod.rs index c3d5a53a22d..9c26f2007a1 100644 --- a/profiling/src/profiling/mod.rs +++ b/profiling/src/profiling/mod.rs @@ -1779,12 +1779,17 @@ impl Profiler { // Casting between two integers of the same size is a no-op, and // Rust uses 2's complement for negative numbers. let local_root_span_id = context.local_root_span_id as i64; - let span_id = context.span_id as i64; labels.push(Label { key: "local root span id", value: LabelValue::Num(local_root_span_id, ""), }); + } + + if context.span_id != 0 { + // Casting between two integers of the same size is a no-op, and + // Rust uses 2's complement for negative numbers. + let span_id = context.span_id as i64; labels.push(Label { key: "span id", diff --git a/tracer/ddtrace.c b/tracer/ddtrace.c index 7f5e9681d92..efbec9887b0 100644 --- a/tracer/ddtrace.c +++ b/tracer/ddtrace.c @@ -57,6 +57,7 @@ #include "live_debugger.h" #include "standalone_limiter.h" #include "priority_sampling/priority_sampling.h" +#include "profiling.h" #include "random.h" #include "autoload_php_files.h" #include "serializer.h" @@ -608,6 +609,7 @@ void ddtrace_rshutdown(bool fast_shutdown) { OBJ_RELEASE(&DDTRACE_G(active_stack)->std); } DDTRACE_G(active_stack) = NULL; + ddtrace_detach_otel_thread_context(); } ddtrace_ffe_flush_exposures(); @@ -656,6 +658,7 @@ bool datadog_alter_dd_trace_disabled_config(zval *old_value, zval *new_value, ze } else if (!datadog_disable) { // if this is true, the request has not been initialized at all ddtrace_close_all_open_spans(false); // All remaining userland spans (and root span) dd_clean_globals(); + ddtrace_detach_otel_thread_context(); } return true; diff --git a/tracer/handlers_fiber.c b/tracer/handlers_fiber.c index 4999f6b0205..45ca77caf83 100644 --- a/tracer/handlers_fiber.c +++ b/tracer/handlers_fiber.c @@ -1,6 +1,7 @@ #include "ddtrace.h" #include "configuration.h" #include "handlers_fiber.h" +#include "profiling.h" #include "span.h" #include #include @@ -130,6 +131,7 @@ static void dd_observe_fiber_switch(zend_fiber_context *from, zend_fiber_context from->reserved[dd_resource_handle] = DDTRACE_G(active_stack); DDTRACE_G(active_stack) = to_stack; + ddtrace_update_otel_thread_context(); } static void dd_observe_fiber_init(zend_fiber_context *context) { diff --git a/tracer/profiling.c b/tracer/profiling.c index ccd675648ab..69ea58ce1fe 100644 --- a/tracer/profiling.c +++ b/tracer/profiling.c @@ -4,6 +4,11 @@ #include "ddtrace.h" #include "span.h" +#ifdef __linux__ +#include +#include +#endif + ZEND_EXTERN_MODULE_GLOBALS(datadog); DATADOG_PUBLIC struct ddtrace_profiling_context ddtrace_get_profiling_context(void) { @@ -18,3 +23,51 @@ DATADOG_PUBLIC struct ddtrace_profiling_context ddtrace_get_profiling_context(vo } return context; } + +#ifdef __linux__ +static inline void ddtrace_write_u64_be(uint8_t dest[8], uint64_t value) { + uint64_t be_value = +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + __builtin_bswap64(value); +#elif __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ + value; +#else +#error "Unsupported byte order" +#endif + memcpy(dest, &be_value, sizeof(be_value)); +} + +static void ddtrace_trace_id_to_otel_bytes(datadog_trace_id trace_id, uint8_t dest[16]) { + ddtrace_write_u64_be(dest, trace_id.high); + ddtrace_write_u64_be(dest + 8, trace_id.low); +} + +void ddtrace_detach_otel_thread_context(void) { + struct ddog_ThreadContextHandle *ctx = ddog_otel_thread_ctx_detach(); + ddog_otel_thread_ctx_free(ctx); +} + +void ddtrace_update_otel_thread_context(void) { + if (!DDTRACE_G(active_stack) || !DDTRACE_G(active_stack)->root_span || !DDTRACE_G(active_stack)->active || + !get_DD_TRACE_ENABLED()) { + ddtrace_detach_otel_thread_context(); + return; + } + + ddtrace_root_span_data *root = DDTRACE_G(active_stack)->root_span; + ddtrace_span_data *span = SPANDATA(DDTRACE_G(active_stack)->active); + + uint8_t trace_id[16]; + uint8_t span_id[8]; + uint8_t local_root_span_id[8]; + + ddtrace_trace_id_to_otel_bytes(root->trace_id, trace_id); + ddtrace_write_u64_be(span_id, span->span_id); + ddtrace_write_u64_be(local_root_span_id, root->span_id); + + ddog_otel_thread_ctx_update(&trace_id, &span_id, &local_root_span_id); +} +#else +void ddtrace_detach_otel_thread_context(void) {} +void ddtrace_update_otel_thread_context(void) {} +#endif diff --git a/tracer/profiling.h b/tracer/profiling.h index 5da0caf40e5..25ef4c0c43d 100644 --- a/tracer/profiling.h +++ b/tracer/profiling.h @@ -22,6 +22,18 @@ BEGIN_EXTERN_C() */ DATADOG_PUBLIC struct ddtrace_profiling_context ddtrace_get_profiling_context(void); +/** + * Publish the current active tracer context through Linux's OTel thread-context + * TLS slot. On non-Linux builds this is a no-op. + */ +void ddtrace_update_otel_thread_context(void); + +/** + * Detach and release the current OTel thread context. On non-Linux builds this + * is a no-op. + */ +void ddtrace_detach_otel_thread_context(void); + END_EXTERN_C() #endif // DDTRACE_PROFILING_H diff --git a/tracer/span.c b/tracer/span.c index a744e020cc2..f3bfe23b76c 100644 --- a/tracer/span.c +++ b/tracer/span.c @@ -25,6 +25,7 @@ #include "standalone_limiter.h" #include "code_origins.h" #include "endpoint_guessing.h" +#include "profiling.h" #define USE_REALTIME_CLOCK 0 #define USE_MONOTONIC_CLOCK 1 @@ -144,6 +145,7 @@ void ddtrace_free_span_stacks(bool silent) { DDTRACE_G(dropped_spans_count) = 0; DDTRACE_G(closed_spans_count) = 0; DDTRACE_G(top_closed_stack) = NULL; + ddtrace_detach_otel_thread_context(); } static ddtrace_span_data *ddtrace_init_span(enum ddtrace_span_dataype type, zend_class_entry *ce) { @@ -307,6 +309,7 @@ ddtrace_span_data *ddtrace_open_span(enum ddtrace_span_dataype type) { span->root = DDTRACE_G(active_stack)->root_span; ddtrace_set_global_span_properties(span); + ddtrace_update_otel_thread_context(); if (root_span) { ddtrace_root_span_data *root = ROOTSPANDATA(&span->std); @@ -583,6 +586,7 @@ void ddtrace_switch_span_stack(ddtrace_span_stack *target_stack) { GC_ADDREF(&target_stack->std); ddtrace_span_stack *active_stack = DDTRACE_G(active_stack); DDTRACE_G(active_stack) = target_stack; + ddtrace_update_otel_thread_context(); OBJ_RELEASE(&active_stack->std); } @@ -949,6 +953,7 @@ void ddtrace_close_top_span_without_stack_swap(ddtrace_span_data *span) { } else { ZVAL_NULL(&stack->property_active); } + ddtrace_update_otel_thread_context(); #if PHP_VERSION_ID < 70400 // On PHP 7.3 and prior PHP will just destroy all unchanged references in cycle collection, in particular given that it does not appear in get_gc // Artificially increase refcount here thus. @@ -1076,6 +1081,7 @@ void ddtrace_drop_span(ddtrace_span_data *span) { } else { ZVAL_NULL(&stack->property_active); } + ddtrace_update_otel_thread_context(); ++DDTRACE_G(dropped_spans_count); --DDTRACE_G(open_spans_count);