From a5638f56acf9dfdfb6558454d85845adafa43d02 Mon Sep 17 00:00:00 2001 From: Alexandre Choura Date: Wed, 1 Oct 2025 09:33:39 +0200 Subject: [PATCH 1/4] feat: add process tags to tracing payloads --- config.m4 | 1 + config.w32 | 1 + ext/configuration.h | 1 + ext/ddtrace.c | 3 + ext/process_tags.c | 356 ++++++++++++++++++++++ ext/process_tags.h | 37 +++ ext/serializer.c | 11 + tests/ext/process_tags_disabled.phpt | 38 +++ tests/ext/process_tags_enabled.phpt | 83 +++++ tests/ext/process_tags_normalization.phpt | 73 +++++ 10 files changed, 604 insertions(+) create mode 100644 ext/process_tags.c create mode 100644 ext/process_tags.h create mode 100644 tests/ext/process_tags_disabled.phpt create mode 100644 tests/ext/process_tags_enabled.phpt create mode 100644 tests/ext/process_tags_normalization.phpt diff --git a/config.m4 b/config.m4 index 8638e80519..7cfde9c81a 100644 --- a/config.m4 +++ b/config.m4 @@ -201,6 +201,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 1f964734d1..977b4f6f60 100644 --- a/config.w32 +++ b/config.w32 @@ -48,6 +48,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 d5c9ed0f86..ec2c559b01 100644 --- a/ext/configuration.h +++ b/ext/configuration.h @@ -258,6 +258,7 @@ enum ddtrace_sampling_rules_format { CONFIG(SET, DD_TRACE_HTTP_SERVER_ERROR_STATUSES, "500-599", .ini_change = zai_config_system_ini_change) \ CONFIG(BOOL, DD_CODE_ORIGIN_FOR_SPANS_ENABLED, "true") \ CONFIG(INT, DD_CODE_ORIGIN_MAX_USER_FRAMES, "8") \ + CONFIG(BOOL, DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED, "false") \ DD_INTEGRATIONS #ifndef _WIN32 diff --git a/ext/ddtrace.c b/ext/ddtrace.c index 1f2e65d822..e35dc0ff4a 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" @@ -1545,6 +1546,7 @@ static PHP_MINIT_FUNCTION(ddtrace) { ddtrace_live_debugger_minit(); ddtrace_minit_remote_config(); ddtrace_trace_source_minit(); + ddtrace_process_tags_minit(); #ifndef _WIN32 ddtrace_signals_minit(); @@ -1606,6 +1608,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 diff --git a/ext/process_tags.c b/ext/process_tags.c new file mode 100644 index 0000000000..1334f50d4b --- /dev/null +++ b/ext/process_tags.c @@ -0,0 +1,356 @@ +#include "process_tags.h" +#include "configuration.h" +#include "ddtrace.h" +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#define getcwd _getcwd +#define PATH_MAX _MAX_PATH +#else +#include +#endif + +#ifndef PATH_MAX +#define PATH_MAX 4096 +#endif + +// Tag name constants +#define TAG_ENTRYPOINT_NAME "entrypoint.name" +#define TAG_ENTRYPOINT_BASEDIR "entrypoint.basedir" +#define TAG_ENTRYPOINT_WORKDIR "entrypoint.workdir" +#define TAG_ENTRYPOINT_TYPE "entrypoint.type" + +// Entrypoint type constants +#define TYPE_SCRIPT "script" +#define TYPE_CLI "cli" +#define TYPE_EXECUTABLE "executable" + +// Maximum number of process tags +#define MAX_PROCESS_TAGS 10 + +typedef struct { + char *key; + char *value; +} process_tag_entry_t; + +typedef struct { + process_tag_entry_t entries[MAX_PROCESS_TAGS]; + size_t count; + zend_string *serialized; +} process_tags_t; + +static process_tags_t process_tags = {0}; + +/** + * Normalize a tag value according to RFC specifications: + * - Convert to lowercase + * - Allow only: a-z, 0-9, /, ., - + * - Replace everything else with _ + * + * @param value The value to normalize + * @return A newly allocated normalized string (caller must free) + */ +static char *normalize_value(const char *value) { + if (!value || !*value) { + return NULL; + } + + size_t len = strlen(value); + char *normalized = (char *)malloc(len + 1); + if (!normalized) { + return NULL; + } + + for (size_t i = 0; i < len; i++) { + char c = value[i]; + + // Convert to lowercase + if (c >= 'A' && c <= 'Z') { + normalized[i] = c + ('a' - 'A'); + } + // Allow: a-z, 0-9, /, ., - + else if ((c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + c == '/' || c == '.' || c == '-') { + normalized[i] = c; + } + // Replace everything else with _ + else { + normalized[i] = '_'; + } + } + normalized[len] = '\0'; + + return normalized; +} + +/** + * Get the base name (last path segment) from a path + * + * @param path The full path + * @return A newly allocated string with the base name (caller must free) + */ +static char *get_basename(const char *path) { + if (!path || !*path) { + return NULL; + } + + const char *last_slash = strrchr(path, '/'); +#ifdef _WIN32 + const char *last_backslash = strrchr(path, '\\'); + if (last_backslash && (!last_slash || last_backslash > last_slash)) { + last_slash = last_backslash; + } +#endif + + const char *basename = last_slash ? last_slash + 1 : path; + + // If empty after slash, return NULL + if (!*basename) { + return NULL; + } + + return strdup(basename); +} + +/** + * Get the directory name (parent directory) from a path + * + * @param path The full path + * @return A newly allocated string with the directory name (caller must free) + */ +static char *get_dirname(const char *path) { + if (!path || !*path) { + return NULL; + } + + char *path_copy = strdup(path); + if (!path_copy) { + return NULL; + } + + char *last_slash = strrchr(path_copy, '/'); +#ifdef _WIN32 + char *last_backslash = strrchr(path_copy, '\\'); + if (last_backslash && (!last_slash || last_backslash > last_slash)) { + last_slash = last_backslash; + } +#endif + + if (last_slash) { + *last_slash = '\0'; + + // Get the basename of the directory + char *basedir = get_basename(path_copy); + free(path_copy); + return basedir; + } + + free(path_copy); + return strdup("."); +} + +/** + * Add a process tag entry + * + * @param key The tag key (will be duplicated) + * @param value The tag value (will be normalized and duplicated) + */ +static void add_process_tag(const char *key, const char *value) { + if (!key || !value || process_tags.count >= MAX_PROCESS_TAGS) { + return; + } + + char *normalized_value = normalize_value(value); + if (!normalized_value) { + return; + } + + process_tags.entries[process_tags.count].key = strdup(key); + process_tags.entries[process_tags.count].value = normalized_value; + + if (process_tags.entries[process_tags.count].key) { + process_tags.count++; + } else { + free(normalized_value); + } +} + +/** + * Comparison function for qsort to sort process tags by key + */ +static int compare_tags(const void *a, const void *b) { + const process_tag_entry_t *tag_a = (const process_tag_entry_t *)a; + const process_tag_entry_t *tag_b = (const process_tag_entry_t *)b; + return strcmp(tag_a->key, tag_b->key); +} + +/** + * Serialize process tags into a comma-separated string + * Format: key1:value1,key2:value2,... + * Keys are sorted alphabetically + */ +static void serialize_process_tags(void) { + if (process_tags.count == 0) { + return; + } + + // Sort tags by key + qsort(process_tags.entries, process_tags.count, sizeof(process_tag_entry_t), compare_tags); + + // Calculate total length needed + size_t total_len = 0; + for (size_t i = 0; i < process_tags.count; i++) { + total_len += strlen(process_tags.entries[i].key); + total_len += 1; // for ':' + total_len += strlen(process_tags.entries[i].value); + if (i < process_tags.count - 1) { + total_len += 1; // for ',' + } + } + + // Allocate and build the serialized string + char *buffer = (char *)malloc(total_len + 1); + if (!buffer) { + return; + } + + char *ptr = buffer; + for (size_t i = 0; i < process_tags.count; i++) { + size_t key_len = strlen(process_tags.entries[i].key); + size_t value_len = strlen(process_tags.entries[i].value); + + memcpy(ptr, process_tags.entries[i].key, key_len); + ptr += key_len; + *ptr++ = ':'; + memcpy(ptr, process_tags.entries[i].value, value_len); + ptr += value_len; + + if (i < process_tags.count - 1) { + *ptr++ = ','; + } + } + *ptr = '\0'; + + // Create a persistent zend_string + process_tags.serialized = zend_string_init(buffer, total_len, 1); + free(buffer); +} + +/** + * Collect process tags from the environment + */ +static void collect_process_tags(void) { + const char *entrypoint_type = NULL; + char *entrypoint_name = NULL; + char *entrypoint_basedir = NULL; + char *entrypoint_workdir = NULL; + + // Determine entrypoint information based on SAPI + // For consistency, always use the executable path at MINIT time + if (strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "phpdbg") == 0) { + entrypoint_type = TYPE_CLI; + } else { + entrypoint_type = TYPE_EXECUTABLE; + } + + // Try to get executable path +#ifdef _WIN32 + char exe_path[PATH_MAX]; + DWORD len = GetModuleFileNameA(NULL, exe_path, PATH_MAX); + if (len > 0 && len < PATH_MAX) { + entrypoint_name = get_basename(exe_path); + entrypoint_basedir = get_dirname(exe_path); + } +#else + char exe_path[PATH_MAX]; + ssize_t len = readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1); + if (len != -1) { + exe_path[len] = '\0'; + entrypoint_name = get_basename(exe_path); + entrypoint_basedir = get_dirname(exe_path); + } else { + // Fallback: use argv[0] or executable_location if available + if (sapi_module.executable_location) { + entrypoint_name = get_basename(sapi_module.executable_location); + entrypoint_basedir = get_dirname(sapi_module.executable_location); + } + } +#endif + + // If we still don't have entrypoint info, set type to executable + if (!entrypoint_name && sapi_module.executable_location) { + entrypoint_name = get_basename(sapi_module.executable_location); + entrypoint_basedir = get_dirname(sapi_module.executable_location); + } + + // Get current working directory + char cwd[PATH_MAX]; + if (getcwd(cwd, sizeof(cwd))) { + entrypoint_workdir = get_basename(cwd); + } + + // Add tags in the order specified in RFC + if (entrypoint_basedir) { + add_process_tag(TAG_ENTRYPOINT_BASEDIR, entrypoint_basedir); + } + if (entrypoint_name) { + add_process_tag(TAG_ENTRYPOINT_NAME, entrypoint_name); + } + if (entrypoint_type) { + add_process_tag(TAG_ENTRYPOINT_TYPE, entrypoint_type); + } + if (entrypoint_workdir) { + add_process_tag(TAG_ENTRYPOINT_WORKDIR, entrypoint_workdir); + } + + // Clean up temporary strings + free(entrypoint_name); + free(entrypoint_basedir); + free(entrypoint_workdir); + + // Serialize the collected tags + serialize_process_tags(); +} + +void ddtrace_process_tags_minit(void) { + // Initialize the process_tags structure + memset(&process_tags, 0, sizeof(process_tags)); + + // Only collect if enabled + if (ddtrace_process_tags_enabled()) { + collect_process_tags(); + } +} + +void ddtrace_process_tags_mshutdown(void) { + // Free all allocated memory + for (size_t i = 0; i < process_tags.count; i++) { + free(process_tags.entries[i].key); + free(process_tags.entries[i].value); + } + + if (process_tags.serialized) { + zend_string_release(process_tags.serialized); + } + + memset(&process_tags, 0, sizeof(process_tags)); +} + +bool ddtrace_process_tags_enabled(void) { + return get_global_DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED(); +} + +zend_string *ddtrace_process_tags_get_serialized(void) { + if (!ddtrace_process_tags_enabled() || !process_tags.serialized) { + return NULL; + } + + return process_tags.serialized; +} + diff --git a/ext/process_tags.h b/ext/process_tags.h new file mode 100644 index 0000000000..2c2b8ad672 --- /dev/null +++ b/ext/process_tags.h @@ -0,0 +1,37 @@ +#ifndef DD_PROCESS_TAGS_H +#define DD_PROCESS_TAGS_H + +#include +#include + +/** + * Initialize process tags collection. + * Should be called once during MINIT phase. + */ +void ddtrace_process_tags_minit(void); + +/** + * Shutdown process tags. + * Should be called during MSHUTDOWN phase. + */ +void ddtrace_process_tags_mshutdown(void); + +/** + * Check if process tags propagation is enabled. + * + * @return true if DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED is true + */ +bool ddtrace_process_tags_enabled(void); + +/** + * Get the serialized process tags as a comma-separated string. + * Format: key1:value1,key2:value2,... + * Keys are sorted alphabetically. + * + * @return zend_string* containing serialized tags, or NULL if disabled or empty + */ +zend_string *ddtrace_process_tags_get_serialized(void); + +#endif // DD_PROCESS_TAGS_H + + diff --git a/ext/serializer.c b/ext/serializer.c index 51c4e1eac9..189ed10abb 100644 --- a/ext/serializer.c +++ b/ext/serializer.c @@ -41,6 +41,7 @@ #include "ip_extraction.h" #include #include "priority_sampling/priority_sampling.h" +#include "process_tags.h" #include "span.h" #include "uri_normalization.h" #include "user_request.h" @@ -831,6 +832,16 @@ void ddtrace_set_root_span_properties(ddtrace_root_span_data *span) { } } + // Add process tags if enabled + if (ddtrace_process_tags_enabled()) { + zend_string *process_tags = ddtrace_process_tags_get_serialized(); + if (process_tags && ZSTR_LEN(process_tags) > 0) { + zval process_tags_zv; + ZVAL_STR_COPY(&process_tags_zv, process_tags); + zend_hash_str_add_new(meta, ZEND_STRL("_dd.tags.process"), &process_tags_zv); + } + } + ddtrace_root_span_data *parent_root = span->stack->parent_stack->root_span; if (parent_root) { ddtrace_inherit_span_properties(&span->span, &parent_root->span); diff --git a/tests/ext/process_tags_disabled.phpt b/tests/ext/process_tags_disabled.phpt new file mode 100644 index 0000000000..32c5ff9fe1 --- /dev/null +++ b/tests/ext/process_tags_disabled.phpt @@ -0,0 +1,38 @@ +--TEST-- +Process tags are not added to root span when disabled +--DESCRIPTION-- +Verifies that process tags are NOT added to the root span metadata +when DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED is set to false (default) +--ENV-- +DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED=0 +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_TRACE_AUTO_FLUSH_ENABLED=0 +--FILE-- +name = 'root_span'; +$span->service = 'test_service'; + +// Close the span +\DDTrace\close_span(); + +// Get the trace +$spans = dd_trace_serialize_closed_spans(); + +// Check if process tags are present +if (isset($spans[0]['meta']['_dd.tags.process'])) { + echo "Process tags found: YES (unexpected)\n"; + echo "Process tags value: " . $spans[0]['meta']['_dd.tags.process'] . "\n"; +} else { + echo "Process tags found: NO (expected)\n"; +} + +// Verify other meta tags still work +echo "Service set: " . (isset($spans[0]['service']) && $spans[0]['service'] === 'test_service' ? 'YES' : 'NO') . "\n"; +?> +--EXPECT-- +Process tags found: NO (expected) +Service set: YES diff --git a/tests/ext/process_tags_enabled.phpt b/tests/ext/process_tags_enabled.phpt new file mode 100644 index 0000000000..aaa84a3492 --- /dev/null +++ b/tests/ext/process_tags_enabled.phpt @@ -0,0 +1,83 @@ +--TEST-- +Process tags are added to root span when enabled +--DESCRIPTION-- +Verifies that process tags are properly added to the root span metadata +when DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED is set to true +--ENV-- +DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED=1 +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_TRACE_AUTO_FLUSH_ENABLED=0 +--FILE-- +name = 'root_span'; +$span->service = 'test_service'; + +// Close the span +\DDTrace\close_span(); + +// Get the trace +$spans = dd_trace_serialize_closed_spans(); + +// Check if process tags are present +if (isset($spans[0]['meta']['_dd.tags.process'])) { + $processTags = $spans[0]['meta']['_dd.tags.process']; + echo "Process tags found: YES\n"; + + // Parse and verify the format + $tags = explode(',', $processTags); + echo "Number of tags: " . count($tags) . "\n"; + + // Verify format (key:value) + $validFormat = true; + $tagKeys = []; + foreach ($tags as $tag) { + if (strpos($tag, ':') === false) { + $validFormat = false; + break; + } + list($key, $value) = explode(':', $tag, 2); + $tagKeys[] = $key; + + // Verify normalization: only lowercase, a-z, 0-9, /, ., - + if (!preg_match('/^[a-z0-9\/\.\-_]+$/', $value)) { + echo "Invalid normalized value: $value\n"; + $validFormat = false; + } + } + + echo "Valid format: " . ($validFormat ? 'YES' : 'NO') . "\n"; + + // Verify keys are sorted + $sortedKeys = $tagKeys; + sort($sortedKeys); + $isSorted = ($tagKeys === $sortedKeys); + echo "Keys sorted: " . ($isSorted ? 'YES' : 'NO') . "\n"; + + // Check for expected keys + $expectedKeys = ['entrypoint.basedir', 'entrypoint.name', 'entrypoint.type', 'entrypoint.workdir']; + $hasExpectedKeys = true; + foreach ($expectedKeys as $expectedKey) { + if (!in_array($expectedKey, $tagKeys)) { + echo "Missing expected key: $expectedKey\n"; + $hasExpectedKeys = false; + } + } + echo "Has expected keys: " . ($hasExpectedKeys ? 'YES' : 'NO') . "\n"; + + // Display the tags for verification + echo "Process tags value: $processTags\n"; +} else { + echo "Process tags found: NO\n"; +} +?> +--EXPECTF-- +Process tags found: YES +Number of tags: 4 +Valid format: YES +Keys sorted: YES +Has expected keys: YES +Process tags value: entrypoint.basedir:%s,entrypoint.name:%s,entrypoint.type:%s,entrypoint.workdir:%s diff --git a/tests/ext/process_tags_normalization.phpt b/tests/ext/process_tags_normalization.phpt new file mode 100644 index 0000000000..b78cf43424 --- /dev/null +++ b/tests/ext/process_tags_normalization.phpt @@ -0,0 +1,73 @@ +--TEST-- +Process tags values are properly normalized +--DESCRIPTION-- +Verifies that process tag values follow the normalization rules: +- Lowercase +- Only a-z, 0-9, /, ., - allowed +- Everything else replaced with _ +--ENV-- +DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED=1 +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_TRACE_AUTO_FLUSH_ENABLED=0 +--FILE-- +name = 'root_span'; +$span->service = 'test_service'; + +// Close the span +\DDTrace\close_span(); + +// Get the trace +$spans = dd_trace_serialize_closed_spans(); + +// Check if process tags are present and normalized +if (isset($spans[0]['meta']['_dd.tags.process'])) { + $processTags = $spans[0]['meta']['_dd.tags.process']; + + // Parse tags + $tags = explode(',', $processTags); + $allNormalized = true; + + foreach ($tags as $tag) { + list($key, $value) = explode(':', $tag, 2); + + // Check if value is normalized (only lowercase, a-z, 0-9, /, ., -, _) + if (!preg_match('/^[a-z0-9\/\.\-_]+$/', $value)) { + echo "Value not normalized: $value\n"; + $allNormalized = false; + } + + // Check if there are any uppercase letters + if (preg_match('/[A-Z]/', $value)) { + echo "Uppercase found in: $value\n"; + $allNormalized = false; + } + } + + echo "All values normalized: " . ($allNormalized ? 'YES' : 'NO') . "\n"; + + // Verify the entrypoint.type is one of the expected values + foreach ($tags as $tag) { + if (strpos($tag, 'entrypoint.type:') === 0) { + list($key, $type) = explode(':', $tag, 2); + $validTypes = ['script', 'cli', 'executable']; + if (in_array($type, $validTypes)) { + echo "Entrypoint type valid: YES\n"; + } else { + echo "Entrypoint type invalid: $type\n"; + } + } + } +} else { + echo "Process tags not found\n"; +} +?> +--EXPECTF-- +All values normalized: YES +Entrypoint type valid: YES + + From 54036c9730823efd3652645bb25747f308902b02 Mon Sep 17 00:00:00 2001 From: Alexandre Choura Date: Fri, 3 Oct 2025 10:32:56 +0200 Subject: [PATCH 2/4] refactor: move process tags initialization to first RINIT and update collection logic --- ext/ddtrace.c | 4 +- ext/process_tags.c | 195 ++++-------------- ext/process_tags.h | 29 +-- .../Custom/Autoloaded/ProcessTagsWebTest.php | 79 +++++++ tests/ext/process_tags_disabled.phpt | 38 ---- tests/ext/process_tags_enabled.phpt | 74 ++----- tests/ext/process_tags_normalization.phpt | 73 ------- 7 files changed, 151 insertions(+), 341 deletions(-) create mode 100644 tests/Integrations/Custom/Autoloaded/ProcessTagsWebTest.php delete mode 100644 tests/ext/process_tags_disabled.phpt delete mode 100644 tests/ext/process_tags_normalization.phpt diff --git a/ext/ddtrace.c b/ext/ddtrace.c index e35dc0ff4a..05d2d41028 100644 --- a/ext/ddtrace.c +++ b/ext/ddtrace.c @@ -1546,7 +1546,6 @@ static PHP_MINIT_FUNCTION(ddtrace) { ddtrace_live_debugger_minit(); ddtrace_minit_remote_config(); ddtrace_trace_source_minit(); - ddtrace_process_tags_minit(); #ifndef _WIN32 ddtrace_signals_minit(); @@ -1631,6 +1630,9 @@ static void dd_rinit_once(void) { */ ddtrace_startup_logging_first_rinit(); + // Collect process tags now that script path is available + 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 index 1334f50d4b..e581c54138 100644 --- a/ext/process_tags.c +++ b/ext/process_tags.c @@ -5,6 +5,7 @@ #include #include #include +#include #ifdef _WIN32 #include @@ -19,18 +20,12 @@ #define PATH_MAX 4096 #endif -// Tag name constants #define TAG_ENTRYPOINT_NAME "entrypoint.name" #define TAG_ENTRYPOINT_BASEDIR "entrypoint.basedir" #define TAG_ENTRYPOINT_WORKDIR "entrypoint.workdir" #define TAG_ENTRYPOINT_TYPE "entrypoint.type" - -// Entrypoint type constants -#define TYPE_SCRIPT "script" +#define TAG_SERVER_TYPE "server.type" #define TYPE_CLI "cli" -#define TYPE_EXECUTABLE "executable" - -// Maximum number of process tags #define MAX_PROCESS_TAGS 10 typedef struct { @@ -46,55 +41,33 @@ typedef struct { static process_tags_t process_tags = {0}; -/** - * Normalize a tag value according to RFC specifications: - * - Convert to lowercase - * - Allow only: a-z, 0-9, /, ., - - * - Replace everything else with _ - * - * @param value The value to normalize - * @return A newly allocated normalized string (caller must free) - */ +// Normalize tag value per RFC: lowercase, allow [a-z0-9/.-], replace rest with _ static char *normalize_value(const char *value) { if (!value || !*value) { return NULL; } size_t len = strlen(value); - char *normalized = (char *)malloc(len + 1); + char *normalized = malloc(len + 1); if (!normalized) { return NULL; } for (size_t i = 0; i < len; i++) { char c = value[i]; - - // Convert to lowercase if (c >= 'A' && c <= 'Z') { normalized[i] = c + ('a' - 'A'); - } - // Allow: a-z, 0-9, /, ., - - else if ((c >= 'a' && c <= 'z') || - (c >= '0' && c <= '9') || - c == '/' || c == '.' || c == '-') { + } else if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || + c == '/' || c == '.' || c == '-') { normalized[i] = c; - } - // Replace everything else with _ - else { + } else { normalized[i] = '_'; } } normalized[len] = '\0'; - return normalized; } -/** - * Get the base name (last path segment) from a path - * - * @param path The full path - * @return A newly allocated string with the base name (caller must free) - */ static char *get_basename(const char *path) { if (!path || !*path) { return NULL; @@ -109,21 +82,9 @@ static char *get_basename(const char *path) { #endif const char *basename = last_slash ? last_slash + 1 : path; - - // If empty after slash, return NULL - if (!*basename) { - return NULL; - } - - return strdup(basename); + return *basename ? strdup(basename) : NULL; } -/** - * Get the directory name (parent directory) from a path - * - * @param path The full path - * @return A newly allocated string with the directory name (caller must free) - */ static char *get_dirname(const char *path) { if (!path || !*path) { return NULL; @@ -142,25 +103,17 @@ static char *get_dirname(const char *path) { } #endif + char *basedir; if (last_slash) { *last_slash = '\0'; - - // Get the basename of the directory - char *basedir = get_basename(path_copy); - free(path_copy); - return basedir; + basedir = get_basename(path_copy); + } else { + basedir = strdup("."); } - free(path_copy); - return strdup("."); + return basedir; } -/** - * Add a process tag entry - * - * @param key The tag key (will be duplicated) - * @param value The tag value (will be normalized and duplicated) - */ static void add_process_tag(const char *key, const char *value) { if (!key || !value || process_tags.count >= MAX_PROCESS_TAGS) { return; @@ -171,56 +124,40 @@ static void add_process_tag(const char *key, const char *value) { return; } - process_tags.entries[process_tags.count].key = strdup(key); - process_tags.entries[process_tags.count].value = normalized_value; - - if (process_tags.entries[process_tags.count].key) { - process_tags.count++; - } else { + char *key_copy = strdup(key); + if (!key_copy) { free(normalized_value); + return; } + + process_tags.entries[process_tags.count].key = key_copy; + process_tags.entries[process_tags.count].value = normalized_value; + process_tags.count++; } -/** - * Comparison function for qsort to sort process tags by key - */ static int compare_tags(const void *a, const void *b) { - const process_tag_entry_t *tag_a = (const process_tag_entry_t *)a; - const process_tag_entry_t *tag_b = (const process_tag_entry_t *)b; - return strcmp(tag_a->key, tag_b->key); + return strcmp(((const process_tag_entry_t *)a)->key, ((const process_tag_entry_t *)b)->key); } -/** - * Serialize process tags into a comma-separated string - * Format: key1:value1,key2:value2,... - * Keys are sorted alphabetically - */ +// Serialize process tags as comma-separated key:value pairs, sorted by key static void serialize_process_tags(void) { if (process_tags.count == 0) { return; } - // Sort tags by key qsort(process_tags.entries, process_tags.count, sizeof(process_tag_entry_t), compare_tags); - // Calculate total length needed size_t total_len = 0; for (size_t i = 0; i < process_tags.count; i++) { - total_len += strlen(process_tags.entries[i].key); - total_len += 1; // for ':' - total_len += strlen(process_tags.entries[i].value); + total_len += strlen(process_tags.entries[i].key) + 1 + strlen(process_tags.entries[i].value); if (i < process_tags.count - 1) { - total_len += 1; // for ',' + total_len++; // comma separator } } - // Allocate and build the serialized string - char *buffer = (char *)malloc(total_len + 1); - if (!buffer) { - return; - } - - char *ptr = buffer; + process_tags.serialized = zend_string_alloc(total_len, 1); // persistent allocation + char *ptr = ZSTR_VAL(process_tags.serialized); + for (size_t i = 0; i < process_tags.count; i++) { size_t key_len = strlen(process_tags.entries[i].key); size_t value_len = strlen(process_tags.entries[i].value); @@ -230,115 +167,69 @@ static void serialize_process_tags(void) { *ptr++ = ':'; memcpy(ptr, process_tags.entries[i].value, value_len); ptr += value_len; - if (i < process_tags.count - 1) { *ptr++ = ','; } } *ptr = '\0'; - - // Create a persistent zend_string - process_tags.serialized = zend_string_init(buffer, total_len, 1); - free(buffer); } -/** - * Collect process tags from the environment - */ static void collect_process_tags(void) { - const char *entrypoint_type = NULL; + bool is_cli = (strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "phpdbg") == 0); char *entrypoint_name = NULL; char *entrypoint_basedir = NULL; char *entrypoint_workdir = NULL; - // Determine entrypoint information based on SAPI - // For consistency, always use the executable path at MINIT time - if (strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "phpdbg") == 0) { - entrypoint_type = TYPE_CLI; - } else { - entrypoint_type = TYPE_EXECUTABLE; - } - - // Try to get executable path -#ifdef _WIN32 - char exe_path[PATH_MAX]; - DWORD len = GetModuleFileNameA(NULL, exe_path, PATH_MAX); - if (len > 0 && len < PATH_MAX) { - entrypoint_name = get_basename(exe_path); - entrypoint_basedir = get_dirname(exe_path); - } -#else - char exe_path[PATH_MAX]; - ssize_t len = readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1); - if (len != -1) { - exe_path[len] = '\0'; - entrypoint_name = get_basename(exe_path); - entrypoint_basedir = get_dirname(exe_path); - } else { - // Fallback: use argv[0] or executable_location if available - if (sapi_module.executable_location) { - entrypoint_name = get_basename(sapi_module.executable_location); - entrypoint_basedir = get_dirname(sapi_module.executable_location); + if (is_cli) { + // CLI: collect script information (not the PHP binary) + if (SG(request_info).path_translated && *SG(request_info).path_translated) { + entrypoint_name = get_basename(SG(request_info).path_translated); + entrypoint_basedir = get_dirname(SG(request_info).path_translated); } - } -#endif - - // If we still don't have entrypoint info, set type to executable - if (!entrypoint_name && sapi_module.executable_location) { - entrypoint_name = get_basename(sapi_module.executable_location); - entrypoint_basedir = get_dirname(sapi_module.executable_location); + } else { + // Web SAPI: collect server type (different requests may execute different scripts) + add_process_tag(TAG_SERVER_TYPE, sapi_module.name); } - // Get current working directory char cwd[PATH_MAX]; if (getcwd(cwd, sizeof(cwd))) { entrypoint_workdir = get_basename(cwd); } - // Add tags in the order specified in RFC if (entrypoint_basedir) { add_process_tag(TAG_ENTRYPOINT_BASEDIR, entrypoint_basedir); } if (entrypoint_name) { add_process_tag(TAG_ENTRYPOINT_NAME, entrypoint_name); } - if (entrypoint_type) { - add_process_tag(TAG_ENTRYPOINT_TYPE, entrypoint_type); + if (is_cli) { + add_process_tag(TAG_ENTRYPOINT_TYPE, TYPE_CLI); } if (entrypoint_workdir) { add_process_tag(TAG_ENTRYPOINT_WORKDIR, entrypoint_workdir); } - // Clean up temporary strings free(entrypoint_name); free(entrypoint_basedir); free(entrypoint_workdir); - // Serialize the collected tags serialize_process_tags(); } -void ddtrace_process_tags_minit(void) { - // Initialize the process_tags structure - memset(&process_tags, 0, sizeof(process_tags)); - - // Only collect if enabled - if (ddtrace_process_tags_enabled()) { +void ddtrace_process_tags_first_rinit(void) { + if (ddtrace_process_tags_enabled() && !process_tags.serialized) { collect_process_tags(); } } void ddtrace_process_tags_mshutdown(void) { - // Free all allocated memory for (size_t i = 0; i < process_tags.count; i++) { free(process_tags.entries[i].key); free(process_tags.entries[i].value); } - if (process_tags.serialized) { zend_string_release(process_tags.serialized); } - memset(&process_tags, 0, sizeof(process_tags)); } @@ -347,10 +238,6 @@ bool ddtrace_process_tags_enabled(void) { } zend_string *ddtrace_process_tags_get_serialized(void) { - if (!ddtrace_process_tags_enabled() || !process_tags.serialized) { - return NULL; - } - - return process_tags.serialized; + return (ddtrace_process_tags_enabled() && process_tags.serialized) ? process_tags.serialized : NULL; } diff --git a/ext/process_tags.h b/ext/process_tags.h index 2c2b8ad672..0ff2767c4e 100644 --- a/ext/process_tags.h +++ b/ext/process_tags.h @@ -4,32 +4,17 @@ #include #include -/** - * Initialize process tags collection. - * Should be called once during MINIT phase. - */ -void ddtrace_process_tags_minit(void); - -/** - * Shutdown process tags. - * Should be called during MSHUTDOWN phase. - */ +// 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. - * - * @return true if DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED is true - */ +// Check if process tags propagation is enabled bool ddtrace_process_tags_enabled(void); -/** - * Get the serialized process tags as a comma-separated string. - * Format: key1:value1,key2:value2,... - * Keys are sorted alphabetically. - * - * @return zend_string* containing serialized tags, or NULL if disabled or empty - */ +// 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/tests/Integrations/Custom/Autoloaded/ProcessTagsWebTest.php b/tests/Integrations/Custom/Autoloaded/ProcessTagsWebTest.php new file mode 100644 index 0000000000..65f1d3a880 --- /dev/null +++ b/tests/Integrations/Custom/Autoloaded/ProcessTagsWebTest.php @@ -0,0 +1,79 @@ + '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.tags.process', $rootSpan['meta']); + $processTags = $rootSpan['meta']['_dd.tags.process']; + + // Parse the process tags + $tags = []; + foreach (explode(',', $processTags) as $pair) { + [$key, $value] = explode(':', $pair, 2); + $tags[$key] = $value; + } + + // Web SAPI should have server.type and entrypoint.workdir + $this->assertArrayHasKey('server.type', $tags, 'server.type should be present for web SAPI'); + $this->assertArrayHasKey('entrypoint.workdir', $tags, 'entrypoint.workdir should be present'); + + // Web SAPI should NOT have entrypoint.name, entrypoint.basedir, or entrypoint.type + $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'); + $this->assertArrayNotHasKey('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['server.type'], + $expectedSapis, + sprintf( + 'server.type should be one of [%s], got: %s', + implode(', ', $expectedSapis), + $tags['server.type'] + ) + ); + + // Verify server.type is normalized (lowercase, only allowed chars) + $this->assertMatchesRegularExpression( + '/^[a-z0-9\/_.-]+$/', + $tags['server.type'], + 'server.type should be normalized' + ); + } +} + diff --git a/tests/ext/process_tags_disabled.phpt b/tests/ext/process_tags_disabled.phpt deleted file mode 100644 index 32c5ff9fe1..0000000000 --- a/tests/ext/process_tags_disabled.phpt +++ /dev/null @@ -1,38 +0,0 @@ ---TEST-- -Process tags are not added to root span when disabled ---DESCRIPTION-- -Verifies that process tags are NOT added to the root span metadata -when DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED is set to false (default) ---ENV-- -DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED=0 -DD_TRACE_GENERATE_ROOT_SPAN=0 -DD_TRACE_AUTO_FLUSH_ENABLED=0 ---FILE-- -name = 'root_span'; -$span->service = 'test_service'; - -// Close the span -\DDTrace\close_span(); - -// Get the trace -$spans = dd_trace_serialize_closed_spans(); - -// Check if process tags are present -if (isset($spans[0]['meta']['_dd.tags.process'])) { - echo "Process tags found: YES (unexpected)\n"; - echo "Process tags value: " . $spans[0]['meta']['_dd.tags.process'] . "\n"; -} else { - echo "Process tags found: NO (expected)\n"; -} - -// Verify other meta tags still work -echo "Service set: " . (isset($spans[0]['service']) && $spans[0]['service'] === 'test_service' ? 'YES' : 'NO') . "\n"; -?> ---EXPECT-- -Process tags found: NO (expected) -Service set: YES diff --git a/tests/ext/process_tags_enabled.phpt b/tests/ext/process_tags_enabled.phpt index aaa84a3492..cda502ae5f 100644 --- a/tests/ext/process_tags_enabled.phpt +++ b/tests/ext/process_tags_enabled.phpt @@ -1,83 +1,51 @@ --TEST-- Process tags are added to root span when enabled ---DESCRIPTION-- -Verifies that process tags are properly added to the root span metadata -when DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED is set to true --ENV-- DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED=1 DD_TRACE_GENERATE_ROOT_SPAN=0 DD_TRACE_AUTO_FLUSH_ENABLED=0 --FILE-- name = 'root_span'; $span->service = 'test_service'; - -// Close the span \DDTrace\close_span(); -// Get the trace $spans = dd_trace_serialize_closed_spans(); // Check if process tags are present if (isset($spans[0]['meta']['_dd.tags.process'])) { $processTags = $spans[0]['meta']['_dd.tags.process']; - echo "Process tags found: YES\n"; + echo "Process tags present: YES\n"; + echo "Process tags: $processTags\n"; - // Parse and verify the format + // Verify format: comma-separated key:value pairs $tags = explode(',', $processTags); - echo "Number of tags: " . count($tags) . "\n"; - - // Verify format (key:value) - $validFormat = true; - $tagKeys = []; - foreach ($tags as $tag) { - if (strpos($tag, ':') === false) { - $validFormat = false; - break; - } - list($key, $value) = explode(':', $tag, 2); - $tagKeys[] = $key; - - // Verify normalization: only lowercase, a-z, 0-9, /, ., - - if (!preg_match('/^[a-z0-9\/\.\-_]+$/', $value)) { - echo "Invalid normalized value: $value\n"; - $validFormat = false; - } - } - - echo "Valid format: " . ($validFormat ? 'YES' : 'NO') . "\n"; - // Verify keys are sorted - $sortedKeys = $tagKeys; + // Verify keys are sorted alphabetically + $keys = array_map(function($tag) { + return explode(':', $tag, 2)[0]; + }, $tags); + $sortedKeys = $keys; sort($sortedKeys); - $isSorted = ($tagKeys === $sortedKeys); - echo "Keys sorted: " . ($isSorted ? 'YES' : 'NO') . "\n"; + echo "Keys sorted: " . ($keys === $sortedKeys ? 'YES' : 'NO') . "\n"; - // Check for expected keys - $expectedKeys = ['entrypoint.basedir', 'entrypoint.name', 'entrypoint.type', 'entrypoint.workdir']; - $hasExpectedKeys = true; - foreach ($expectedKeys as $expectedKey) { - if (!in_array($expectedKey, $tagKeys)) { - echo "Missing expected key: $expectedKey\n"; - $hasExpectedKeys = false; + // Verify all values are normalized (lowercase, a-z0-9/.-_ only) + $allNormalized = true; + foreach ($tags as $tag) { + $value = explode(':', $tag, 2)[1]; + if (!preg_match('/^[a-z0-9\/\.\-_]+$/', $value)) { + $allNormalized = false; + echo "Non-normalized value found: $value\n"; } } - echo "Has expected keys: " . ($hasExpectedKeys ? 'YES' : 'NO') . "\n"; - - // Display the tags for verification - echo "Process tags value: $processTags\n"; + echo "Values normalized: " . ($allNormalized ? 'YES' : 'NO') . "\n"; } else { - echo "Process tags found: NO\n"; + echo "Process tags present: NO\n"; } ?> --EXPECTF-- -Process tags found: YES -Number of tags: 4 -Valid format: YES +Process tags present: YES +Process tags: entrypoint.basedir:ext,entrypoint.name:process_tags_enabled.php,entrypoint.type:cli,entrypoint.workdir:%s Keys sorted: YES -Has expected keys: YES -Process tags value: entrypoint.basedir:%s,entrypoint.name:%s,entrypoint.type:%s,entrypoint.workdir:%s +Values normalized: YES diff --git a/tests/ext/process_tags_normalization.phpt b/tests/ext/process_tags_normalization.phpt deleted file mode 100644 index b78cf43424..0000000000 --- a/tests/ext/process_tags_normalization.phpt +++ /dev/null @@ -1,73 +0,0 @@ ---TEST-- -Process tags values are properly normalized ---DESCRIPTION-- -Verifies that process tag values follow the normalization rules: -- Lowercase -- Only a-z, 0-9, /, ., - allowed -- Everything else replaced with _ ---ENV-- -DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED=1 -DD_TRACE_GENERATE_ROOT_SPAN=0 -DD_TRACE_AUTO_FLUSH_ENABLED=0 ---FILE-- -name = 'root_span'; -$span->service = 'test_service'; - -// Close the span -\DDTrace\close_span(); - -// Get the trace -$spans = dd_trace_serialize_closed_spans(); - -// Check if process tags are present and normalized -if (isset($spans[0]['meta']['_dd.tags.process'])) { - $processTags = $spans[0]['meta']['_dd.tags.process']; - - // Parse tags - $tags = explode(',', $processTags); - $allNormalized = true; - - foreach ($tags as $tag) { - list($key, $value) = explode(':', $tag, 2); - - // Check if value is normalized (only lowercase, a-z, 0-9, /, ., -, _) - if (!preg_match('/^[a-z0-9\/\.\-_]+$/', $value)) { - echo "Value not normalized: $value\n"; - $allNormalized = false; - } - - // Check if there are any uppercase letters - if (preg_match('/[A-Z]/', $value)) { - echo "Uppercase found in: $value\n"; - $allNormalized = false; - } - } - - echo "All values normalized: " . ($allNormalized ? 'YES' : 'NO') . "\n"; - - // Verify the entrypoint.type is one of the expected values - foreach ($tags as $tag) { - if (strpos($tag, 'entrypoint.type:') === 0) { - list($key, $type) = explode(':', $tag, 2); - $validTypes = ['script', 'cli', 'executable']; - if (in_array($type, $validTypes)) { - echo "Entrypoint type valid: YES\n"; - } else { - echo "Entrypoint type invalid: $type\n"; - } - } - } -} else { - echo "Process tags not found\n"; -} -?> ---EXPECTF-- -All values normalized: YES -Entrypoint type valid: YES - - From 71888a9edb4e4021ecc02f484e418a13a302638e Mon Sep 17 00:00:00 2001 From: Alexandre Choura Date: Fri, 3 Oct 2025 11:11:59 +0200 Subject: [PATCH 3/4] php 7.2- compatibility --- tests/Integrations/Custom/Autoloaded/ProcessTagsWebTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Integrations/Custom/Autoloaded/ProcessTagsWebTest.php b/tests/Integrations/Custom/Autoloaded/ProcessTagsWebTest.php index 65f1d3a880..59b21be6da 100644 --- a/tests/Integrations/Custom/Autoloaded/ProcessTagsWebTest.php +++ b/tests/Integrations/Custom/Autoloaded/ProcessTagsWebTest.php @@ -69,7 +69,7 @@ public function testProcessTagsEnabledForWebSapi() ); // Verify server.type is normalized (lowercase, only allowed chars) - $this->assertMatchesRegularExpression( + $this->assertRegularExpression( '/^[a-z0-9\/_.-]+$/', $tags['server.type'], 'server.type should be normalized' From fe999f052f0a98735992b0e05ac76619db2a0f8a Mon Sep 17 00:00:00 2001 From: Alexandre Choura Date: Fri, 3 Oct 2025 14:21:26 +0200 Subject: [PATCH 4/4] php 7.0 compatibilty --- tests/Integrations/Custom/Autoloaded/ProcessTagsWebTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Integrations/Custom/Autoloaded/ProcessTagsWebTest.php b/tests/Integrations/Custom/Autoloaded/ProcessTagsWebTest.php index 59b21be6da..97a1d37607 100644 --- a/tests/Integrations/Custom/Autoloaded/ProcessTagsWebTest.php +++ b/tests/Integrations/Custom/Autoloaded/ProcessTagsWebTest.php @@ -43,7 +43,7 @@ public function testProcessTagsEnabledForWebSapi() // Parse the process tags $tags = []; foreach (explode(',', $processTags) as $pair) { - [$key, $value] = explode(':', $pair, 2); + list($key, $value) = explode(':', $pair, 2); $tags[$key] = $value; }