diff --git a/.gitlab/build-sidecar.sh b/.gitlab/build-sidecar.sh index bad3171853d..b3314c4d8ca 100755 --- a/.gitlab/build-sidecar.sh +++ b/.gitlab/build-sidecar.sh @@ -21,4 +21,5 @@ fi SHARED=1 PROFILE=tracer-release host_os="${HOST_OS}" ./compile_rust.sh cp -v "${CARGO_TARGET_DIR:-target}/tracer-release/libdatadog_php.a" "libdatadog_php_$(uname -m)${suffix}.a" -objcopy --compress-debug-sections "${CARGO_TARGET_DIR:-target}/tracer-release/libdatadog_php.so" "libdatadog_php_$(uname -m)${suffix}.so" +output="libdatadog_php_$(uname -m)${suffix}.so" +objcopy --compress-debug-sections "${CARGO_TARGET_DIR:-target}/tracer-release/libdatadog_php.so" "${output}" diff --git a/.gitlab/compile_extension.sh b/.gitlab/compile_extension.sh index 7eae904efee..b43416e47b7 100755 --- a/.gitlab/compile_extension.sh +++ b/.gitlab/compile_extension.sh @@ -25,5 +25,10 @@ make -j static & wait # Link extension -sed -i 's/-export-symbols .*\/datadog\.sym/-Wl,--retain-symbols-file=datadog.sym/g' ${EXTENSION_DIR}/ddtrace.ldflags +if [ "$(uname -s)" = "Linux" ]; then + export_symbols_file="datadog-linux.sym" +else + export_symbols_file="datadog.sym" +fi +sed -i -E "s#-export-symbols [^ ]+#-Wl,--retain-symbols-file=${export_symbols_file}#g" "${EXTENSION_DIR}/ddtrace.ldflags" cc -shared -Wl,-whole-archive ${MODULES_DIR}/ddtrace.a -Wl,-no-whole-archive $(cat ${EXTENSION_DIR}/ddtrace.ldflags) ${CARGO_TARGET_DIR}/debug/libdatadog_php.a -Wl,-soname -Wl,ddtrace.so -o ${MODULES_DIR}/ddtrace.so diff --git a/.gitlab/link-tracing-extension.sh b/.gitlab/link-tracing-extension.sh index 7378c7879c3..971d6df7be7 100755 --- a/.gitlab/link-tracing-extension.sh +++ b/.gitlab/link-tracing-extension.sh @@ -3,7 +3,11 @@ set -e -o pipefail suffix="${1:-}" -sed -i 's/-export-symbols .*\/datadog\.sym/-Wl,--retain-symbols-file=datadog.sym/g' "ddtrace_$(uname -m)${suffix}.ldflags" +export_symbols_file="datadog.sym" +if [ "$(uname -s)" = "Linux" ]; then + export_symbols_file="datadog-linux.sym" +fi +sed -i -E "s#-export-symbols [^ ]+#-Wl,--retain-symbols-file=${export_symbols_file}#g" "ddtrace_$(uname -m)${suffix}.ldflags" pids=() for archive in extensions_$(uname -m)/*.a; do ( diff --git a/Cargo.lock b/Cargo.lock index 7d190dfa02c..516724fe8d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1339,6 +1339,7 @@ dependencies = [ "libdd-crashtracker", "libdd-crashtracker-ffi", "libdd-data-pipeline", + "libdd-library-config", "libdd-library-config-ffi", "libdd-remote-config", "libdd-telemetry", @@ -1384,8 +1385,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", @@ -2956,6 +2959,7 @@ dependencies = [ "tracing", "uuid", "web-time", + "zstd", ] [[package]] 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..afdb0270181 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,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 }' ) @@ -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 04134a1711e..f7c9472d2fe 100644 --- a/appsec/cmake/ddtrace.cmake +++ b/appsec/cmake/ddtrace.cmake @@ -26,9 +26,9 @@ 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-linux.sym; echo -e 'local:\\n*;\\n};'; } > '${EXPORTS_FILE}'" BYPRODUCTS ${EXPORTS_FILE} - DEPENDS ${CMAKE_SOURCE_DIR}/../datadog.sym + DEPENDS ${CMAKE_SOURCE_DIR}/../datadog-linux.sym VERBATIM ) elseif(APPLE) diff --git a/components-rs/Cargo.toml b/components-rs/Cargo.toml index 7c78ff2cad6..841ae2f8de5 100644 --- a/components-rs/Cargo.toml +++ b/components-rs/Cargo.toml @@ -56,6 +56,9 @@ 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"] } + [build-dependencies] cbindgen = "0.27" diff --git a/components-rs/build.rs b/components-rs/build.rs index fc5779e6ba1..bda6910746e 100644 --- a/components-rs/build.rs +++ b/components-rs/build.rs @@ -1,6 +1,6 @@ fn main() { // On Linux, set ddog_spawn_direct_entry as the ELF entry point for the - // cdylib build (libdatadog_php.so in SSI deployments). This allows ld.so + // cdylib build (libdatadog_php.so in SSI deployments). This allows ld.so // to exec the library directly without a trampoline binary. if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("linux") { println!("cargo:rustc-cdylib-link-arg=-Wl,-e,ddog_spawn_direct_entry"); diff --git a/components-rs/datadog.h b/components-rs/datadog.h index 21c7f656fe4..bac89449eb1 100644 --- a/components-rs/datadog.h +++ b/components-rs/datadog.h @@ -41,6 +41,11 @@ 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 process_tags); + +#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 ae44c380288..00c26337eea 100644 --- a/components-rs/lib.rs +++ b/components-rs/lib.rs @@ -90,6 +90,72 @@ 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.to_utf8_lossy().into_owned() +} + +#[cfg(target_os = "linux")] +fn hostname() -> String { + let max_len = unsafe { libc::sysconf(libc::_SC_HOST_NAME_MAX) }; + let max_len = usize::try_from(max_len).unwrap_or(255); + let mut buffer = vec![0; max_len.saturating_add(1)]; + + if unsafe { libc::gethostname(buffer.as_mut_ptr().cast(), buffer.len()) } != 0 { + return String::new(); + } + + let len = buffer + .iter() + .position(|&byte| byte == 0) + .unwrap_or(buffer.len()); + String::from_utf8_lossy(&buffer[..len]).into_owned() +} + +/// 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(process_tags: CharSlice<'_>) -> bool { + use libdd_library_config::otel_process_ctx; + use libdd_library_config::tracer_metadata::{ThreadLocalMetadata, TracerMetadata}; + + let metadata = TracerMetadata { + // Safety: the runtime ID is only mutated from single-threaded contexts. + runtime_id: Some(unsafe { datadog_runtime_id.as_hyphenated().to_string() }), + tracer_language: "php".to_owned(), + tracer_version: include_str!("../VERSION").trim().to_owned(), + hostname: hostname(), + process_tags: Some(char_slice_string(process_tags)), + container_id: get_container_id().map(str::to_owned), + 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() + }; + + let mut context = metadata.to_otel_process_ctx(); + // TracerMetadata emits empty resource attributes for absent optional fields to advertise + // support. These values can vary independently on every PHP request, so their values and + // resource keys must be omitted from the process-wide context. The keys remain discoverable + // through threadlocal.attribute_key_map. + if let Some(resource) = context.resource.as_mut() { + resource.attributes.retain(|attribute| { + !matches!( + attribute.key.as_str(), + "service.name" | "service.version" | "deployment.environment.name" + ) + }); + } + + otel_process_ctx::publish(&context).is_ok() +} + #[must_use] #[no_mangle] pub extern "C" fn ddtrace_get_container_id() -> CharSlice<'static> { diff --git a/config.m4 b/config.m4 index aa69d309a43..f89e3461e36 100644 --- a/config.m4 +++ b/config.m4 @@ -100,6 +100,19 @@ if test "$PHP_DDTRACE" != "no"; then EXTRA_CFLAGS="$EXTRA_CFLAGS -Wno-microsoft-anon-tag" ]) + case "$host_os:$host_cpu" in + linux*:x86_64) + AC_LIBTOOL_COMPILER_OPTION([whether -mtls-dialect=gnu2 is a valid compiler argument], + lt_cv_ddtrace_tls_dialect_gnu2, + [-mtls-dialect=gnu2], [], + [ + CFLAGS="$CFLAGS -mtls-dialect=gnu2" + EXTRA_CFLAGS="$EXTRA_CFLAGS -mtls-dialect=gnu2" + ], + [AC_MSG_ERROR([x86-64 Linux OTel context sharing requires compiler support for -mtls-dialect=gnu2])]) + ;; + esac + DD_TRACE_VENDOR_SOURCES="\ tracer/vendor/mpack/mpack.c \ tracer/vendor/mt19937/mt19937-64.c \ @@ -168,6 +181,12 @@ if test "$PHP_DDTRACE" != "no"; then " fi + case "$host_os" in + linux*) + EXTRA_TRACER_SOURCES="$EXTRA_TRACER_SOURCES tracer/otel_context.c" + ;; + esac + dnl datadog.c/ddtrace.c comes first, then everything else alphabetically DATADOG_PHP_SOURCES="$EXTRA_DATADOG_SOURCES \ ext/datadog.c @@ -307,10 +326,14 @@ 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_srcdir/datadog-linux.sym" ;; + *) 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/datadog-linux.sym b/datadog-linux.sym new file mode 100644 index 00000000000..227c07946a1 --- /dev/null +++ b/datadog-linux.sym @@ -0,0 +1,46 @@ +ddtrace_close_all_spans_and_flush +datadog_get_formatted_session_id +ddtrace_get_profiling_context +ddtrace_get_root_span +datadog_process_tags_get_serialized +datadog_get_sidecar_queue_id +ddtrace_get_priority_sampling_on_span_zobj +ddtrace_set_priority_sampling_on_span_zobj +ddtrace_add_propagated_tag_on_span_zobj +datadog_runtime_id +ddtrace_user_req_add_listeners +ddtrace_ip_extraction_find +datadog_set_all_thread_vm_interrupt +datadog_get_telemetry_rc_info +datadog_metric_register_buffer +datadog_metric_add_point +ddtrace_emit_asm_event +datadog_loaded_by_ssi +datadog_ssi_forced_injection_enabled +ddtrace_guess_endpoint_from_url +ddog_remote_config_reader_for_path +ddog_remote_config_read +ddog_remote_config_reader_drop +get_module +ddog_crashtracker_entry_point +ddog_daemon_entry_point +ddog_set_rc_notify_fn +ddog_remote_config_path +ddog_remote_config_path_free +ddog_library_configurator_new +ddog_library_configurator_with_local_path +ddog_library_configurator_with_fleet_path +ddog_library_configurator_with_detect_process_info +ddog_library_configurator_get +ddog_library_config_source_to_string +ddog_library_config_drop +ddog_Error_message +ddog_Error_drop +ddog_library_configurator_drop +ddog_sidecar_enqueue_telemetry_log +ddog_sidecar_enqueue_telemetry_point +ddog_sidecar_enqueue_telemetry_metric +ddog_sidecar_connect +ddog_sidecar_ping +ddog_sidecar_transport_drop +otel_thread_ctx_v1 diff --git a/ext/datadog.c b/ext/datadog.c index fbd031d1ca7..5dae1929034 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 "ffi_utils.h" #include "phpinfo.h" #include "process_tags.h" #include "remote_config.h" @@ -547,6 +548,10 @@ static void dd_rinit_once(void) { datadog_process_tags_first_rinit(); datadog_sidecar_update_process_tags(); } +#ifdef __linux__ + zend_string *process_tags = datadog_process_tags_get_serialized(); + datadog_publish_otel_process_context(dd_zend_string_to_CharSlice(process_tags)); +#endif // Uses config, cannot run earlier #ifndef _WIN32 @@ -704,7 +709,12 @@ static PHP_MINFO_FUNCTION(datadog) { void datadog_internal_handle_fork(void) { // CHILD PROCESS + datadog_generate_runtime_id(); datadog_sidecar_handle_fork(); +#ifdef __linux__ + zend_string *process_tags = datadog_process_tags_get_serialized(); + datadog_publish_otel_process_context(dd_zend_string_to_CharSlice(process_tags)); +#endif #ifdef DDTRACE ddtrace_internal_handle_fork(); 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 95610de06a7..5761c06ff1c 160000 --- a/libdatadog +++ b/libdatadog @@ -1 +1 @@ -Subproject commit 95610de06a776b8d645fe77ad8b8e1848ecd53b7 +Subproject commit 5761c06ff1cee9fd0568e0917b4c56f8a16515bd diff --git a/loader/dd_library_loader.c b/loader/dd_library_loader.c index 4eaff1efa7f..6d7b40ce08a 100644 --- a/loader/dd_library_loader.c +++ b/loader/dd_library_loader.c @@ -322,6 +322,7 @@ injected_ext ddloader_injected_ext_config[EXT_COUNT] = { ZEND_MOD_OPTIONAL("ddtrace") ZEND_MOD_OPTIONAL("ddtrace_injected") ZEND_MOD_OPTIONAL("datadog-profiling") + ZEND_MOD_OPTIONAL("opentelemetry") ZEND_MOD_OPTIONAL("ev") ZEND_MOD_OPTIONAL("event") ZEND_MOD_OPTIONAL("libevent") 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..4f27a8e94cb 100644 --- a/profiling/src/lib.rs +++ b/profiling/src/lib.rs @@ -9,6 +9,8 @@ mod pthread; mod sapi; mod wall_time; +mod process_context; + #[cfg(php_run_time_cache)] mod string_set; @@ -94,13 +96,17 @@ 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(|| { + #[cfg(target_os = "linux")] + let runtime_id = process_context::runtime_id().unwrap_or_else(|| runtime_id().to_string()); + #[cfg(not(target_os = "linux"))] + let runtime_id = 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 @@ -159,10 +165,13 @@ extern "C" { } /// Module dependencies for the profiler extension. -static MODULE_DEPS: [zend::ModuleDep; 8] = [ +static MODULE_DEPS: [zend::ModuleDep; 9] = [ zend::ModuleDep::required(cstr!("standard")), zend::ModuleDep::required(cstr!("json")), + // Load after optional context publishers so their Process and Thread Context + // are available when profiling starts. zend::ModuleDep::optional(cstr!("ddtrace")), + zend::ModuleDep::optional(cstr!("opentelemetry")), // Optionally, be dependent on these event extensions so that the functions they provide // are registered in the function table and we can hook into them. zend::ModuleDep::optional(cstr!("ev")), @@ -703,6 +712,9 @@ extern "C" fn rinit(_type: c_int, _module_number: c_int) -> ZendResult { Profiler::init(system_settings); if system_settings.profiling_enabled { + #[cfg(target_os = "linux")] + 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 +724,39 @@ 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 + #[cfg(target_os = "linux")] + let service = process_identity + .service + .as_ref() + .or(locals.service.as_ref()); + #[cfg(not(target_os = "linux"))] + let service = locals.service.as_ref(); + #[cfg(target_os = "linux")] + let environment = process_identity + .environment + .as_ref() + .or(locals.env.as_ref()); + #[cfg(not(target_os = "linux"))] + let environment = locals.env.as_ref(); + #[cfg(target_os = "linux")] + let version = process_identity + .version + .as_ref() + .or(locals.version.as_ref()); + #[cfg(not(target_os = "linux"))] + let version = 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 8c7d061a079..f35210bfa62 100644 --- a/profiling/src/module_globals.rs +++ b/profiling/src/module_globals.rs @@ -5,6 +5,11 @@ use core::mem::MaybeUninit; 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))] @@ -21,6 +26,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, /// Per-thread allocation sampling state. Kept in PHP globals so allocator /// hooks can reuse an already-resolved TSRM cache instead of accessing Rust TLS. pub allocation_profiling_stats: UnsafeCell>, @@ -41,6 +48,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()), allocation_profiling_stats: UnsafeCell::new(MaybeUninit::uninit()), }; @@ -127,6 +136,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())); (*globals).allocation_profiling_stats = UnsafeCell::new(MaybeUninit::uninit()); } @@ -143,9 +155,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 9a82c509c6f..74acea9eafa 100644 --- a/profiling/src/php_ffi.c +++ b/profiling/src/php_ffi.c @@ -20,6 +20,25 @@ static void locate_datadog_runtime_id(const zend_extension *extension) { datadog_runtime_id = DL_FETCH_SYMBOL(extension->handle, "datadog_runtime_id"); } +#ifdef __linux__ +// Standard OTel context providers export this TLS symbol globally. Cache its +// address per thread because dynamic TLS addresses differ between threads. +static void *datadog_php_profiling_inactive_otel_thread_context = NULL; +static __thread void **datadog_php_profiling_otel_thread_context_slot = NULL; + +const void *datadog_php_profiling_get_otel_thread_context(void) { + if (!datadog_php_profiling_otel_thread_context_slot) { + datadog_php_profiling_otel_thread_context_slot = + DL_FETCH_SYMBOL(NULL, "otel_thread_ctx_v1"); + if (!datadog_php_profiling_otel_thread_context_slot) { + datadog_php_profiling_otel_thread_context_slot = + &datadog_php_profiling_inactive_otel_thread_context; + } + } + return *datadog_php_profiling_otel_thread_context_slot; +} +#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"); diff --git a/profiling/src/php_ffi.h b/profiling/src/php_ffi.h index 51a36b08590..ec7dcba417b 100644 --- a/profiling/src/php_ffi.h +++ b/profiling/src/php_ffi.h @@ -99,6 +99,14 @@ 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. + */ +#ifdef __linux__ +const void *datadog_php_profiling_get_otel_thread_context(void); +#endif + /** * Called by this zend_extension's .startup handler. Does things that are * burdensome in Rust, like locating the ddtrace extension in the module diff --git a/profiling/src/process_context.rs b/profiling/src/process_context.rs new file mode 100644 index 00000000000..0083ec37bf0 --- /dev/null +++ b/profiling/src/process_context.rs @@ -0,0 +1,57 @@ +// 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")] +impl From> for ProcessIdentity { + fn from(identity: ProcessIdentityRef<'_>) -> Self { + Self { + service: identity.service.map(str::to_owned), + environment: identity.environment.map(str::to_owned), + version: identity.version.map(str::to_owned), + } + } +} + +#[cfg(target_os = "linux")] +pub(crate) enum ThreadContextRead { + /// No valid OTel Thread Context is currently attached. + Inactive, + Active(ThreadContext), +} + +#[cfg(target_os = "linux")] +#[path = "process_context/linux.rs"] +mod platform; + +#[cfg(target_os = "linux")] +pub(crate) use platform::thread_context; +#[cfg(target_os = "linux")] +pub(crate) use platform::ProcessContextCache; +#[cfg(target_os = "linux")] +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..36672dc45de --- /dev/null +++ b/profiling/src/process_context/linux.rs @@ -0,0 +1,611 @@ +// 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) -> ProcessIdentityRef<'_> { + ProcessIdentityRef { + service: self.resource_string(self.offsets.resource.service_name), + environment: self.resource_string(self.offsets.resource.deployment_environment_name), + version: self.resource_string(self.offsets.resource.service_version), + } + } +} + +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, + /// Consecutive samples that observed an unknown key index. Recovery is + /// attempted at powers of two and suppressed once this saturates. + unknown_index_observations: u8, +} + +impl ProcessContextCache { + pub(crate) const fn new() -> Self { + Self { + context: None, + unknown_index_observations: 0, + } + } + + 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.read_process_context().is_err() { + self.context = Some(CachedProcessContext::default()); + } + } + + fn read_process_context(&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 unknown_index_refresh_due(&mut self) -> bool { + self.unknown_index_observations = self.unknown_index_observations.saturating_add(1); + self.unknown_index_observations.is_power_of_two() + } + + /// Cold relative to sampling. With the current fixed key map, this is only + /// expected while restoring a cache invalidated before fork. + #[cold] + #[inline(never)] + fn refresh(&mut self) -> bool { + if !self.unknown_index_refresh_due() { + return false; + } + if self.read_process_context().is_err() { + return false; + } + + self.unknown_index_observations = 0; + true + } + + 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) + } +} + +// Standalone Rust tests do not run inside PHP and therefore have no TSRM +// module globals. Use Rust TLS to preserve the per-thread cache semantics +// without entering the PHP module-globals path. +#[cfg(test)] +std::thread_local! { + static TEST_CACHE: std::cell::RefCell = + const { std::cell::RefCell::new(ProcessContextCache::new()) }; +} + +#[cfg(test)] +fn with_cache(f: impl FnOnce(&std::cell::RefCell) -> R) -> R { + TEST_CACHE.with(f) +} + +#[cfg(not(test))] +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(|cached| cached.identity().into()) + }) + .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; + }; + if !cache.refresh() { + return decoded; + } + cache.decode_thread_attributes(attributes, defaults).0 + }); + 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)), + unknown_index_observations: 0, + } + } + + 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 backs_off_unknown_index_refreshes() { + let mut cache = ProcessContextCache::new(); + let refresh_observations: Vec<_> = (1..=300) + .filter(|_| cache.unknown_index_refresh_due()) + .collect(); + + assert_eq!(refresh_observations, [1, 2, 4, 8, 16, 32, 64, 128]); + assert_eq!(cache.unknown_index_observations, u8::MAX); + + cache.unknown_index_observations = 0; + assert!(cache.unknown_index_refresh_due()); + assert_eq!(cache.unknown_index_observations, 1); + } + + #[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, Some("checkout")); + assert_eq!(identity.environment, Some("production")); + assert_eq!(identity.version, 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 574320c1665..6c7b95c46a3 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