From 9d6028efab2edb135f4d029a65ec427332fc180d Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Tue, 9 Jun 2026 18:55:24 -0600 Subject: [PATCH] feat: otel context sharing --- Cargo.lock | 18 ++++ Makefile | 11 ++- components-rs/Cargo.toml | 3 + components-rs/lib.rs | 24 +++-- components-rs/otel-thread-ctx.h | 35 +++++++ datadog.sym | 2 + profiling/build.rs | 1 + profiling/src/bindings/mod.rs | 3 + profiling/src/lib.rs | 7 ++ profiling/src/php_ffi.c | 20 ++++ profiling/src/php_ffi.h | 11 ++- tracer/ddtrace.c | 3 + tracer/handlers_fiber.c | 2 + tracer/profiling.c | 157 ++++++++++++++++++++++++++++++++ tracer/profiling.h | 21 +++++ tracer/span.c | 6 ++ 16 files changed, 308 insertions(+), 16 deletions(-) create mode 100644 components-rs/otel-thread-ctx.h diff --git a/Cargo.lock b/Cargo.lock index 9ae31f85407..b9425899220 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1312,6 +1312,7 @@ dependencies = [ "libdd-crashtracker-ffi", "libdd-data-pipeline", "libdd-library-config-ffi", + "libdd-otel-thread-ctx-ffi", "libdd-telemetry", "libdd-telemetry-ffi", "libdd-tinybytes", @@ -2994,6 +2995,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 1f9328e630c..a26ad4b4d0f 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 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,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-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,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-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 33ebe86033a..dac0e09c12b 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..5deb0c3c07a 100644 --- a/datadog.sym +++ b/datadog.sym @@ -1,6 +1,8 @@ ddtrace_close_all_spans_and_flush datadog_get_formatted_session_id ddtrace_get_profiling_context +ddtrace_get_profiling_otel_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 4a289514b42..df815f60eb8 100644 --- a/profiling/build.rs +++ b/profiling/build.rs @@ -270,6 +270,7 @@ 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_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..32e4c253ca0 100644 --- a/profiling/src/bindings/mod.rs +++ b/profiling/src/bindings/mod.rs @@ -340,6 +340,9 @@ extern "C" { /// Must be called from a PHP thread during a request. pub fn datadog_php_profiling_vm_interrupt_addr() -> *const AtomicBool; + /// Returns the ddtrace profiling context API selected during startup. + 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 286505c57bc..0dead276d34 100644 --- a/profiling/src/lib.rs +++ b/profiling/src/lib.rs @@ -648,6 +648,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 14f4c2dad0f..4be68aeccff 100644 --- a/profiling/src/php_ffi.c +++ b/profiling/src/php_ffi.c @@ -15,16 +15,31 @@ 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("ddtrace_get_profiling_otel_context"); +static const zai_str datadog_php_profiling_context_api_legacy = ZAI_STRL("ddtrace_get_profiling_context"); +static zai_str datadog_php_profiling_context_api = ZAI_STRL("none"); static void locate_datadog_runtime_id(const zend_extension *extension) { datadog_runtime_id = DL_FETCH_SYMBOL(extension->handle, "datadog_runtime_id"); } static void locate_ddtrace_get_profiling_context(const zend_extension *extension) { +#ifdef __linux__ + ddtrace_profiling_context (*get_profiling_otel)(void) = + DL_FETCH_SYMBOL(extension->handle, "ddtrace_get_profiling_otel_context"); + if (EXPECTED(get_profiling_otel)) { + datadog_php_profiling_get_profiling_context = get_profiling_otel; + datadog_php_profiling_context_api = datadog_php_profiling_context_api_otel; + return; + } +#endif + 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_context_api = datadog_php_profiling_context_api_legacy; } } @@ -153,6 +168,7 @@ void datadog_php_profiling_startup(zend_extension *extension) { datadog_php_profiling_get_profiling_context = noop_get_profiling_context; datadog_php_profiling_get_process_tags_serialized = noop_get_process_tags_serialized; + datadog_php_profiling_context_api = datadog_php_profiling_context_api_none; /* Due to the optional dependency on ddtrace, the profiling module will be * loaded after ddtrace if it's present, so ddtrace should always be found @@ -177,6 +193,10 @@ void datadog_php_profiling_startup(zend_extension *extension) { #endif } +zai_str datadog_php_profiling_context_api_name(void) { + return datadog_php_profiling_context_api; +} + 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) { diff --git a/profiling/src/php_ffi.h b/profiling/src/php_ffi.h index cd61b95407d..9a386eb9820 100644 --- a/profiling/src/php_ffi.h +++ b/profiling/src/php_ffi.h @@ -83,8 +83,9 @@ 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 tracer's profiling-context function if it was found, + * otherwise points to a function which just returns {0, 0}. On Linux this + * prefers ddtrace_get_profiling_otel_context when available. */ extern ddtrace_profiling_context (*datadog_php_profiling_get_profiling_context)(void); @@ -101,6 +102,12 @@ extern zend_string *(*datadog_php_profiling_get_process_tags_serialized)(void); */ void datadog_php_profiling_startup(zend_extension *extension); +/** + * Returns the ddtrace profiling context API selected during zend_extension + * startup, 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/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..013c2ce7652 100644 --- a/tracer/profiling.c +++ b/tracer/profiling.c @@ -4,6 +4,14 @@ #include "ddtrace.h" #include "span.h" +#ifdef __linux__ +#include +#include +#include +#include +#include +#endif + ZEND_EXTERN_MODULE_GLOBALS(datadog); DATADOG_PUBLIC struct ddtrace_profiling_context ddtrace_get_profiling_context(void) { @@ -18,3 +26,152 @@ DATADOG_PUBLIC struct ddtrace_profiling_context ddtrace_get_profiling_context(vo } return context; } + +#ifdef __linux__ +typedef struct ddtrace_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[ddog_MAX_ATTRS_DATA_SIZE]; +} ddtrace_otel_thread_context_record; + +_Static_assert(sizeof(ddtrace_otel_thread_context_record) == 640, "unexpected OTel thread context record size"); +_Static_assert(offsetof(ddtrace_otel_thread_context_record, trace_id) == 0, + "unexpected OTel thread context trace_id offset"); +_Static_assert(offsetof(ddtrace_otel_thread_context_record, span_id) == 16, + "unexpected OTel thread context span_id offset"); +_Static_assert(offsetof(ddtrace_otel_thread_context_record, valid) == 24, + "unexpected OTel thread context valid offset"); +_Static_assert(offsetof(ddtrace_otel_thread_context_record, reserved) == 25, + "unexpected OTel thread context reserved offset"); +_Static_assert(offsetof(ddtrace_otel_thread_context_record, attrs_data_size) == 26, + "unexpected OTel thread context attrs_data_size offset"); +_Static_assert(offsetof(ddtrace_otel_thread_context_record, attrs_data) == 28, + "unexpected OTel thread context attrs_data offset"); + +extern void **libdd_get_otel_thread_ctx_v1(void); + +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 inline uint64_t ddtrace_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 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); +} + +static inline uint8_t ddtrace_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 ddtrace_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 = ddtrace_hex_to_u4(hex[i]); + if (nibble == UINT8_MAX) { + return false; + } + result = (result << 4) | nibble; + } + + *value = result; + return true; +} + +static uint64_t ddtrace_otel_context_local_root_span_id(const ddtrace_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 (!ddtrace_parse_u64_hex(record->attrs_data + 2, &local_root_span_id)) { + return 0; + } + + return local_root_span_id; +} + +DATADOG_PUBLIC struct ddtrace_profiling_context ddtrace_get_profiling_otel_context(void) { + struct ddtrace_profiling_context context = {0, 0}; + ddtrace_otel_thread_context_record *record = + (ddtrace_otel_thread_context_record *)*libdd_get_otel_thread_ctx_v1(); + if (!record || atomic_load_explicit(&record->valid, memory_order_relaxed) != 1) { + return context; + } + + atomic_signal_fence(memory_order_acquire); + + context.span_id = ddtrace_read_u64_be(record->span_id); + context.local_root_span_id = ddtrace_otel_context_local_root_span_id(record); + + atomic_signal_fence(memory_order_acquire); + + if (atomic_load_explicit(&record->valid, memory_order_relaxed) != 1) { + return (struct ddtrace_profiling_context){0, 0}; + } + + return context; +} + +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..ee42e29f509 100644 --- a/tracer/profiling.h +++ b/tracer/profiling.h @@ -22,6 +22,27 @@ BEGIN_EXTERN_C() */ DATADOG_PUBLIC struct ddtrace_profiling_context ddtrace_get_profiling_context(void); +#ifdef __linux__ +/** + * Provide the active trace information for the profiler from Linux's OTel + * thread-context TLS slot. If there isn't an active published context, return + * 0 for both values. + */ +DATADOG_PUBLIC struct ddtrace_profiling_context ddtrace_get_profiling_otel_context(void); +#endif + +/** + * 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);