From 8c942a0b4cf93f35e883b1efb4045db963445f23 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Wed, 29 Jul 2026 16:43:03 -0600 Subject: [PATCH 01/37] feat(profiling): publish Linux OTel contexts Publish standard OTel process and thread contexts from the tracer and make the profiler consume them for runtime identity and effective service metadata. Handle span, stack, Fiber, configuration, and fork lifecycle changes while retaining the legacy non-Linux path. --- Cargo.lock | 21 + Cargo.toml | 19 +- Makefile | 2 +- appsec/cmake/ddtrace.cmake | 6 +- components-rs/Cargo.toml | 4 + components-rs/datadog.h | 22 + components-rs/lib.rs | 105 ++++ config.m4 | 13 +- config.w32 | 1 + datadog-linux.sym | 1 + ext/datadog.c | 4 + ext/otel_context.c | 79 +++ ext/otel_context.h | 22 + ext/sidecar.c | 6 +- libdatadog | 2 +- package.xml | 1 + profiling/Cargo.toml | 4 + profiling/src/lib.rs | 30 +- profiling/src/module_globals.rs | 24 +- profiling/src/php_ffi.c | 25 + profiling/src/php_ffi.h | 6 + profiling/src/process_context.rs | 66 ++ profiling/src/process_context/linux.rs | 568 ++++++++++++++++++ profiling/src/profiling/mod.rs | 255 +++++++- profiling/src/pthread.rs | 1 + .../fibers/otel_thread_context_switch.phpt | 49 ++ tests/ext/includes/otel_thread_context.inc | 92 +++ tests/ext/otel_process_context.phpt | 124 ++++ tests/ext/otel_thread_context_lifecycle.phpt | 80 +++ .../ext/otel_thread_context_stack_switch.phpt | 55 ++ .../ext/pcntl/otel_process_context_fork.phpt | 125 ++++ tooling/bin/verify-dd-trace-php.sh | 52 +- tracer/ddtrace.c | 9 + tracer/functions.c | 4 + tracer/handlers_fiber.c | 2 + tracer/priority_sampling/priority_sampling.c | 3 + tracer/profiling.c | 73 +++ tracer/profiling.h | 11 + tracer/span.c | 6 + zend_abstract_interface/components_rs.cmake | 6 +- 40 files changed, 1925 insertions(+), 53 deletions(-) create mode 100644 datadog-linux.sym create mode 100644 ext/otel_context.c create mode 100644 ext/otel_context.h create mode 100644 profiling/src/process_context.rs create mode 100644 profiling/src/process_context/linux.rs create mode 100644 tests/ext/fibers/otel_thread_context_switch.phpt create mode 100644 tests/ext/includes/otel_thread_context.inc create mode 100644 tests/ext/otel_process_context.phpt create mode 100644 tests/ext/otel_thread_context_lifecycle.phpt create mode 100644 tests/ext/otel_thread_context_stack_switch.phpt create mode 100644 tests/ext/pcntl/otel_process_context_fork.phpt diff --git a/Cargo.lock b/Cargo.lock index 7d190dfa02c..dcf39737696 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1339,7 +1339,9 @@ dependencies = [ "libdd-crashtracker", "libdd-crashtracker-ffi", "libdd-data-pipeline", + "libdd-library-config", "libdd-library-config-ffi", + "libdd-otel-thread-ctx", "libdd-remote-config", "libdd-telemetry", "libdd-telemetry-ffi", @@ -1384,8 +1386,10 @@ dependencies = [ "libc 0.2.186", "libdd-alloc", "libdd-common", + "libdd-library-config", "libdd-library-config-ffi", "libdd-profiling", + "libdd-trace-protobuf", "log", "mach2", "perfcnt", @@ -1650,6 +1654,12 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "elf" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4445909572dbd556c457c849c4ca58623d84b27c8fff1e74b0b4227d8b90d17b" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -2956,6 +2966,7 @@ dependencies = [ "tracing", "uuid", "web-time", + "zstd", ] [[package]] @@ -3038,6 +3049,16 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "libdd-otel-thread-ctx" +version = "1.0.0" +dependencies = [ + "anyhow", + "elf", + "libdd-otel-thread-ctx", + "object 0.36.7", +] + [[package]] name = "libdd-profiling" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index ed732446587..61b26ea0ba8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,22 +42,21 @@ inherits = "release" # level, so they are mirrored here too. [workspace.dependencies] anyhow = { version = "1.0", default-features = false } -arc-swap = "1.7.1" -hyper = { version = "1.6", features = [ - "http1", - "client", -], default-features = false } -hyper-util = { version = "0.1.10", features = [ - "http1", - "client", - "client-legacy", -] } +arc-swap = { version = "1.7.1", default-features = false } +futures = { version = "0.3", default-features = false } +hyper = { version = "1.6", default-features = false } +hyper-util = { version = "0.1.10", default-features = false } +io-lifetimes = { version = "1.0", default-features = false } +libc = { version = "0.2", default-features = true } prost-build = { version = "0.14.1", default-features = false } protoc-bin-vendored = { version = "3.0.0", default-features = false } +rustls = { version = "0.23", default-features = false } serde = { version = "1.0", default-features = false } serde_json = { version = "1.0", default-features = false, features = ["alloc"] } +syn = { version = "^2", default-features = false } tokio = { version = "1.36", default-features = false } tracing = { version = "0.1", default-features = false } +uuid = { version = "1.7.0", default-features = false } [workspace.lints] # empty for compat with libdatadog diff --git a/Makefile b/Makefile index aa1ba9dad6f..38a926cc410 100644 --- a/Makefile +++ b/Makefile @@ -106,7 +106,7 @@ JUNIT_RESULTS_DIR := $(shell pwd) all: $(BUILD_DIR)/configure $(SO_FILE) -$(BUILD_DIR)/configure: $(M4_FILES) $(BUILD_DIR)/datadog.sym $(BUILD_DIR)/VERSION +$(BUILD_DIR)/configure: $(M4_FILES) $(BUILD_DIR)/datadog.sym $(BUILD_DIR)/datadog-linux.sym $(BUILD_DIR)/VERSION $(Q) (cd $(BUILD_DIR); phpize && $(SED_I) 's/\/FAILED/\/\\bFAILED/' $(BUILD_DIR)/run-tests.php) # Fix PHP 5.4 exit code bug when running selected tests (FAILED vs XFAILED) $(BUILD_DIR)/run-tests.php: $(if $(ASSUME_COMPILED),, $(BUILD_DIR)/configure) diff --git a/appsec/cmake/ddtrace.cmake b/appsec/cmake/ddtrace.cmake index 376115a0685..4bc2e77cea4 100644 --- a/appsec/cmake/ddtrace.cmake +++ b/appsec/cmake/ddtrace.cmake @@ -26,9 +26,11 @@ add_custom_target(libdatadog_stamp if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(EXPORTS_FILE "${CMAKE_BINARY_DIR}/ddtrace_exports.version") add_custom_target(ddtrace_exports - COMMAND bash -c "{ echo -e '{\\nglobal:'; sed 's/$/;/' '${CMAKE_SOURCE_DIR}'/../datadog.sym; echo -e 'local:\\n*;\\n};'; } > '${EXPORTS_FILE}'" + COMMAND bash -c "{ echo -e '{\\nglobal:'; sed 's/$/;/' '${CMAKE_SOURCE_DIR}'/../datadog.sym '${CMAKE_SOURCE_DIR}'/../datadog-linux.sym; echo -e 'local:\\n*;\\n};'; } > '${EXPORTS_FILE}'" BYPRODUCTS ${EXPORTS_FILE} - DEPENDS ${CMAKE_SOURCE_DIR}/../datadog.sym + DEPENDS + ${CMAKE_SOURCE_DIR}/../datadog.sym + ${CMAKE_SOURCE_DIR}/../datadog-linux.sym VERBATIM ) elseif(APPLE) diff --git a/components-rs/Cargo.toml b/components-rs/Cargo.toml index 7c78ff2cad6..2b288088c8b 100644 --- a/components-rs/Cargo.toml +++ b/components-rs/Cargo.toml @@ -56,6 +56,10 @@ libc = "0.2" bincode = { version = "1.3.3" } hashbrown = "0.15" +[target.'cfg(target_os = "linux")'.dependencies] +libdd-library-config = { path = "../libdatadog/libdd-library-config", default-features = false, features = ["otel-thread-ctx"] } +libdd-otel-thread-ctx = { path = "../libdatadog/libdd-otel-thread-ctx" } + [build-dependencies] cbindgen = "0.27" diff --git a/components-rs/datadog.h b/components-rs/datadog.h index 21c7f656fe4..da8c9c78624 100644 --- a/components-rs/datadog.h +++ b/components-rs/datadog.h @@ -41,6 +41,28 @@ void datadog_generate_session_id(void); void datadog_format_runtime_id(uint8_t (*buf)[36]); +#ifdef __linux__ +bool datadog_publish_otel_process_context(ddog_CharSlice runtime_id, + ddog_CharSlice tracer_version, + ddog_CharSlice service, + ddog_CharSlice env, + ddog_CharSlice version, + ddog_CharSlice hostname, + ddog_CharSlice container_id, + ddog_CharSlice process_tags); + +void datadog_update_otel_thread_context(const uint8_t (*trace_id)[16], + const uint8_t (*span_id)[8], + uint8_t trace_flags, + const uint8_t (*local_root_span_id)[8], + ddog_CharSlice service, + ddog_CharSlice env, + ddog_CharSlice version, + ddog_CharSlice thread_id); + +void datadog_detach_otel_thread_context(void); +#endif + ddog_CharSlice ddtrace_get_container_id(void); void ddtrace_set_container_cgroup_path(ddog_CharSlice path); diff --git a/components-rs/lib.rs b/components-rs/lib.rs index c63ab3fa4f4..3f0f788dbb0 100644 --- a/components-rs/lib.rs +++ b/components-rs/lib.rs @@ -93,6 +93,111 @@ pub extern "C" fn datadog_format_runtime_id(buf: &mut [u8; 36]) { unsafe { datadog_runtime_id.as_hyphenated().encode_lower(buf) }; } +#[cfg(target_os = "linux")] +fn char_slice_string(value: CharSlice<'_>) -> String { + value + .try_to_utf8() + .map(ToOwned::to_owned) + .unwrap_or_default() +} + +/// Publish or update dd-trace-php's standard Linux OTel Process Context. +#[cfg(target_os = "linux")] +#[no_mangle] +pub extern "C" fn datadog_publish_otel_process_context( + runtime_id: CharSlice<'_>, + tracer_version: CharSlice<'_>, + service: CharSlice<'_>, + env: CharSlice<'_>, + version: CharSlice<'_>, + hostname: CharSlice<'_>, + container_id: CharSlice<'_>, + process_tags: CharSlice<'_>, +) -> bool { + use libdd_library_config::otel_process_ctx; + use libdd_library_config::tracer_metadata::{ThreadLocalMetadata, TracerMetadata}; + + let metadata = TracerMetadata { + runtime_id: Some(char_slice_string(runtime_id)), + tracer_language: "php".to_owned(), + tracer_version: char_slice_string(tracer_version), + hostname: char_slice_string(hostname), + service_name: Some(char_slice_string(service)), + service_env: Some(char_slice_string(env)), + service_version: Some(char_slice_string(version)), + process_tags: Some(char_slice_string(process_tags)), + container_id: Some(char_slice_string(container_id)), + threadlocal_metadata: Some(ThreadLocalMetadata { + attribute_keys: vec![ + "service.name".to_owned(), + "deployment.environment.name".to_owned(), + "service.version".to_owned(), + "thread.id".to_owned(), + ], + ..Default::default() + }), + ..Default::default() + }; + + otel_process_ctx::publish(&metadata.to_otel_process_ctx()).is_ok() +} + +/// Update the calling thread's standard Linux OTel Thread Context record. +#[cfg(target_os = "linux")] +#[no_mangle] +pub extern "C" fn datadog_update_otel_thread_context( + trace_id: &[u8; 16], + span_id: &[u8; 8], + trace_flags: u8, + local_root_span_id: &[u8; 8], + service: CharSlice<'_>, + env: CharSlice<'_>, + version: CharSlice<'_>, + thread_id: CharSlice<'_>, +) { + use libdd_otel_thread_ctx::linux::ThreadContext; + + let service = service.try_to_utf8().unwrap_or_default(); + let env = env.try_to_utf8().unwrap_or_default(); + let version = version.try_to_utf8().unwrap_or_default(); + let thread_id = thread_id.try_to_utf8().unwrap_or_default(); + let mut attrs = [(0, ""); 4]; + let mut attrs_len = 0; + if !service.is_empty() { + attrs[attrs_len] = (1, service); + attrs_len += 1; + } + if !env.is_empty() { + attrs[attrs_len] = (2, env); + attrs_len += 1; + } + if !version.is_empty() { + attrs[attrs_len] = (3, version); + attrs_len += 1; + } + if !thread_id.is_empty() { + attrs[attrs_len] = (4, thread_id); + attrs_len += 1; + } + + ThreadContext::update( + *trace_id, + *span_id, + trace_flags, + *local_root_span_id, + &attrs[..attrs_len], + ); +} + +/// Detach and release the calling thread's Linux OTel Thread Context record. +#[cfg(target_os = "linux")] +#[no_mangle] +pub extern "C" fn datadog_detach_otel_thread_context() { + use libdd_otel_thread_ctx::linux::ThreadContext; + + drop(ThreadContext::detach()); +} + #[must_use] #[no_mangle] pub extern "C" fn ddtrace_get_container_id() -> CharSlice<'static> { diff --git a/config.m4 b/config.m4 index aa69d309a43..ed607786435 100644 --- a/config.m4 +++ b/config.m4 @@ -183,6 +183,7 @@ if test "$PHP_DDTRACE" != "no"; then ext/handlers_signal.c \ ext/logging.c \ ext/otel_config.c \ + ext/otel_context.c \ ext/phpinfo.c \ ext/process_tags.c \ ext/remote_config.c \ @@ -307,10 +308,18 @@ if test "$PHP_DDTRACE" != "no"; then AC_CHECK_HEADER(time.h, [], [AC_MSG_ERROR([Cannot find or include time.h])]) if test "$ext_shared" = "yes"; then - dnl Only export symbols defined in datadog.sym, which should all be marked as + dnl Only export the platform's listed symbols, which should all be marked as dnl DATADOG_PUBLIC in their source files as well. EXTRA_CFLAGS="$EXTRA_CFLAGS -fvisibility=hidden" - EXTRA_LDFLAGS="$EXTRA_LDFLAGS -export-symbols $ext_srcdir/datadog.sym -flto -fuse-linker-plugin" + case $host_os in + linux*) + DDTRACE_EXPORT_SYMBOLS="$ext_builddir/datadog-linux-exports.sym" + cat "$ext_srcdir/datadog.sym" "$ext_srcdir/datadog-linux.sym" > "$DDTRACE_EXPORT_SYMBOLS" \ + || AC_MSG_ERROR([failed to generate Linux export-symbol list]) + ;; + *) DDTRACE_EXPORT_SYMBOLS="$ext_srcdir/datadog.sym" ;; + esac + EXTRA_LDFLAGS="$EXTRA_LDFLAGS -export-symbols $DDTRACE_EXPORT_SYMBOLS -flto -fuse-linker-plugin" dnl On Linux: set the ELF entry point so ddtrace.so can be exec'd directly by ld.so dnl for sidecar spawning (no trampoline binary, no memfd, no temp files). diff --git a/config.w32 b/config.w32 index 5ff5b2e9d70..95eacd93c49 100644 --- a/config.w32 +++ b/config.w32 @@ -27,6 +27,7 @@ if (PHP_DDTRACE != 'no') { DDTRACE_EXT_SOURCES += " handlers_pcntl.c"; DDTRACE_EXT_SOURCES += " logging.c"; DDTRACE_EXT_SOURCES += " otel_config.c"; + DDTRACE_EXT_SOURCES += " otel_context.c"; DDTRACE_EXT_SOURCES += " phpinfo.c"; DDTRACE_EXT_SOURCES += " process_tags.c"; DDTRACE_EXT_SOURCES += " remote_config.c"; diff --git a/datadog-linux.sym b/datadog-linux.sym new file mode 100644 index 00000000000..65ea88b5021 --- /dev/null +++ b/datadog-linux.sym @@ -0,0 +1 @@ +otel_thread_ctx_v1 diff --git a/ext/datadog.c b/ext/datadog.c index fc2cd4622c4..c65e4822f14 100644 --- a/ext/datadog.c +++ b/ext/datadog.c @@ -11,6 +11,7 @@ #include "excluded_modules.h" #include "agent_info.h" #include "logging.h" +#include "otel_context.h" #include "phpinfo.h" #include "process_tags.h" #include "remote_config.h" @@ -547,6 +548,7 @@ static void dd_rinit_once(void) { datadog_process_tags_first_rinit(); datadog_sidecar_update_process_tags(); } + datadog_otel_process_context_publish(); // Uses config, cannot run earlier #ifndef _WIN32 @@ -704,7 +706,9 @@ static PHP_MINFO_FUNCTION(datadog) { void datadog_internal_handle_fork(void) { // CHILD PROCESS + datadog_generate_runtime_id(); datadog_sidecar_handle_fork(); + datadog_otel_process_context_publish(); #ifdef DDTRACE ddtrace_internal_handle_fork(); diff --git a/ext/otel_context.c b/ext/otel_context.c new file mode 100644 index 00000000000..9853ede4303 --- /dev/null +++ b/ext/otel_context.c @@ -0,0 +1,79 @@ +#include "otel_context.h" + +#ifdef __linux__ + +#include +#include + +#include "configuration.h" +#include "datadog.h" +#include "ffi_utils.h" +#include "process_tags.h" +#include "target_metadata.h" +#include + +void datadog_otel_process_context_publish_config( + zend_string *configured_service, + zend_string *env, + zend_string *version) { + if (!zai_config_is_initialized()) { + return; + } + + uint8_t runtime_id[36]; + datadog_format_runtime_id(&runtime_id); + + zend_string *service = configured_service; + bool release_service = false; + if (!ZSTR_LEN(service)) { + service = datadog_default_service_name(); + release_service = true; + } + + char detected_hostname[HOST_NAME_MAX + 1] = {0}; + zend_string *configured_hostname = get_DD_HOSTNAME(); + ddog_CharSlice hostname = dd_zend_string_to_CharSlice(configured_hostname); + if (!hostname.len && gethostname(detected_hostname, HOST_NAME_MAX) == 0) { + hostname = (ddog_CharSlice){ + .ptr = detected_hostname, + .len = strnlen(detected_hostname, HOST_NAME_MAX), + }; + } + + zend_string *process_tags = datadog_process_tags_get_serialized(); + datadog_publish_otel_process_context( + (ddog_CharSlice){.ptr = (const char *)runtime_id, .len = sizeof(runtime_id)}, + DDOG_CHARSLICE_C(PHP_DDTRACE_VERSION), + dd_zend_string_to_CharSlice(service), + dd_zend_string_to_CharSlice(env), + dd_zend_string_to_CharSlice(version), + hostname, + ddtrace_get_container_id(), + dd_zend_string_to_CharSlice(process_tags)); + + if (release_service) { + zend_string_release(service); + } +} + +void datadog_otel_process_context_publish(void) { + datadog_otel_process_context_publish_config( + get_DD_SERVICE(), + get_DD_ENV(), + get_DD_VERSION()); +} + +#else + +void datadog_otel_process_context_publish(void) {} + +void datadog_otel_process_context_publish_config( + zend_string *service, + zend_string *env, + zend_string *version) { + (void)service; + (void)env; + (void)version; +} + +#endif diff --git a/ext/otel_context.h b/ext/otel_context.h new file mode 100644 index 00000000000..19a87fdbaa1 --- /dev/null +++ b/ext/otel_context.h @@ -0,0 +1,22 @@ +#ifndef DD_OTEL_CONTEXT_H +#define DD_OTEL_CONTEXT_H + +#include + +/** + * Publish or update the configured process defaults and Linux OTel thread + * reference data. This is a no-op on non-Linux platforms. + */ +void datadog_otel_process_context_publish(void); + +/** + * Publish using explicit service, environment, and version values. This is + * used by configuration callbacks, which run before ZAI installs the new + * memoized value. + */ +void datadog_otel_process_context_publish_config( + zend_string *service, + zend_string *env, + zend_string *version); + +#endif diff --git a/ext/sidecar.c b/ext/sidecar.c index 224774d3a55..1c9c8d9b6fd 100644 --- a/ext/sidecar.c +++ b/ext/sidecar.c @@ -469,7 +469,11 @@ void datadog_sidecar_handle_fork(void) { return; } - datadog_force_new_instance_id(); + if (datadog_sidecar_instance_id) { + ddog_sidecar_instanceId_drop(datadog_sidecar_instance_id); + datadog_sidecar_instance_id = NULL; + } + dd_set_resettable_sidecar_globals(); // After fork only one thread (the one that called fork) survives, so we only // need to drop and reconnect the current thread's transport. diff --git a/libdatadog b/libdatadog index f2010b616fe..5761c06ff1c 160000 --- a/libdatadog +++ b/libdatadog @@ -1 +1 @@ -Subproject commit f2010b616feac15557a2b07e6ffd8c1899c69c63 +Subproject commit 5761c06ff1cee9fd0568e0917b4c56f8a16515bd diff --git a/package.xml b/package.xml index b69cba5d1dd..013638deb17 100644 --- a/package.xml +++ b/package.xml @@ -75,6 +75,7 @@ ${changelog} ${codefiles} + diff --git a/profiling/Cargo.toml b/profiling/Cargo.toml index 37f9dea8dd8..c2ae57147d7 100644 --- a/profiling/Cargo.toml +++ b/profiling/Cargo.toml @@ -41,6 +41,10 @@ uuid = { version = "1.0", features = ["v4"] } [target.'cfg(target_vendor = "apple")'.dependencies] mach2 = "0.6.0" +[target.'cfg(target_os = "linux")'.dependencies] +libdd-library-config = { path = "../libdatadog/libdd-library-config", default-features = false, features = ["process-context-reader"] } +libdd-trace-protobuf = { path = "../libdatadog/libdd-trace-protobuf" } + [dependencies.tracing-subscriber] version = "0.3" optional = true diff --git a/profiling/src/lib.rs b/profiling/src/lib.rs index f815f1514c4..de647898544 100644 --- a/profiling/src/lib.rs +++ b/profiling/src/lib.rs @@ -4,6 +4,7 @@ mod clocks; mod config; mod logging; pub mod module_globals; +mod process_context; pub mod profiling; mod pthread; mod sapi; @@ -94,13 +95,14 @@ static mut RUNTIME_PHP_VERSION: &str = { /// The first time this is accessed must be after config is initialized in /// the first RINIT and before mshutdown! static GLOBAL_TAGS: LazyLock> = LazyLock::new(|| { + let runtime_id = process_context::runtime_id().unwrap_or_else(|| runtime_id().to_string()); let mut tags = vec![ tag!("language", "php"), tag!("profiler_version", env!("PROFILER_VERSION")), // SAFETY: calling getpid() is safe. Tag::new("process_id", unsafe { libc::getpid() }.to_string()) .expect("process_id tag to be valid"), - Tag::new("runtime-id", runtime_id().to_string()).expect("runtime-id tag to be valid"), + Tag::new("runtime-id", runtime_id).expect("runtime-id tag to be valid"), ]; // This should probably be "language_version", but this is the @@ -703,6 +705,8 @@ extern "C" fn rinit(_type: c_int, _module_number: c_int) -> ZendResult { Profiler::init(system_settings); if system_settings.profiling_enabled { + let process_identity = process_context::identity(); + // Not logging, rinit could be quite spammy. _ = REQUEST_LOCALS.try_with_borrow(|locals| { let cpu_time_enabled = system_settings.profiling_experimental_cpu_time_enabled; @@ -712,18 +716,30 @@ extern "C" fn rinit(_type: c_int, _module_number: c_int) -> ZendResult { TAGS.set({ // SAFETY: accessing in RINIT after config is initialized. let globals = GLOBAL_TAGS.deref(); - let extra_tags_len = locals.service.is_some() as usize - + locals.env.is_some() as usize - + locals.version.is_some() as usize + let service = process_identity + .service + .as_ref() + .or(locals.service.as_ref()); + let environment = process_identity + .environment + .as_ref() + .or(locals.env.as_ref()); + let version = process_identity + .version + .as_ref() + .or(locals.version.as_ref()); + let extra_tags_len = service.is_some() as usize + + environment.is_some() as usize + + version.is_some() as usize + locals.git_commit_sha.is_some() as usize + locals.git_repository_url.is_some() as usize; let mut tags = Vec::new(); tags.reserve_exact(globals.len() + extra_tags_len + locals.tags.len()); tags.extend_from_slice(globals.as_slice()); - add_optional_tag(&mut tags, "service", &locals.service); - add_optional_tag(&mut tags, "env", &locals.env); - add_optional_tag(&mut tags, "version", &locals.version); + add_optional_tag(&mut tags, "service", &service); + add_optional_tag(&mut tags, "env", &environment); + add_optional_tag(&mut tags, "version", &version); add_optional_tag(&mut tags, "git.commit.sha", &locals.git_commit_sha); add_optional_tag(&mut tags, "git.repository_url", &locals.git_repository_url); tags.extend_from_slice(locals.tags.as_slice()); diff --git a/profiling/src/module_globals.rs b/profiling/src/module_globals.rs index c91559656e4..d2762a78924 100644 --- a/profiling/src/module_globals.rs +++ b/profiling/src/module_globals.rs @@ -4,6 +4,11 @@ use core::ffi::c_void; use core::ptr; use core::sync::atomic::AtomicU32; +#[cfg(target_os = "linux")] +use crate::process_context::ProcessContextCache; +#[cfg(target_os = "linux")] +use core::cell::RefCell; + #[cfg(php_zend_mm_set_custom_handlers_ex)] use crate::allocation::allocation_ge84::ZendMMState; #[cfg(not(php_zend_mm_set_custom_handlers_ex))] @@ -20,6 +25,8 @@ pub struct ProfilerGlobals { /// the PHP thread, so the value must remain atomic despite living in /// thread-local PHP module globals. pub interrupt_count: AtomicU32, + #[cfg(target_os = "linux")] + pub(crate) process_context: RefCell, } /// We need TSRM to call into GINIT and GSHUTDOWN to observe spawning and @@ -37,6 +44,8 @@ pub static mut GLOBALS_ID: i32 = 0; pub static mut GLOBALS: ProfilerGlobals = ProfilerGlobals { zend_mm_state: Cell::new(ZendMMState::new()), interrupt_count: AtomicU32::new(0), + #[cfg(target_os = "linux")] + process_context: RefCell::new(ProcessContextCache::new()), }; #[cfg(php_zts)] @@ -122,6 +131,9 @@ pub unsafe extern "C" fn ginit(_globals_ptr: *mut c_void) { let globals = _globals_ptr.cast::(); (*globals).zend_mm_state = Cell::new(ZendMMState::new()); (*globals).interrupt_count = AtomicU32::new(0); + #[cfg(target_os = "linux")] + ptr::addr_of_mut!((*globals).process_context) + .write(RefCell::new(ProcessContextCache::new())); } // SAFETY: this is called in thread ginit as expected, and no other places. @@ -137,9 +149,15 @@ pub unsafe extern "C" fn gshutdown(_globals_ptr: *mut c_void) { #[cfg(php_zts)] crate::timeline::timeline_gshutdown(); - // TODO: Florian, do we need this? - // let globals = globals_ptr.cast::(); - // (*globals).zend_mm_state = ZendMMState::new(); + #[cfg(target_os = "linux")] + { + let globals = _globals_ptr.cast::(); + if let Ok(mut cache) = (*globals).process_context.try_borrow_mut() { + cache.reset(); + } + #[cfg(php_zts)] + ptr::drop_in_place(ptr::addr_of_mut!((*globals).process_context)); + } // SAFETY: this is called in thread gshutdown as expected, no other places. allocation::gshutdown(); diff --git a/profiling/src/php_ffi.c b/profiling/src/php_ffi.c index 1e906f3cbd3..fb0a477ce86 100644 --- a/profiling/src/php_ffi.c +++ b/profiling/src/php_ffi.c @@ -16,10 +16,32 @@ const char *datadog_module_build_id(void) { return ZEND_MODULE_BUILD_ID; } uint8_t *datadog_runtime_id = NULL; +#ifdef __linux__ +static void *datadog_php_profiling_ddtrace_handle = NULL; +static __thread void **datadog_php_profiling_otel_thread_context_slot = NULL; +#endif + static void locate_datadog_runtime_id(const zend_extension *extension) { datadog_runtime_id = DL_FETCH_SYMBOL(extension->handle, "datadog_runtime_id"); } +const void *datadog_php_profiling_get_otel_thread_context(void) { +#ifdef __linux__ + if (!datadog_php_profiling_ddtrace_handle) { + return NULL; + } + if (!datadog_php_profiling_otel_thread_context_slot) { + datadog_php_profiling_otel_thread_context_slot = + DL_FETCH_SYMBOL(datadog_php_profiling_ddtrace_handle, "otel_thread_ctx_v1"); + } + return datadog_php_profiling_otel_thread_context_slot + ? *datadog_php_profiling_otel_thread_context_slot + : NULL; +#else + return NULL; +#endif +} + static void locate_ddtrace_get_profiling_context(const zend_extension *extension) { ddtrace_profiling_context (*get_profiling)(void) = DL_FETCH_SYMBOL(extension->handle, "ddtrace_get_profiling_context"); @@ -166,6 +188,9 @@ void datadog_php_profiling_startup(zend_extension *extension) { for (const zend_llist_element *item = list->head; item; item = item->next) { const zend_extension *maybe_ddtrace = (zend_extension *)item->data; if (maybe_ddtrace != extension && is_ddtrace_extension(maybe_ddtrace)) { +#ifdef __linux__ + datadog_php_profiling_ddtrace_handle = maybe_ddtrace->handle; +#endif locate_ddtrace_get_profiling_context(maybe_ddtrace); locate_datadog_runtime_id(maybe_ddtrace); locate_datadog_process_tags_get_serialized(maybe_ddtrace); diff --git a/profiling/src/php_ffi.h b/profiling/src/php_ffi.h index 558c3de4413..eb9450f5707 100644 --- a/profiling/src/php_ffi.h +++ b/profiling/src/php_ffi.h @@ -94,6 +94,12 @@ extern ddtrace_profiling_context (*datadog_php_profiling_get_profiling_context)( */ extern zend_string *(*datadog_php_profiling_get_process_tags_serialized)(void); +/** + * Returns the calling thread's record published through the standard Linux + * `otel_thread_ctx_v1` TLS symbol, or NULL when unavailable. + */ +const void *datadog_php_profiling_get_otel_thread_context(void); + /** * Called by this zend_extension's .startup handler. Does things that are * burdensome in Rust, like locating the ddtrace extension in the module diff --git a/profiling/src/process_context.rs b/profiling/src/process_context.rs new file mode 100644 index 00000000000..d65f96fdd56 --- /dev/null +++ b/profiling/src/process_context.rs @@ -0,0 +1,66 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(target_os = "linux")] +#[derive(Debug, Default)] +pub(crate) struct ThreadContext { + pub(crate) local_root_span_id: u64, + pub(crate) span_id: u64, + pub(crate) thread_id: Option, + pub(crate) service: Option, + pub(crate) environment: Option, + pub(crate) version: Option, +} + +#[derive(Default)] +pub(crate) struct ProcessIdentity { + pub(crate) service: Option, + pub(crate) environment: Option, + pub(crate) version: Option, +} + +#[cfg(target_os = "linux")] +#[derive(Clone, Copy, Default)] +pub(crate) struct ProcessIdentityRef<'a> { + pub(crate) service: Option<&'a str>, + pub(crate) environment: Option<&'a str>, + pub(crate) version: Option<&'a str>, +} + +#[cfg(target_os = "linux")] +pub(crate) enum ThreadContextRead { + /// No valid OTel Thread Context is currently attached. + #[cfg(target_os = "linux")] + Inactive, + #[cfg(target_os = "linux")] + Active(ThreadContext), +} + +#[cfg(target_os = "linux")] +#[path = "process_context/linux.rs"] +mod platform; + +#[cfg(not(target_os = "linux"))] +mod platform { + pub(crate) fn initialize() {} + + pub(crate) fn invalidate_before_fork() {} + + pub(crate) fn identity() -> super::ProcessIdentity { + super::ProcessIdentity::default() + } + + pub(crate) fn process_tags() -> Option { + None + } + + pub(crate) fn runtime_id() -> Option { + None + } +} + +#[cfg(target_os = "linux")] +pub(crate) use platform::thread_context; +#[cfg(target_os = "linux")] +pub(crate) use platform::ProcessContextCache; +pub(crate) use platform::{identity, initialize, invalidate_before_fork, process_tags, runtime_id}; diff --git a/profiling/src/process_context/linux.rs b/profiling/src/process_context/linux.rs new file mode 100644 index 00000000000..ad01c4fa83d --- /dev/null +++ b/profiling/src/process_context/linux.rs @@ -0,0 +1,568 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use super::{ProcessIdentity, ProcessIdentityRef, ThreadContext, ThreadContextRead}; +use crate::bindings::datadog_php_profiling_get_otel_thread_context; +use libdd_library_config::otel_process_ctx::ProcessContextSelfReader; +use libdd_trace_protobuf::opentelemetry::proto::common::v1::{any_value, KeyValue, ProcessContext}; + +const THREAD_CONTEXT_HEADER_SIZE: usize = 28; +const MAX_THREAD_ATTRIBUTES_SIZE: usize = 612; +const THREADLOCAL_ATTRIBUTE_KEY_MAP: &str = "threadlocal.attribute_key_map"; + +#[derive(Default)] +struct ResourceOffsets { + service_name: Option, + service_version: Option, + deployment_environment_name: Option, + service_instance_id: Option, +} + +#[derive(Default)] +struct ExtraAttributeOffsets { + process_tags: Option, +} + +#[derive(Default)] +struct ThreadAttributeOffsets { + key_count: usize, + local_root_span_id: Option, + service_name: Option, + service_version: Option, + deployment_environment_name: Option, + thread_id: Option, +} + +#[derive(Default)] +struct ProcessContextOffsets { + resource: ResourceOffsets, + extra: ExtraAttributeOffsets, + thread: ThreadAttributeOffsets, +} + +struct CachedProcessContext { + context: ProcessContext, + offsets: ProcessContextOffsets, +} + +impl Default for CachedProcessContext { + fn default() -> Self { + Self::new(ProcessContext::default()) + } +} + +impl CachedProcessContext { + fn new(context: ProcessContext) -> Self { + let offsets = ProcessContextOffsets::from_context(&context); + Self { context, offsets } + } + + fn resource_string(&self, offset: Option) -> Option<&str> { + offset + .and_then(|offset| self.context.resource.as_ref()?.attributes.get(offset)) + .and_then(string_value) + } + + fn extra_string(&self, offset: Option) -> Option<&str> { + offset + .and_then(|offset| self.context.extra_attributes.get(offset)) + .and_then(string_value) + } + + fn identity(&self) -> ProcessIdentity { + ProcessIdentity { + service: self + .resource_string(self.offsets.resource.service_name) + .map(str::to_owned), + environment: self + .resource_string(self.offsets.resource.deployment_environment_name) + .map(str::to_owned), + version: self + .resource_string(self.offsets.resource.service_version) + .map(str::to_owned), + } + } +} + +fn string_value(attribute: &KeyValue) -> Option<&str> { + let any_value::Value::StringValue(value) = attribute.value.as_ref()?.value.as_ref()? else { + return None; + }; + (!value.is_empty()).then_some(value.as_str()) +} + +impl ProcessContextOffsets { + fn from_context(context: &ProcessContext) -> Self { + let mut offsets = Self::default(); + + if let Some(resource) = context.resource.as_ref() { + for (index, attribute) in resource.attributes.iter().enumerate() { + match attribute.key.as_str() { + "service.name" => offsets.resource.service_name = Some(index), + "service.version" => offsets.resource.service_version = Some(index), + "deployment.environment.name" => { + offsets.resource.deployment_environment_name = Some(index); + } + "service.instance.id" => offsets.resource.service_instance_id = Some(index), + _ => {} + } + } + } + + for (index, attribute) in context.extra_attributes.iter().enumerate() { + match attribute.key.as_str() { + "datadog.process_tags" => offsets.extra.process_tags = Some(index), + THREADLOCAL_ATTRIBUTE_KEY_MAP => { + offsets.thread = ThreadAttributeOffsets::from_attribute(attribute); + } + _ => {} + } + } + + offsets + } +} + +impl ThreadAttributeOffsets { + fn from_attribute(attribute: &KeyValue) -> Self { + let Some(any_value::Value::ArrayValue(key_map)) = attribute + .value + .as_ref() + .and_then(|value| value.value.as_ref()) + else { + return Self::default(); + }; + + let mut offsets = Self { + // Thread Context key indices are u8, so entries beyond this cannot + // be referenced by the v1 record. + key_count: key_map.values.len().min(u8::MAX as usize + 1), + ..Self::default() + }; + for (index, key) in key_map.values.iter().enumerate() { + let Ok(index) = u8::try_from(index) else { + break; + }; + let Some(any_value::Value::StringValue(key)) = key.value.as_ref() else { + continue; + }; + match key.as_str() { + "datadog.local_root_span_id" => offsets.local_root_span_id = Some(index), + "service.name" => offsets.service_name = Some(index), + "service.version" => offsets.service_version = Some(index), + "deployment.environment.name" => { + offsets.deployment_environment_name = Some(index); + } + "thread.id" => offsets.thread_id = Some(index), + _ => {} + } + } + offsets + } +} + +/// Per-PHP-thread cache of the decoded OTel Process Context. +/// +/// The reader is deliberately short-lived. A refresh discovers the current +/// mapping, decodes it, and immediately closes the reader's copy pipe. +pub(crate) struct ProcessContextCache { + context: Option, +} + +impl ProcessContextCache { + pub(crate) const fn new() -> Self { + Self { context: None } + } + + pub(crate) fn reset(&mut self) { + *self = Self::new(); + } + + /// Reads Process Context on this PHP thread's first request. A failed read + /// installs an empty context so later requests do not repeat discovery. + pub(crate) fn initialize(&mut self) { + if self.context.is_some() { + return; + } + if self.refresh().is_err() { + self.context = Some(CachedProcessContext::default()); + } + } + + fn refresh(&mut self) -> std::io::Result<()> { + let result = ProcessContextSelfReader::new().and_then(|reader| reader.read()); + match result { + Ok(context) => { + self.context = Some(CachedProcessContext::new(context)); + Ok(()) + } + Err(error) => Err(error), + } + } + + fn decode_thread_attributes( + &self, + attributes: &[u8], + defaults: ProcessIdentityRef<'_>, + ) -> (ThreadContext, bool) { + let offsets = self.context.as_ref().map(|cached| &cached.offsets.thread); + let mut context = ThreadContext::default(); + let mut unknown_index = false; + let mut offset = 0; + + while offset + 2 <= attributes.len() { + let key_index = attributes[offset] as usize; + let value_size = attributes[offset + 1] as usize; + offset += 2; + + let Some(value_end) = offset.checked_add(value_size) else { + break; + }; + if value_end > attributes.len() { + break; + } + + let Some(offsets) = offsets else { + unknown_index = true; + offset = value_end; + continue; + }; + if key_index >= offsets.key_count { + unknown_index = true; + offset = value_end; + continue; + } + + let key_index = key_index as u8; + let interesting = offsets.local_root_span_id == Some(key_index) + || offsets.service_name == Some(key_index) + || offsets.service_version == Some(key_index) + || offsets.deployment_environment_name == Some(key_index) + || offsets.thread_id == Some(key_index); + if !interesting { + offset = value_end; + continue; + } + + let Ok(value) = std::str::from_utf8(&attributes[offset..value_end]) else { + offset = value_end; + continue; + }; + if !value.is_empty() { + if offsets.local_root_span_id == Some(key_index) { + context.local_root_span_id = u64::from_str_radix(value, 16).unwrap_or_default(); + } else if offsets.service_name == Some(key_index) { + if Some(value) != defaults.service { + context.service = Some(value.to_owned()); + } + } else if offsets.deployment_environment_name == Some(key_index) { + if Some(value) != defaults.environment { + context.environment = Some(value.to_owned()); + } + } else if offsets.service_version == Some(key_index) { + if Some(value) != defaults.version { + context.version = Some(value.to_owned()); + } + } else if offsets.thread_id == Some(key_index) { + context.thread_id = value.parse().ok().filter(|id| *id >= 0); + } + } + + offset = value_end; + } + + (context, unknown_index) + } + + fn decode_with_refresh( + &mut self, + attributes: &[u8], + defaults: ProcessIdentityRef<'_>, + ) -> ThreadContext { + let (decoded, unknown_index) = self.decode_thread_attributes(attributes, defaults); + if !unknown_index { + return decoded; + } + + if self.refresh().is_err() { + return decoded; + } + + self.decode_thread_attributes(attributes, defaults).0 + } +} + +fn with_cache(f: impl FnOnce(&std::cell::RefCell) -> R) -> R { + // SAFETY: PHP module globals are initialized by GINIT and are local to the + // current PHP thread in ZTS builds. NTS executes PHP on one thread. + let globals = unsafe { &*crate::module_globals::get_profiler_globals() }; + f(&globals.process_context) +} + +pub(crate) fn initialize() { + with_cache(|cache| { + if let Ok(mut cache) = cache.try_borrow_mut() { + cache.initialize(); + } + }); +} + +pub(crate) fn invalidate_before_fork() { + with_cache(|cache| { + if let Ok(mut cache) = cache.try_borrow_mut() { + cache.reset(); + } + }); +} + +pub(crate) fn identity() -> ProcessIdentity { + with_cache(|cache| { + cache + .try_borrow() + .ok() + .and_then(|cache| cache.context.as_ref().map(CachedProcessContext::identity)) + .unwrap_or_default() + }) +} + +pub(crate) fn process_tags() -> Option { + with_cache(|cache| { + let cache = cache.try_borrow().ok()?; + let cached = cache.context.as_ref()?; + cached + .extra_string(cached.offsets.extra.process_tags) + .map(str::to_owned) + }) +} + +pub(crate) fn runtime_id() -> Option { + with_cache(|cache| { + let cache = cache.try_borrow().ok()?; + let cached = cache.context.as_ref()?; + cached + .resource_string(cached.offsets.resource.service_instance_id) + .map(str::to_owned) + }) +} + +pub(crate) fn thread_context(defaults: ProcessIdentityRef<'_>) -> ThreadContextRead { + let record = unsafe { datadog_php_profiling_get_otel_thread_context() }.cast::(); + if record.is_null() { + return ThreadContextRead::Inactive; + } + + // The record belongs to the calling PHP thread. The tracer cannot mutate + // it while the profiler is executing on that same thread. + let header = unsafe { std::slice::from_raw_parts(record, THREAD_CONTEXT_HEADER_SIZE) }; + if header[24] != 1 { + return ThreadContextRead::Inactive; + } + + let attributes_size = u16::from_ne_bytes([header[26], header[27]]) as usize; + if attributes_size > MAX_THREAD_ATTRIBUTES_SIZE { + return ThreadContextRead::Inactive; + } + + let attributes = unsafe { + std::slice::from_raw_parts(record.add(THREAD_CONTEXT_HEADER_SIZE), attributes_size) + }; + let mut context = with_cache(|cell| { + let Ok(cache) = cell.try_borrow() else { + return ThreadContext::default(); + }; + let (decoded, unknown_index) = cache.decode_thread_attributes(attributes, defaults); + if !unknown_index { + return decoded; + } + drop(cache); + + let Ok(mut cache) = cell.try_borrow_mut() else { + return decoded; + }; + cache.decode_with_refresh(attributes, defaults) + }); + context.span_id = u64::from_be_bytes( + header[16..24] + .try_into() + .expect("the span-id field has a fixed eight-byte size"), + ); + + ThreadContextRead::Active(context) +} + +#[cfg(test)] +mod tests { + use super::*; + use libdd_trace_protobuf::opentelemetry::proto::common::v1::{AnyValue, ArrayValue}; + use libdd_trace_protobuf::opentelemetry::proto::resource::v1::Resource; + + fn string_attribute(key: &str, value: &str) -> KeyValue { + KeyValue { + key: key.to_owned(), + value: Some(AnyValue { + value: Some(any_value::Value::StringValue(value.to_owned())), + }), + key_ref: 0, + } + } + + fn key_map(keys: &[&str]) -> KeyValue { + KeyValue { + key: THREADLOCAL_ATTRIBUTE_KEY_MAP.to_owned(), + value: Some(AnyValue { + value: Some(any_value::Value::ArrayValue(ArrayValue { + values: keys + .iter() + .map(|key| AnyValue { + value: Some(any_value::Value::StringValue((*key).to_owned())), + }) + .collect(), + })), + }), + key_ref: 0, + } + } + + fn context(resource: Vec, extra_attributes: Vec) -> ProcessContext { + ProcessContext { + resource: Some(Resource { + attributes: resource, + dropped_attributes_count: 0, + entity_refs: vec![], + }), + extra_attributes, + } + } + + fn cache(context: ProcessContext) -> ProcessContextCache { + ProcessContextCache { + context: Some(CachedProcessContext::new(context)), + } + } + + fn encoded_attributes(attributes: &[(u8, &[u8])]) -> Vec { + let mut encoded = Vec::new(); + for (key, value) in attributes { + encoded.push(*key); + encoded.push(value.len().try_into().expect("test value fits in u8")); + encoded.extend_from_slice(value); + } + encoded + } + + #[test] + fn caches_process_identity_runtime_id_and_tags_by_discovered_offset() { + let cached = CachedProcessContext::new(context( + vec![ + string_attribute("unrelated", "ignored"), + string_attribute("service.version", "1.2.3"), + string_attribute("service.instance.id", "runtime-id-from-publisher"), + string_attribute("service.name", "checkout"), + string_attribute("deployment.environment.name", "production"), + ], + vec![ + key_map(&[ + "datadog.local_root_span_id", + "service.name", + "deployment.environment.name", + "service.version", + "thread.id", + ]), + string_attribute("datadog.process_tags", "region:us-east-1"), + ], + )); + + let identity = cached.identity(); + assert_eq!(identity.service.as_deref(), Some("checkout")); + assert_eq!(identity.environment.as_deref(), Some("production")); + assert_eq!(identity.version.as_deref(), Some("1.2.3")); + assert_eq!( + cached.resource_string(cached.offsets.resource.service_instance_id), + Some("runtime-id-from-publisher") + ); + assert_eq!( + cached.extra_string(cached.offsets.extra.process_tags), + Some("region:us-east-1") + ); + } + + #[test] + fn decodes_semantic_thread_attributes_from_the_process_key_map() { + let cache = cache(context( + vec![], + vec![key_map(&[ + "ignored", + "thread.id", + "service.version", + "datadog.local_root_span_id", + "service.name", + "deployment.environment.name", + ])], + )); + let attributes = encoded_attributes(&[ + (0, b"not interesting"), + (1, b"42"), + (2, b"2.0.0"), + (3, b"fedcba9876543210"), + (4, b"root-service"), + (5, b"configured-env"), + ]); + + let (decoded, unknown_index) = cache.decode_thread_attributes( + &attributes, + ProcessIdentityRef { + service: Some("configured-service"), + environment: Some("configured-env"), + version: Some("configured-version"), + }, + ); + + assert!(!unknown_index); + assert_eq!(decoded.thread_id, Some(42)); + assert_eq!(decoded.local_root_span_id, 0xfedc_ba98_7654_3210); + assert_eq!(decoded.service.as_deref(), Some("root-service")); + assert_eq!(decoded.environment, None); + assert_eq!(decoded.version.as_deref(), Some("2.0.0")); + } + + #[test] + fn malformed_empty_and_unknown_attributes_do_not_erase_defaults() { + let cache = cache(context( + vec![], + vec![key_map(&[ + "datadog.local_root_span_id", + "service.name", + "deployment.environment.name", + "service.version", + "thread.id", + ])], + )); + + let attributes = encoded_attributes(&[ + (1, b""), + (2, &[0xff]), + (3, b"configured-version"), + (4, b"-1"), + (6, b"new-key"), + ]); + let (decoded, unknown_index) = cache.decode_thread_attributes( + &attributes, + ProcessIdentityRef { + service: Some("configured-service"), + environment: Some("configured-env"), + version: Some("configured-version"), + }, + ); + + assert!(unknown_index); + assert_eq!(decoded.service, None); + assert_eq!(decoded.environment, None); + assert_eq!(decoded.version, None); + assert_eq!(decoded.thread_id, None); + + let (truncated, unknown_index) = + cache.decode_thread_attributes(&[1, 10, b'a'], ProcessIdentityRef::default()); + assert!(!unknown_index); + assert_eq!(truncated.service, None); + } +} diff --git a/profiling/src/profiling/mod.rs b/profiling/src/profiling/mod.rs index 759066c9379..e186b5c15c2 100644 --- a/profiling/src/profiling/mod.rs +++ b/profiling/src/profiling/mod.rs @@ -18,12 +18,16 @@ use crate::bindings::ddog_php_prof_get_active_fiber; use crate::bindings::ddog_php_prof_get_active_fiber_test as ddog_php_prof_get_active_fiber; use crate::allocation::ALLOCATION_PROFILING_INTERVAL; +#[cfg(not(target_os = "linux"))] +use crate::bindings::datadog_php_profiling_get_profiling_context; use crate::bindings::{ - datadog_php_profiling_get_process_tags_serialized, datadog_php_profiling_get_profiling_context, - zai_str_from_zstr, zend_execute_data, + datadog_php_profiling_get_process_tags_serialized, zai_str_from_zstr, zend_execute_data, }; use crate::config::SystemSettings; use crate::exception::EXCEPTION_PROFILING_INTERVAL; +use crate::process_context::ProcessIdentity; +#[cfg(target_os = "linux")] +use crate::process_context::{ProcessIdentityRef, ThreadContextRead}; use crate::{Clocks, RefCellExt, CLOCKS, REQUEST_LOCALS, TAGS}; use chrono::Utc; use core::mem::forget; @@ -43,6 +47,7 @@ use std::borrow::Cow; use std::collections::HashMap; use std::hash::Hash; use std::num::NonZeroI64; +use std::ops::{Deref, DerefMut}; use std::sync::atomic::{AtomicBool, AtomicPtr, AtomicU64, AtomicUsize, Ordering}; use std::sync::{Arc, Barrier, OnceLock}; use std::thread::JoinHandle; @@ -166,6 +171,82 @@ pub struct Label { pub value: LabelValue, } +struct SampleLabels { + labels: Vec