Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config.m4
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
1 change: 1 addition & 0 deletions config.w32
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
1 change: 1 addition & 0 deletions ext/configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions ext/ddtrace.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -1606,6 +1607,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
Expand All @@ -1628,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();
Expand Down
243 changes: 243 additions & 0 deletions ext/process_tags.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
#include "process_tags.h"
#include "configuration.h"
#include "ddtrace.h"
#include <SAPI.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>

#ifdef _WIN32
#include <windows.h>
#include <direct.h>
#define getcwd _getcwd
#define PATH_MAX _MAX_PATH
#else
#include <unistd.h>
#endif

#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_SERVER_TYPE "server.type"
#define TYPE_CLI "cli"
#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 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 = malloc(len + 1);
if (!normalized) {
return NULL;
}

for (size_t i = 0; i < len; i++) {
char c = value[i];
if (c >= 'A' && c <= 'Z') {
normalized[i] = c + ('a' - 'A');
} else if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') ||
c == '/' || c == '.' || c == '-') {
normalized[i] = c;
} else {
normalized[i] = '_';
}
}
normalized[len] = '\0';
return normalized;
}

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;
return *basename ? strdup(basename) : NULL;
}

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

char *basedir;
if (last_slash) {
*last_slash = '\0';
basedir = get_basename(path_copy);
} else {
basedir = strdup(".");
}
free(path_copy);
return basedir;
}

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;
}

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++;
}

static int compare_tags(const void *a, const void *b) {
return strcmp(((const process_tag_entry_t *)a)->key, ((const process_tag_entry_t *)b)->key);
}

// Serialize process tags as comma-separated key:value pairs, sorted by key
static void serialize_process_tags(void) {
if (process_tags.count == 0) {
return;
}

qsort(process_tags.entries, process_tags.count, sizeof(process_tag_entry_t), compare_tags);

size_t total_len = 0;
for (size_t i = 0; i < process_tags.count; i++) {
total_len += strlen(process_tags.entries[i].key) + 1 + strlen(process_tags.entries[i].value);
if (i < process_tags.count - 1) {
total_len++; // comma separator
}
}

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);

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';
}

static void collect_process_tags(void) {
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;

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);
}
} else {
// Web SAPI: collect server type (different requests may execute different scripts)
add_process_tag(TAG_SERVER_TYPE, sapi_module.name);
}

char cwd[PATH_MAX];
if (getcwd(cwd, sizeof(cwd))) {
entrypoint_workdir = get_basename(cwd);
}

if (entrypoint_basedir) {
add_process_tag(TAG_ENTRYPOINT_BASEDIR, entrypoint_basedir);
}
if (entrypoint_name) {
add_process_tag(TAG_ENTRYPOINT_NAME, entrypoint_name);
}
if (is_cli) {
add_process_tag(TAG_ENTRYPOINT_TYPE, TYPE_CLI);
}
if (entrypoint_workdir) {
add_process_tag(TAG_ENTRYPOINT_WORKDIR, entrypoint_workdir);
}

free(entrypoint_name);
free(entrypoint_basedir);
free(entrypoint_workdir);

serialize_process_tags();
}

void ddtrace_process_tags_first_rinit(void) {
if (ddtrace_process_tags_enabled() && !process_tags.serialized) {
collect_process_tags();
}
}

void ddtrace_process_tags_mshutdown(void) {
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) {
return (ddtrace_process_tags_enabled() && process_tags.serialized) ? process_tags.serialized : NULL;
}

22 changes: 22 additions & 0 deletions ext/process_tags.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#ifndef DD_PROCESS_TAGS_H
#define DD_PROCESS_TAGS_H

#include <stdbool.h>
#include <php.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


11 changes: 11 additions & 0 deletions ext/serializer.c
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
#include "ip_extraction.h"
#include <components/log/log.h>
#include "priority_sampling/priority_sampling.h"
#include "process_tags.h"
#include "span.h"
#include "uri_normalization.h"
#include "user_request.h"
Expand Down Expand Up @@ -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);
Expand Down
Loading