From 4f6193ab5e8902f53d1911e2a4f6674eaa0f2670 Mon Sep 17 00:00:00 2001 From: Louis Tricot Date: Fri, 9 Jan 2026 16:31:32 +0100 Subject: [PATCH 1/7] init process tags for APM Co-Authored-By: PROFeNoM --- components-rs/ddtrace.h | 2 + components-rs/lib.rs | 37 +++ config.m4 | 1 + config.w32 | 1 + ext/configuration.h | 1 + ext/ddtrace.c | 7 + ext/process_tags.c | 228 ++++++++++++++++++ ext/process_tags.h | 20 ++ ext/span.c | 16 +- test_process_tags.php | 53 ++++ .../Custom/Autoloaded/ProcessTagsWebTest.php | 73 ++++++ tests/ext/process_tags.phpt | 52 ++++ 12 files changed, 487 insertions(+), 4 deletions(-) create mode 100644 ext/process_tags.c create mode 100644 ext/process_tags.h create mode 100644 test_process_tags.php create mode 100644 tests/Integrations/Custom/Autoloaded/ProcessTagsWebTest.php create mode 100644 tests/ext/process_tags.phpt diff --git a/components-rs/ddtrace.h b/components-rs/ddtrace.h index 35d06a61820..7c8ae5ae3b9 100644 --- a/components-rs/ddtrace.h +++ b/components-rs/ddtrace.h @@ -239,4 +239,6 @@ void ddog_add_zstr_span_meta_struct_CharSlice(ddog_SpanBytes *ptr, struct _zend_string *key, ddog_CharSlice val); +const char *ddog_normalize_process_tag_value(ddog_CharSlice val); + #endif /* DDTRACE_PHP_H */ diff --git a/components-rs/lib.rs b/components-rs/lib.rs index 5cbfc845312..62636bbc5fe 100644 --- a/components-rs/lib.rs +++ b/components-rs/lib.rs @@ -132,3 +132,40 @@ pub unsafe extern "C" fn posix_spawn_file_actions_addchdir_np( -libc::ENOSYS } } + +const MAX_TAG_VALUE_LENGTH: usize = 100; + +#[no_mangle] +pub extern "C" fn ddog_normalize_process_tag_value( + tag_value: CharSlice, +) -> *const c_char { + let value = tag_value.try_to_utf8().unwrap().to_string(); + + let mut out = String::new(); + let mut prev_underscore = false; + let mut started = false; + for c in value.chars().take(MAX_TAG_VALUE_LENGTH) { + if c.is_alphanumeric() || matches!(c, '/' | '.' | '-') { + for lc in c.to_lowercase() { + out.push(lc); + } + started = true; + prev_underscore = false; + } else { + if started && !prev_underscore { + out.push('_'); + prev_underscore = true; + } + } + } + + // trim trailing underscores + if out.ends_with('_') { + out.pop(); + } + + let c_string = std::ffi::CString::new(out).unwrap(); + let out_ptr = c_string.as_ptr(); + std::mem::forget(c_string); + out_ptr +} \ No newline at end of file diff --git a/config.m4 b/config.m4 index a30f435268f..a86f0d03808 100644 --- a/config.m4 +++ b/config.m4 @@ -203,6 +203,7 @@ if test "$PHP_DDTRACE" != "no"; then ext/memory_limit.c \ ext/otel_config.c \ ext/priority_sampling/priority_sampling.c \ + ext/process_tags.c \ ext/profiling.c \ ext/random.c \ ext/remote_config.c \ diff --git a/config.w32 b/config.w32 index bb74057a216..06b1a0ff048 100644 --- a/config.w32 +++ b/config.w32 @@ -49,6 +49,7 @@ if (PHP_DDTRACE != 'no') { DDTRACE_EXT_SOURCES += " logging.c"; DDTRACE_EXT_SOURCES += " memory_limit.c"; DDTRACE_EXT_SOURCES += " otel_config.c"; + DDTRACE_EXT_SOURCES += " process_tags.c"; DDTRACE_EXT_SOURCES += " profiling.c"; DDTRACE_EXT_SOURCES += " random.c"; DDTRACE_EXT_SOURCES += " remote_config.c"; diff --git a/ext/configuration.h b/ext/configuration.h index 362591df462..8984674f682 100644 --- a/ext/configuration.h +++ b/ext/configuration.h @@ -263,6 +263,7 @@ enum ddtrace_sampling_rules_format { CONFIG(INT, DD_CODE_ORIGIN_MAX_USER_FRAMES, "8") \ CONFIG(BOOL, DD_TRACE_RESOURCE_RENAMING_ENABLED, "false") \ CONFIG(BOOL, DD_TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT, "false") \ + CONFIG(BOOL, DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED, "false") \ DD_INTEGRATIONS #ifndef _WIN32 diff --git a/ext/ddtrace.c b/ext/ddtrace.c index 359576d8aac..d841c309603 100644 --- a/ext/ddtrace.c +++ b/ext/ddtrace.c @@ -68,6 +68,7 @@ #include "limiter/limiter.h" #include "standalone_limiter.h" #include "priority_sampling/priority_sampling.h" +#include "process_tags.h" #include "random.h" #include "autoload_php_files.h" #include "remote_config.h" @@ -1637,6 +1638,12 @@ static void dd_rinit_once(void) { */ ddtrace_startup_logging_first_rinit(); + // Collect process tags now that script path is available + if (get_global_DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED()) { + ddtrace_process_tags_first_rinit(); + } + + // Uses config, cannot run earlier #ifndef _WIN32 ddtrace_signals_first_rinit(); diff --git a/ext/process_tags.c b/ext/process_tags.c new file mode 100644 index 00000000000..d6a31a946b2 --- /dev/null +++ b/ext/process_tags.c @@ -0,0 +1,228 @@ +#include "php.h" +#include +#include +#include +#include +#include "process_tags.h" +#include "configuration.h" +#include "Zend/zend_smart_str.h" +#include "components-rs/ddtrace.h" +#include "SAPI.h" + +#ifndef PATH_MAX +#define PATH_MAX 4096 +#endif + +#define TAG_ENTRYPOINT_NAME "entrypoint.name" +#define TAG_ENTRYPOINT_BASEDIR "entrypoint.basedir" +#define TAG_ENTRYPOINT_WORKDIR "entrypoint.workdir" +#define TAG_ENTRYPOINT_TYPE "entrypoint.type" +#define TAG_RUNTIME_SAPI "runtime.sapi" + +#define TYPE_SCRIPT "script" +#define TYPE_EXECUTABLE "executable" + +#define MAX_TAG_VALUE_LENGTH 100 + +typedef struct { + const char *key; + const char *value; +} process_tag_entry_t; + +typedef struct { + process_tag_entry_t *tag_list; + size_t count; + size_t capacity; + zend_string *serialized; +} process_tags_t; + +static process_tags_t process_tags = {0}; + +static inline const char *get_basename(const char *path) { + if (!path || !*path) return NULL; + + const char *base = path; + for (const char *p = path; *p; p++) { + if ((*p == '/' || *p == '\\') && p[1] != '\0') { + base = p + 1; + } + } + return base; +} + +static void get_basedir(const char* script_path, char *out, size_t out_size) { + if (!script_path || !*script_path || !out || out_size == 0) return; + + const char *last_slash = strrchr(script_path, '/'); + if (!last_slash) { + out[0] = '\0'; + return; + } + + const char *prev_slash = NULL; + for (const char *p = script_path; p < last_slash; p++) { + if (*p == '/') prev_slash = p; + } + + const char *start = prev_slash ? prev_slash + 1 : script_path; + size_t len = last_slash - start; + + if (len >= out_size) len = out_size - 1; + memcpy(out, start, len); + out[len] = '\0'; +} + +static void strip_extension(const char *filename, char *out, size_t out_size) { + const char *dot = strrchr(filename, '.'); + size_t len = dot && dot != filename ? (size_t)(dot - filename) + : strlen(filename); + + if (len >= out_size) len = out_size - 1; + memcpy(out, filename, len); + out[len] = '\0'; +} + +static void add_process_tag(const char* tag_key, const char* tag_value) { + if (!tag_key || !tag_value) { + return; + } + + const char* normalized_value = ddog_normalize_process_tag_value((ddog_CharSlice){ + .ptr = tag_value, + .len = strlen(tag_value) + }); + if (!normalized_value) { + return; + } + + size_t count = process_tags.count; + if (count == process_tags.capacity) { + process_tags.capacity *= 2; + process_tag_entry_t* bigger_list = realloc( + process_tags.tag_list, + process_tags.capacity * sizeof(process_tag_entry_t) + ); + if (!bigger_list) { + process_tags.capacity /= 2; + return; + } + process_tags.tag_list = bigger_list; + } + + process_tags.tag_list[count].key = tag_key; + process_tags.tag_list[count].value = normalized_value; + process_tags.count++; + +} + +static void collect_process_tags(void) { + // entrypoint.workdir + char cwd[PATH_MAX]; + if (VCWD_GETCWD(cwd, sizeof(cwd))) { + const char* entrypoint_workdir = get_basename(cwd); + if (entrypoint_workdir) { + add_process_tag(TAG_ENTRYPOINT_WORKDIR, entrypoint_workdir); + } + } + + // entrypoint.name + const char *entrypoint_name = NULL; + const char *base_dir = NULL; + const char *script = NULL; + + if (SG(request_info).path_translated && *SG(request_info).path_translated) { + script = SG(request_info).path_translated; + } else if (SG(request_info).argv && SG(request_info).argc > 0 && SG(request_info).argv[0]) { + script = SG(request_info).argv[0]; + } + + + entrypoint_name = get_basename(script); + if (entrypoint_name) { + char name_without_ext[PATH_MAX]; + strip_extension(entrypoint_name, name_without_ext, sizeof(name_without_ext)); + + add_process_tag(TAG_ENTRYPOINT_NAME, name_without_ext); + add_process_tag(TAG_ENTRYPOINT_TYPE, TYPE_SCRIPT); + } else { + add_process_tag(TAG_ENTRYPOINT_NAME, "php"); + add_process_tag(TAG_ENTRYPOINT_TYPE, TYPE_EXECUTABLE); + } + + char basedir_buffer[PATH_MAX]; + get_basedir(script, basedir_buffer, sizeof(basedir_buffer)); + base_dir = basedir_buffer[0] ? basedir_buffer : NULL; + + if (base_dir) { + add_process_tag(TAG_ENTRYPOINT_BASEDIR, base_dir); + } + + add_process_tag(TAG_RUNTIME_SAPI, sapi_module.name); +} + +int cmp_process_tag_by_key(const void *tag1, const void* tag2) { + const process_tag_entry_t *tag_entry_1 = tag1; + const process_tag_entry_t *tag_entry_2 = tag2; + + return strcmp(tag_entry_1->key, tag_entry_2->key); +} + +static void serialize_process_tags(void) { + if (!ddtrace_process_tags_enabled() || !process_tags.count) { + return; + } + + // sort process_tags by alphabetical order on the value + qsort(process_tags.tag_list, process_tags.count, sizeof(process_tag_entry_t), cmp_process_tag_by_key); + + smart_str buf = {0}; + for (int i = 0; i < process_tags.count; i++) { + smart_str_appends(&buf, process_tags.tag_list[i].key); + smart_str_appendl(&buf, ":", 1); + smart_str_appends(&buf, process_tags.tag_list[i].value); + if (i < process_tags.count - 1) { + smart_str_appendl(&buf, ",", 1); + } + } + if (buf.s) { + smart_str_0(&buf); + process_tags.serialized = zend_string_init(ZSTR_VAL(buf.s), ZSTR_LEN(buf.s), 1); + } + + smart_str_free(&buf); +} + +zend_string *ddtrace_process_tags_get_serialized(void) { + return (ddtrace_process_tags_enabled() && process_tags.serialized) ? process_tags.serialized : NULL; +} + +bool ddtrace_process_tags_enabled(void){ + return get_global_DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED(); +} + +void ddtrace_process_tags_first_rinit(void) { + // process_tags struct initializations + process_tags.count = 0; + process_tags.capacity = 4; + process_tags.tag_list = malloc(process_tags.capacity * sizeof(process_tag_entry_t)); + + if (!process_tags.tag_list) { + process_tags.capacity = 0; + return; + } + + collect_process_tags(); + serialize_process_tags(); +} + +void ddtrace_process_tags_mshutdown(void) { + for (size_t i = 0; i < process_tags.count; i++) { + free((void*)process_tags.tag_list[i].value); + } + free(process_tags.tag_list); + + if (process_tags.serialized) { + zend_string_release(process_tags.serialized); + } + memset(&process_tags, 0, sizeof(process_tags)); +} \ No newline at end of file diff --git a/ext/process_tags.h b/ext/process_tags.h new file mode 100644 index 00000000000..2ff0c3bcf21 --- /dev/null +++ b/ext/process_tags.h @@ -0,0 +1,20 @@ +#ifndef DD_PROCESS_TAGS_H +#define DD_PROCESS_TAGS_H + +#include +#include "Zend/zend_types.h" + +// Called at first RINIT to collect process tags +void ddtrace_process_tags_first_rinit(void); + +// Called at MSHUTDOWN to free resources +void ddtrace_process_tags_mshutdown(void); + +// Check if process tags propagation is enabled +bool ddtrace_process_tags_enabled(void); + +// Get the serialized process tags (comma-separated, sorted) +// Returns NULL if disabled or not yet collected +zend_string *ddtrace_process_tags_get_serialized(void); + +#endif // DD_PROCESS_TAGS_H diff --git a/ext/span.c b/ext/span.c index 7ee7708a033..c3ce07e17c1 100644 --- a/ext/span.c +++ b/ext/span.c @@ -25,6 +25,7 @@ #include "standalone_limiter.h" #include "code_origins.h" #include "endpoint_guessing.h" +#include "process_tags.h" #define USE_REALTIME_CLOCK 0 #define USE_MONOTONIC_CLOCK 1 @@ -1110,10 +1111,17 @@ void ddtrace_serialize_closed_spans(ddog_TracesBytes *traces, bool fast_shutdown // Note this ->next: We always splice in new spans at next, so start at next to mostly preserve order ddtrace_span_data *span = stack->closed_ring_flush->next, *end = span; stack->closed_ring_flush = NULL; - do { - ddtrace_span_data *tmp = span; - span = tmp->next; - ddtrace_serialize_span_to_rust_span(tmp, trace); + do { + ddtrace_span_data *tmp = span; + span = tmp->next; + bool is_first_span = (ddog_get_trace_size(trace) == 0); + ddog_SpanBytes *rust_span = ddtrace_serialize_span_to_rust_span(tmp, trace); + if (is_first_span) { + zend_string *process_tags = ddtrace_process_tags_get_serialized(); + if (process_tags) { + ddog_add_str_span_meta_zstr(rust_span, "_dd.process_tags", process_tags); + } + } #if PHP_VERSION_ID < 70400 // remove the artificially increased RC while closing again GC_SET_REFCOUNT(&tmp->std, GC_REFCOUNT(&tmp->std) - DD_RC_CLOSED_MARKER); diff --git a/test_process_tags.php b/test_process_tags.php new file mode 100644 index 00000000000..a7aadfb595d --- /dev/null +++ b/test_process_tags.php @@ -0,0 +1,53 @@ +name = 'test.root_span'; + $rootSpan->service = 'test-service'; + $rootSpan->resource = 'root-resource'; + $rootSpan->meta['root.tag'] = 'root-value'; + + echo "✓ Root span created\n"; + + // // Create 5 child spans + // $childSpans = []; + // for ($i = 1; $i <= 5; $i++) { + // usleep(1000); // 1ms between spans + + // $childSpan = \DDTrace\start_span(); + // $childSpan->name = "test.child_span_$i"; + // $childSpan->service = 'test-service'; + // $childSpan->resource = "child-resource-$i"; + // $childSpan->meta['child.number'] = (string)$i; + + // echo "✓ Child span $i created\n"; + + // // Simulate work in child span + // usleep(500); + + // // Close the child span + // \DDTrace\close_span(); + // echo "✓ Child span $i closed\n"; + // } + + // Simulate more work in root span + usleep(1000); + + // Close the root span - this will trigger serialization and flush + \DDTrace\close_span(); +} +echo "✓ Root span closed\n"; +echo "Script completed successfully\n"; diff --git a/tests/Integrations/Custom/Autoloaded/ProcessTagsWebTest.php b/tests/Integrations/Custom/Autoloaded/ProcessTagsWebTest.php new file mode 100644 index 00000000000..1b3a710638d --- /dev/null +++ b/tests/Integrations/Custom/Autoloaded/ProcessTagsWebTest.php @@ -0,0 +1,73 @@ + 'true', + 'DD_TRACE_GENERATE_ROOT_SPAN' => '1', + 'DD_TRACE_AUTO_FLUSH_ENABLED' => '1', + ]); + } + + public function testProcessTagsEnabledForWebSapi() + { + $traces = $this->tracesFromWebRequest(function () { + $spec = new RequestSpec( + __FUNCTION__, + 'GET', + '/simple', + [] + ); + return $this->call($spec); + }); + + $this->assertCount(1, $traces); + $rootSpan = $traces[0][0]; + + // Verify _dd.tags.process exists + $this->assertArrayHasKey('_dd.process_tags', $rootSpan['meta']); + $processTags = $rootSpan['meta']['_dd.process_tags']; + + // Parse the process tags + $tags = []; + foreach (explode(',', $processTags) as $pair) { + list($key, $value) = explode(':', $pair, 2); + $tags[$key] = $value; + } + + $this->assertArrayHasKey('entrypoint.workdir', $tags, 'entrypoint.workdir should be present'); + $this->assertArrayHasKey('entrypoint.name', $tags, 'entrypoint.name should not be present for web SAPI'); + $this->assertArrayHasKey('entrypoint.basedir', $tags, 'entrypoint.basedir should not be present for web SAPI'); + $this->assertArrayHasKey('entrypoint.type', $tags, 'entrypoint.type should not be present for web SAPI'); + + // Verify server.type is one of the expected SAPIs tested in CI + $expectedSapis = ['cli-server', 'cgi-fcgi', 'apache2handler', 'fpm-fcgi']; + $this->assertContains( + $tags['runtime.sapi'], + $expectedSapis, + sprintf( + 'runtime.sapi should be one of [%s], got: %s', + implode(', ', $expectedSapis), + $tags['runtime.sapi'] + ) + ); + $this->assertEquals($tags['entrypoint.name'], 'index'); + $this->assertEquals($tags['entrypoint.workdir'], 'app'); + $this->assertEquals($tags['entrypoint.type'], 'script'); + $this->assertEquals($tags['entrypoint.basedir'], 'public'); + + // $this->assert(1 == 0); + } +} diff --git a/tests/ext/process_tags.phpt b/tests/ext/process_tags.phpt new file mode 100644 index 00000000000..bba165be80b --- /dev/null +++ b/tests/ext/process_tags.phpt @@ -0,0 +1,52 @@ +--TEST-- +Process tags are added to root span when enabled +--ENV-- +DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED=1 +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_TRACE_AUTO_FLUSH_ENABLED=0 +--FILE-- +name = 'root_span'; +$parent_span->service = 'test_service'; + +$child_span = \DDTrace\start_span(); +$child_span->name = 'root_span'; +$child_span->service = 'test_service'; + +\DDTrace\close_span(); +\DDTrace\close_span(); + +$spans = dd_trace_serialize_closed_spans(); + +// Check if process tags are present +if (isset($spans[0]['meta']['_dd.process_tags'])) { + $processTags = $spans[0]['meta']['_dd.process_tags']; + echo "Process tags present in root span: YES\n"; + echo "Process tags: $processTags\n"; + + // Verify format: comma-separated key:value pairs + $tags = explode(',', $processTags); + + // Verify keys are sorted alphabetically + $keys = array_map(function($tag) { + return explode(':', $tag, 2)[0]; + }, $tags); + $sortedKeys = $keys; + sort($sortedKeys); + echo "Keys sorted: " . ($keys === $sortedKeys ? 'YES' : 'NO') . "\n"; +} else { + echo "Process tags present in root span: NO\n"; +} + +if (isset($spans[1]['meta']['_dd.process_tags'])) { + echo "Process tags present in child span: YES\n"; +} else { + echo "Process tags present in child span: NO\n"; +} +?> +--EXPECTF-- +Process tags present in root span: YES +Process tags: entrypoint.basedir:ext,entrypoint.name:process_tags,entrypoint.type:script,entrypoint.workdir:app,runtime.sapi:cli +Keys sorted: YES +Process tags present in child span: NO \ No newline at end of file From 10469842ba87b43df4a43392b4b866ce298eef9f Mon Sep 17 00:00:00 2001 From: Louis Tricot Date: Fri, 9 Jan 2026 16:31:55 +0100 Subject: [PATCH 2/7] feat(process_tags): add process_tags to tracing payloads --- components-rs/ddtrace.h | 2 + components-rs/lib.rs | 26 +++- ext/ddtrace.c | 21 +++ ext/ddtrace.stub.php | 11 +- ext/ddtrace_arginfo.h | 6 + ext/process_tags.c | 50 +++---- ext/span.c | 20 +-- test_process_tags.php | 53 -------- .../Custom/Autoloaded/ProcessTagsWebTest.php | 6 +- .../ext/process_tag_value_normalization.phpt | 126 ++++++++++++++++++ 10 files changed, 222 insertions(+), 99 deletions(-) delete mode 100644 test_process_tags.php create mode 100644 tests/ext/process_tag_value_normalization.phpt diff --git a/components-rs/ddtrace.h b/components-rs/ddtrace.h index 7c8ae5ae3b9..a74474c1a69 100644 --- a/components-rs/ddtrace.h +++ b/components-rs/ddtrace.h @@ -241,4 +241,6 @@ void ddog_add_zstr_span_meta_struct_CharSlice(ddog_SpanBytes *ptr, const char *ddog_normalize_process_tag_value(ddog_CharSlice val); +void ddog_free_normalized_tag_value(const char* normalized_val); + #endif /* DDTRACE_PHP_H */ diff --git a/components-rs/lib.rs b/components-rs/lib.rs index 62636bbc5fe..07ff0cb0223 100644 --- a/components-rs/lib.rs +++ b/components-rs/lib.rs @@ -139,7 +139,7 @@ const MAX_TAG_VALUE_LENGTH: usize = 100; pub extern "C" fn ddog_normalize_process_tag_value( tag_value: CharSlice, ) -> *const c_char { - let value = tag_value.try_to_utf8().unwrap().to_string(); + let value = tag_value.to_utf8_lossy(); let mut out = String::new(); let mut prev_underscore = false; @@ -164,8 +164,22 @@ pub extern "C" fn ddog_normalize_process_tag_value( out.pop(); } - let c_string = std::ffi::CString::new(out).unwrap(); - let out_ptr = c_string.as_ptr(); - std::mem::forget(c_string); - out_ptr -} \ No newline at end of file + match std::ffi::CString::new(out) { + Ok(c_string) => { + let out_ptr = c_string.as_ptr(); + std::mem::forget(c_string); + out_ptr + } + Err(_) => std::ptr::null(), + } +} + +#[no_mangle] +pub extern "C" fn ddog_free_normalized_tag_value(ptr: *const c_char) { + if ptr.is_null() { + return; + } + unsafe { + drop(std::ffi::CString::from_raw(ptr as *mut c_char)); + } +} diff --git a/ext/ddtrace.c b/ext/ddtrace.c index d841c309603..2a01877e0b3 100644 --- a/ext/ddtrace.c +++ b/ext/ddtrace.c @@ -1616,6 +1616,7 @@ static PHP_MSHUTDOWN_FUNCTION(ddtrace) { ddtrace_sidecar_shutdown(); ddtrace_live_debugger_mshutdown(); + ddtrace_process_tags_mshutdown(); #if PHP_VERSION_ID >= 80000 && PHP_VERSION_ID < 80100 // See dd_register_span_data_ce for explanation @@ -2617,6 +2618,26 @@ PHP_FUNCTION(DDTrace_Testing_trigger_error) { } } +PHP_FUNCTION(DDTrace_Testing_normalize_tag_value) { + ddtrace_string value; + if (zend_parse_parameters(ZEND_NUM_ARGS(), "s", &value.ptr, &value.len) != SUCCESS) { + RETURN_EMPTY_STRING(); + } + + const char* normalized = ddog_normalize_process_tag_value((ddog_CharSlice){ + .ptr = value.ptr, + .len = value.len + }); + + if (normalized) { + zend_string *result = zend_string_init(normalized, strlen(normalized), 0); + ddog_free_normalized_tag_value(normalized); + RETURN_STR(result); + } else { + RETURN_EMPTY_STRING(); + } +} + PHP_FUNCTION(DDTrace_Internal_add_span_flag) { zend_object *span; zend_long flag; diff --git a/ext/ddtrace.stub.php b/ext/ddtrace.stub.php index fbc182c45c3..31c30ec4d8b 100644 --- a/ext/ddtrace.stub.php +++ b/ext/ddtrace.stub.php @@ -904,6 +904,13 @@ function trigger_error(string $message, int $errorType): void {} * Emits an asm event */ function emit_asm_event(): void {} + /** + * Normalizes a process tag value + * + * @param string $value The tag value to normalize + * @return string The normalized tag value + */ + function normalize_tag_value(string $value): string {} } namespace DDTrace\Internal { @@ -947,7 +954,7 @@ function handle_fork(): void {} * - id: string, Unique identifier of the user. Should be the same id and format used on set_user * @param array $metadata User metadata added to the root span */ -function track_user_login_success(string $login, string|array|null $user = null, array $metadata = []): void {} + function track_user_login_success(string $login, string|array|null $user = null, array $metadata = []): void {} /** * Track a user login failure event. @@ -956,7 +963,7 @@ function track_user_login_success(string $login, string|array|null $user = null, * @param bool $exists Whether the user exists in the system * @param array $metadata User metadata added to the root span */ -function track_user_login_failure(string $login, bool $exists, array $metadata = []): void {} + function track_user_login_failure(string $login, bool $exists, array $metadata = []): void {} } namespace { diff --git a/ext/ddtrace_arginfo.h b/ext/ddtrace_arginfo.h index 9c005f69d09..96b71596621 100644 --- a/ext/ddtrace_arginfo.h +++ b/ext/ddtrace_arginfo.h @@ -203,6 +203,10 @@ ZEND_END_ARG_INFO() #define arginfo_DDTrace_Testing_emit_asm_event arginfo_DDTrace_flush +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_Testing_normalize_tag_value, 0, 1, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, value, IS_STRING, 0) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_Internal_add_span_flag, 0, 2, IS_VOID, 0) ZEND_ARG_OBJ_INFO(0, span, DDTrace\\SpanData, 0) ZEND_ARG_TYPE_INFO(0, flag, IS_LONG, 0) @@ -383,6 +387,7 @@ ZEND_FUNCTION(DDTrace_UserRequest_notify_commit); ZEND_FUNCTION(DDTrace_UserRequest_set_blocking_function); ZEND_FUNCTION(DDTrace_Testing_trigger_error); ZEND_FUNCTION(DDTrace_Testing_emit_asm_event); +ZEND_FUNCTION(DDTrace_Testing_normalize_tag_value); ZEND_FUNCTION(DDTrace_Internal_add_span_flag); ZEND_FUNCTION(DDTrace_Internal_handle_fork); ZEND_FUNCTION(datadog_appsec_v2_track_user_login_success); @@ -473,6 +478,7 @@ static const zend_function_entry ext_functions[] = { ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace\\UserRequest", "set_blocking_function"), zif_DDTrace_UserRequest_set_blocking_function, arginfo_DDTrace_UserRequest_set_blocking_function, 0, NULL, NULL) ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace\\Testing", "trigger_error"), zif_DDTrace_Testing_trigger_error, arginfo_DDTrace_Testing_trigger_error, 0, NULL, NULL) ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace\\Testing", "emit_asm_event"), zif_DDTrace_Testing_emit_asm_event, arginfo_DDTrace_Testing_emit_asm_event, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace\\Testing", "normalize_tag_value"), zif_DDTrace_Testing_normalize_tag_value, arginfo_DDTrace_Testing_normalize_tag_value, 0, NULL, NULL) ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace\\Internal", "add_span_flag"), zif_DDTrace_Internal_add_span_flag, arginfo_DDTrace_Internal_add_span_flag, 0, NULL, NULL) ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace\\Internal", "handle_fork"), zif_DDTrace_Internal_handle_fork, arginfo_DDTrace_Internal_handle_fork, 0, NULL, NULL) ZEND_RAW_FENTRY(ZEND_NS_NAME("datadog\\appsec\\v2", "track_user_login_success"), zif_datadog_appsec_v2_track_user_login_success, arginfo_datadog_appsec_v2_track_user_login_success, 0, NULL, NULL) diff --git a/ext/process_tags.c b/ext/process_tags.c index d6a31a946b2..e088c3294be 100644 --- a/ext/process_tags.c +++ b/ext/process_tags.c @@ -22,8 +22,6 @@ #define TYPE_SCRIPT "script" #define TYPE_EXECUTABLE "executable" -#define MAX_TAG_VALUE_LENGTH 100 - typedef struct { const char *key; const char *value; @@ -51,21 +49,28 @@ static inline const char *get_basename(const char *path) { } static void get_basedir(const char* script_path, char *out, size_t out_size) { - if (!script_path || !*script_path || !out || out_size == 0) return; + if (!script_path || !*script_path || !out || out_size == 0) { + out[0] = '\0'; + return; + } + + const char *last_sep = NULL; + for (const char *p = script_path; *p; p++) { + if (*p == '/' || *p == '\\') last_sep = p; + } - const char *last_slash = strrchr(script_path, '/'); - if (!last_slash) { + if (!last_sep) { out[0] = '\0'; return; } - const char *prev_slash = NULL; - for (const char *p = script_path; p < last_slash; p++) { - if (*p == '/') prev_slash = p; + const char *prev_sep = NULL; + for (const char *p = script_path; p < last_sep; p++) { + if (*p == '/' || *p == '\\') prev_sep = p; } - const char *start = prev_slash ? prev_slash + 1 : script_path; - size_t len = last_slash - start; + const char *start = prev_sep ? prev_sep + 1 : script_path; + size_t len = last_sep - start; if (len >= out_size) len = out_size - 1; memcpy(out, start, len); @@ -103,6 +108,7 @@ static void add_process_tag(const char* tag_key, const char* tag_value) { process_tags.capacity * sizeof(process_tag_entry_t) ); if (!bigger_list) { + ddog_free_normalized_tag_value(normalized_value); process_tags.capacity /= 2; return; } @@ -116,7 +122,6 @@ static void add_process_tag(const char* tag_key, const char* tag_value) { } static void collect_process_tags(void) { - // entrypoint.workdir char cwd[PATH_MAX]; if (VCWD_GETCWD(cwd, sizeof(cwd))) { const char* entrypoint_workdir = get_basename(cwd); @@ -125,19 +130,14 @@ static void collect_process_tags(void) { } } - // entrypoint.name - const char *entrypoint_name = NULL; - const char *base_dir = NULL; const char *script = NULL; - if (SG(request_info).path_translated && *SG(request_info).path_translated) { script = SG(request_info).path_translated; } else if (SG(request_info).argv && SG(request_info).argc > 0 && SG(request_info).argv[0]) { script = SG(request_info).argv[0]; } - - entrypoint_name = get_basename(script); + const char *entrypoint_name = get_basename(script); if (entrypoint_name) { char name_without_ext[PATH_MAX]; strip_extension(entrypoint_name, name_without_ext, sizeof(name_without_ext)); @@ -151,7 +151,7 @@ static void collect_process_tags(void) { char basedir_buffer[PATH_MAX]; get_basedir(script, basedir_buffer, sizeof(basedir_buffer)); - base_dir = basedir_buffer[0] ? basedir_buffer : NULL; + const char *base_dir = basedir_buffer[0] ? basedir_buffer : NULL; if (base_dir) { add_process_tag(TAG_ENTRYPOINT_BASEDIR, base_dir); @@ -160,7 +160,7 @@ static void collect_process_tags(void) { add_process_tag(TAG_RUNTIME_SAPI, sapi_module.name); } -int cmp_process_tag_by_key(const void *tag1, const void* tag2) { +static int cmp_process_tag_by_key(const void *tag1, const void* tag2) { const process_tag_entry_t *tag_entry_1 = tag1; const process_tag_entry_t *tag_entry_2 = tag2; @@ -172,16 +172,16 @@ static void serialize_process_tags(void) { return; } - // sort process_tags by alphabetical order on the value + // sort process_tags by key alphabetical order qsort(process_tags.tag_list, process_tags.count, sizeof(process_tag_entry_t), cmp_process_tag_by_key); smart_str buf = {0}; - for (int i = 0; i < process_tags.count; i++) { + for (size_t i = 0; i < process_tags.count; i++) { smart_str_appends(&buf, process_tags.tag_list[i].key); - smart_str_appendl(&buf, ":", 1); + smart_str_appendc(&buf, ':'); smart_str_appends(&buf, process_tags.tag_list[i].value); if (i < process_tags.count - 1) { - smart_str_appendl(&buf, ",", 1); + smart_str_appendc(&buf, ','); } } if (buf.s) { @@ -217,7 +217,7 @@ void ddtrace_process_tags_first_rinit(void) { void ddtrace_process_tags_mshutdown(void) { for (size_t i = 0; i < process_tags.count; i++) { - free((void*)process_tags.tag_list[i].value); + ddog_free_normalized_tag_value(process_tags.tag_list[i].value); } free(process_tags.tag_list); @@ -225,4 +225,4 @@ void ddtrace_process_tags_mshutdown(void) { zend_string_release(process_tags.serialized); } memset(&process_tags, 0, sizeof(process_tags)); -} \ No newline at end of file +} diff --git a/ext/span.c b/ext/span.c index c3ce07e17c1..ed37b155c51 100644 --- a/ext/span.c +++ b/ext/span.c @@ -1111,17 +1111,17 @@ void ddtrace_serialize_closed_spans(ddog_TracesBytes *traces, bool fast_shutdown // Note this ->next: We always splice in new spans at next, so start at next to mostly preserve order ddtrace_span_data *span = stack->closed_ring_flush->next, *end = span; stack->closed_ring_flush = NULL; - do { - ddtrace_span_data *tmp = span; - span = tmp->next; - bool is_first_span = (ddog_get_trace_size(trace) == 0); - ddog_SpanBytes *rust_span = ddtrace_serialize_span_to_rust_span(tmp, trace); - if (is_first_span) { - zend_string *process_tags = ddtrace_process_tags_get_serialized(); - if (process_tags) { - ddog_add_str_span_meta_zstr(rust_span, "_dd.process_tags", process_tags); + do { + ddtrace_span_data *tmp = span; + span = tmp->next; + bool is_first_span = (ddog_get_trace_size(trace) == 0); + ddog_SpanBytes *rust_span = ddtrace_serialize_span_to_rust_span(tmp, trace); + if (is_first_span) { + zend_string *process_tags = ddtrace_process_tags_get_serialized(); + if (process_tags) { + ddog_add_str_span_meta_zstr(rust_span, "_dd.process_tags", process_tags); + } } - } #if PHP_VERSION_ID < 70400 // remove the artificially increased RC while closing again GC_SET_REFCOUNT(&tmp->std, GC_REFCOUNT(&tmp->std) - DD_RC_CLOSED_MARKER); diff --git a/test_process_tags.php b/test_process_tags.php deleted file mode 100644 index a7aadfb595d..00000000000 --- a/test_process_tags.php +++ /dev/null @@ -1,53 +0,0 @@ -name = 'test.root_span'; - $rootSpan->service = 'test-service'; - $rootSpan->resource = 'root-resource'; - $rootSpan->meta['root.tag'] = 'root-value'; - - echo "✓ Root span created\n"; - - // // Create 5 child spans - // $childSpans = []; - // for ($i = 1; $i <= 5; $i++) { - // usleep(1000); // 1ms between spans - - // $childSpan = \DDTrace\start_span(); - // $childSpan->name = "test.child_span_$i"; - // $childSpan->service = 'test-service'; - // $childSpan->resource = "child-resource-$i"; - // $childSpan->meta['child.number'] = (string)$i; - - // echo "✓ Child span $i created\n"; - - // // Simulate work in child span - // usleep(500); - - // // Close the child span - // \DDTrace\close_span(); - // echo "✓ Child span $i closed\n"; - // } - - // Simulate more work in root span - usleep(1000); - - // Close the root span - this will trigger serialization and flush - \DDTrace\close_span(); -} -echo "✓ Root span closed\n"; -echo "Script completed successfully\n"; diff --git a/tests/Integrations/Custom/Autoloaded/ProcessTagsWebTest.php b/tests/Integrations/Custom/Autoloaded/ProcessTagsWebTest.php index 1b3a710638d..e0f0d5be0d3 100644 --- a/tests/Integrations/Custom/Autoloaded/ProcessTagsWebTest.php +++ b/tests/Integrations/Custom/Autoloaded/ProcessTagsWebTest.php @@ -48,9 +48,9 @@ public function testProcessTagsEnabledForWebSapi() } $this->assertArrayHasKey('entrypoint.workdir', $tags, 'entrypoint.workdir should be present'); - $this->assertArrayHasKey('entrypoint.name', $tags, 'entrypoint.name should not be present for web SAPI'); - $this->assertArrayHasKey('entrypoint.basedir', $tags, 'entrypoint.basedir should not be present for web SAPI'); - $this->assertArrayHasKey('entrypoint.type', $tags, 'entrypoint.type should not be present for web SAPI'); + $this->assertArrayHasKey('entrypoint.name', $tags, 'entrypoint.name should be present for web SAPI'); + $this->assertArrayHasKey('entrypoint.basedir', $tags, 'entrypoint.basedir should be present for web SAPI'); + $this->assertArrayHasKey('entrypoint.type', $tags, 'entrypoint.type should be present for web SAPI'); // Verify server.type is one of the expected SAPIs tested in CI $expectedSapis = ['cli-server', 'cgi-fcgi', 'apache2handler', 'fpm-fcgi']; diff --git a/tests/ext/process_tag_value_normalization.phpt b/tests/ext/process_tag_value_normalization.phpt new file mode 100644 index 00000000000..71950c53311 --- /dev/null +++ b/tests/ext/process_tag_value_normalization.phpt @@ -0,0 +1,126 @@ +--TEST-- +Process tag value normalization +--FILE-- +# ', 'hashtag_spaces', 'Complex special chars and spaces'], + [':testing', 'testing', 'Leading colon removed'], + ['_foo', 'foo', 'Leading underscore removed'], + [':::test', 'test', 'Multiple leading colons'], + ['contiguous_____underscores', 'contiguous_underscores', 'Collapse underscores'], + ['foo_', 'foo', 'Trailing underscore removed'], + ["\u{017F}odd_\u{017F}case\u{017F}", "\u{017F}odd_\u{017F}case\u{017F}", 'Latin small letter long s (U+017F)'], + ['', '', 'Empty string'], + [' ', '', 'Only spaces'], + ['ok', 'ok', 'Simple valid string'], + ['™Ö™Ö™™Ö™', 'ö_ö_ö', 'Trademark symbols and umlauts'], + ['AlsO:ök', 'also_ök', 'Mixed case with colon and umlaut'], + [':still_ok', 'still_ok', 'Leading colon with underscore'], + ['___trim', 'trim', 'Multiple leading underscores'], + ['12.:trim@', '12._trim', 'Digit, dot, colon, at sign'], + ['12.:trim@@', '12._trim', 'Multiple at signs collapsed'], + ['fun:ky__tag/1', 'fun_ky_tag/1', 'Colon, underscores, slash'], + ['fun:ky@tag/2', 'fun_ky_tag/2', 'Colon, at sign, slash'], + ['fun:ky@@@tag/3', 'fun_ky_tag/3', 'Multiple at signs'], + ['tag:1/2.3', 'tag_1/2.3', 'Slash and dot preserved'], + ['---fun:k####y_ta@#g/1_@@#', '---fun_k_y_ta_g/1', 'Complex mix with trailing underscore'], + ['AlsO:œ#@ö))œk', 'also_œ_ö_œk', 'Mixed case with special Latin chars'], + ["test\x99\x8faaa", 'test_aaa', 'Invalid UTF-8 with trailing valid chars'], + ["test\x99\x8f", 'test', 'Invalid UTF-8 at end'], + [str_repeat('a', 888), str_repeat('a', 100), 'Truncate at 100 chars'], + ['a' . str_repeat('🐶', 799) . 'b', 'a', 'Multi-byte emoji truncation'], + ['a' . "\u{FFFD}", 'a', 'Replacement character trailing'], + ['a' . "\u{FFFD}" . "\u{FFFD}", 'a', 'Multiple replacement characters'], + ['a' . "\u{FFFD}" . "\u{FFFD}" . 'b', 'a_b', 'Replacement characters between letters'], + ['A' . str_repeat('0', 97) . ' ' . str_repeat('0', 11), 'a' . str_repeat('0', 97) . '_0', 'Truncate at 100 char limit (space at boundary is trimmed)'], +]; + +$passed = 0; +$failed = 0; +$failed_tests = []; + +foreach ($test_cases as $idx => $test) { + list($input, $expected, $description) = $test; + + $result = \DDTrace\Testing\normalize_tag_value($input); + + if ($result === $expected) { + $passed++; + echo sprintf("✓ Test %3d PASS: %s\n", $idx + 1, $description); + } else { + $failed++; + $failed_tests[] = [ + 'num' => $idx + 1, + 'description' => $description, + 'input' => $input, + 'expected' => $expected, + 'got' => $result + ]; + echo sprintf("✗ Test %3d FAIL: %s\n", $idx + 1, $description); + } +} + +if ($failed > 0) { + foreach ($failed_tests as $test) { + echo sprintf("\nTest %d: %s\n", $test['num'], $test['description']); + echo " Input: " . var_export($test['input'], true) . "\n"; + echo " Expected: " . var_export($test['expected'], true) . "\n"; + echo " Got: " . var_export($test['got'], true) . "\n"; + } + exit(1); +} +?> +--EXPECT-- +✓ Test 1 PASS: Remove leading hash +✓ Test 2 PASS: Lowercase conversion +✓ Test 3 PASS: Special characters to underscores +✓ Test 4 PASS: Remove leading special chars +✓ Test 5 PASS: Colons to underscores +✓ Test 6 PASS: Leading digit allowed +✓ Test 7 PASS: Leading slash allowed +✓ Test 8 PASS: Unicode preserved +✓ Test 9 PASS: Unicode with colon +✓ Test 10 PASS: Emoji and CJK characters +✓ Test 11 PASS: Trim spaces +✓ Test 12 PASS: Complex special chars and spaces +✓ Test 13 PASS: Leading colon removed +✓ Test 14 PASS: Leading underscore removed +✓ Test 15 PASS: Multiple leading colons +✓ Test 16 PASS: Collapse underscores +✓ Test 17 PASS: Trailing underscore removed +✓ Test 18 PASS: Latin small letter long s (U+017F) +✓ Test 19 PASS: Empty string +✓ Test 20 PASS: Only spaces +✓ Test 21 PASS: Simple valid string +✓ Test 22 PASS: Trademark symbols and umlauts +✓ Test 23 PASS: Mixed case with colon and umlaut +✓ Test 24 PASS: Leading colon with underscore +✓ Test 25 PASS: Multiple leading underscores +✓ Test 26 PASS: Digit, dot, colon, at sign +✓ Test 27 PASS: Multiple at signs collapsed +✓ Test 28 PASS: Colon, underscores, slash +✓ Test 29 PASS: Colon, at sign, slash +✓ Test 30 PASS: Multiple at signs +✓ Test 31 PASS: Slash and dot preserved +✓ Test 32 PASS: Complex mix with trailing underscore +✓ Test 33 PASS: Mixed case with special Latin chars +✓ Test 34 PASS: Invalid UTF-8 with trailing valid chars +✓ Test 35 PASS: Invalid UTF-8 at end +✓ Test 36 PASS: Truncate at 100 chars +✓ Test 37 PASS: Multi-byte emoji truncation +✓ Test 38 PASS: Replacement character trailing +✓ Test 39 PASS: Multiple replacement characters +✓ Test 40 PASS: Replacement characters between letters +✓ Test 41 PASS: Truncate at 100 char limit (space at boundary is trimmed) \ No newline at end of file From 6501e1e6594d1b6dcd6dd02725923a31b64f17c0 Mon Sep 17 00:00:00 2001 From: Louis Tricot Date: Mon, 12 Jan 2026 11:23:28 +0100 Subject: [PATCH 3/7] small auto review and fix test --- ext/ddtrace.c | 1 - ext/ddtrace.stub.php | 30 +++++++++---------- .../ext/process_tag_value_normalization.phpt | 2 +- tests/ext/process_tags.phpt | 4 +-- 4 files changed, 18 insertions(+), 19 deletions(-) diff --git a/ext/ddtrace.c b/ext/ddtrace.c index 2a01877e0b3..aaab86113bc 100644 --- a/ext/ddtrace.c +++ b/ext/ddtrace.c @@ -1644,7 +1644,6 @@ static void dd_rinit_once(void) { ddtrace_process_tags_first_rinit(); } - // Uses config, cannot run earlier #ifndef _WIN32 ddtrace_signals_first_rinit(); diff --git a/ext/ddtrace.stub.php b/ext/ddtrace.stub.php index 31c30ec4d8b..9eda128d9ec 100644 --- a/ext/ddtrace.stub.php +++ b/ext/ddtrace.stub.php @@ -946,23 +946,23 @@ function handle_fork(): void {} namespace datadog\appsec\v2 { /** - * Track a user login success event. - * - * @param string $login is the data used by the user to authenticate - * @param string|array $user when string, it represents the user id. When array it represents the user information. - * The array should at least contain the following keys: - * - id: string, Unique identifier of the user. Should be the same id and format used on set_user - * @param array $metadata User metadata added to the root span - */ + * Track a user login success event. + * + * @param string $login is the data used by the user to authenticate + * @param string|array $user when string, it represents the user id. When array it represents the user information. + * The array should at least contain the following keys: + * - id: string, Unique identifier of the user. Should be the same id and format used on set_user + * @param array $metadata User metadata added to the root span + */ function track_user_login_success(string $login, string|array|null $user = null, array $metadata = []): void {} -/** - * Track a user login failure event. - * - * @param string $login is the data used by the user to authenticate - * @param bool $exists Whether the user exists in the system - * @param array $metadata User metadata added to the root span - */ + /** + * Track a user login failure event. + * + * @param string $login is the data used by the user to authenticate + * @param bool $exists Whether the user exists in the system + * @param array $metadata User metadata added to the root span + */ function track_user_login_failure(string $login, bool $exists, array $metadata = []): void {} } diff --git a/tests/ext/process_tag_value_normalization.phpt b/tests/ext/process_tag_value_normalization.phpt index 71950c53311..5b32680e77e 100644 --- a/tests/ext/process_tag_value_normalization.phpt +++ b/tests/ext/process_tag_value_normalization.phpt @@ -123,4 +123,4 @@ if ($failed > 0) { ✓ Test 38 PASS: Replacement character trailing ✓ Test 39 PASS: Multiple replacement characters ✓ Test 40 PASS: Replacement characters between letters -✓ Test 41 PASS: Truncate at 100 char limit (space at boundary is trimmed) \ No newline at end of file +✓ Test 41 PASS: Truncate at 100 char limit (space at boundary is trimmed) diff --git a/tests/ext/process_tags.phpt b/tests/ext/process_tags.phpt index bba165be80b..da04bafec50 100644 --- a/tests/ext/process_tags.phpt +++ b/tests/ext/process_tags.phpt @@ -47,6 +47,6 @@ if (isset($spans[1]['meta']['_dd.process_tags'])) { ?> --EXPECTF-- Process tags present in root span: YES -Process tags: entrypoint.basedir:ext,entrypoint.name:process_tags,entrypoint.type:script,entrypoint.workdir:app,runtime.sapi:cli +Process tags: entrypoint.basedir:ext,entrypoint.name:process_tags,entrypoint.type:script,entrypoint.workdir:%s,runtime.sapi:cli Keys sorted: YES -Process tags present in child span: NO \ No newline at end of file +Process tags present in child span: NO From 127c555a44339cd6cc844f937f8ea67e90613a2e Mon Sep 17 00:00:00 2001 From: Louis Tricot Date: Tue, 13 Jan 2026 16:23:18 +0100 Subject: [PATCH 4/7] bwoebi review --- ext/process_tags.c | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/ext/process_tags.c b/ext/process_tags.c index e088c3294be..e4811bc1531 100644 --- a/ext/process_tags.c +++ b/ext/process_tags.c @@ -103,16 +103,11 @@ static void add_process_tag(const char* tag_key, const char* tag_value) { size_t count = process_tags.count; if (count == process_tags.capacity) { process_tags.capacity *= 2; - process_tag_entry_t* bigger_list = realloc( + process_tags.tag_list = perealloc( process_tags.tag_list, - process_tags.capacity * sizeof(process_tag_entry_t) + process_tags.capacity * sizeof(process_tag_entry_t), + 1 ); - if (!bigger_list) { - ddog_free_normalized_tag_value(normalized_value); - process_tags.capacity /= 2; - return; - } - process_tags.tag_list = bigger_list; } process_tags.tag_list[count].key = tag_key; @@ -122,6 +117,8 @@ static void add_process_tag(const char* tag_key, const char* tag_value) { } static void collect_process_tags(void) { + bool is_cli = (strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "phpdbg") == 0); + char cwd[PATH_MAX]; if (VCWD_GETCWD(cwd, sizeof(cwd))) { const char* entrypoint_workdir = get_basename(cwd); @@ -130,6 +127,8 @@ static void collect_process_tags(void) { } } + add_process_tag(TAG_RUNTIME_SAPI, sapi_module.name); + const char *script = NULL; if (SG(request_info).path_translated && *SG(request_info).path_translated) { script = SG(request_info).path_translated; @@ -139,25 +138,26 @@ static void collect_process_tags(void) { const char *entrypoint_name = get_basename(script); if (entrypoint_name) { - char name_without_ext[PATH_MAX]; - strip_extension(entrypoint_name, name_without_ext, sizeof(name_without_ext)); - - add_process_tag(TAG_ENTRYPOINT_NAME, name_without_ext); + if (is_cli) { + char name_without_ext[PATH_MAX]; + strip_extension(entrypoint_name, name_without_ext, sizeof(name_without_ext)); + add_process_tag(TAG_ENTRYPOINT_NAME, name_without_ext); + } add_process_tag(TAG_ENTRYPOINT_TYPE, TYPE_SCRIPT); } else { add_process_tag(TAG_ENTRYPOINT_NAME, "php"); add_process_tag(TAG_ENTRYPOINT_TYPE, TYPE_EXECUTABLE); } - char basedir_buffer[PATH_MAX]; - get_basedir(script, basedir_buffer, sizeof(basedir_buffer)); - const char *base_dir = basedir_buffer[0] ? basedir_buffer : NULL; + if (is_cli) { + char basedir_buffer[PATH_MAX]; + get_basedir(script, basedir_buffer, sizeof(basedir_buffer)); + const char *base_dir = basedir_buffer[0] ? basedir_buffer : NULL; - if (base_dir) { - add_process_tag(TAG_ENTRYPOINT_BASEDIR, base_dir); + if (base_dir) { + add_process_tag(TAG_ENTRYPOINT_BASEDIR, base_dir); + } } - - add_process_tag(TAG_RUNTIME_SAPI, sapi_module.name); } static int cmp_process_tag_by_key(const void *tag1, const void* tag2) { @@ -204,7 +204,7 @@ void ddtrace_process_tags_first_rinit(void) { // process_tags struct initializations process_tags.count = 0; process_tags.capacity = 4; - process_tags.tag_list = malloc(process_tags.capacity * sizeof(process_tag_entry_t)); + process_tags.tag_list = pemalloc(process_tags.capacity * sizeof(process_tag_entry_t), 1); if (!process_tags.tag_list) { process_tags.capacity = 0; @@ -219,7 +219,7 @@ void ddtrace_process_tags_mshutdown(void) { for (size_t i = 0; i < process_tags.count; i++) { ddog_free_normalized_tag_value(process_tags.tag_list[i].value); } - free(process_tags.tag_list); + pefree(process_tags.tag_list, 1); if (process_tags.serialized) { zend_string_release(process_tags.serialized); From 3a8038ca66aa61ba3ad1a6622713a6113f8e9325 Mon Sep 17 00:00:00 2001 From: Louis Tricot Date: Tue, 13 Jan 2026 16:42:40 +0100 Subject: [PATCH 5/7] fix test --- .../Custom/Autoloaded/ProcessTagsWebTest.php | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/tests/Integrations/Custom/Autoloaded/ProcessTagsWebTest.php b/tests/Integrations/Custom/Autoloaded/ProcessTagsWebTest.php index e0f0d5be0d3..d453f81291f 100644 --- a/tests/Integrations/Custom/Autoloaded/ProcessTagsWebTest.php +++ b/tests/Integrations/Custom/Autoloaded/ProcessTagsWebTest.php @@ -48,9 +48,9 @@ public function testProcessTagsEnabledForWebSapi() } $this->assertArrayHasKey('entrypoint.workdir', $tags, 'entrypoint.workdir should be present'); - $this->assertArrayHasKey('entrypoint.name', $tags, 'entrypoint.name should be present for web SAPI'); - $this->assertArrayHasKey('entrypoint.basedir', $tags, 'entrypoint.basedir should be present for web SAPI'); $this->assertArrayHasKey('entrypoint.type', $tags, 'entrypoint.type should be present for web SAPI'); + $this->assertArrayNotHasKey('entrypoint.name', $tags, 'entrypoint.name should not be present for web SAPI'); + $this->assertArrayNotHasKey('entrypoint.basedir', $tags, 'entrypoint.basedir should not be present for web SAPI'); // Verify server.type is one of the expected SAPIs tested in CI $expectedSapis = ['cli-server', 'cgi-fcgi', 'apache2handler', 'fpm-fcgi']; @@ -63,11 +63,6 @@ public function testProcessTagsEnabledForWebSapi() $tags['runtime.sapi'] ) ); - $this->assertEquals($tags['entrypoint.name'], 'index'); - $this->assertEquals($tags['entrypoint.workdir'], 'app'); $this->assertEquals($tags['entrypoint.type'], 'script'); - $this->assertEquals($tags['entrypoint.basedir'], 'public'); - - // $this->assert(1 == 0); } } From 70d937fd15384162adec0d89f116a5a18b09a651 Mon Sep 17 00:00:00 2001 From: Louis Tricot Date: Fri, 16 Jan 2026 10:49:48 +0100 Subject: [PATCH 6/7] Adds process_tags to live debugger payloads --- components-rs/live-debugger.h | 3 +- ext/live_debugger.c | 11 +++- libdatadog | 2 +- .../ext/live-debugger/debugger_log_probe.phpt | 4 +- .../debugger_log_probe_process_tags.phpt | 61 +++++++++++++++++++ .../debugger_span_decoration_probe.phpt | 4 +- .../live-debugger/exception-replay_001.phpt | 8 ++- .../live-debugger/exception-replay_002.phpt | 4 +- ...ion-replay_non_regression_2989_mysqli.phpt | 4 +- 9 files changed, 92 insertions(+), 9 deletions(-) create mode 100644 tests/ext/live-debugger/debugger_log_probe_process_tags.phpt diff --git a/components-rs/live-debugger.h b/components-rs/live-debugger.h index 550a6093f4d..9c6f404b9fa 100644 --- a/components-rs/live-debugger.h +++ b/components-rs/live-debugger.h @@ -58,7 +58,8 @@ struct ddog_DebuggerPayload *ddog_create_log_probe_snapshot(const struct ddog_Pr const ddog_CharSlice *message, ddog_CharSlice service, ddog_CharSlice language, - uint64_t timestamp); + uint64_t timestamp, + ddog_CharSlice process_tags); void ddog_update_payload_message(struct ddog_DebuggerPayload *payload, ddog_CharSlice message); diff --git a/ext/live_debugger.c b/ext/live_debugger.c index 72907c49a44..e408a7a1f10 100644 --- a/ext/live_debugger.c +++ b/ext/live_debugger.c @@ -13,6 +13,7 @@ #include "zend_hrtime.h" #include "components-rs/common.h" #include "zend_generators.h" +#include "process_tags.h" ZEND_EXTERN_MODULE_GLOBALS(ddtrace); @@ -390,8 +391,16 @@ static void dd_log_probe_ensure_payload(dd_log_probe_dyn *dyn, dd_log_probe_def if (dyn->payload) { ddog_update_payload_message(dyn->payload, *msg); } else { + zend_string *process_tags = ddtrace_process_tags_get_serialized(); + dyn->service = ddtrace_active_service_name(); - dyn->payload = ddog_create_log_probe_snapshot(&def->parent.probe, msg, dd_zend_string_to_CharSlice(dyn->service), DDOG_CHARSLICE_C("php"), ddtrace_nanoseconds_realtime() / 1000000); + dyn->payload = ddog_create_log_probe_snapshot( + &def->parent.probe, + msg, + dd_zend_string_to_CharSlice(dyn->service), + DDOG_CHARSLICE_C("php"), + ddtrace_nanoseconds_realtime() / 1000000, + process_tags ? dd_zend_string_to_CharSlice(process_tags) : DDOG_CHARSLICE_C("")); } } diff --git a/libdatadog b/libdatadog index 1caf15157a8..8f9441e9264 160000 --- a/libdatadog +++ b/libdatadog @@ -1 +1 @@ -Subproject commit 1caf15157a8251d398715349a75d5607cb5545c2 +Subproject commit 8f9441e9264ac9e97f4a2c17058fde18d60652ac diff --git a/tests/ext/live-debugger/debugger_log_probe.phpt b/tests/ext/live-debugger/debugger_log_probe.phpt index 3e7aece3c3a..7edb7199ba9 100644 --- a/tests/ext/live-debugger/debugger_log_probe.phpt +++ b/tests/ext/live-debugger/debugger_log_probe.phpt @@ -62,7 +62,7 @@ reset_request_replayer(); ?> --EXPECTF-- int(30) -array(5) { +array(6) { ["service"]=> string(22) "debugger_log_probe.php" ["ddsource"]=> @@ -247,4 +247,6 @@ array(5) { [true] [true] " + ["process_tags"]=> + NULL } diff --git a/tests/ext/live-debugger/debugger_log_probe_process_tags.phpt b/tests/ext/live-debugger/debugger_log_probe_process_tags.phpt new file mode 100644 index 00000000000..0ed61760f9f --- /dev/null +++ b/tests/ext/live-debugger/debugger_log_probe_process_tags.phpt @@ -0,0 +1,61 @@ +--TEST-- +Live debugger log probe includes process_tags +--SKIPIF-- + +--ENV-- +DD_AGENT_HOST=request-replayer +DD_TRACE_AGENT_PORT=80 +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_DYNAMIC_INSTRUMENTATION_ENABLED=1 +DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS=0.01 +DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED=1 +--INI-- +datadog.trace.agent_test_session_token=live-debugger/log_probe_process_tags +--FILE-- + ["methodName" => "simple_function"], + "captureSnapshot" => true, + "segments" => [ + ["str" => "Simple message"], + ], + ]); + + \DDTrace\start_span(); // submit span data +}); + +simple_function(); + +$dlr = new DebuggerLogReplayer; +$log = $dlr->waitForDebuggerDataAndReplay(); +$payload = json_decode($log["body"], true)[0]; + +if (isset($payload["process_tags"])) { + echo "Process tags found in payload\n"; + $processTags = $payload["process_tags"]; + + var_dump($processTags); +} else { + echo "ERROR: process_tags not found in payload\n"; +} + +?> +--CLEAN-- + +--EXPECTF-- +Process tags found in payload +string(%d) "entrypoint.basedir:live-debugger,entrypoint.name:debugger_log_probe_process_tags,entrypoint.type:script,entrypoint.workdir:%s,runtime.sapi:cli" + diff --git a/tests/ext/live-debugger/debugger_span_decoration_probe.phpt b/tests/ext/live-debugger/debugger_span_decoration_probe.phpt index 9320df9cb9c..9716393f6d7 100644 --- a/tests/ext/live-debugger/debugger_span_decoration_probe.phpt +++ b/tests/ext/live-debugger/debugger_span_decoration_probe.phpt @@ -101,7 +101,7 @@ array(2) { string(%d) "/debugger/v1/input?ddtags=debugger_version:1.%s,env:none,version:,runtime_id:%s-%s-%s-%s-%s,host_name:%s" array(1) { [0]=> - array(5) { + array(6) { ["service"]=> string(34) "debugger_span_decoration_probe.php" ["ddsource"]=> @@ -142,5 +142,7 @@ array(1) { } ["message"]=> string(32) "Evaluation errors for probe id 2" + ["process_tags"]=> + NULL } } diff --git a/tests/ext/live-debugger/exception-replay_001.phpt b/tests/ext/live-debugger/exception-replay_001.phpt index ef1888464a3..4b23b4cccae 100644 --- a/tests/ext/live-debugger/exception-replay_001.phpt +++ b/tests/ext/live-debugger/exception-replay_001.phpt @@ -47,7 +47,7 @@ reset_request_replayer(); --EXPECTF-- array(2) { [0]=> - array(5) { + array(6) { ["service"]=> string(24) "exception-replay_001.php" ["ddsource"]=> @@ -126,9 +126,11 @@ array(2) { } ["message"]=> NULL + ["process_tags"]=> + NULL } [1]=> - array(5) { + array(6) { ["service"]=> string(24) "exception-replay_001.php" ["ddsource"]=> @@ -231,5 +233,7 @@ array(2) { } ["message"]=> NULL + ["process_tags"]=> + NULL } } \ No newline at end of file diff --git a/tests/ext/live-debugger/exception-replay_002.phpt b/tests/ext/live-debugger/exception-replay_002.phpt index 1880a57a5b7..80f7d247faf 100644 --- a/tests/ext/live-debugger/exception-replay_002.phpt +++ b/tests/ext/live-debugger/exception-replay_002.phpt @@ -76,7 +76,7 @@ require __DIR__ . "/live_debugger.inc"; reset_request_replayer(); ?> --EXPECTF-- -array(5) { +array(6) { ["service"]=> string(24) "exception-replay_002.php" ["ddsource"]=> @@ -1029,4 +1029,6 @@ array(5) { } ["message"]=> NULL + ["process_tags"]=> + NULL } diff --git a/tests/ext/live-debugger/exception-replay_non_regression_2989_mysqli.phpt b/tests/ext/live-debugger/exception-replay_non_regression_2989_mysqli.phpt index a684465fab3..524bac94f28 100644 --- a/tests/ext/live-debugger/exception-replay_non_regression_2989_mysqli.phpt +++ b/tests/ext/live-debugger/exception-replay_non_regression_2989_mysqli.phpt @@ -49,7 +49,7 @@ reset_request_replayer(); ?> --EXPECTF-- Warning: mysqli::__construct(): php_network_getaddresses: getaddrinfo %s -%Aarray(5) { +%Aarray(6) { ["service"]=> string(47) "exception-replay_non_regression_2989_mysqli.php" ["ddsource"]=> @@ -124,4 +124,6 @@ Warning: mysqli::__construct(): php_network_getaddresses: getaddrinfo %s } ["message"]=> NULL + ["process_tags"]=> + NULL } From 2d5fbfc3624a7305e3133458da3ae265b4dd90da Mon Sep 17 00:00:00 2001 From: Louis Tricot Date: Mon, 19 Jan 2026 11:20:38 +0100 Subject: [PATCH 7/7] temporary libdatadog bump --- libdatadog | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libdatadog b/libdatadog index 8f9441e9264..e5c143e27e3 160000 --- a/libdatadog +++ b/libdatadog @@ -1 +1 @@ -Subproject commit 8f9441e9264ac9e97f4a2c17058fde18d60652ac +Subproject commit e5c143e27e3bcfef1dbdafb8ce321c347034d632