diff --git a/Cargo.lock b/Cargo.lock index e99f11eddf4..d16c06d9fc9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1325,7 +1325,9 @@ dependencies = [ "libdd-common-ffi", "libdd-crashtracker-ffi", "libdd-data-pipeline", + "libdd-library-config", "libdd-library-config-ffi", + "libdd-otel-thread-ctx-ffi", "libdd-remote-config", "libdd-telemetry", "libdd-telemetry-ffi", @@ -3014,6 +3016,23 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "libdd-otel-thread-ctx" +version = "1.0.0" +dependencies = [ + "build_common", + "cc", +] + +[[package]] +name = "libdd-otel-thread-ctx-ffi" +version = "1.0.0" +dependencies = [ + "build_common", + "libdd-common-ffi", + "libdd-otel-thread-ctx", +] + [[package]] name = "libdd-profiling" version = "1.0.0" diff --git a/Makefile b/Makefile index fb02702cc05..afd8190cc0a 100644 --- a/Makefile +++ b/Makefile @@ -48,7 +48,7 @@ RUN_TESTS_CMD := DD_SERVICE= DD_ENV= DD_TRACE_RETRY_INTERVAL=1 REPORT_EXIT_STATU C_FILES = $(shell find components components-rs ext src/dogstatsd tracer zend_abstract_interface -name '*.c' -o -name '*.h' | awk '{ printf "$(BUILD_DIR)/%s\n", $$1 }' ) TEST_FILES = $(shell find tests/ext -name '*.php*' -o -name '*.inc' -o -name '*.json' -o -name '*.yaml' -o -name 'CONFLICTS' | awk '{ printf "$(BUILD_DIR)/%s\n", $$1 }' ) -RUST_FILES = $(BUILD_DIR)/Cargo.toml $(BUILD_DIR)/Cargo.lock $(shell find components-rs -name '*.c' -o -name '*.rs' -o -name 'Cargo.toml' | awk '{ printf "$(BUILD_DIR)/%s\n", $$1 }' ) $(shell find libdatadog/{build-common,datadog-ffe,datadog-ipc,datadog-ipc-macros,datadog-live-debugger,datadog-live-debugger-ffi,libdd-remote-config,datadog-sidecar,datadog-sidecar-ffi,datadog-sidecar-macros,libdd-alloc,libdd-capabilities,libdd-capabilities-impl,libdd-common,libdd-common-ffi,libdd-crashtracker,libdd-crashtracker-ffi,libdd-data-pipeline,libdd-ddsketch,libdd-dogstatsd-client,libdd-library-config,libdd-library-config-ffi,libdd-log,libdd-shared-runtime,libdd-telemetry,libdd-telemetry-ffi,libdd-tinybytes,libdd-trace-*,spawn_worker,tools/{cc_utils,sidecar_mockgen},libdd-trace-*,Cargo.toml} \( -type l -o -type f \) \( -path "*/src*" -o -path "*/examples*" -o -path "*Cargo.toml" -o -path "*/build.rs" -o -path "*/tests/dataservice.rs" -o -path "*/tests/service_functional.rs" \) -not -path "*/datadog-ipc/build.rs" -not -path "*/datadog-sidecar-ffi/build.rs") +RUST_FILES = $(BUILD_DIR)/Cargo.toml $(BUILD_DIR)/Cargo.lock $(shell find components-rs -name '*.c' -o -name '*.rs' -o -name 'Cargo.toml' | awk '{ printf "$(BUILD_DIR)/%s\n", $$1 }' ) $(shell find libdatadog/{build-common,datadog-ffe,datadog-ipc,datadog-ipc-macros,datadog-live-debugger,datadog-live-debugger-ffi,libdd-remote-config,datadog-sidecar,datadog-sidecar-ffi,datadog-sidecar-macros,libdd-alloc,libdd-capabilities,libdd-capabilities-impl,libdd-common,libdd-common-ffi,libdd-crashtracker,libdd-crashtracker-ffi,libdd-data-pipeline,libdd-ddsketch,libdd-dogstatsd-client,libdd-library-config,libdd-library-config-ffi,libdd-log,libdd-otel-thread-ctx*,libdd-shared-runtime,libdd-telemetry,libdd-telemetry-ffi,libdd-tinybytes,libdd-trace-*,spawn_worker,tools/{cc_utils,sidecar_mockgen},libdd-trace-*,Cargo.toml} \( -type l -o -type f \) \( -path "*/src*" -o -path "*/examples*" -o -path "*Cargo.toml" -o -path "*/build.rs" -o -path "*/tests/dataservice.rs" -o -path "*/tests/service_functional.rs" -o -name "tls-dynamic-list.txt" \) -not -path "*/datadog-ipc/build.rs" -not -path "*/datadog-sidecar-ffi/build.rs") ALL_OBJECT_FILES = $(C_FILES) $(RUST_FILES) $(BUILD_DIR)/Makefile TEST_OPCACHE_FILES = $(shell find tests/opcache -name '*.php*' -o -name '.gitkeep' | awk '{ printf "$(BUILD_DIR)/%s\n", $$1 }' ) TEST_STUB_FILES = $(shell find tests/ext -type d -name 'stubs' -exec find '{}' -type f \; | awk '{ printf "$(BUILD_DIR)/%s\n", $$1 }' ) @@ -423,9 +423,9 @@ clang_format_fix: cbindgen: remove_cbindgen generate_cbindgen remove_cbindgen: - rm -f components-rs/datadog.h components-rs/live-debugger.h components-rs/telemetry.h components-rs/sidecar.h components-rs/common.h components-rs/crashtracker.h components-rs/library-config.h + rm -f components-rs/datadog.h components-rs/live-debugger.h components-rs/telemetry.h components-rs/sidecar.h components-rs/common.h components-rs/crashtracker.h components-rs/library-config.h components-rs/otel-thread-ctx.h -generate_cbindgen: cbindgen_binary # Regenerate components-rs/datadog.h components-rs/live-debugger.h components-rs/telemetry.h components-rs/sidecar.h components-rs/common.h components-rs/crashtracker.h components-rs/library-config.h +generate_cbindgen: cbindgen_binary # Regenerate components-rs/datadog.h components-rs/live-debugger.h components-rs/telemetry.h components-rs/sidecar.h components-rs/common.h components-rs/crashtracker.h components-rs/library-config.h components-rs/otel-thread-ctx.h ( \ $(command rustup && echo run nightly --) cbindgen --crate datadog-php \ --config cbindgen.toml \ @@ -449,11 +449,14 @@ generate_cbindgen: cbindgen_binary # Regenerate components-rs/datadog.h componen $(command rustup && echo run nightly --) cbindgen --crate libdd-library-config-ffi \ --config libdd-library-config-ffi/cbindgen.toml \ --output $(PROJECT_ROOT)/components-rs/library-config.h; \ + $(command rustup && echo run nightly --) cbindgen --crate libdd-otel-thread-ctx-ffi \ + --config libdd-otel-thread-ctx-ffi/cbindgen.toml \ + --output $(PROJECT_ROOT)/components-rs/otel-thread-ctx.h; \ if test -d $(PROJECT_ROOT)/tmp; then \ mkdir -pv "$(BUILD_DIR)"; \ export CARGO_TARGET_DIR="$(BUILD_DIR)/target"; \ fi; \ - cargo run -p tools --bin dedup_headers -- $(PROJECT_ROOT)/components-rs/common.h $(PROJECT_ROOT)/components-rs/datadog.h $(PROJECT_ROOT)/components-rs/live-debugger.h $(PROJECT_ROOT)/components-rs/telemetry.h $(PROJECT_ROOT)/components-rs/sidecar.h $(PROJECT_ROOT)/components-rs/crashtracker.h $(PROJECT_ROOT)/components-rs/library-config.h \ + cargo run -p tools --bin dedup_headers -- $(PROJECT_ROOT)/components-rs/common.h $(PROJECT_ROOT)/components-rs/datadog.h $(PROJECT_ROOT)/components-rs/live-debugger.h $(PROJECT_ROOT)/components-rs/telemetry.h $(PROJECT_ROOT)/components-rs/sidecar.h $(PROJECT_ROOT)/components-rs/crashtracker.h $(PROJECT_ROOT)/components-rs/library-config.h $(PROJECT_ROOT)/components-rs/otel-thread-ctx.h \ ) cbindgen_binary: diff --git a/appsec/src/extension/ddappsec.c b/appsec/src/extension/ddappsec.c index 9e07e9b2477..6e05567de8e 100644 --- a/appsec/src/extension/ddappsec.c +++ b/appsec/src/extension/ddappsec.c @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -55,6 +56,7 @@ static atomic_int _thread_count; static void _check_enabled(void); #ifdef TESTING static void _register_testing_objects(void); +volatile int ddappsec_debugger_wait_continue; #endif static PHP_MINIT_FUNCTION(ddappsec); @@ -481,6 +483,32 @@ static PHP_FUNCTION(datadog_appsec_testing_stop_for_debugger) RETURN_TRUE; } +static PHP_FUNCTION(datadog_appsec_testing_wait_for_debugger) +{ + if (zend_parse_parameters_none() == FAILURE) { + RETURN_FALSE; + } + ddappsec_debugger_wait_continue = 0; + + int fd = open( + "/tmp/pid", O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, 0600); // NOLINT + if (fd < 0) { + RETURN_FALSE; + } + char pid[sizeof("-2147483648")] = ""; + sprintf(pid, "%" PRIi32, (int32_t)getpid()); // NOLINT + ATTR_UNUSED ssize_t unused_ = write(fd, pid, strlen(pid)); + close(fd); + + while (!ddappsec_debugger_wait_continue) { + usleep(10000); // NOLINT + } + ddappsec_debugger_wait_continue = 0; + unlink("/tmp/pid"); // NOLINT + + RETURN_TRUE; +} + static PHP_FUNCTION(datadog_appsec_testing_request_exec) { zend_array *data = NULL; @@ -632,6 +660,7 @@ static const zend_function_entry testing_request_control_functions[] = { ZEND_RAW_FENTRY(DD_TESTING_NS "rinit", PHP_FN(datadog_appsec_testing_rinit), void_ret_bool_arginfo, 0, NULL, NULL) ZEND_RAW_FENTRY(DD_TESTING_NS "rshutdown", PHP_FN(datadog_appsec_testing_rshutdown), void_ret_bool_arginfo, 0, NULL, NULL) ZEND_RAW_FENTRY(DD_TESTING_NS "request_exec", PHP_FN(datadog_appsec_testing_request_exec), request_exec_arginfo, 0, NULL, NULL) + ZEND_RAW_FENTRY(DD_TESTING_NS "wait_for_debugger", PHP_FN(datadog_appsec_testing_wait_for_debugger), void_ret_bool_arginfo, 0, NULL, NULL) PHP_FE_END }; static const zend_function_entry testing_functions[] = { diff --git a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/OtelThreadContextTests.groovy b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/OtelThreadContextTests.groovy new file mode 100644 index 00000000000..119bce6ddb7 --- /dev/null +++ b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/OtelThreadContextTests.groovy @@ -0,0 +1,261 @@ +package com.datadog.appsec.php.integration + +import com.datadog.appsec.php.docker.AppSecContainer +import com.datadog.appsec.php.docker.InspectContainerHelper +import groovy.json.JsonSlurper +import groovy.util.logging.Slf4j +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.condition.DisabledIf +import org.testcontainers.containers.Container.ExecResult +import org.testcontainers.junit.jupiter.Container +import org.testcontainers.junit.jupiter.Testcontainers + +import java.net.http.HttpResponse +import java.util.concurrent.CompletableFuture +import java.util.concurrent.TimeUnit + +import static com.datadog.appsec.php.integration.TestParams.getPhpVersion +import static com.datadog.appsec.php.integration.TestParams.getVariant +import static java.net.http.HttpResponse.BodyHandlers.ofString + +@Testcontainers +@Slf4j +@DisabledIf('isDisabled') +class OtelThreadContextTests { + private static final String PID_FILE = '/tmp/pid' + private static final String GDB_SCRIPT = + '/project/appsec/tests/integration/src/test/resources/otel_context_gdb.py' + private static final String GDB_TIMEOUT = '20s' + private static final String EXPECTED_PROCESS_CONTEXT_SIGNATURE = 'OTEL_CTX' + private static final String EXPECTED_PROCESS_CONTEXT_VERSION = '2' + + static boolean disabled = phpVersion != '8.3' + + @Container + public static final AppSecContainer CONTAINER = + new AppSecContainer( + workVolume: this.name, + baseTag: 'apache2-mod-php', + phpVersion: phpVersion, + phpVariant: variant, + www: 'base', + ) + + static void main(String[] args) { + InspectContainerHelper.run(CONTAINER) + } + + @AfterEach + void afterEach() { + CONTAINER.clearTraces() + } + + @Test + void 'otel thread context matches trace ids during regular request lifecycle'() { + PausedRequest pausedRequest = startPausedRequest('/otel_context_regular.php') + boolean released = false + + try { + Map threadContext = inspectThreadLocalAndContinue(pausedRequest.pid) + released = true + HttpResponse response = awaitResponse(pausedRequest) + Map responseBody = parseJsonResponse(response) + + assertThreadContextMatchesResponse(threadContext, responseBody) + } finally { + if (!released) { + continuePausedRequestQuietly(pausedRequest.pid) + } + } + } + + @Test + void 'otel thread context matches trace ids during user request lifecycle'() { + PausedRequest pausedRequest = startPausedRequest('/otel_context_user_request.php') + boolean released = false + + try { + Map threadContext = inspectThreadLocalAndContinue(pausedRequest.pid) + released = true + HttpResponse response = awaitResponse(pausedRequest) + Map responseBody = parseJsonResponse(response) + + assert responseBody.outer_span_id != responseBody.span_id + assertThreadContextMatchesResponse(threadContext, responseBody) + } finally { + if (!released) { + continuePausedRequestQuietly(pausedRequest.pid) + } + } + } + + @Test + void 'otel process context shared memory has expected threadlocal schema and version'() { + PausedRequest pausedRequest = startPausedRequest('/otel_context_regular.php') + boolean released = false + + try { + Map processContext = inspectProcessContext(pausedRequest.pid) + continuePausedRequest(pausedRequest.pid) + released = true + + HttpResponse response = awaitResponse(pausedRequest) + parseJsonResponse(response) + + assert processContext.present == 'true' + assert processContext.signature == EXPECTED_PROCESS_CONTEXT_SIGNATURE + assert processContext.version == EXPECTED_PROCESS_CONTEXT_VERSION + assert processContext.payload_size.toInteger() > 0 + assert processContext.published_at.toBigInteger() > 0 + assert processContext.has_threadlocal_schema_key == 'true' + assert processContext.has_threadlocal_schema_value == 'true' + assert processContext.has_threadlocal_attribute_key_map == 'true' + assert processContext.has_local_root_span_key == 'true' + } finally { + if (!released) { + continuePausedRequestQuietly(pausedRequest.pid) + } + } + } + + private static void assertThreadContextMatchesResponse( + Map threadContext, Map responseBody) { + assert responseBody.waited == true + assert threadContext.ctx != '0x0' + assert threadContext.valid == '1' + assert threadContext.attrs_data_size.toInteger() >= 18 + assert threadContext.attr0_key == '0' + assert threadContext.attr0_len == '16' + assert threadContext.trace_id == responseBody.trace_id + assert threadContext.span_id == responseBody.span_id + assert threadContext.local_root_span_id == responseBody.local_root_span_id + } + + private static Map parseJsonResponse(HttpResponse response) { + assert response.statusCode() == 200 + new JsonSlurper().parseText(response.body()) as Map + } + + private static PausedRequest startPausedRequest(String path) { + CONTAINER.execInContainer('rm', '-f', PID_FILE) + + def request = CONTAINER.buildReq(path).GET().build() + CompletableFuture> responseFuture = + CONTAINER.httpClient.sendAsync(request, ofString()) + + new PausedRequest( + pid: waitForPausedPid(responseFuture), + responseFuture: responseFuture) + } + + private static String waitForPausedPid(CompletableFuture> responseFuture) { + long deadline = System.currentTimeMillis() + 15_000 + + while (System.currentTimeMillis() < deadline) { + if (responseFuture.isDone()) { + HttpResponse response = responseFuture.getNow(null) + throw new AssertionError( + "Request completed before the debugger pause: HTTP ${response.statusCode()}\n${response.body()}".toString()) + } + + ExecResult res = CONTAINER.execInContainer( + 'bash', '-lc', + "test -s ${PID_FILE} && cat ${PID_FILE} || true".toString()) + if (res.exitCode == 0) { + String pid = res.stdout.trim() + if (pid) { + return pid + } + } + Thread.sleep(100) + } + + throw new AssertionError('Timed out waiting for the paused PHP worker pid') + } + + private static HttpResponse awaitResponse(PausedRequest pausedRequest) { + pausedRequest.responseFuture.get(30, TimeUnit.SECONDS) + } + + private static Map inspectThreadLocalAndContinue(String pid) { + List commands = [ + 'set pagination off', + 'otel-thread-context', + 'ddappsec-continue', + 'detach', + 'quit', + ] + + ExecResult res = runGdb(pid, commands) + parseKeyValueOutput(res.stdout) + } + + private static void continuePausedRequest(String pid) { + runGdb(pid, [ + 'set pagination off', + 'ddappsec-continue', + 'detach', + 'quit', + ]) + } + + private static void continuePausedRequestQuietly(String pid) { + try { + continuePausedRequest(pid) + } catch (Throwable ignored) { + // The original failure is more useful than a best-effort cleanup error. + } + } + + private static ExecResult runGdb(String pid, List commands) { + List args = [ + 'timeout', + GDB_TIMEOUT, + 'gdb', + '--batch', + '--quiet', + '-p', + pid, + '-ex', + "python exec(open('${GDB_SCRIPT}').read())".toString(), + ] + commands.each { + args.add('-ex') + args.add(it) + } + + ExecResult res = CONTAINER.execInContainer(args as String[]) + if (res.exitCode != 0 || res.stderr =~ /(Traceback|Python Exception|No symbol|Undefined command)/) { + throw new AssertionError( + "gdb failed with exit code ${res.exitCode}\nstdout:\n${res.stdout}\nstderr:\n${res.stderr}".toString()) + } + res + } + + private static Map inspectProcessContext(String pid) { + ExecResult res = runGdb(pid, [ + 'set pagination off', + 'otel-process-context', + 'detach', + 'quit', + ]) + parseKeyValueOutput(res.stdout) + } + + private static Map parseKeyValueOutput(String output) { + Map result = [:] + output.readLines().each { String line -> + if (line ==~ /^[A-Za-z_][A-Za-z0-9_]*=.*/) { + int idx = line.indexOf('=') + result[line.substring(0, idx).trim()] = line.substring(idx + 1).trim() + } + } + result + } + + private static class PausedRequest { + String pid + CompletableFuture> responseFuture + } +} diff --git a/appsec/tests/integration/src/test/resources/otel_context_gdb.py b/appsec/tests/integration/src/test/resources/otel_context_gdb.py new file mode 100644 index 00000000000..a70c474ff5b --- /dev/null +++ b/appsec/tests/integration/src/test/resources/otel_context_gdb.py @@ -0,0 +1,233 @@ +# /// script +# requires-python = ">=3.8" +# /// + +import gdb +import struct +import sys + + +WAIT_FLAG = "ddappsec_debugger_wait_continue" +WAIT_FRAME_MARKER = "datadog_appsec_testing_wait_for_debugger" +TLS_SYMBOL = "otel_thread_ctx_v1" +THREAD_CONTEXT_SIZE = 640 +EXPECTED_PROCESS_CONTEXT_MAPPING = "OTEL_CTX" +EXPECTED_PROCESS_CONTEXT_SIGNATURE = b"OTEL_CTX" + + +def print_kv(key, value): + print(f"{key}={value}") + + +def bool_value(value): + return "true" if value else "false" + + +def inferior(): + return gdb.selected_inferior() + + +def read_memory(address, size): + return bytes(inferior().read_memory(address, size)) + + +def read_pointer(address): + pointer_size = gdb.lookup_type("void").pointer().sizeof + return int.from_bytes(read_memory(address, pointer_size), sys.byteorder) + + +def call_pointer(expression): + try: + return int(gdb.parse_and_eval(expression)) + except gdb.error: + return 0 + + +def c_string(value): + return value.replace("\\", "\\\\").replace('"', '\\"') + + +def frame_names(thread): + names = [] + thread.switch() + + try: + frame = gdb.newest_frame() + except gdb.error: + return names + + while frame: + try: + name = frame.name() + except gdb.error: + name = None + + if name: + names.append(name) + + try: + frame = frame.older() + except gdb.error: + break + + return names + + +def select_wait_for_debugger_thread(emit=True): + threads = inferior().threads() + if len(threads) == 1: + threads[0].switch() + if emit: + print_kv("thread", threads[0].num) + return + + inspected = [] + for thread in threads: + names = frame_names(thread) + inspected.append(f"{thread.num}:{'|'.join(names[:8])}") + if any(WAIT_FRAME_MARKER in name for name in names): + thread.switch() + if emit: + print_kv("thread", thread.num) + return + + raise gdb.GdbError( + "Could not find thread stopped in wait_for_debugger; inspected " + + "; ".join(inspected) + ) + + +def find_tls_slot(): + slot = call_pointer(f"(void *) &{TLS_SYMBOL}") + if slot: + return slot + + slot = call_pointer(f'(void *) dlsym((void *) 0, "{TLS_SYMBOL}")') + if slot: + return slot + + for objfile in gdb.objfiles(): + if not objfile.filename or not objfile.filename.endswith("ddtrace.so"): + continue + + handle = call_pointer(f'(void *) dlopen("{c_string(objfile.filename)}", 6)') + if handle: + slot = call_pointer(f'(void *) dlsym((void *) {handle}, "{TLS_SYMBOL}")') + if slot: + return slot + + return 0 + + +class OtelThreadContext(gdb.Command): + def __init__(self): + super().__init__("otel-thread-context", gdb.COMMAND_DATA) + + def invoke(self, arg, from_tty): + del arg, from_tty + + select_wait_for_debugger_thread() + slot = find_tls_slot() + print_kv("slot", f"0x{slot:x}" if slot else "0x0") + if slot == 0: + return + + ctx = read_pointer(slot) + print_kv("ctx", f"0x{ctx:x}" if ctx else "0x0") + if ctx == 0: + return + + data = read_memory(ctx, THREAD_CONTEXT_SIZE) + attrs_data_size = struct.unpack_from(" 'missing root span']); + return; +} + +$waited = \datadog\appsec\testing\wait_for_debugger(); + +header('Content-Type: application/json'); +echo json_encode([ + 'waited' => $waited, + 'trace_id' => $rootSpan->traceId, + 'span_id' => $rootSpan->hexId(), + 'local_root_span_id' => $rootSpan->hexId(), +]); diff --git a/appsec/tests/integration/src/test/www/base/public/otel_context_user_request.php b/appsec/tests/integration/src/test/www/base/public/otel_context_user_request.php new file mode 100644 index 00000000000..3182ba18ff8 --- /dev/null +++ b/appsec/tests/integration/src/test/www/base/public/otel_context_user_request.php @@ -0,0 +1,41 @@ +name = 'otel_context.user_request'; +$userRequestSpan->resource = 'otel_context.user_request'; + +\DDTrace\UserRequest\notify_start($userRequestSpan, [ + '_GET' => $_GET, + '_POST' => $_POST, + '_SERVER' => $_SERVER, + '_FILES' => $_FILES, + '_COOKIE' => $_COOKIE, +]); + +if ($outerSpan) { + \DDTrace\switch_stack($outerSpan); +} + +$waited = \datadog\appsec\testing\wait_for_debugger(); + +$response = [ + 'waited' => $waited, + 'trace_id' => $userRequestSpan->traceId, + 'span_id' => $userRequestSpan->hexId(), + 'local_root_span_id' => $userRequestSpan->hexId(), + 'outer_span_id' => $outerSpan ? $outerSpan->hexId() : null, +]; + +\DDTrace\switch_stack($userRequestSpan); +\DDTrace\UserRequest\notify_commit($userRequestSpan, 200, [ + 'Content-Type' => ['application/json'], +]); +\DDTrace\close_span(); + +if ($outerSpan) { + \DDTrace\switch_stack($outerSpan); +} + +header('Content-Type: application/json'); +echo json_encode($response); diff --git a/compile_rust.sh b/compile_rust.sh index fe62f8c304e..85069e7713d 100755 --- a/compile_rust.sh +++ b/compile_rust.sh @@ -13,6 +13,33 @@ case "${host_os}" in ;; esac +# GCC < 9 doesn't support -fuse-ld=lld (emitted by libdd-otel-thread-ctx-ffi/build.rs). +# Intercept CC calls and replace -fuse-ld=lld with -B where /ld -> ld.lld. +_gcc_major=$(cc -dumpversion 2>/dev/null | cut -d. -f1) +if [ -n "${_gcc_major}" ] && [ "${_gcc_major:-99}" -lt 9 ] 2>/dev/null; then + _sysroot=$(rustc --print sysroot 2>/dev/null) + _tgt=$(rustc -vV 2>/dev/null | sed -n 's/^host: //p') + _lld="${_sysroot}/lib/rustlib/${_tgt}/bin/gcc-ld/ld.lld" + if [ -x "${_lld}" ]; then + _wd=$(mktemp -d) + ln -sf "${_lld}" "${_wd}/ld" + _real_cc=$(command -v cc) + cat > "${_wd}/cc" << EOF +#!/bin/sh +_a= +for _x in "\$@"; do + case "\$_x" in + -fuse-ld=lld) _a="\$_a -B${_wd}" ;; + *) _a="\$_a \$_x" ;; + esac +done +exec ${_real_cc} \$_a +EOF + chmod +x "${_wd}/cc" + export PATH="${_wd}:${PATH}" + fi +fi + set -x if test -n "$COMPILE_ASAN"; then diff --git a/components-rs/Cargo.toml b/components-rs/Cargo.toml index c4b8de1c341..8f65392737e 100644 --- a/components-rs/Cargo.toml +++ b/components-rs/Cargo.toml @@ -24,6 +24,7 @@ libdd-tinybytes = { path = "../libdatadog/libdd-tinybytes" } libdd-trace-utils = { path = "../libdatadog/libdd-trace-utils" } libdd-trace-stats = { path = "../libdatadog/libdd-trace-stats" } libdd-crashtracker-ffi = { path = "../libdatadog/libdd-crashtracker-ffi", default-features = false, features = ["collector"] } +libdd-library-config = { path = "../libdatadog/libdd-library-config", features = ["otel-thread-ctx"] } libdd-library-config-ffi = { path = "../libdatadog/libdd-library-config-ffi", default-features = false } spawn_worker = { path = "../libdatadog/spawn_worker" } anyhow = { version = "1.0" } @@ -54,6 +55,9 @@ libc = "0.2" bincode = { version = "1.3.3" } hashbrown = "0.15" +[target.'cfg(target_os = "linux")'.dependencies] +libdd-otel-thread-ctx-ffi = { path = "../libdatadog/libdd-otel-thread-ctx-ffi", default-features = false } + [build-dependencies] cbindgen = "0.27" diff --git a/components-rs/datadog.h b/components-rs/datadog.h index bb08a554b10..8aee8d85a0b 100644 --- a/components-rs/datadog.h +++ b/components-rs/datadog.h @@ -41,6 +41,8 @@ void datadog_generate_session_id(void); void datadog_format_runtime_id(uint8_t (*buf)[36]); +bool datadog_publish_otel_process_context(void); + 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 f73beec72d2..c21d83a1d1e 100644 --- a/components-rs/lib.rs +++ b/components-rs/lib.rs @@ -13,25 +13,32 @@ pub mod telemetry; pub mod trace_filter; pub mod bytes; -use libdd_common::entity_id::{get_container_id, set_cgroup_file}; +pub use datadog_sidecar_ffi::*; +pub use libdd_crashtracker_ffi::*; +pub use libdd_common_ffi::*; +pub use libdd_library_config_ffi::*; +pub use libdd_telemetry_ffi::*; + use http::uri::{PathAndQuery, Scheme}; use http::Uri; +use libdd_common::entity_id::{get_container_id, set_cgroup_file}; +use libdd_common::{parse_uri, Endpoint}; +use libdd_common_ffi::slice::AsBytes; use std::borrow::Cow; use std::ffi::{c_char, OsStr}; -#[cfg(unix)] -use std::path::Path; use std::ptr::null_mut; use uuid::Uuid; -pub use libdd_crashtracker_ffi::*; -pub use libdd_library_config_ffi::*; -pub use datadog_sidecar_ffi::*; -use libdd_common::{parse_uri, Endpoint}; #[cfg(unix)] use libdd_common::connector::uds::socket_path_to_uri; -use libdd_common_ffi::slice::AsBytes; -pub use libdd_common_ffi::*; -pub use libdd_telemetry_ffi::*; +#[cfg(unix)] +use std::path::Path; + +#[cfg(target_os = "linux")] +pub use libdd_otel_thread_ctx_ffi::*; + +#[cfg(target_os = "linux")] +use libdd_library_config::tracer_metadata::{store_tracer_metadata, TracerMetadata}; #[no_mangle] #[allow(non_upper_case_globals)] @@ -92,6 +99,35 @@ 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")] +#[no_mangle] +pub extern "C" fn datadog_publish_otel_process_context() -> bool { + let runtime_id = unsafe { + (!datadog_runtime_id.is_nil()).then(|| datadog_runtime_id.as_hyphenated().to_string()) + }; + + let metadata = TracerMetadata { + runtime_id, + tracer_language: "php".to_owned(), + threadlocal_attribute_keys: Some(std::vec::Vec::new()), + ..Default::default() + }; + + match store_tracer_metadata(&metadata) { + Ok(_) => true, + Err(error) => { + tracing::debug!("failed to publish OTel process context: {error}"); + false + } + } +} + +#[cfg(not(target_os = "linux"))] +#[no_mangle] +pub extern "C" fn datadog_publish_otel_process_context() -> bool { + false +} + #[must_use] #[no_mangle] pub extern "C" fn ddtrace_get_container_id() -> CharSlice<'static> { diff --git a/components-rs/otel-thread-ctx.h b/components-rs/otel-thread-ctx.h new file mode 100644 index 00000000000..0799390d5b4 --- /dev/null +++ b/components-rs/otel-thread-ctx.h @@ -0,0 +1,35 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +#ifndef DDOG_OTEL_THREAD_CTX_H +#define DDOG_OTEL_THREAD_CTX_H + +#pragma once + +#include +#include +#include + +#ifdef __linux__ + +#define ddog_MAX_ATTRS_DATA_SIZE 612 + +typedef struct ddog_ThreadContextHandle ddog_ThreadContextHandle; + +struct ddog_ThreadContextHandle *ddog_otel_thread_ctx_new(const uint8_t (*trace_id)[16], + const uint8_t (*span_id)[8], + const uint8_t (*local_root_span_id)[8]); + +void ddog_otel_thread_ctx_free(struct ddog_ThreadContextHandle *ctx); + +struct ddog_ThreadContextHandle *ddog_otel_thread_ctx_attach(struct ddog_ThreadContextHandle *ctx); + +struct ddog_ThreadContextHandle *ddog_otel_thread_ctx_detach(void); + +void ddog_otel_thread_ctx_update(const uint8_t (*trace_id)[16], + const uint8_t (*span_id)[8], + const uint8_t (*local_root_span_id)[8]); + +#endif + +#endif /* DDOG_OTEL_THREAD_CTX_H */ diff --git a/config.m4 b/config.m4 index aa69d309a43..0bc79e7e5c8 100644 --- a/config.m4 +++ b/config.m4 @@ -224,6 +224,7 @@ if test "$PHP_DDTRACE" != "no"; then tracer/live_debugger.c \ tracer/limiter/limiter.c \ tracer/memory_limit.c \ + tracer/otel_context.c \ tracer/tracer_otel_config.c \ tracer/priority_sampling/priority_sampling.c \ tracer/profiling.c \ diff --git a/config.w32 b/config.w32 index 5ff5b2e9d70..860834f86ab 100644 --- a/config.w32 +++ b/config.w32 @@ -66,6 +66,7 @@ if (PHP_DDTRACE != 'no') { DDTRACE_TRACER_SOURCES += " ip_extraction.c"; DDTRACE_TRACER_SOURCES += " live_debugger.c"; DDTRACE_TRACER_SOURCES += " memory_limit.c"; + DDTRACE_TRACER_SOURCES += " otel_context.c"; DDTRACE_TRACER_SOURCES += " tracer_otel_config.c"; DDTRACE_TRACER_SOURCES += " profiling.c"; DDTRACE_TRACER_SOURCES += " random.c"; @@ -199,6 +200,7 @@ if (PHP_DDTRACE != 'no') { deffile.WriteLine("EXPORTS"); var contents = FSO.OpenTextFile(configure_module_dirname + "/datadog.sym", 1).ReadAll(); contents = contents.replace(/ddog_crashtracker_entry_point\s*/, ""); // unix-only symbol + contents = contents.replace(/otel_thread_ctx_v1\s*/, ""); // linux-only TLS variable contents = contents + "\n" + FSO.OpenTextFile(configure_module_dirname + "/datadog-windows.sym", 1).ReadAll(); if (!PHP_DDTRACE_SHARED) { contents = contents.replace(/get_module\s*/, ""); diff --git a/datadog.sym b/datadog.sym index 510519066f2..9e86ca34e33 100644 --- a/datadog.sym +++ b/datadog.sym @@ -1,6 +1,7 @@ ddtrace_close_all_spans_and_flush datadog_get_formatted_session_id ddtrace_get_profiling_context +otel_thread_ctx_v1 ddtrace_get_root_span datadog_process_tags_get_serialized datadog_get_sidecar_queue_id diff --git a/ext/datadog.c b/ext/datadog.c index fc2cd4622c4..0518911395d 100644 --- a/ext/datadog.c +++ b/ext/datadog.c @@ -157,6 +157,7 @@ static void dd_activate_once(void) { if (dd_main_pid != getpid()) { // equal to session id if not a fork datadog_generate_runtime_id(); } + datadog_publish_otel_process_context(); // must run before the first zai_hook_activate as tracer telemetry setup installs a global hook if (!datadog_disable) { diff --git a/profiling/build.rs b/profiling/build.rs index 59ce4cf2a58..230a4fae8c3 100644 --- a/profiling/build.rs +++ b/profiling/build.rs @@ -275,6 +275,8 @@ fn generate_bindings(php_config_includes: &str, fibers: bool, zend_error_observe .raw_line("pub type zend_vm_opcode_handler_func_t = *const ::std::ffi::c_void;") // Block a few of functions that we'll provide defs for manually .blocklist_item("datadog_php_profiling_vm_interrupt_addr") + .blocklist_item("datadog_php_profiling_rinit") + .blocklist_item("datadog_php_profiling_context_api_name") // I had to block these for some reason *shrug* .blocklist_item("FP_INFINITE") .blocklist_item("FP_INT_DOWNWARD") diff --git a/profiling/src/bindings/mod.rs b/profiling/src/bindings/mod.rs index 220be2cd123..b8cedeff4d8 100644 --- a/profiling/src/bindings/mod.rs +++ b/profiling/src/bindings/mod.rs @@ -340,6 +340,14 @@ extern "C" { /// Must be called from a PHP thread during a request. pub fn datadog_php_profiling_vm_interrupt_addr() -> *const AtomicBool; + /// Initializes per-thread profiler FFI state. + /// # Safety + /// Must be called from a PHP thread during a request. + pub fn datadog_php_profiling_rinit(); + + /// Returns the profiling context API selected for this request. + pub fn datadog_php_profiling_context_api_name() -> ZaiStr<'static>; + /// Registers the extension. Note that it's kept in a zend_llist and gets /// pemalloc'd + memcpy'd into place. The engine says this is a mutable /// pointer, but in practice it's const. diff --git a/profiling/src/lib.rs b/profiling/src/lib.rs index 4afd0e8e816..71d6758e38c 100644 --- a/profiling/src/lib.rs +++ b/profiling/src/lib.rs @@ -584,6 +584,8 @@ extern "C" fn rinit(_type: c_int, _module_number: c_int) -> ZendResult { let result = REQUEST_LOCALS.try_with_borrow_mut(|locals| { // SAFETY: we are in rinit on a PHP thread. locals.vm_interrupt_addr = unsafe { zend::datadog_php_profiling_vm_interrupt_addr() }; + // SAFETY: we are in rinit on a PHP thread. + unsafe { zend::datadog_php_profiling_rinit() }; // SAFETY: We are after first rinit and before mshutdown. unsafe { @@ -663,6 +665,13 @@ extern "C" fn rinit(_type: c_int, _module_number: c_int) -> ZendResult { let once = unsafe { &*ptr::addr_of!(RINIT_ONCE) }; once.call_once(|| { if system_settings.profiling_enabled { + // SAFETY: this returns a view of a static string owned by php_ffi.c. + let context_api = unsafe { bindings::datadog_php_profiling_context_api_name() }; + info!( + "Profiling context API selected: {}.", + context_api.to_string_lossy() + ); + // SAFETY: sapi_module is initialized by rinit and shouldn't be // modified at this point (safe to read values). let sapi_module = unsafe { &*ptr::addr_of!(zend::sapi_module) }; diff --git a/profiling/src/php_ffi.c b/profiling/src/php_ffi.c index 1e906f3cbd3..4dac4903771 100644 --- a/profiling/src/php_ffi.c +++ b/profiling/src/php_ffi.c @@ -7,14 +7,172 @@ #include #include "SAPI.h" -#if CFG_STACK_WALKING_TESTS +#if CFG_STACK_WALKING_TESTS || defined(__linux__) #include // for dlsym #endif +#ifdef __linux__ +#include +#include +#endif const char *datadog_extension_build_id(void) { return ZEND_EXTENSION_BUILD_ID; } const char *datadog_module_build_id(void) { return ZEND_MODULE_BUILD_ID; } uint8_t *datadog_runtime_id = NULL; +static const zai_str datadog_php_profiling_context_api_none = ZAI_STRL("none"); +static const zai_str datadog_php_profiling_context_api_otel = ZAI_STRL("otel_thread_ctx_v1"); +static const zai_str datadog_php_profiling_context_api_legacy = ZAI_STRL("ddtrace_get_profiling_context"); + +static ddtrace_profiling_context noop_get_profiling_context(void) { + return (ddtrace_profiling_context){0, 0}; +} + +static ddtrace_profiling_context datadog_php_profiling_get_context(void); + +static ddtrace_profiling_context (*datadog_php_profiling_get_legacy_context)(void) = + noop_get_profiling_context; + +#ifdef __linux__ +#define DATADOG_PHP_PROFILING_OTEL_ATTRS_DATA_SIZE 612 +#define DATADOG_PHP_PROFILING_OTEL_TLS_SYMBOL "otel_thread_ctx_v1" + +typedef struct datadog_php_profiling_otel_thread_context_record { + uint8_t trace_id[16]; + uint8_t span_id[8]; + _Atomic uint8_t valid; + uint8_t reserved; + uint16_t attrs_data_size; + uint8_t attrs_data[DATADOG_PHP_PROFILING_OTEL_ATTRS_DATA_SIZE]; +} datadog_php_profiling_otel_thread_context_record; + +_Static_assert(sizeof(datadog_php_profiling_otel_thread_context_record) == 640, + "unexpected OTel thread context record size"); +_Static_assert(offsetof(datadog_php_profiling_otel_thread_context_record, trace_id) == 0, + "unexpected OTel thread context trace_id offset"); +_Static_assert(offsetof(datadog_php_profiling_otel_thread_context_record, span_id) == 16, + "unexpected OTel thread context span_id offset"); +_Static_assert(offsetof(datadog_php_profiling_otel_thread_context_record, valid) == 24, + "unexpected OTel thread context valid offset"); +_Static_assert(offsetof(datadog_php_profiling_otel_thread_context_record, reserved) == 25, + "unexpected OTel thread context reserved offset"); +_Static_assert(offsetof(datadog_php_profiling_otel_thread_context_record, attrs_data_size) == 26, + "unexpected OTel thread context attrs_data_size offset"); +_Static_assert(offsetof(datadog_php_profiling_otel_thread_context_record, attrs_data) == 28, + "unexpected OTel thread context attrs_data offset"); + +static __thread void **datadog_php_profiling_otel_thread_ctx_slot = NULL; + +static inline uint64_t datadog_php_profiling_read_u64_be(const uint8_t src[8]) { + uint64_t be_value; + memcpy(&be_value, src, sizeof(be_value)); + +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + return __builtin_bswap64(be_value); +#elif __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ + return be_value; +#else +#error "Unsupported byte order" +#endif +} + +static inline uint8_t datadog_php_profiling_hex_to_u4(uint8_t hex) { + if (hex >= '0' && hex <= '9') { + return (uint8_t)(hex - '0'); + } + if (hex >= 'a' && hex <= 'f') { + return (uint8_t)(hex - 'a' + 10); + } + if (hex >= 'A' && hex <= 'F') { + return (uint8_t)(hex - 'A' + 10); + } + return UINT8_MAX; +} + +static bool datadog_php_profiling_parse_u64_hex(const uint8_t hex[16], uint64_t *value) { + uint64_t result = 0; + + for (size_t i = 0; i < 16; ++i) { + uint8_t nibble = datadog_php_profiling_hex_to_u4(hex[i]); + if (nibble == UINT8_MAX) { + return false; + } + result = (result << 4) | nibble; + } + + *value = result; + return true; +} + +static uint64_t datadog_php_profiling_otel_context_local_root_span_id( + const datadog_php_profiling_otel_thread_context_record *record) { + if (record->attrs_data_size < 18 || record->attrs_data[0] != 0 || record->attrs_data[1] != 16) { + return 0; + } + + uint64_t local_root_span_id = 0; + if (!datadog_php_profiling_parse_u64_hex(record->attrs_data + 2, &local_root_span_id)) { + return 0; + } + + return local_root_span_id; +} + +static ddtrace_profiling_context datadog_php_profiling_read_otel_context(void) { + ddtrace_profiling_context context = {0, 0}; + if (!datadog_php_profiling_otel_thread_ctx_slot) { + return context; + } + + datadog_php_profiling_otel_thread_context_record *record = + (datadog_php_profiling_otel_thread_context_record *)*datadog_php_profiling_otel_thread_ctx_slot; + if (!record || atomic_load_explicit(&record->valid, memory_order_relaxed) != 1) { + return context; + } + + atomic_signal_fence(memory_order_acquire); + + context.span_id = datadog_php_profiling_read_u64_be(record->span_id); + context.local_root_span_id = datadog_php_profiling_otel_context_local_root_span_id(record); + + atomic_signal_fence(memory_order_acquire); + + if (atomic_load_explicit(&record->valid, memory_order_relaxed) != 1) { + return (ddtrace_profiling_context){0, 0}; + } + + return context; +} + +static void *datadog_php_profiling_find_otel_thread_ctx_symbol(void) { + void *tls_symbol = dlsym(RTLD_DEFAULT, DATADOG_PHP_PROFILING_OTEL_TLS_SYMBOL); + if (tls_symbol) { + return tls_symbol; + } + + const zend_llist *extensions = &zend_extensions; + for (const zend_llist_element *item = extensions->head; item; item = item->next) { + const zend_extension *extension = (zend_extension *)item->data; + if (extension && extension->handle) { + tls_symbol = DL_FETCH_SYMBOL(extension->handle, DATADOG_PHP_PROFILING_OTEL_TLS_SYMBOL); + if (tls_symbol) { + return tls_symbol; + } + } + } + + zend_module_entry *module; + ZEND_HASH_FOREACH_PTR(&module_registry, module) { + if (module && module->handle) { + tls_symbol = DL_FETCH_SYMBOL(module->handle, DATADOG_PHP_PROFILING_OTEL_TLS_SYMBOL); + if (tls_symbol) { + return tls_symbol; + } + } + } ZEND_HASH_FOREACH_END(); + + return NULL; +} +#endif static void locate_datadog_runtime_id(const zend_extension *extension) { datadog_runtime_id = DL_FETCH_SYMBOL(extension->handle, "datadog_runtime_id"); @@ -24,7 +182,7 @@ static void locate_ddtrace_get_profiling_context(const zend_extension *extension ddtrace_profiling_context (*get_profiling)(void) = DL_FETCH_SYMBOL(extension->handle, "ddtrace_get_profiling_context"); if (EXPECTED(get_profiling)) { - datadog_php_profiling_get_profiling_context = get_profiling; + datadog_php_profiling_get_legacy_context = get_profiling; } } @@ -40,10 +198,6 @@ static bool is_ddtrace_extension(const zend_extension *ext) { return ext && ext->name && strcmp(ext->name, "ddtrace") == 0; } -static ddtrace_profiling_context noop_get_profiling_context(void) { - return (ddtrace_profiling_context){0, 0}; -} - static zend_string *noop_get_process_tags_serialized(void) { return NULL; } @@ -155,7 +309,8 @@ void datadog_php_profiling_startup(zend_extension *extension) { _ignore_run_time_cache = strcmp(sapi_module.name, "cli") == 0; #endif - datadog_php_profiling_get_profiling_context = noop_get_profiling_context; + datadog_php_profiling_get_profiling_context = datadog_php_profiling_get_context; + datadog_php_profiling_get_legacy_context = noop_get_profiling_context; datadog_php_profiling_get_process_tags_serialized = noop_get_process_tags_serialized; /* Due to the optional dependency on ddtrace, the profiling module will be @@ -181,14 +336,43 @@ void datadog_php_profiling_startup(zend_extension *extension) { #endif } +void datadog_php_profiling_rinit(void) { +#ifdef __linux__ + datadog_php_profiling_otel_thread_ctx_slot = + (void **)datadog_php_profiling_find_otel_thread_ctx_symbol(); +#endif +} + +zai_str datadog_php_profiling_context_api_name(void) { +#ifdef __linux__ + if (datadog_php_profiling_otel_thread_ctx_slot) { + return datadog_php_profiling_context_api_otel; + } +#endif + if (datadog_php_profiling_get_legacy_context != noop_get_profiling_context) { + return datadog_php_profiling_context_api_legacy; + } + return datadog_php_profiling_context_api_none; +} + void *datadog_php_profiling_vm_interrupt_addr(void) { return &EG(vm_interrupt); } zend_module_entry *datadog_get_module_entry(const char *str, uintptr_t len) { return zend_hash_str_find_ptr(&module_registry, str, len); } +static ddtrace_profiling_context datadog_php_profiling_get_context(void) { +#ifdef __linux__ + ddtrace_profiling_context otel_context = datadog_php_profiling_read_otel_context(); + if (otel_context.local_root_span_id || otel_context.span_id) { + return otel_context; + } +#endif + return datadog_php_profiling_get_legacy_context(); +} + ddtrace_profiling_context (*datadog_php_profiling_get_profiling_context)(void) = - noop_get_profiling_context; + datadog_php_profiling_get_context; zend_string *(*datadog_php_profiling_get_process_tags_serialized)(void) = noop_get_process_tags_serialized; diff --git a/profiling/src/php_ffi.h b/profiling/src/php_ffi.h index 558c3de4413..6771f900244 100644 --- a/profiling/src/php_ffi.h +++ b/profiling/src/php_ffi.h @@ -73,9 +73,9 @@ zend_module_entry *datadog_get_module_entry(const char *str, uintptr_t len); void *datadog_php_profiling_vm_interrupt_addr(void); /** - * For Code Hotspots, we need the tracer's local root span id and the current - * span id. This is a cross-product struct, so keep it in sync with tracer's - * version of this struct. + * For Code Hotspots, we need the local root span id and the current span id. + * The legacy ddtrace_get_profiling_context ABI also uses this struct, so keep + * it in sync with tracer's version. * todo: re-use the tracer's header? */ typedef struct ddtrace_profiling_context_s { @@ -83,8 +83,10 @@ typedef struct ddtrace_profiling_context_s { } ddtrace_profiling_context; /** - * A pointer to the tracer's ddtrace_get_profiling_context function if it was - * found, otherwise points to a function which just returns {0, 0}. + * A pointer to the profiling-context function. On Linux it first reads the + * OTel thread-context ABI directly when available, then falls back to the + * tracer's legacy ddtrace_get_profiling_context function if it was found. + * Otherwise it returns {0, 0}. */ extern ddtrace_profiling_context (*datadog_php_profiling_get_profiling_context)(void); @@ -101,6 +103,18 @@ extern zend_string *(*datadog_php_profiling_get_process_tags_serialized)(void); */ void datadog_php_profiling_startup(zend_extension *extension); +/** + * Called by this zend_extension's .activate handler to initialize per-thread + * profiler FFI state. + */ +void datadog_php_profiling_rinit(void); + +/** + * Returns the profiling context API selected for this request, or "none" when + * no provider was found. + */ +zai_str datadog_php_profiling_context_api_name(void); + /** * Used to hold information for overwriting the internal function handler * pointer in the Zend Engine. diff --git a/profiling/src/profiling/mod.rs b/profiling/src/profiling/mod.rs index c3d5a53a22d..9c26f2007a1 100644 --- a/profiling/src/profiling/mod.rs +++ b/profiling/src/profiling/mod.rs @@ -1779,12 +1779,17 @@ impl Profiler { // Casting between two integers of the same size is a no-op, and // Rust uses 2's complement for negative numbers. let local_root_span_id = context.local_root_span_id as i64; - let span_id = context.span_id as i64; labels.push(Label { key: "local root span id", value: LabelValue::Num(local_root_span_id, ""), }); + } + + if context.span_id != 0 { + // Casting between two integers of the same size is a no-op, and + // Rust uses 2's complement for negative numbers. + let span_id = context.span_id as i64; labels.push(Label { key: "span id", diff --git a/tracer/ddtrace.c b/tracer/ddtrace.c index 7f5e9681d92..feaeb521b05 100644 --- a/tracer/ddtrace.c +++ b/tracer/ddtrace.c @@ -57,6 +57,7 @@ #include "live_debugger.h" #include "standalone_limiter.h" #include "priority_sampling/priority_sampling.h" +#include "otel_context.h" #include "random.h" #include "autoload_php_files.h" #include "serializer.h" @@ -500,6 +501,8 @@ void ddtrace_rinit_early(void) { } void ddtrace_rinit(void) { + ddtrace_detach_otel_thread_context(); + if (!DDTRACE_G(agent_config_reader) && !get_global_DD_TRACE_IGNORE_AGENT_SAMPLING_RATES()) { if (get_global_DD_TRACE_SIDECAR_TRACE_SENDER()) { if (datadog_endpoint) { @@ -610,6 +613,8 @@ void ddtrace_rshutdown(bool fast_shutdown) { DDTRACE_G(active_stack) = NULL; } + ddtrace_clear_otel_thread_context_root_span(); + ddtrace_ffe_flush_exposures(); ddtrace_ffe_flush_evaluation_metrics(); diff --git a/tracer/ddtrace_globals.h b/tracer/ddtrace_globals.h index 3865b2e48f9..14edbfc4270 100644 --- a/tracer/ddtrace_globals.h +++ b/tracer/ddtrace_globals.h @@ -51,6 +51,9 @@ typedef struct { zend_long default_priority_sampling; zend_long propagated_priority_sampling; ddtrace_span_stack *active_stack; // never NULL except tracer is disabled +#ifdef __linux__ + ddtrace_root_span_data *otel_context_root_span_override; +#endif ddtrace_span_stack *top_closed_stack; HashTable traced_spans; // tie a span to a specific active execute_data uint32_t open_spans_count; diff --git a/tracer/handlers_fiber.c b/tracer/handlers_fiber.c index 4999f6b0205..fe3b74f2b4c 100644 --- a/tracer/handlers_fiber.c +++ b/tracer/handlers_fiber.c @@ -1,6 +1,7 @@ #include "ddtrace.h" #include "configuration.h" #include "handlers_fiber.h" +#include "otel_context.h" #include "span.h" #include #include @@ -130,6 +131,7 @@ static void dd_observe_fiber_switch(zend_fiber_context *from, zend_fiber_context from->reserved[dd_resource_handle] = DDTRACE_G(active_stack); DDTRACE_G(active_stack) = to_stack; + ddtrace_update_otel_thread_context(); } static void dd_observe_fiber_init(zend_fiber_context *context) { diff --git a/tracer/otel_context.c b/tracer/otel_context.c new file mode 100644 index 00000000000..17980d83328 --- /dev/null +++ b/tracer/otel_context.c @@ -0,0 +1,140 @@ +#include "otel_context.h" + +#include "ddtrace.h" +#include "span.h" + +#ifdef __linux__ +#include "configuration.h" +#include +#include +#endif + +ZEND_EXTERN_MODULE_GLOBALS(datadog); + +#ifdef __linux__ +static ddtrace_root_span_data *ddtrace_root_span_from_zobj(zend_object *root_span); +static DDOG_CHECK_RETURN ddtrace_root_span_data *ddtrace_replace_otel_context_root_span_override( + ddtrace_root_span_data *root); +static ddtrace_span_data *ddtrace_otel_context_span(void); +static inline void ddtrace_write_u64_be(uint8_t dest[8], uint64_t value); +static void ddtrace_trace_id_to_otel_bytes(datadog_trace_id trace_id, uint8_t dest[16]); + +void ddtrace_set_otel_thread_context_root_span(zend_object *root_span) { + ddtrace_root_span_data *root = ddtrace_root_span_from_zobj(root_span); + if (DDTRACE_G(otel_context_root_span_override) == root) { + return; + } + + ddtrace_root_span_data *old = ddtrace_replace_otel_context_root_span_override(root); + ddtrace_update_otel_thread_context(); + if (old) { + OBJ_RELEASE(&old->std); + } +} + +void ddtrace_clear_otel_thread_context_root_span(void) { + ddtrace_root_span_data *old = ddtrace_replace_otel_context_root_span_override(NULL); + if (!old) { + return; + } + ddtrace_update_otel_thread_context(); + OBJ_RELEASE(&old->std); +} + +void ddtrace_detach_otel_thread_context(void) { + ddtrace_root_span_data *old = ddtrace_replace_otel_context_root_span_override(NULL); + struct ddog_ThreadContextHandle *ctx = ddog_otel_thread_ctx_detach(); + if (ctx) { + ddog_otel_thread_ctx_free(ctx); + } + if (old) { + OBJ_RELEASE(&old->std); + } +} + +void ddtrace_update_otel_thread_context(void) { + ddtrace_span_data *span = ddtrace_otel_context_span(); + if (!span || !span->root) { + ddtrace_detach_otel_thread_context(); + return; + } + + ddtrace_root_span_data *root = span->root; + + uint8_t trace_id[16]; + uint8_t span_id[8]; + uint8_t local_root_span_id[8]; + + ddtrace_trace_id_to_otel_bytes(root->trace_id, trace_id); + ddtrace_write_u64_be(span_id, span->span_id); + ddtrace_write_u64_be(local_root_span_id, root->span_id); + + ddog_otel_thread_ctx_update(&trace_id, &span_id, &local_root_span_id); +} + +static ddtrace_root_span_data *ddtrace_root_span_from_zobj(zend_object *root_span) { + if (!root_span || root_span->ce != ddtrace_ce_root_span_data) { + return NULL; + } + + return ROOTSPANDATA(root_span); +} + +static DDOG_CHECK_RETURN ddtrace_root_span_data *ddtrace_replace_otel_context_root_span_override( + ddtrace_root_span_data *root) { + ddtrace_root_span_data *old = DDTRACE_G(otel_context_root_span_override); + if (root) { + GC_ADDREF(&root->std); + } + DDTRACE_G(otel_context_root_span_override) = root; + return old; +} + +static ddtrace_span_data *ddtrace_otel_context_span(void) { + if (!get_DD_TRACE_ENABLED()) { + return NULL; + } + + if (DDTRACE_G(otel_context_root_span_override)) { + ddtrace_root_span_data *root = DDTRACE_G(otel_context_root_span_override); + + if (DDTRACE_G(active_stack) && DDTRACE_G(active_stack)->active) { + ddtrace_span_data *active = SPANDATA(DDTRACE_G(active_stack)->active); + if (active->root == root) { + return active; + } + } + + return &root->span; + } + + if (DDTRACE_G(active_stack) && DDTRACE_G(active_stack)->root_span && DDTRACE_G(active_stack)->active) { + return SPANDATA(DDTRACE_G(active_stack)->active); + } + + return NULL; +} + +static inline void ddtrace_write_u64_be(uint8_t dest[8], uint64_t value) { + uint64_t be_value = +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + __builtin_bswap64(value); +#elif __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ + value; +#else +#error "Unsupported byte order" +#endif + memcpy(dest, &be_value, sizeof(be_value)); +} + +static void ddtrace_trace_id_to_otel_bytes(datadog_trace_id trace_id, uint8_t dest[16]) { + ddtrace_write_u64_be(dest, trace_id.high); + ddtrace_write_u64_be(dest + 8, trace_id.low); +} +#else // !__linux__ + +void ddtrace_set_otel_thread_context_root_span(zend_object *root_span) {} +void ddtrace_clear_otel_thread_context_root_span(void) {} +void ddtrace_detach_otel_thread_context(void) {} +void ddtrace_update_otel_thread_context(void) {} +#endif diff --git a/tracer/otel_context.h b/tracer/otel_context.h new file mode 100644 index 00000000000..958c74e3263 --- /dev/null +++ b/tracer/otel_context.h @@ -0,0 +1,64 @@ +#ifndef DDTRACE_OTEL_CONTEXT_H +#define DDTRACE_OTEL_CONTEXT_H + +#include + +BEGIN_EXTERN_C() + +/** + * Prefer this root span when publishing OTel thread context. UserRequest uses + * this to keep the context aligned with the notified request root. + * + * While this override is set, the published trace id and local root span id + * come from this root span. The published active span id follows + * DDTRACE_G(active_stack)->active only when that active span belongs to this + * root. If the active stack points at another trace/root, the active span id + * falls back to this root span id. + * + * When this changes the override, it calls ddtrace_update_otel_thread_context(); + * callers need not call it again after setting the override. + */ +void ddtrace_set_otel_thread_context_root_span(zend_object *root_span); + +/** + * Stop preferring the current root span override. + * + * When this clears an existing override, it calls + * ddtrace_update_otel_thread_context(); callers need not call it again after + * clearing the override. + * + * Call also when cleaning up other globals (e.g. on request shutdown), as a + * defensive measure in case the normal UserRequest finish path is skipped. + */ +void ddtrace_clear_otel_thread_context_root_span(void); + +/** + * Publish the selected tracer context through Linux's OTel thread-context TLS + * slot. + * + * On non-Linux builds this is a no-op. + * + * Call this after changing which tracer span should be visible to OTel: + * opening a span, closing or dropping the active span, switching the active + * span stack, switching fibers, or setting/clearing the root span override. + * When there is no selected tracer span/root, this detaches the OTel thread + * context instead. + */ +void ddtrace_update_otel_thread_context(void); + +/** + * Detach and release the current OTel thread context. This also clears the root + * span override set by ddtrace_set_otel_thread_context_root_span(), if any. + * + * On non-Linux builds this is a no-op. + * + * Call this at hard context boundaries where nothing from the previous tracer + * context should remain visible to OTel: request start, span-stack cleanup + * during request shutdown or tracing disable, or when + * ddtrace_update_otel_thread_context() finds no selected tracer span/root. + */ +void ddtrace_detach_otel_thread_context(void); + +END_EXTERN_C() + +#endif // DDTRACE_OTEL_CONTEXT_H diff --git a/tracer/span.c b/tracer/span.c index a744e020cc2..32a12d65869 100644 --- a/tracer/span.c +++ b/tracer/span.c @@ -25,6 +25,7 @@ #include "standalone_limiter.h" #include "code_origins.h" #include "endpoint_guessing.h" +#include "otel_context.h" #define USE_REALTIME_CLOCK 0 #define USE_MONOTONIC_CLOCK 1 @@ -144,6 +145,8 @@ void ddtrace_free_span_stacks(bool silent) { DDTRACE_G(dropped_spans_count) = 0; DDTRACE_G(closed_spans_count) = 0; DDTRACE_G(top_closed_stack) = NULL; + + ddtrace_detach_otel_thread_context(); } static ddtrace_span_data *ddtrace_init_span(enum ddtrace_span_dataype type, zend_class_entry *ce) { @@ -307,6 +310,7 @@ ddtrace_span_data *ddtrace_open_span(enum ddtrace_span_dataype type) { span->root = DDTRACE_G(active_stack)->root_span; ddtrace_set_global_span_properties(span); + ddtrace_update_otel_thread_context(); if (root_span) { ddtrace_root_span_data *root = ROOTSPANDATA(&span->std); @@ -583,6 +587,7 @@ void ddtrace_switch_span_stack(ddtrace_span_stack *target_stack) { GC_ADDREF(&target_stack->std); ddtrace_span_stack *active_stack = DDTRACE_G(active_stack); DDTRACE_G(active_stack) = target_stack; + ddtrace_update_otel_thread_context(); OBJ_RELEASE(&active_stack->std); } @@ -949,6 +954,7 @@ void ddtrace_close_top_span_without_stack_swap(ddtrace_span_data *span) { } else { ZVAL_NULL(&stack->property_active); } + ddtrace_update_otel_thread_context(); #if PHP_VERSION_ID < 70400 // On PHP 7.3 and prior PHP will just destroy all unchanged references in cycle collection, in particular given that it does not appear in get_gc // Artificially increase refcount here thus. @@ -1076,6 +1082,7 @@ void ddtrace_drop_span(ddtrace_span_data *span) { } else { ZVAL_NULL(&stack->property_active); } + ddtrace_update_otel_thread_context(); ++DDTRACE_G(dropped_spans_count); --DDTRACE_G(open_spans_count); diff --git a/tracer/user_request.c b/tracer/user_request.c index 5305518f027..b08450e5a73 100644 --- a/tracer/user_request.c +++ b/tracer/user_request.c @@ -2,6 +2,9 @@ #include
#include "configuration.h" #include "ddtrace.h" +#ifdef __linux__ +#include "otel_context.h" +#endif #include "span.h" #define NS "DDTrace\\UserRequest\\" @@ -70,6 +73,11 @@ PHP_FUNCTION(DDTrace_UserRequest_notify_start) RETURN_NULL(); } + span_data->notify_user_req_end = true; +#ifdef __linux__ + ddtrace_set_otel_thread_context_root_span(span); +#endif + zend_array *replacement_resp = NULL; for (size_t i = 0; i < reg_listeners.size; i++) { ddtrace_user_req_listeners *listener = reg_listeners.listeners[i]; @@ -81,8 +89,6 @@ PHP_FUNCTION(DDTrace_UserRequest_notify_start) } } - span_data->notify_user_req_end = true; - if (replacement_resp != NULL) { RETURN_ARR(replacement_resp); } else { @@ -196,6 +202,9 @@ void ddtrace_user_req_notify_finish(ddtrace_span_data *span) ddtrace_user_req_listeners *listener = reg_listeners.listeners[i]; listener->finish_user_req(listener, &span->std); } +#ifdef __linux__ + ddtrace_clear_otel_thread_context_root_span(); +#endif } PHP_FUNCTION(DDTrace_UserRequest_set_blocking_function);