From 0e050b6feea0e268320ac4178f89a40050b973f7 Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Thu, 4 Jun 2026 14:05:10 +0000 Subject: [PATCH 01/18] Import process context reference implementation Imported `otel_process_ctx.c` (renamed to cpp) and `otel_process_ctx.h` from with no changes. --- src/datadog/otel_process_ctx.cpp | 876 +++++++++++++++++++++++++++++++ src/datadog/otel_process_ctx.h | 153 ++++++ 2 files changed, 1029 insertions(+) create mode 100644 src/datadog/otel_process_ctx.cpp create mode 100644 src/datadog/otel_process_ctx.h diff --git a/src/datadog/otel_process_ctx.cpp b/src/datadog/otel_process_ctx.cpp new file mode 100644 index 000000000..a68dac9d0 --- /dev/null +++ b/src/datadog/otel_process_ctx.cpp @@ -0,0 +1,876 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache License (Version 2.0). +// This product includes software developed at Datadog (https://www.datadoghq.com/) Copyright 2025 Datadog, Inc. + +#include "otel_process_ctx.h" + +#ifndef _GNU_SOURCE + #define _GNU_SOURCE +#endif + +// Note: Things here are needed for NOOP. Things that are only for non-NOOP get added further below. + +#include + +#define ADD_QUOTES_HELPER(x) #x +#define ADD_QUOTES(x) ADD_QUOTES_HELPER(x) + +static const otel_process_ctx_data empty_data = { + .deployment_environment_name = NULL, + .service_instance_id = NULL, + .service_name = NULL, + .service_version = NULL, + .telemetry_sdk_language = NULL, + .telemetry_sdk_version = NULL, + .telemetry_sdk_name = NULL, + .resource_attributes = NULL, + .extra_attributes = NULL, + .thread_ctx_config = NULL +}; + +#if (defined(OTEL_PROCESS_CTX_NOOP) && OTEL_PROCESS_CTX_NOOP) || !defined(__linux__) + // NOOP implementations when OTEL_PROCESS_CTX_NOOP is defined or not on Linux + + otel_process_ctx_result otel_process_ctx_publish(const otel_process_ctx_data *data) { + (void) data; // Suppress unused parameter warning + return (otel_process_ctx_result) {.success = false, .error_message = "OTEL_PROCESS_CTX_NOOP mode is enabled - no-op implementation (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } + + bool otel_process_ctx_drop_current(void) { + return true; // Nothing to do, this always succeeds + } + + #ifndef OTEL_PROCESS_CTX_NO_READ + otel_process_ctx_read_result otel_process_ctx_read(void) { + return (otel_process_ctx_read_result) {.success = false, .error_message = "OTEL_PROCESS_CTX_NOOP mode is enabled - no-op implementation (" __FILE__ ":" ADD_QUOTES(__LINE__) ")", .data = empty_data}; + } + + bool otel_process_ctx_read_drop(otel_process_ctx_read_result *result) { + (void) result; // Suppress unused parameter warning + return false; + } + #endif // OTEL_PROCESS_CTX_NO_READ +#else // OTEL_PROCESS_CTX_NOOP + +#ifdef __cplusplus + #include + using std::atomic_thread_fence; + using std::memory_order_seq_cst; +#else + #include +#endif +#include +#include +#include +#include +#include +#include +#include + +#define KEY_VALUE_LIMIT 4096 +#define UINT14_MAX 16383 +#define OTEL_CTX_SIGNATURE "OTEL_CTX" + +#ifndef PR_SET_VMA + #define PR_SET_VMA 0x53564d41 + #define PR_SET_VMA_ANON_NAME 0 +#endif + +#ifndef MFD_NOEXEC_SEAL + #define MFD_NOEXEC_SEAL 8U +#endif + +/** + * The process context data that's written into the published anonymous mapping. + * + * An outside-of-process reader will read this struct + otel_process_payload to get the data. + */ +typedef struct __attribute__((packed, aligned(8))) { + char otel_process_ctx_signature[8]; // Always "OTEL_CTX" + uint32_t otel_process_ctx_version; // Always > 0, incremented when the data structure changes, currently v2 + uint32_t otel_process_payload_size; // Always > 0, size of storage + uint64_t otel_process_monotonic_published_at_ns; // Timestamp from when the context was published in nanoseconds from CLOCK_BOOTTIME. 0 during updates. + char *otel_process_payload; // Always non-null, points to the storage for the data; expected to be a protobuf map of string key/value pairs, null-terminated +} otel_process_ctx_mapping; + +/** + * The full state of a published process context. + * + * It is used to store the all data for the process context and that needs to be kept around while the context is published. + */ +typedef struct { + // The pid of the process that published the context. + pid_t publisher_pid; + // The actual mapping of the process context. Note that because we `madvise(..., MADV_DONTFORK)` this mapping is not + // propagated to child processes and thus `mapping` is only valid on the process that published the context. + otel_process_ctx_mapping *mapping; + // The process context payload. + char *payload; +} otel_process_ctx_state; + +/** + * Only one context is active, so we keep its state as a global. + */ +static otel_process_ctx_state published_state; + +static otel_process_ctx_result otel_process_ctx_update(uint64_t monotonic_published_at_ns, const otel_process_ctx_data *data); +static otel_process_ctx_result otel_process_ctx_encode_protobuf_payload(char **out, uint32_t *out_size, otel_process_ctx_data data); + +static uint64_t monotonic_time_now_ns(void) { + struct timespec ts; + if (clock_gettime(CLOCK_BOOTTIME, &ts) == -1) return 0; + return ts.tv_sec * 1000000000ULL + ts.tv_nsec; +} + +static bool ctx_is_published(otel_process_ctx_state state) { + return state.mapping != NULL && state.mapping != MAP_FAILED && getpid() == state.publisher_pid; +} + +// The process context is designed to be read by an outside-of-process reader. Thus, for concurrency purposes the steps +// on this method are ordered in a way to avoid races, or if not possible to avoid, to allow the reader to detect if there was a race. +otel_process_ctx_result otel_process_ctx_publish(const otel_process_ctx_data *data) { + if (!data) return (otel_process_ctx_result) {.success = false, .error_message = "otel_process_ctx_data is NULL (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + + uint64_t monotonic_published_at_ns = monotonic_time_now_ns(); + if (monotonic_published_at_ns == 0) { + return (otel_process_ctx_result) {.success = false, .error_message = "Failed to get current time (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } + + // Step: If the context has been published by this process, update it in place + if (ctx_is_published(published_state)) return otel_process_ctx_update(monotonic_published_at_ns, data); + + // Step: Drop any previous context state if it exists + // No state should be around anywhere after this step. + if (!otel_process_ctx_drop_current()) { + return (otel_process_ctx_result) {.success = false, .error_message = "Failed to drop previous context (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } + + // Step: Prepare the payload to be published + // The payload SHOULD be ready and valid before trying to actually create the mapping. + uint32_t payload_size = 0; + otel_process_ctx_result result = otel_process_ctx_encode_protobuf_payload(&published_state.payload, &payload_size, *data); + if (!result.success) return result; + + // Step: Create the mapping + const ssize_t mapping_size = sizeof(otel_process_ctx_mapping); + published_state.publisher_pid = getpid(); // This allows us to detect in forks that we shouldn't touch the mapping + int fd = memfd_create("OTEL_CTX", MFD_CLOEXEC | MFD_ALLOW_SEALING | MFD_NOEXEC_SEAL); + if (fd < 0) { + // MFD_NOEXEC_SEAL is a newer flag; older kernels reject unknown flags, so let's retry without it + fd = memfd_create("OTEL_CTX", MFD_CLOEXEC | MFD_ALLOW_SEALING); + } + bool failed_to_close_fd = false; + if (fd >= 0) { + // Try to create mapping from memfd + if (ftruncate(fd, mapping_size) == -1) { + close(fd); // Swallow errors here, truncation already failed anyway + otel_process_ctx_drop_current(); + return (otel_process_ctx_result) {.success = false, .error_message = "Failed to truncate memfd (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } + published_state.mapping = (otel_process_ctx_mapping *) mmap(NULL, mapping_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0); + failed_to_close_fd = (close(fd) == -1); + } else { + // Fallback: Use an anonymous mapping instead + published_state.mapping = (otel_process_ctx_mapping *) mmap(NULL, mapping_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + } + if (published_state.mapping == MAP_FAILED || failed_to_close_fd) { + otel_process_ctx_drop_current(); + + if (failed_to_close_fd) { + return (otel_process_ctx_result) {.success = false, .error_message = "Failed to close memfd (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } else { + return (otel_process_ctx_result) {.success = false, .error_message = "Failed to allocate mapping (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } + } + + // Step: Setup MADV_DONTFORK + // This ensures that the mapping is not propagated to child processes (they should call update/publish again). + if (madvise(published_state.mapping, mapping_size, MADV_DONTFORK) == -1) { + if (otel_process_ctx_drop_current()) { + return (otel_process_ctx_result) {.success = false, .error_message = "Failed to setup MADV_DONTFORK (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } else { + return (otel_process_ctx_result) {.success = false, .error_message = "Failed to drop context (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } + } + + // Step: Populate the mapping + // The payload and any extra fields must come first and not be reordered with the monotonic_published_at_ns by the compiler. + *published_state.mapping = (otel_process_ctx_mapping) { + .otel_process_ctx_signature = { 'O', 'T', 'E', 'L', '_', 'C', 'T', 'X' }, + .otel_process_ctx_version = 2, + .otel_process_payload_size = payload_size, + .otel_process_monotonic_published_at_ns = 0, // Set in "Step: Populate the monotonic_published_at_ns into the mapping" below + .otel_process_payload = published_state.payload + }; + + // Step: Synchronization - Mapping has been filled and is missing monotonic_published_at_ns + // Make sure the initialization of the mapping + payload above does not get reordered with setting the monotonic_published_at_ns below. Setting + // the monotonic_published_at_ns is what tells an outside reader that the context is fully published. + atomic_thread_fence(memory_order_seq_cst); + + // Step: Populate the monotonic_published_at_ns into the mapping + // The monotonic_published_at_ns must come last and not be reordered with the fields above by the compiler. After this step, external readers + // can read the monotonic_published_at_ns and know that the payload is ready to be read. + published_state.mapping->otel_process_monotonic_published_at_ns = monotonic_published_at_ns; + + // Step: Attempt to name the mapping so outside readers can: + // * Find it by name + // * Hook on prctl to detect when new mappings are published + if (prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, published_state.mapping, mapping_size, OTEL_CTX_SIGNATURE) == -1) { + // Naming an anonymous mapping is an optional Linux 5.17+ feature (`CONFIG_ANON_VMA_NAME`). + // Many distros, such as Ubuntu and Arch enable it. On earlier kernel versions or kernels without the feature, this call can fail. + // + // It's OK for this to fail because (per-usecase): + // 1. "Find it by name" => As a fallback, it's possible to scan the mappings and for the memfd name. + // 2. "Hook on prctl" => When hooking on prctl via eBPF it's still possible to see this call, even when it's not supported/enabled. + // This works even on older kernels! For this reason we unconditionally make this call even on older kernels -- to + // still allow detection via hooking onto prctl. + } + + // All done! + + return (otel_process_ctx_result) {.success = true, .error_message = NULL}; +} + +bool otel_process_ctx_drop_current(void) { + otel_process_ctx_state state = published_state; + + // Zero out the state and make sure no operations below are reordered with zeroing + published_state = (otel_process_ctx_state) {.publisher_pid = 0, .mapping = NULL, .payload = NULL}; + atomic_thread_fence(memory_order_seq_cst); + + bool success = true; + + // The mapping only exists if it was created by the current process; if it was inherited by a fork it doesn't exist anymore + // (due to the MADV_DONTFORK) and we don't need to do anything to it. + if (ctx_is_published(state)) { + success = munmap(state.mapping, sizeof(otel_process_ctx_mapping)) == 0; + } + + // The payload may have been inherited from a parent. This is a regular malloc so we need to free it so we don't leak. + free(state.payload); + + return success; +} + +static otel_process_ctx_result otel_process_ctx_update(uint64_t monotonic_published_at_ns, const otel_process_ctx_data *data) { + if (data == NULL || !ctx_is_published(published_state)) { + return (otel_process_ctx_result) {.success = false, .error_message = "Unexpected: otel_process_ctx_data is NULL or context is not published (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } + + if (monotonic_published_at_ns == published_state.mapping->otel_process_monotonic_published_at_ns) { + // Advance published_at_ns to allow readers to detect the update + monotonic_published_at_ns++; + } + + // Step: Prepare the new payload to be published + // The payload SHOULD be ready and valid before trying to actually update the mapping. + uint32_t payload_size = 0; + char *payload; + otel_process_ctx_result result = otel_process_ctx_encode_protobuf_payload(&payload, &payload_size, *data); + if (!result.success) return result; + + // Step: Zero out monotonic_published_at_ns in the mapping + // This enables readers to detect that an update is in-progress + published_state.mapping->otel_process_monotonic_published_at_ns = 0; + + // Step: Synchronization - Make sure readers observe the zeroing above before anything else below + atomic_thread_fence(memory_order_seq_cst); + + // Step: Install updated data + published_state.mapping->otel_process_payload_size = payload_size; + published_state.mapping->otel_process_payload = payload; + + // Step: Synchronization - Make sure readers observe the updated data before anything else below + atomic_thread_fence(memory_order_seq_cst); + + // Step: Install new monotonic_published_at_ns + // The update is now complete -- readers that observe the new timestamp will observe the updated payload + published_state.mapping->otel_process_monotonic_published_at_ns = monotonic_published_at_ns; + + // Step: Attempt to name the mapping so outside readers can detect the update + if (prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, published_state.mapping, sizeof(otel_process_ctx_mapping), OTEL_CTX_SIGNATURE) == -1) { + // It's OK for this to fail -- see otel_process_ctx_publish for why + } + + // Step: Update bookkeeping + free(published_state.payload); // This was still pointing to the old payload + published_state.payload = payload; + + // All done! + + return (otel_process_ctx_result) {.success = true, .error_message = NULL}; +} + +// The caller is responsible for enforcing that value fits within UINT14_MAX +static size_t protobuf_varint_size(uint16_t value) { return value >= 128 ? 2 : 1; } + +// Field tag for record + varint len + data +static size_t protobuf_record_size(size_t len) { return 1 + protobuf_varint_size(len) + len; } + +static size_t protobuf_string_size(const char *str) { return protobuf_record_size(strlen(str)); } + +static size_t protobuf_otel_keyvalue_string_size(const char *key, const char *value) { + size_t key_field_size = protobuf_string_size(key); // String + size_t value_field_size = protobuf_record_size(protobuf_string_size(value)); // Nested AnyValue message with a string inside + return key_field_size + value_field_size; // Does not include the keyvalue record tag + size, only its payload +} + +static size_t protobuf_otel_array_value_content_size(const char **strings) { + size_t total = 0; + for (size_t i = 0; strings[i] != NULL; i++) { + total += protobuf_record_size(protobuf_string_size(strings[i])); // ArrayValue.values[i]: AnyValue{string_value} + } + return total; +} + +// As a simplification, we enforce that keys and values are <= 4096 (KEY_VALUE_LIMIT) so that their size + extra bytes always fits within UINT14_MAX +static otel_process_ctx_result validate_and_calculate_protobuf_payload_size(size_t *out_pairs_size, const char **pairs) { + size_t num_entries = 0; + for (size_t i = 0; pairs[i] != NULL; i++) num_entries++; + if (num_entries % 2 != 0) { + return (otel_process_ctx_result) {.success = false, .error_message = "Value in otel_process_ctx_data is NULL (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } + + *out_pairs_size = 0; + for (size_t i = 0; pairs[i * 2] != NULL; i++) { + const char *key = pairs[i * 2]; + const char *value = pairs[i * 2 + 1]; + + if (strlen(key) > KEY_VALUE_LIMIT) { + return (otel_process_ctx_result) {.success = false, .error_message = "Length of key in otel_process_ctx_data exceeds 4096 limit (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } + if (strlen(value) > KEY_VALUE_LIMIT) { + return (otel_process_ctx_result) {.success = false, .error_message = "Length of value in otel_process_ctx_data exceeds 4096 limit (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } + + *out_pairs_size += protobuf_record_size(protobuf_otel_keyvalue_string_size(key, value)); // KeyValue message + } + return (otel_process_ctx_result) {.success = true, .error_message = NULL}; +} + +/** + * Writes a protobuf varint encoding for the given value. + * As a simplification, only supports values that fit in 1 or 2 bytes (0-16383 UINT14_MAX). + */ +static void write_protobuf_varint(char **ptr, uint16_t value) { + if (protobuf_varint_size(value) == 1) { + *(*ptr)++ = (char)value; + } else { + // Two bytes: first byte has MSB set, second byte has value + *(*ptr)++ = (char)((value & 0x7F) | 0x80); // Low 7 bits + continuation bit + *(*ptr)++ = (char)(value >> 7); // High 7 bits + } +} + +static void write_protobuf_string(char **ptr, const char *str) { + size_t len = strlen(str); + write_protobuf_varint(ptr, len); + memcpy(*ptr, str, len); + *ptr += len; +} + +static void write_protobuf_tag(char **ptr, uint8_t field_number) { + *(*ptr)++ = (char)((field_number << 3) | 2); // Field type is always 2 (LEN) +} + +static void write_attribute(char **ptr, uint8_t field_number, const char *key, const char *value) { + write_protobuf_tag(ptr, field_number); + write_protobuf_varint(ptr, protobuf_otel_keyvalue_string_size(key, value)); + + // KeyValue + write_protobuf_tag(ptr, 1); // KeyValue.key (field 1) + write_protobuf_string(ptr, key); + write_protobuf_tag(ptr, 2); // KeyValue.value (field 2) + write_protobuf_varint(ptr, protobuf_string_size(value)); + + // AnyValue + write_protobuf_tag(ptr, 1); // AnyValue.string_value (field 1) + write_protobuf_string(ptr, value); +} + +static void write_array_attribute(char **ptr, uint8_t field_number, const char *key, const char **strings) { + size_t array_value_content_size = protobuf_otel_array_value_content_size(strings); + size_t any_value_content_size = protobuf_record_size(array_value_content_size); + size_t kv_content_size = protobuf_string_size(key) + protobuf_record_size(any_value_content_size); + + write_protobuf_tag(ptr, field_number); + write_protobuf_varint(ptr, kv_content_size); + + write_protobuf_tag(ptr, 1); // KeyValue.key (field 1) + write_protobuf_string(ptr, key); + + write_protobuf_tag(ptr, 2); // KeyValue.value (field 2) = AnyValue message + write_protobuf_varint(ptr, any_value_content_size); + + write_protobuf_tag(ptr, 5); // AnyValue.array_value (field 5) = ArrayValue message + write_protobuf_varint(ptr, array_value_content_size); + + for (size_t i = 0; strings[i] != NULL; i++) { // ArrayValue.values (field 1) - repeated AnyValue entries + write_protobuf_tag(ptr, 1); // ArrayValue.values[i] + write_protobuf_varint(ptr, protobuf_string_size(strings[i])); // Inner AnyValue size + write_protobuf_tag(ptr, 1); // AnyValue.string_value (field 1) + write_protobuf_string(ptr, strings[i]); + } +} + +// Encode the payload as protobuf bytes. +// +// This method implements an extremely compact but limited protobuf encoder for the ProcessContext message. +// It encodes all fields as Resource attributes (KeyValue pairs). +// For extra compact code, it fixes strings at up to 4096 bytes. +static otel_process_ctx_result otel_process_ctx_encode_protobuf_payload(char **out, uint32_t *out_size, otel_process_ctx_data data) { + const char *pairs[] = { + "deployment.environment.name", data.deployment_environment_name, + "service.instance.id", data.service_instance_id, + "service.name", data.service_name, + "service.version", data.service_version, + "telemetry.sdk.language", data.telemetry_sdk_language, + "telemetry.sdk.version", data.telemetry_sdk_version, + "telemetry.sdk.name", data.telemetry_sdk_name, + NULL + }; + + size_t pairs_size = 0; + otel_process_ctx_result validation_result = validate_and_calculate_protobuf_payload_size(&pairs_size, (const char **) pairs); + if (!validation_result.success) return validation_result; + + size_t resource_attributes_pairs_size = 0; + if (data.resource_attributes != NULL) { + validation_result = validate_and_calculate_protobuf_payload_size(&resource_attributes_pairs_size, data.resource_attributes); + if (!validation_result.success) return validation_result; + } + + size_t extra_attributes_pairs_size = 0; + if (data.extra_attributes != NULL) { + validation_result = validate_and_calculate_protobuf_payload_size(&extra_attributes_pairs_size, data.extra_attributes); + if (!validation_result.success) return validation_result; + } + + size_t thread_ctx_pairs_size = 0; + if (data.thread_ctx_config != NULL) { + if (data.thread_ctx_config->schema_version != NULL) { + const char *thread_ctx_pairs[] = {"threadlocal.schema_version", data.thread_ctx_config->schema_version, NULL}; + validation_result = validate_and_calculate_protobuf_payload_size(&thread_ctx_pairs_size, thread_ctx_pairs); + if (!validation_result.success) return validation_result; + } + if (data.thread_ctx_config->attribute_key_map != NULL) { + if (data.thread_ctx_config->schema_version == NULL) { + return (otel_process_ctx_result) {.success = false, .error_message = "attribute_key_map requires schema_version to be set (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } + for (size_t i = 0; data.thread_ctx_config->attribute_key_map[i] != NULL; i++) { + if (strlen(data.thread_ctx_config->attribute_key_map[i]) > KEY_VALUE_LIMIT) { + return (otel_process_ctx_result) {.success = false, .error_message = "Length of attribute_key_map entry exceeds 4096 limit (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } + } + size_t array_value_content_size = protobuf_otel_array_value_content_size(data.thread_ctx_config->attribute_key_map); + size_t any_value_content_size = protobuf_record_size(array_value_content_size); + size_t kv_content_size = protobuf_string_size("threadlocal.attribute_key_map") + protobuf_record_size(any_value_content_size); + if (kv_content_size > UINT14_MAX) { + return (otel_process_ctx_result) {.success = false, .error_message = "Encoded size of attribute_key_map exceeds UINT14_MAX limit (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } + thread_ctx_pairs_size += protobuf_record_size(kv_content_size); + } + } + + size_t resource_size = pairs_size + resource_attributes_pairs_size; + if (resource_size > UINT14_MAX) { + return (otel_process_ctx_result) {.success = false, .error_message = "Encoded size of resource attributes exceeds UINT14_MAX limit (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } + size_t total_size = protobuf_record_size(resource_size) + extra_attributes_pairs_size + thread_ctx_pairs_size; + + char *encoded = (char *) calloc(total_size, 1); + if (!encoded) { + return (otel_process_ctx_result) {.success = false, .error_message = "Failed to allocate memory for payload (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } + char *ptr = encoded; + + // ProcessContext.resource (field 1) + write_protobuf_tag(&ptr, 1); + write_protobuf_varint(&ptr, resource_size); + + for (size_t i = 0; pairs[i * 2] != NULL; i++) { + write_attribute(&ptr, 1, pairs[i * 2], pairs[i * 2 + 1]); + } + + for (size_t i = 0; data.resource_attributes != NULL && data.resource_attributes[i * 2] != NULL; i++) { + write_attribute(&ptr, 1, data.resource_attributes[i * 2], data.resource_attributes[i * 2 + 1]); + } + + // ProcessContext.extra_attributes (field 2) + for (size_t i = 0; data.extra_attributes != NULL && data.extra_attributes[i * 2] != NULL; i++) { + write_attribute(&ptr, 2, data.extra_attributes[i * 2], data.extra_attributes[i * 2 + 1]); + } + + if (data.thread_ctx_config != NULL) { + if (data.thread_ctx_config->schema_version != NULL) { + write_attribute(&ptr, 2, "threadlocal.schema_version", data.thread_ctx_config->schema_version); + } + if (data.thread_ctx_config->attribute_key_map != NULL) { + write_array_attribute(&ptr, 2, "threadlocal.attribute_key_map", data.thread_ctx_config->attribute_key_map); + } + } + + *out = encoded; + *out_size = (uint32_t) total_size; + + return (otel_process_ctx_result) {.success = true, .error_message = NULL}; +} + +#ifndef OTEL_PROCESS_CTX_NO_READ + #include + #include + #include + #include + + // Note: The below parsing code is only for otel_process_ctx_read and is only provided for debugging + // and testing purposes. + + static void *parse_mapping_start(char *line) { + char *endptr = NULL; + unsigned long long start = strtoull(line, &endptr, 16); + if (start == 0 || start == ULLONG_MAX) return NULL; + return (void *)(uintptr_t) start; + } + + static otel_process_ctx_mapping *try_finding_mapping(void) { + char line[8192]; + otel_process_ctx_mapping *result = NULL; + + FILE *fp = fopen("/proc/self/maps", "r"); + if (!fp) return result; + + while (fgets(line, sizeof(line), fp)) { + bool is_process_ctx = strstr(line, "[anon_shmem:OTEL_CTX]") != NULL || strstr(line, "[anon:OTEL_CTX]") != NULL || strstr(line, "/memfd:OTEL_CTX") != NULL; + if (is_process_ctx) { + result = (otel_process_ctx_mapping *)parse_mapping_start(line); + break; + } + } + + fclose(fp); + return result; + } + + // Helper function to read a protobuf varint (limited to 1-2 bytes, max value UINT14_MAX, matching write_protobuf_varint above) + static bool read_protobuf_varint(char **ptr, char *end_ptr, uint16_t *value) { + if (*ptr >= end_ptr) return false; + + unsigned char first_byte = (unsigned char)**ptr; + (*ptr)++; + + if (first_byte < 128) { + *value = first_byte; + return true; + } else { + if (*ptr >= end_ptr) return false; + unsigned char second_byte = (unsigned char)**ptr; + (*ptr)++; + + *value = (first_byte & 0x7F) | (second_byte << 7); + return *value <= UINT14_MAX; + } + } + + // Helper function to read a protobuf string into a buffer, within the same limits as the encoder imposes + static bool read_protobuf_string(char **ptr, char *end_ptr, char *buffer) { + uint16_t len; + if (!read_protobuf_varint(ptr, end_ptr, &len) || len >= KEY_VALUE_LIMIT + 1 || *ptr + len > end_ptr) return false; + + memcpy(buffer, *ptr, len); + buffer[len] = '\0'; + *ptr += len; + + return true; + } + + // Reads field name and validates the fixed LEN wire type + static bool read_protobuf_tag(char **ptr, char *end_ptr, uint8_t *field_number) { + if (*ptr >= end_ptr) return false; + + unsigned char tag = (unsigned char)**ptr; + (*ptr)++; + + uint8_t wire_type = tag & 0x07; + *field_number = tag >> 3; + + return wire_type == 2; // We only need the LEN wire type for now + } + + // Peeks at the key of an OTel KeyValue message without advancing the pointer. + static bool peek_protobuf_key(char *ptr, char *end_ptr, char *key_buffer) { + char *p = ptr; + uint8_t kv_field; + if (!read_protobuf_tag(&p, end_ptr, &kv_field)) return false; + if (kv_field != 1) return false; // KeyValue.key is field 1 + return read_protobuf_string(&p, end_ptr, key_buffer); + } + + // Reads an OTel KeyValue message (key string + AnyValue-wrapped string) into the provided buffers. + static bool read_protobuf_keyvalue(char **ptr, char *end_ptr, char *key_buffer, char *value_buffer) { + bool key_found = false; + bool value_found = false; + + while (*ptr < end_ptr) { + uint8_t kv_field; + if (!read_protobuf_tag(ptr, end_ptr, &kv_field)) return false; + + if (kv_field == 1) { // KeyValue.key + if (!read_protobuf_string(ptr, end_ptr, key_buffer)) return false; + key_found = true; + } else if (kv_field == 2) { // KeyValue.value (AnyValue) + uint16_t _any_len; // Unused, but we still need to consume + validate the varint + if (!read_protobuf_varint(ptr, end_ptr, &_any_len)) return false; + uint8_t any_field; + if (!read_protobuf_tag(ptr, end_ptr, &any_field)) return false; + + if (any_field == 1) { // AnyValue.string_value + if (!read_protobuf_string(ptr, end_ptr, value_buffer)) return false; + value_found = true; + } + } + } + + return key_found && value_found; + } + + // Reads an AnyValue.array_value (field 5) from ptr; ptr must be at KeyValue.value (tag 2). + // Allocates a NULL-terminated array of strings and sets *out_array immediately. On error the caller must free it. + static bool read_protobuf_array_value_strings(char **ptr, char *end_ptr, char *value_buffer, const char ***out_array) { + uint8_t field; + if (!read_protobuf_tag(ptr, end_ptr, &field) || field != 2) return false; + uint16_t any_len; + if (!read_protobuf_varint(ptr, end_ptr, &any_len)) return false; + char *any_end = *ptr + any_len; + if (any_end > end_ptr) return false; + + if (!read_protobuf_tag(ptr, any_end, &field) || field != 5) return false; + uint16_t array_len; + if (!read_protobuf_varint(ptr, any_end, &array_len)) return false; + char *array_end = *ptr + array_len; + if (array_end > any_end) return false; + + size_t max = 100; + size_t capacity = max + 1; + const char **arr = (const char **) calloc(capacity, sizeof(char *)); + if (!arr) return false; + *out_array = arr; + size_t count = 0; + + while (*ptr < array_end) { + if (count >= max) return false; + if (!read_protobuf_tag(ptr, array_end, &field) || field != 1) return false; + uint16_t item_len; + if (!read_protobuf_varint(ptr, array_end, &item_len)) return false; + char *item_end = *ptr + item_len; + if (item_end > array_end) return false; + if (!read_protobuf_tag(ptr, item_end, &field) || field != 1) return false; + if (!read_protobuf_string(ptr, item_end, value_buffer)) return false; + char *dup = strdup(value_buffer); + if (!dup) return false; + arr[count++] = dup; + } + + return true; + } + + static bool ensure_thread_ctx_config(otel_process_ctx_data *data_out) { + if (data_out->thread_ctx_config) return true; + otel_thread_ctx_config_data *setup = (otel_thread_ctx_config_data *) calloc(1, sizeof(otel_thread_ctx_config_data)); + if (!setup) return false; + data_out->thread_ctx_config = setup; + return true; + } + + // Simplified protobuf decoder to match the exact encoder above. If the protobuf data doesn't match the encoder, this will + // return false. + static bool otel_process_ctx_decode_payload(char *payload, uint32_t payload_size, otel_process_ctx_data *data_out, char *key_buffer, char *value_buffer) { + char *ptr = payload; + char *end_ptr = payload + payload_size; + + *data_out = empty_data; + + // Parse ProcessContext wrapper - expect field 1 (resource) + uint8_t process_ctx_field; + if (!read_protobuf_tag(&ptr, end_ptr, &process_ctx_field) || process_ctx_field != 1) return false; + + uint16_t resource_len; + if (!read_protobuf_varint(&ptr, end_ptr, &resource_len)) return false; + char *resource_end = ptr + resource_len; + if (resource_end > end_ptr) return false; + + size_t resource_index = 0; + size_t resource_capacity = 201; // Allocate space for 100 pairs + NULL terminator entry + data_out->resource_attributes = (const char **) calloc(resource_capacity, sizeof(char *)); + if (data_out->resource_attributes == NULL) return false; + + size_t extra_attributes_index = 0; + size_t extra_attributes_capacity = 201; // Allocate space for 100 pairs + NULL terminator entry + data_out->extra_attributes = (const char **) calloc(extra_attributes_capacity, sizeof(char *)); + if (data_out->extra_attributes == NULL) return false; + + // Parse resource attributes (field 1) + while (ptr < resource_end) { + uint8_t field_number; + if (!read_protobuf_tag(&ptr, resource_end, &field_number) || field_number != 1) return false; + + uint16_t kv_len; + if (!read_protobuf_varint(&ptr, resource_end, &kv_len)) return false; + char *kv_end = ptr + kv_len; + if (kv_end > resource_end) return false; + + if (!read_protobuf_keyvalue(&ptr, kv_end, key_buffer, value_buffer)) return false; + + char *value = strdup(value_buffer); + if (!value) return false; + + // Dispatch based on key + const char **field = NULL; + if (strcmp(key_buffer, "deployment.environment.name") == 0) { field = &data_out->deployment_environment_name; } + else if (strcmp(key_buffer, "service.instance.id") == 0) { field = &data_out->service_instance_id; } + else if (strcmp(key_buffer, "service.name") == 0) { field = &data_out->service_name; } + else if (strcmp(key_buffer, "service.version") == 0) { field = &data_out->service_version; } + else if (strcmp(key_buffer, "telemetry.sdk.language") == 0) { field = &data_out->telemetry_sdk_language; } + else if (strcmp(key_buffer, "telemetry.sdk.version") == 0) { field = &data_out->telemetry_sdk_version; } + else if (strcmp(key_buffer, "telemetry.sdk.name") == 0) { field = &data_out->telemetry_sdk_name; } + + if (field != NULL) { + if (*field != NULL) { free(value); return false; } + *field = value; + } else { + char *key = strdup(key_buffer); + + if (!key || resource_index + 2 >= resource_capacity) { + free(key); + free(value); + return false; + } + data_out->resource_attributes[resource_index] = key; + data_out->resource_attributes[resource_index + 1] = value; + resource_index += 2; + } + } + + // Parse extra attributes (field 2) + while (ptr < end_ptr) { + uint8_t extra_ctx_field; + if (!read_protobuf_tag(&ptr, end_ptr, &extra_ctx_field) || extra_ctx_field != 2) return false; + + uint16_t kv_len; + if (!read_protobuf_varint(&ptr, end_ptr, &kv_len)) return false; + char *kv_end = ptr + kv_len; + if (kv_end > end_ptr) return false; + + if (!peek_protobuf_key(ptr, kv_end, key_buffer)) return false; + + if (strcmp(key_buffer, "threadlocal.attribute_key_map") == 0) { + // Consume key to advance ptr + uint8_t kv_field; + if (!read_protobuf_tag(&ptr, kv_end, &kv_field) || kv_field != 1) return false; + if (!read_protobuf_string(&ptr, kv_end, key_buffer)) return false; + if (!ensure_thread_ctx_config(data_out)) return false; + if (data_out->thread_ctx_config->attribute_key_map != NULL) return false; + if (!read_protobuf_array_value_strings(&ptr, kv_end, value_buffer, &((otel_thread_ctx_config_data *)data_out->thread_ctx_config)->attribute_key_map)) return false; + } else { + if (!read_protobuf_keyvalue(&ptr, kv_end, key_buffer, value_buffer)) return false; + + char *value = strdup(value_buffer); + if (!value) return false; + + // Dispatch based on key + if (strcmp(key_buffer, "threadlocal.schema_version") == 0) { + if (!ensure_thread_ctx_config(data_out)) { free(value); return false; } + if (data_out->thread_ctx_config->schema_version != NULL) { free(value); return false; } + ((otel_thread_ctx_config_data *)data_out->thread_ctx_config)->schema_version = value; + } else { + char *key = strdup(key_buffer); + if (!key || extra_attributes_index + 2 >= extra_attributes_capacity) { + free(key); + free(value); + return false; + } + data_out->extra_attributes[extra_attributes_index] = key; + data_out->extra_attributes[extra_attributes_index + 1] = value; + extra_attributes_index += 2; + } + } + } + + // Validate all required fields were found + return data_out->deployment_environment_name != NULL && + data_out->service_instance_id != NULL && + data_out->service_name != NULL && + data_out->service_version != NULL && + data_out->telemetry_sdk_language != NULL && + data_out->telemetry_sdk_version != NULL && + data_out->telemetry_sdk_name != NULL; + } + + void otel_process_ctx_read_data_drop(otel_process_ctx_data data) { + if (data.deployment_environment_name) free((void *)data.deployment_environment_name); + if (data.service_instance_id) free((void *)data.service_instance_id); + if (data.service_name) free((void *)data.service_name); + if (data.service_version) free((void *)data.service_version); + if (data.telemetry_sdk_language) free((void *)data.telemetry_sdk_language); + if (data.telemetry_sdk_version) free((void *)data.telemetry_sdk_version); + if (data.telemetry_sdk_name) free((void *)data.telemetry_sdk_name); + if (data.resource_attributes) { + for (int i = 0; data.resource_attributes[i] != NULL; i++) free((void *)data.resource_attributes[i]); + free((void *)data.resource_attributes); + } + if (data.extra_attributes) { + for (int i = 0; data.extra_attributes[i] != NULL; i++) free((void *)data.extra_attributes[i]); + free((void *)data.extra_attributes); + } + if (data.thread_ctx_config) { + if (data.thread_ctx_config->schema_version) free((void *)data.thread_ctx_config->schema_version); + if (data.thread_ctx_config->attribute_key_map) { + for (int i = 0; data.thread_ctx_config->attribute_key_map[i] != NULL; i++) { + free((void *)data.thread_ctx_config->attribute_key_map[i]); + } + free((void *)data.thread_ctx_config->attribute_key_map); + } + free((void *)data.thread_ctx_config); + } + } + + otel_process_ctx_read_result otel_process_ctx_read(void) { + otel_process_ctx_mapping *mapping = try_finding_mapping(); + if (!mapping) { + return (otel_process_ctx_read_result) {.success = false, .error_message = "No OTEL_CTX mapping found (" __FILE__ ":" ADD_QUOTES(__LINE__) ")", .data = empty_data}; + } + + if (strncmp(mapping->otel_process_ctx_signature, OTEL_CTX_SIGNATURE, sizeof(mapping->otel_process_ctx_signature)) != 0 || mapping->otel_process_ctx_version != 2) { + return (otel_process_ctx_read_result) {.success = false, .error_message = "Invalid OTEL_CTX signature or version (" __FILE__ ":" ADD_QUOTES(__LINE__) ")", .data = empty_data}; + } + + otel_process_ctx_data data = empty_data; + + char *key_buffer = (char *) calloc(KEY_VALUE_LIMIT + 1, 1); + char *value_buffer = (char *) calloc(KEY_VALUE_LIMIT + 1, 1); + if (!key_buffer || !value_buffer) { + free(key_buffer); + free(value_buffer); + return (otel_process_ctx_read_result) {.success = false, .error_message = "Failed to allocate decode buffers (" __FILE__ ":" ADD_QUOTES(__LINE__) ")", .data = empty_data}; + } + + bool success = otel_process_ctx_decode_payload(mapping->otel_process_payload, mapping->otel_process_payload_size, &data, key_buffer, value_buffer); + free(key_buffer); + free(value_buffer); + + if (!success) { + otel_process_ctx_read_data_drop(data); + return (otel_process_ctx_read_result) {.success = false, .error_message = "Failed to decode payload (" __FILE__ ":" ADD_QUOTES(__LINE__) ")", .data = empty_data}; + } + + return (otel_process_ctx_read_result) {.success = true, .error_message = NULL, .data = data}; + } + + bool otel_process_ctx_read_drop(otel_process_ctx_read_result *result) { + if (!result || !result->success) return false; + otel_process_ctx_read_data_drop(result->data); + *result = (otel_process_ctx_read_result) {.success = false, .error_message = "Data dropped", .data = empty_data}; + return true; + } +#endif // OTEL_PROCESS_CTX_NO_READ + +#endif // OTEL_PROCESS_CTX_NOOP diff --git a/src/datadog/otel_process_ctx.h b/src/datadog/otel_process_ctx.h new file mode 100644 index 000000000..909cd01ef --- /dev/null +++ b/src/datadog/otel_process_ctx.h @@ -0,0 +1,153 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache License (Version 2.0). +// This product includes software developed at Datadog (https://www.datadoghq.com/) Copyright 2025 Datadog, Inc. + +#pragma once + +#define OTEL_PROCESS_CTX_VERSION_MAJOR 1 +#define OTEL_PROCESS_CTX_VERSION_MINOR 0 +#define OTEL_PROCESS_CTX_VERSION_PATCH 0 +#define OTEL_PROCESS_CTX_VERSION_STRING "1.0.0" + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** + * # OpenTelemetry Process Context reference implementation + * + * `otel_process_ctx.h` and `otel_process_ctx.c` provide a reference implementation for the OpenTelemetry + * process-level context sharing specification. + * (https://github.com/open-telemetry/opentelemetry-specification/blob/main/oteps/profiles/4719-process-ctx.md) + * + * This reference implementation is Linux-only, as the specification currently only covers Linux. + * On non-Linux OS's (or when OTEL_PROCESS_CTX_NOOP is defined) no-op versions of functions are supplied. + */ + + /** + * Config for the experimental thread context sharing mechanism, see + * https://github.com/open-telemetry/opentelemetry-specification/pull/4947 for usage + * details. + */ +typedef struct { + const char *schema_version; + // NULL-terminated array of attribute key strings to be used in thread context. + // Can be NULL if not needed. + const char **attribute_key_map; +} otel_thread_ctx_config_data; + +/** + * Data that can be published as a process context. + * + * Every string MUST be valid for the duration of the call to `otel_process_ctx_publish`. + * Strings will be copied into the context. + * + * Strings MUST be: + * * Non-NULL + * * UTF-8 encoded + * * Not longer than INT16_MAX bytes + * + * Strings MAY be: + * * Empty + */ +typedef struct { + // https://opentelemetry.io/docs/specs/semconv/registry/attributes/deployment/#deployment-environment-name + const char *deployment_environment_name; + // https://opentelemetry.io/docs/specs/semconv/registry/attributes/service/#service-instance-id + const char *service_instance_id; + // https://opentelemetry.io/docs/specs/semconv/registry/attributes/service/#service-name + const char *service_name; + // https://opentelemetry.io/docs/specs/semconv/registry/attributes/service/#service-version + const char *service_version; + // https://opentelemetry.io/docs/specs/semconv/registry/attributes/telemetry/#telemetry-sdk-language + const char *telemetry_sdk_language; + // https://opentelemetry.io/docs/specs/semconv/registry/attributes/telemetry/#telemetry-sdk-version + const char *telemetry_sdk_version; + // https://opentelemetry.io/docs/specs/semconv/registry/attributes/telemetry/#telemetry-sdk-name + const char *telemetry_sdk_name; + // Additional key/value pairs as resource attributes https://opentelemetry.io/docs/specs/otel/resource/sdk/ + // Can be NULL if no resource attributes are needed; if non-NULL, this array MUST be terminated with a NULL entry. + // Every even entry is a key, every odd entry is a value (E.g. "key1", "value1", "key2", "value2", NULL). + const char **resource_attributes; + // Additional key/value pairs as extra attributes (ProcessContext.extra_attributes in process_context.proto) + // Can be NULL if no extra attributes are needed; if non-NULL, this array MUST be terminated with a NULL entry. + // Every even entry is a key, every odd entry is a value (E.g. "key1", "value1", "key2", "value2", NULL). + const char **extra_attributes; + // Experimental thread context sharing mechanism configuration. See struct definition for details. Can be NULL. + const otel_thread_ctx_config_data *thread_ctx_config; +} otel_process_ctx_data; + +/** Number of entries in the `otel_process_ctx_data` struct. Can be used to easily detect when the struct is updated. */ +#define OTEL_PROCESS_CTX_DATA_ENTRIES sizeof(otel_process_ctx_data) / sizeof(char *) + +typedef struct { + bool success; + const char *error_message; // Static strings only, non-NULL if success is false +} otel_process_ctx_result; + +/** + * Publishes a OpenTelemetry process context with the given data. + * + * The context should remain alive until the application exits (or is just about to exit). + * This method is NOT thread-safe. + * + * Calling `publish` multiple times is supported and will replace a previous context (only one is published at any given + * time). Calling `publish` multiple times usually happens when: + * * Some of the `otel_process_ctx_data` changes due to a live system reconfiguration for the same process + * * The process is forked (to provide a new `service_instance_id`) + * + * This API can be called in a fork of the process that published the previous context, even though + * the context is not carried over into forked processes (although part of its memory allocations are). + * + * @param data Pointer to the data to publish. This data is copied into the context and only needs to be valid for the duration of + * the call. Must not be `NULL`. + * @return The result of the operation. + */ +otel_process_ctx_result otel_process_ctx_publish(const otel_process_ctx_data *data); + +/** + * Drops the current OpenTelemetry process context, if any. + * + * This method is safe to call even there's no current context. + * This method is NOT thread-safe. + * + * This API can be called in a fork of the process that published the current context to clean memory allocations + * related to the parent's context (even though the context itself is not carried over into forked processes). + * + * @return `true` if the context was successfully dropped or no context existed, `false` otherwise. + */ +bool otel_process_ctx_drop_current(void); + +/** This can be disabled if no read support is required. */ +#ifndef OTEL_PROCESS_CTX_NO_READ + typedef struct { + bool success; + const char *error_message; // Static strings only, non-NULL if success is false + otel_process_ctx_data data; // Strings are allocated using `malloc` and the caller is responsible for `free`ing them + } otel_process_ctx_read_result; + + /** + * Reads the current OpenTelemetry process context, if any. + * + * Useful for debugging and testing purposes. Underlying returned strings in `data` are dynamically allocated using + * `malloc` and `otel_process_ctx_read_drop` must be called to free them. + * + * Thread-safety: This function assumes there is no concurrent mutation of the process context. + * + * @return The result of the operation. If successful, `data` contains the retrieved context data. + */ + otel_process_ctx_read_result otel_process_ctx_read(void); + + /** + * Drops the data resulting from a previous call to `otel_process_ctx_read`. + * + * @param result The result of a previous call to `otel_process_ctx_read`. Must not be `NULL`. + * @return `true` if the data was successfully dropped, `false` otherwise. + */ + bool otel_process_ctx_read_drop(otel_process_ctx_read_result *result); +#endif + +#ifdef __cplusplus +} +#endif From 2db7a3588bec1b91b8a5c1724544045fac3c28be Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Thu, 4 Jun 2026 15:15:34 +0000 Subject: [PATCH 02/18] feat: [PROF-14789] Publish OTel process context **What does this PR do?** This PR adds support for publishing the OpenTelemetry Process Context (aka OTEP 4719 -- ). This mechanism is very similar to Datadog's existing "process discovery"/"tracer-info" mechanism, so the actual change in `trace.cpp` is very minimal (this is by design :p ). This data will be read by OTel-compliant tools, of which the eBPF Profiler (which Datadog will also support as part of our Continuous Profiler product) is the first one. **Motivation:** We're adding support for OTEP 4719 to all Datadog SDKs, so dd-trace-cpp is actually one of the last ones to adopt this. See (Datadog-only link) for an up-to-date list of all SDKs. **Additional Notes:** The `otel_process_ctx.cpp` and `otel_process_ctx.h` come from the reference implementation we're maintaining upstream with OTel in . They are already in use in dd-trace-java as well. Review-wise, my suggestion for those would be to focus on correctness -- ideally we want to be able to quickly update to the latest version when needed, which is made easier if we have a minimal delta on upstream. (I'm the one maintaining this upstream so I can propagate fixes/changes as well so we don't have to diverge). **How to test the change?** This change includes test coverage. Also, I've tested this in practice using the `otel_process_ctx_dump.sh` script from the https://github.com/open-telemetry/sig-profiling/ repo. Here's how that looks: ``` $ sudo ./otel_process_ctx_dump.sh 2394947 Found OTEL context for PID 2394947 Start address: 7c072aace000 00000000 4f 54 45 4c 5f 43 54 58 02 00 00 00 c3 01 00 00 |OTEL_CTX........| 00000010 68 5b 15 20 9a 25 21 00 60 f0 6e 16 c5 5b 00 00 |h[. .%!.`.n..[..| 00000020 Parsed struct: otel_process_ctx_signature : "OTEL_CTX" otel_process_ctx_version : 2 otel_process_payload_size : 451 otel_process_monotonic_published_at_ns : 9330018124913512 otel_process_payload : 0x00005bc5166ef060 Payload dump (451 bytes): 00000000 0a af 02 0a 21 0a 1b 64 65 70 6c 6f 79 6d 65 6e |....!..deploymen| 00000010 74 2e 65 6e 76 69 72 6f 6e 6d 65 6e 74 2e 6e 61 |t.environment.na| 00000020 6d 65 12 02 0a 00 0a 3d 0a 13 73 65 72 76 69 63 |me.....=..servic| 00000030 65 2e 69 6e 73 74 61 6e 63 65 2e 69 64 12 26 0a |e.instance.id.&.| 00000040 24 38 32 65 39 38 38 38 64 2d 33 36 65 62 2d 34 |$82e9888d-36eb-4| 00000050 39 63 66 2d 61 63 63 38 2d 36 38 32 39 34 39 63 |9cf-acc8-682949c| 00000060 34 39 32 37 33 0a 20 0a 0c 73 65 72 76 69 63 65 |49273. ..service| 00000070 2e 6e 61 6d 65 12 10 0a 0e 63 70 70 2d 65 78 70 |.name....cpp-exp| 00000080 65 72 69 6d 65 6e 74 0a 15 0a 0f 73 65 72 76 69 |eriment....servi| 00000090 63 65 2e 76 65 72 73 69 6f 6e 12 02 0a 00 0a 1f |ce.version......| 000000a0 0a 16 74 65 6c 65 6d 65 74 72 79 2e 73 64 6b 2e |..telemetry.sdk.| 000000b0 6c 61 6e 67 75 61 67 65 12 05 0a 03 63 70 70 0a |language....cpp.| 000000c0 21 0a 15 74 65 6c 65 6d 65 74 72 79 2e 73 64 6b |!..telemetry.sdk| 000000d0 2e 76 65 72 73 69 6f 6e 12 08 0a 06 76 32 2e 31 |.version....v2.1| 000000e0 2e 31 0a 24 0a 12 74 65 6c 65 6d 65 74 72 79 2e |.1.$..telemetry.| 000000f0 73 64 6b 2e 6e 61 6d 65 12 0e 0a 0c 64 64 2d 74 |sdk.name....dd-t| 00000100 72 61 63 65 2d 63 70 70 0a 0f 0a 09 68 6f 73 74 |race-cpp....host| 00000110 2e 6e 61 6d 65 12 02 0a 00 0a 17 0a 0c 63 6f 6e |.name........con| 00000120 74 61 69 6e 65 72 2e 69 64 12 07 0a 05 31 32 39 |tainer.id....129| 00000130 37 30 12 8e 01 0a 14 64 61 74 61 64 6f 67 2e 70 |70.....datadog.p| 00000140 72 6f 63 65 73 73 5f 74 61 67 73 12 76 0a 74 65 |rocess_tags.v.te| 00000150 6e 74 72 79 70 6f 69 6e 74 2e 62 61 73 65 64 69 |ntrypoint.basedi| 00000160 72 3a 62 75 69 6c 64 2c 65 6e 74 72 79 70 6f 69 |r:build,entrypoi| 00000170 6e 74 2e 77 6f 72 6b 64 69 72 3a 63 70 70 2d 65 |nt.workdir:cpp-e| 00000180 78 70 65 72 69 6d 65 6e 74 2c 65 6e 74 72 79 70 |xperiment,entryp| 00000190 6f 69 6e 74 2e 74 79 70 65 3a 65 78 65 63 75 74 |oint.type:execut| 000001a0 61 62 6c 65 2c 65 6e 74 72 79 70 6f 69 6e 74 2e |able,entrypoint.| 000001b0 6e 61 6d 65 3a 63 70 70 2d 65 78 70 65 72 69 6d |name:cpp-experim| 000001c0 65 6e 74 |ent| 000001c3 Protobuf decode: resource { attributes { key: "deployment.environment.name" value { string_value: "" } } attributes { key: "service.instance.id" value { string_value: "82e9888d-36eb-49cf-acc8-682949c49273" } } attributes { key: "service.name" value { string_value: "cpp-experiment" } } attributes { key: "service.version" value { string_value: "" } } attributes { key: "telemetry.sdk.language" value { string_value: "cpp" } } attributes { key: "telemetry.sdk.version" value { string_value: "v2.1.1" } } attributes { key: "telemetry.sdk.name" value { string_value: "dd-trace-cpp" } } attributes { key: "host.name" value { string_value: "" } } attributes { key: "container.id" value { string_value: "12970" } } } extra_attributes { key: "datadog.process_tags" value { string_value: "entrypoint.basedir:build,entrypoint.workdir:cpp-experiment,entrypoint.type:executable,entrypoint.name:cpp-experiment" } } ``` --- BUILD.bazel | 2 + CMakeLists.txt | 1 + include/datadog/tracer.h | 3 + src/datadog/otel_process_ctx.cpp | 8 +++ src/datadog/tracer.cpp | 75 ++++++++++++++++++---- test/CMakeLists.txt | 1 + test/test_otel_process_ctx.cpp | 107 +++++++++++++++++++++++++++++++ 7 files changed, 183 insertions(+), 14 deletions(-) create mode 100644 test/test_otel_process_ctx.cpp diff --git a/BUILD.bazel b/BUILD.bazel index df17ac563..db9e0e655 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -39,6 +39,8 @@ cc_library( "src/datadog/msgpack.cpp", "src/datadog/msgpack.h", "src/datadog/null_logger.h", + "src/datadog/otel_process_ctx.cpp", + "src/datadog/otel_process_ctx.h", "src/datadog/parse_util.cpp", "src/datadog/parse_util.h", "src/datadog/platform_util.h", diff --git a/CMakeLists.txt b/CMakeLists.txt index 8cc154f17..a892ad0a4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -194,6 +194,7 @@ target_sources(dd-trace-cpp-objects src/datadog/limiter.cpp src/datadog/logger.cpp src/datadog/msgpack.cpp + src/datadog/otel_process_ctx.cpp src/datadog/parse_util.cpp src/datadog/propagation_style.cpp src/datadog/random.cpp diff --git a/include/datadog/tracer.h b/include/datadog/tracer.h index 6a032a133..692139116 100644 --- a/include/datadog/tracer.h +++ b/include/datadog/tracer.h @@ -65,6 +65,9 @@ class Tracer { Tracer(const FinalizedTracerConfig& config, const std::shared_ptr& generator); + // Drops all state, including process discovery and process context. + ~Tracer(); + // Create a new trace and return the root span of the trace. Optionally // specify a `config` indicating the attributes of the root span. Span create_span(); diff --git a/src/datadog/otel_process_ctx.cpp b/src/datadog/otel_process_ctx.cpp index a68dac9d0..bf16e9ccc 100644 --- a/src/datadog/otel_process_ctx.cpp +++ b/src/datadog/otel_process_ctx.cpp @@ -7,6 +7,14 @@ #define _GNU_SOURCE #endif +#if defined(__GNUC__) && defined(__cplusplus) && __cplusplus < 202002L + // This file uses C99 compound literals with designated initializers + // throughout. They are standard in C (since C99) and in C++ (since C++20), + // but trigger -Wpedantic when compiled as C++17 or older. Silence them at + // translation-unit scope so the file builds cleanly under strict flags. + #pragma GCC diagnostic ignored "-Wpedantic" +#endif + // Note: Things here are needed for NOOP. Things that are only for non-NOOP get added further below. #include diff --git a/src/datadog/tracer.cpp b/src/datadog/tracer.cpp index 4209ac228..219e69a44 100644 --- a/src/datadog/tracer.cpp +++ b/src/datadog/tracer.cpp @@ -22,6 +22,7 @@ #include "hex.h" #include "json.hpp" #include "msgpack.h" +#include "otel_process_ctx.h" #include "platform_util.h" #include "random.h" #include "root_session_id.h" @@ -120,6 +121,10 @@ Tracer::Tracer(const FinalizedTracerConfig& config, store_config(process_tags); } +Tracer::~Tracer() { + otel_process_ctx_drop_current(); +} + std::string Tracer::config() const { // clang-format off auto config = nlohmann::json::object({ @@ -160,9 +165,16 @@ void Tracer::store_config( metadata_file_ = std::make_unique(std::move(*maybe_file)); - auto defaults = config_manager_->span_defaults(); - - std::string container_id = ""; + const auto defaults = config_manager_->span_defaults(); + const std::string runtime_id_string = runtime_id_.string(); + const std::string tracer_version = signature_.library_version; + const std::string tracer_language(signature_.library_language); + const std::string hostname_value = hostname_.value_or(""); + const std::string service_name = defaults->service; + const std::string service_env = defaults->environment; + const std::string service_version = defaults->version; + const std::string process_tags_joined = join_tags(process_tags); + std::string container_id; if (auto maybe_container_id = container::get_id()) { container_id = maybe_container_id->value; } @@ -172,23 +184,58 @@ void Tracer::store_config( // clang-format off msgpack::pack_map( - buffer, - "schema_version", [&](auto& buffer) { msgpack::pack_integer(buffer, std::uint64_t(2)); return Expected{}; }, - "runtime_id", [&](auto& buffer) { return msgpack::pack_string(buffer, runtime_id_.string()); }, - "tracer_version", [&](auto& buffer) { return msgpack::pack_string(buffer, signature_.library_version); }, - "tracer_language", [&](auto& buffer) { return msgpack::pack_string(buffer, signature_.library_language); }, - "hostname", [&](auto& buffer) { return msgpack::pack_string(buffer, hostname_.value_or("")); }, - "service_name", [&](auto& buffer) { return msgpack::pack_string(buffer, defaults->service); }, - "service_env", [&](auto& buffer) { return msgpack::pack_string(buffer, defaults->environment); }, - "service_version", [&](auto& buffer) { return msgpack::pack_string(buffer, defaults->version); }, - "process_tags", [&](auto& buffer) { return msgpack::pack_string(buffer, join_tags(process_tags)); }, - "container_id", [&](auto& buffer) { return msgpack::pack_string(buffer, container_id); } + buffer, + "schema_version", [&](auto& buffer) { msgpack::pack_integer(buffer, std::uint64_t(2)); return Expected{}; }, + "runtime_id", [&](auto& buffer) { return msgpack::pack_string(buffer, runtime_id_string); }, + "tracer_version", [&](auto& buffer) { return msgpack::pack_string(buffer, tracer_version); }, + "tracer_language", [&](auto& buffer) { return msgpack::pack_string(buffer, tracer_language); }, + "hostname", [&](auto& buffer) { return msgpack::pack_string(buffer, hostname_value); }, + "service_name", [&](auto& buffer) { return msgpack::pack_string(buffer, service_name); }, + "service_env", [&](auto& buffer) { return msgpack::pack_string(buffer, service_env); }, + "service_version", [&](auto& buffer) { return msgpack::pack_string(buffer, service_version); }, + "process_tags", [&](auto& buffer) { return msgpack::pack_string(buffer, process_tags_joined); }, + "container_id", [&](auto& buffer) { return msgpack::pack_string(buffer, container_id); } ); // clang-format on if (!metadata_file_->write_then_seal(buffer)) { logger_->log_error("Either failed to write or seal the configuration file"); + return; + } + +#ifdef __linux__ + // Publish the same metadata as an OpenTelemetry process context. + const char* resource_attrs[] = { + "host.name", hostname_value.c_str(), + "container.id", container_id.c_str(), + nullptr, + }; + const char* extra_attrs[] = { + "datadog.process_tags", + process_tags_joined.c_str(), + nullptr, + }; + + otel_process_ctx_data otel_data = {}; + otel_data.deployment_environment_name = service_env.c_str(); + otel_data.service_instance_id = runtime_id_string.c_str(); + otel_data.service_name = service_name.c_str(); + otel_data.service_version = service_version.c_str(); + otel_data.telemetry_sdk_language = tracer_language.c_str(); + otel_data.telemetry_sdk_version = tracer_version.c_str(); + otel_data.telemetry_sdk_name = "dd-trace-cpp"; + otel_data.resource_attributes = resource_attrs; + otel_data.extra_attributes = extra_attrs; + otel_data.thread_ctx_config = nullptr; + + const auto otel_result = otel_process_ctx_publish(&otel_data); + if (!otel_result.success) { + logger_->log_error([&](std::ostream& log) { + log << "Failed to publish OpenTelemetry process context: " + << otel_result.error_message; + }); } +#endif } Span Tracer::create_span() { return create_span(SpanConfig{}); } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 7571aa8b7..e2298776e 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -29,6 +29,7 @@ add_executable(tests test_glob.cpp test_limiter.cpp test_msgpack.cpp + test_otel_process_ctx.cpp test_platform_util.cpp test_parse_util.cpp test_smoke.cpp diff --git a/test/test_otel_process_ctx.cpp b/test/test_otel_process_ctx.cpp new file mode 100644 index 000000000..e331f72b7 --- /dev/null +++ b/test/test_otel_process_ctx.cpp @@ -0,0 +1,107 @@ +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "mocks/collectors.h" +#include "mocks/loggers.h" +#include "otel_process_ctx.h" +#include "platform_util.h" +#include "string_util.h" +#include "test.h" + +namespace fs = std::filesystem; +using namespace datadog::tracing; + +#define OTEL_CTX_TEST(x) TEST_CASE(x, "[otel_process_ctx]") + +namespace { + +std::map to_map(const char** kv) { + std::map out; + if (kv == nullptr) return out; + for (std::size_t i = 0; kv[i] != nullptr && kv[i + 1] != nullptr; i += 2) { + out.emplace(kv[i], kv[i + 1]); + } + return out; +} + +} // namespace + +OTEL_CTX_TEST("Tracer construction publishes OTel process context") { +#ifndef __linux__ + SUCCEED("OpenTelemetry process context is Linux-only"); + return; +#endif + + std::string expected_container_id; + if (auto id = container::get_id()) { + expected_container_id = id->value; + } + const RuntimeID expected_runtime_id = RuntimeID::generate(); + + std::unordered_map expected_process_tags = { + {"custom.tag", "custom-value"}, + }; + expected_process_tags.emplace("entrypoint.name", get_process_name()); + expected_process_tags.emplace("entrypoint.type", "executable"); + expected_process_tags.emplace("entrypoint.workdir", + fs::current_path().filename().string()); + if (auto path = get_process_path()) { + expected_process_tags.emplace("entrypoint.basedir", + path->parent_path().filename().string()); + } + + TracerConfig config; + config.service = "otel-ctx-svc"; + config.environment = "otel-ctx-env"; + config.version = "1.2.3"; + config.runtime_id = expected_runtime_id; + config.report_hostname = true; + config.process_tags = {{"custom.tag", "custom-value"}}; + config.collector = std::make_shared(); + config.logger = std::make_shared(); + + auto finalized = finalize_config(config); + REQUIRE(finalized); + + { + Tracer tracer{*finalized}; + + auto read_result = otel_process_ctx_read(); + REQUIRE(read_result.success); + const auto& data = read_result.data; + + CHECK(std::string(data.service_name) == "otel-ctx-svc"); + CHECK(std::string(data.deployment_environment_name) == "otel-ctx-env"); + CHECK(std::string(data.service_version) == "1.2.3"); + CHECK(std::string(data.service_instance_id) == expected_runtime_id.string()); + CHECK(std::string(data.telemetry_sdk_language) == "cpp"); + CHECK(std::string(data.telemetry_sdk_name) == "dd-trace-cpp"); + CHECK(std::string(data.telemetry_sdk_version) == tracer_version); + + const std::map expected_resource = { + {"host.name", get_hostname()}, + {"container.id", expected_container_id}, + }; + CHECK(to_map(data.resource_attributes) == expected_resource); + + const std::map expected_extra = { + {"datadog.process_tags", join_tags(expected_process_tags)}, + }; + CHECK(to_map(data.extra_attributes) == expected_extra); + + REQUIRE(otel_process_ctx_read_drop(&read_result)); + } + + auto post_read = otel_process_ctx_read(); + CHECK_FALSE(post_read.success); + if (post_read.success) { + otel_process_ctx_read_drop(&post_read); + } +} From 68137698c2dce8067752e4dc79357a25645cd4ed Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Fri, 5 Jun 2026 08:15:58 +0000 Subject: [PATCH 03/18] Make autoformatter happy --- src/datadog/otel_process_ctx.cpp | 4 ++++ src/datadog/otel_process_ctx.h | 4 ++++ src/datadog/tracer.cpp | 7 ++----- test/test_otel_process_ctx.cpp | 3 ++- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/datadog/otel_process_ctx.cpp b/src/datadog/otel_process_ctx.cpp index bf16e9ccc..3d5194fe5 100644 --- a/src/datadog/otel_process_ctx.cpp +++ b/src/datadog/otel_process_ctx.cpp @@ -1,3 +1,7 @@ +// clang-format off +// This file is a verbatim copy of an upstream reference implementation; do +// not let clang-format rewrite it. + // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License (Version 2.0). // This product includes software developed at Datadog (https://www.datadoghq.com/) Copyright 2025 Datadog, Inc. diff --git a/src/datadog/otel_process_ctx.h b/src/datadog/otel_process_ctx.h index 909cd01ef..ca0473222 100644 --- a/src/datadog/otel_process_ctx.h +++ b/src/datadog/otel_process_ctx.h @@ -1,3 +1,7 @@ +// clang-format off +// This file is a verbatim copy of an upstream reference implementation; do +// not let clang-format rewrite it. + // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License (Version 2.0). // This product includes software developed at Datadog (https://www.datadoghq.com/) Copyright 2025 Datadog, Inc. diff --git a/src/datadog/tracer.cpp b/src/datadog/tracer.cpp index 219e69a44..146c0f290 100644 --- a/src/datadog/tracer.cpp +++ b/src/datadog/tracer.cpp @@ -121,9 +121,7 @@ Tracer::Tracer(const FinalizedTracerConfig& config, store_config(process_tags); } -Tracer::~Tracer() { - otel_process_ctx_drop_current(); -} +Tracer::~Tracer() { otel_process_ctx_drop_current(); } std::string Tracer::config() const { // clang-format off @@ -206,8 +204,7 @@ void Tracer::store_config( #ifdef __linux__ // Publish the same metadata as an OpenTelemetry process context. const char* resource_attrs[] = { - "host.name", hostname_value.c_str(), - "container.id", container_id.c_str(), + "host.name", hostname_value.c_str(), "container.id", container_id.c_str(), nullptr, }; const char* extra_attrs[] = { diff --git a/test/test_otel_process_ctx.cpp b/test/test_otel_process_ctx.cpp index e331f72b7..bb8f30e12 100644 --- a/test/test_otel_process_ctx.cpp +++ b/test/test_otel_process_ctx.cpp @@ -80,7 +80,8 @@ OTEL_CTX_TEST("Tracer construction publishes OTel process context") { CHECK(std::string(data.service_name) == "otel-ctx-svc"); CHECK(std::string(data.deployment_environment_name) == "otel-ctx-env"); CHECK(std::string(data.service_version) == "1.2.3"); - CHECK(std::string(data.service_instance_id) == expected_runtime_id.string()); + CHECK(std::string(data.service_instance_id) == + expected_runtime_id.string()); CHECK(std::string(data.telemetry_sdk_language) == "cpp"); CHECK(std::string(data.telemetry_sdk_name) == "dd-trace-cpp"); CHECK(std::string(data.telemetry_sdk_version) == tracer_version); From 35bb0137cc1e455952471eae64eaa6f7afbb4525 Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Fri, 5 Jun 2026 08:32:06 +0000 Subject: [PATCH 04/18] Tweak to make CI happy --- src/datadog/otel_process_ctx.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/datadog/otel_process_ctx.cpp b/src/datadog/otel_process_ctx.cpp index 3d5194fe5..415a50b18 100644 --- a/src/datadog/otel_process_ctx.cpp +++ b/src/datadog/otel_process_ctx.cpp @@ -14,9 +14,11 @@ #if defined(__GNUC__) && defined(__cplusplus) && __cplusplus < 202002L // This file uses C99 compound literals with designated initializers // throughout. They are standard in C (since C99) and in C++ (since C++20), - // but trigger -Wpedantic when compiled as C++17 or older. Silence them at - // translation-unit scope so the file builds cleanly under strict flags. + // but trigger -Wpedantic (older GCC) or -Wc++20-extensions (newer GCC) when + // compiled as C++17 or older. Silence both at translation-unit scope so the + // file builds cleanly under strict flags. #pragma GCC diagnostic ignored "-Wpedantic" + #pragma GCC diagnostic ignored "-Wc++20-extensions" #endif // Note: Things here are needed for NOOP. Things that are only for non-NOOP get added further below. From 76267e5bd20c0e93fc611816e29f07d0cee0e5ff Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Fri, 5 Jun 2026 08:41:28 +0000 Subject: [PATCH 05/18] Another try at making CI happy --- src/datadog/otel_process_ctx.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/datadog/otel_process_ctx.cpp b/src/datadog/otel_process_ctx.cpp index 415a50b18..af488d161 100644 --- a/src/datadog/otel_process_ctx.cpp +++ b/src/datadog/otel_process_ctx.cpp @@ -15,8 +15,9 @@ // This file uses C99 compound literals with designated initializers // throughout. They are standard in C (since C99) and in C++ (since C++20), // but trigger -Wpedantic (older GCC) or -Wc++20-extensions (newer GCC) when - // compiled as C++17 or older. Silence both at translation-unit scope so the - // file builds cleanly under strict flags. + // compiled as C++17 or older. Silencing -Wpragmas first lets GCC versions + // that don't know -Wc++20-extensions accept the pragma silently. + #pragma GCC diagnostic ignored "-Wpragmas" #pragma GCC diagnostic ignored "-Wpedantic" #pragma GCC diagnostic ignored "-Wc++20-extensions" #endif From 71d782518b1573054165849ae5c903c47a7395bd Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Fri, 5 Jun 2026 09:11:31 +0000 Subject: [PATCH 06/18] More small tweaks to make CI happy --- src/datadog/otel_process_ctx.cpp | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/datadog/otel_process_ctx.cpp b/src/datadog/otel_process_ctx.cpp index af488d161..3ae2f4394 100644 --- a/src/datadog/otel_process_ctx.cpp +++ b/src/datadog/otel_process_ctx.cpp @@ -29,17 +29,19 @@ #define ADD_QUOTES_HELPER(x) #x #define ADD_QUOTES(x) ADD_QUOTES_HELPER(x) +// Positional aggregate init (no designated initializers) so this file's NOOP +// path compiles on MSVC at /std:c++17. static const otel_process_ctx_data empty_data = { - .deployment_environment_name = NULL, - .service_instance_id = NULL, - .service_name = NULL, - .service_version = NULL, - .telemetry_sdk_language = NULL, - .telemetry_sdk_version = NULL, - .telemetry_sdk_name = NULL, - .resource_attributes = NULL, - .extra_attributes = NULL, - .thread_ctx_config = NULL + NULL, // deployment_environment_name + NULL, // service_instance_id + NULL, // service_name + NULL, // service_version + NULL, // telemetry_sdk_language + NULL, // telemetry_sdk_version + NULL, // telemetry_sdk_name + NULL, // resource_attributes + NULL, // extra_attributes + NULL // thread_ctx_config }; #if (defined(OTEL_PROCESS_CTX_NOOP) && OTEL_PROCESS_CTX_NOOP) || !defined(__linux__) @@ -47,7 +49,8 @@ static const otel_process_ctx_data empty_data = { otel_process_ctx_result otel_process_ctx_publish(const otel_process_ctx_data *data) { (void) data; // Suppress unused parameter warning - return (otel_process_ctx_result) {.success = false, .error_message = "OTEL_PROCESS_CTX_NOOP mode is enabled - no-op implementation (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + otel_process_ctx_result result = {false, "OTEL_PROCESS_CTX_NOOP mode is enabled - no-op implementation (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + return result; } bool otel_process_ctx_drop_current(void) { @@ -56,7 +59,8 @@ static const otel_process_ctx_data empty_data = { #ifndef OTEL_PROCESS_CTX_NO_READ otel_process_ctx_read_result otel_process_ctx_read(void) { - return (otel_process_ctx_read_result) {.success = false, .error_message = "OTEL_PROCESS_CTX_NOOP mode is enabled - no-op implementation (" __FILE__ ":" ADD_QUOTES(__LINE__) ")", .data = empty_data}; + otel_process_ctx_read_result result = {false, "OTEL_PROCESS_CTX_NOOP mode is enabled - no-op implementation (" __FILE__ ":" ADD_QUOTES(__LINE__) ")", empty_data}; + return result; } bool otel_process_ctx_read_drop(otel_process_ctx_read_result *result) { From bcda89db485ad4daa009fd0e6f271543503817bf Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Fri, 5 Jun 2026 10:12:20 +0000 Subject: [PATCH 07/18] More CI fixes --- src/datadog/tracer.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/datadog/tracer.cpp b/src/datadog/tracer.cpp index 146c0f290..9c1746650 100644 --- a/src/datadog/tracer.cpp +++ b/src/datadog/tracer.cpp @@ -165,7 +165,7 @@ void Tracer::store_config( const auto defaults = config_manager_->span_defaults(); const std::string runtime_id_string = runtime_id_.string(); - const std::string tracer_version = signature_.library_version; + const std::string tracer_version_value = signature_.library_version; const std::string tracer_language(signature_.library_language); const std::string hostname_value = hostname_.value_or(""); const std::string service_name = defaults->service; @@ -185,7 +185,7 @@ void Tracer::store_config( buffer, "schema_version", [&](auto& buffer) { msgpack::pack_integer(buffer, std::uint64_t(2)); return Expected{}; }, "runtime_id", [&](auto& buffer) { return msgpack::pack_string(buffer, runtime_id_string); }, - "tracer_version", [&](auto& buffer) { return msgpack::pack_string(buffer, tracer_version); }, + "tracer_version", [&](auto& buffer) { return msgpack::pack_string(buffer, tracer_version_value); }, "tracer_language", [&](auto& buffer) { return msgpack::pack_string(buffer, tracer_language); }, "hostname", [&](auto& buffer) { return msgpack::pack_string(buffer, hostname_value); }, "service_name", [&](auto& buffer) { return msgpack::pack_string(buffer, service_name); }, @@ -219,7 +219,7 @@ void Tracer::store_config( otel_data.service_name = service_name.c_str(); otel_data.service_version = service_version.c_str(); otel_data.telemetry_sdk_language = tracer_language.c_str(); - otel_data.telemetry_sdk_version = tracer_version.c_str(); + otel_data.telemetry_sdk_version = tracer_version_value.c_str(); otel_data.telemetry_sdk_name = "dd-trace-cpp"; otel_data.resource_attributes = resource_attrs; otel_data.extra_attributes = extra_attrs; From 38359e2823b8aa91f8300f43dbdd05895b78d7fb Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Fri, 5 Jun 2026 10:30:53 +0000 Subject: [PATCH 08/18] More making CI happy --- test/test_otel_process_ctx.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/test_otel_process_ctx.cpp b/test/test_otel_process_ctx.cpp index bb8f30e12..ad8e1f6a0 100644 --- a/test/test_otel_process_ctx.cpp +++ b/test/test_otel_process_ctx.cpp @@ -36,9 +36,7 @@ std::map to_map(const char** kv) { OTEL_CTX_TEST("Tracer construction publishes OTel process context") { #ifndef __linux__ SUCCEED("OpenTelemetry process context is Linux-only"); - return; -#endif - +#else std::string expected_container_id; if (auto id = container::get_id()) { expected_container_id = id->value; @@ -105,4 +103,5 @@ OTEL_CTX_TEST("Tracer construction publishes OTel process context") { if (post_read.success) { otel_process_ctx_read_drop(&post_read); } +#endif } From 1690f104c160945644f0e06f7ec0adb8e850c8a9 Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Mon, 8 Jun 2026 09:14:50 +0100 Subject: [PATCH 09/18] Add links to where `otel_process_ctx.h/.cpp This makes it more clear where they came from so in the future we can keep them up-to-date/in-sync. --- src/datadog/otel_process_ctx.cpp | 3 +++ src/datadog/otel_process_ctx.h | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/datadog/otel_process_ctx.cpp b/src/datadog/otel_process_ctx.cpp index 3ae2f4394..aff2de53b 100644 --- a/src/datadog/otel_process_ctx.cpp +++ b/src/datadog/otel_process_ctx.cpp @@ -2,6 +2,9 @@ // This file is a verbatim copy of an upstream reference implementation; do // not let clang-format rewrite it. +// The upstream home for this file is +// https://github.com/open-telemetry/sig-profiling/blob/main/process-context/c-and-cpp/otel_process_ctx.c + // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License (Version 2.0). // This product includes software developed at Datadog (https://www.datadoghq.com/) Copyright 2025 Datadog, Inc. diff --git a/src/datadog/otel_process_ctx.h b/src/datadog/otel_process_ctx.h index ca0473222..a1ebec6b6 100644 --- a/src/datadog/otel_process_ctx.h +++ b/src/datadog/otel_process_ctx.h @@ -2,6 +2,9 @@ // This file is a verbatim copy of an upstream reference implementation; do // not let clang-format rewrite it. +// The upstream home for this file is +// https://github.com/open-telemetry/sig-profiling/blob/main/process-context/c-and-cpp/otel_process_ctx.h + // Unless explicitly stated otherwise all files in this repository are licensed under the Apache License (Version 2.0). // This product includes software developed at Datadog (https://www.datadoghq.com/) Copyright 2025 Datadog, Inc. From 4f6916ddc618c9ed35f3e02f021aa2905541e627 Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Thu, 18 Jun 2026 15:56:10 +0000 Subject: [PATCH 10/18] Introduce new constant with library name, rather than hardcoding in random places --- include/datadog/version.h | 3 +++ src/datadog/error.cpp | 5 +++-- src/datadog/tracer.cpp | 2 +- src/datadog/version.cpp | 4 +++- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/include/datadog/version.h b/include/datadog/version.h index 90f4ffca5..96f9889eb 100644 --- a/include/datadog/version.h +++ b/include/datadog/version.h @@ -4,6 +4,9 @@ namespace datadog::tracing { +// This library's name +extern const char *const tracer_library_name; + // The release version at or before this code revision, e.g. "v0.1.12". // That is, this code is at least as recent as `tracer_version`, but may be // more recent. diff --git a/src/datadog/error.cpp b/src/datadog/error.cpp index 8aa8d2f7f..ba7ab6f46 100644 --- a/src/datadog/error.cpp +++ b/src/datadog/error.cpp @@ -1,4 +1,5 @@ #include +#include #include @@ -6,8 +7,8 @@ namespace datadog { namespace tracing { std::ostream& operator<<(std::ostream& stream, const Error& error) { - return stream << "[dd-trace-cpp error code " << int(error.code) << "] " - << error.message; + return stream << "[" << tracer_library_name << " error code " + << int(error.code) << "] " << error.message; } Error Error::with_prefix(StringView prefix) const { diff --git a/src/datadog/tracer.cpp b/src/datadog/tracer.cpp index 9c1746650..f777d3c5a 100644 --- a/src/datadog/tracer.cpp +++ b/src/datadog/tracer.cpp @@ -220,7 +220,7 @@ void Tracer::store_config( otel_data.service_version = service_version.c_str(); otel_data.telemetry_sdk_language = tracer_language.c_str(); otel_data.telemetry_sdk_version = tracer_version_value.c_str(); - otel_data.telemetry_sdk_name = "dd-trace-cpp"; + otel_data.telemetry_sdk_name = tracer_library_name; otel_data.resource_attributes = resource_attrs; otel_data.extra_attributes = extra_attrs; otel_data.thread_ctx_config = nullptr; diff --git a/src/datadog/version.cpp b/src/datadog/version.cpp index 7c5b4d29e..2b25a2690 100644 --- a/src/datadog/version.cpp +++ b/src/datadog/version.cpp @@ -2,10 +2,12 @@ namespace datadog::tracing { +#define DD_TRACE_LIBRARY_NAME "dd-trace-cpp" #define DD_TRACE_VERSION "v2.1.1" +const char* const tracer_library_name = DD_TRACE_LIBRARY_NAME; const char* const tracer_version = DD_TRACE_VERSION; const char* const tracer_version_string = - "[dd-trace-cpp version " DD_TRACE_VERSION "]"; + "[" DD_TRACE_LIBRARY_NAME " version " DD_TRACE_VERSION "]"; } // namespace datadog::tracing From afddc6daebde1b6dfa62ee4206a9c44144350302 Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Thu, 18 Jun 2026 16:15:30 +0000 Subject: [PATCH 11/18] Minor: Remove redundant includes --- test/test_otel_process_ctx.cpp | 8 -------- 1 file changed, 8 deletions(-) diff --git a/test/test_otel_process_ctx.cpp b/test/test_otel_process_ctx.cpp index ad8e1f6a0..5bded8c9f 100644 --- a/test/test_otel_process_ctx.cpp +++ b/test/test_otel_process_ctx.cpp @@ -1,12 +1,4 @@ -#include #include -#include -#include - -#include -#include -#include -#include #include "mocks/collectors.h" #include "mocks/loggers.h" From 8b9a8a39b87b96e619450d6cc0e2dc5f57456874 Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Thu, 18 Jun 2026 16:17:16 +0000 Subject: [PATCH 12/18] Minor: Use references when building up metadata --- src/datadog/tracer.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/datadog/tracer.cpp b/src/datadog/tracer.cpp index f777d3c5a..4ae1b96ca 100644 --- a/src/datadog/tracer.cpp +++ b/src/datadog/tracer.cpp @@ -164,13 +164,13 @@ void Tracer::store_config( metadata_file_ = std::make_unique(std::move(*maybe_file)); const auto defaults = config_manager_->span_defaults(); - const std::string runtime_id_string = runtime_id_.string(); - const std::string tracer_version_value = signature_.library_version; + const std::string& runtime_id_string = runtime_id_.string(); + const std::string& tracer_version_value = signature_.library_version; const std::string tracer_language(signature_.library_language); const std::string hostname_value = hostname_.value_or(""); - const std::string service_name = defaults->service; - const std::string service_env = defaults->environment; - const std::string service_version = defaults->version; + const std::string& service_name = defaults->service; + const std::string& service_env = defaults->environment; + const std::string& service_version = defaults->version; const std::string process_tags_joined = join_tags(process_tags); std::string container_id; if (auto maybe_container_id = container::get_id()) { From c191436497f5f1aab24da2dc9fecd6d78470db28 Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Thu, 18 Jun 2026 17:10:51 +0000 Subject: [PATCH 13/18] Minor: Don't compare unordered maps as it can easily break --- test/test_otel_process_ctx.cpp | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/test/test_otel_process_ctx.cpp b/test/test_otel_process_ctx.cpp index 5bded8c9f..d80426bd3 100644 --- a/test/test_otel_process_ctx.cpp +++ b/test/test_otel_process_ctx.cpp @@ -4,7 +4,6 @@ #include "mocks/loggers.h" #include "otel_process_ctx.h" #include "platform_util.h" -#include "string_util.h" #include "test.h" namespace fs = std::filesystem; @@ -23,6 +22,16 @@ std::map to_map(const char** kv) { return out; } +std::map parse_joined_tags(const std::string& s) { + std::map out; + std::istringstream in(s); + for (std::string pair; std::getline(in, pair, ',');) { + const auto colon = pair.find(':'); + out.emplace(pair.substr(0, colon), pair.substr(colon + 1)); + } + return out; +} + } // namespace OTEL_CTX_TEST("Tracer construction publishes OTel process context") { @@ -35,7 +44,7 @@ OTEL_CTX_TEST("Tracer construction publishes OTel process context") { } const RuntimeID expected_runtime_id = RuntimeID::generate(); - std::unordered_map expected_process_tags = { + std::map expected_process_tags = { {"custom.tag", "custom-value"}, }; expected_process_tags.emplace("entrypoint.name", get_process_name()); @@ -82,10 +91,11 @@ OTEL_CTX_TEST("Tracer construction publishes OTel process context") { }; CHECK(to_map(data.resource_attributes) == expected_resource); - const std::map expected_extra = { - {"datadog.process_tags", join_tags(expected_process_tags)}, - }; - CHECK(to_map(data.extra_attributes) == expected_extra); + const auto extra = to_map(data.extra_attributes); + REQUIRE(extra.size() == 1); + REQUIRE(extra.count("datadog.process_tags") == 1); + CHECK(parse_joined_tags(extra.at("datadog.process_tags")) == + expected_process_tags); REQUIRE(otel_process_ctx_read_drop(&read_result)); } From 9d050df8826a599d19f6878b0e4096512ffb0c55 Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Fri, 19 Jun 2026 12:12:25 +0000 Subject: [PATCH 14/18] Tweak behavior when report_hostname is disabled + add test --- src/datadog/tracer.cpp | 8 +++++++- test/test_otel_process_ctx.cpp | 27 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/datadog/tracer.cpp b/src/datadog/tracer.cpp index 4ae1b96ca..bf72f9592 100644 --- a/src/datadog/tracer.cpp +++ b/src/datadog/tracer.cpp @@ -203,10 +203,16 @@ void Tracer::store_config( #ifdef __linux__ // Publish the same metadata as an OpenTelemetry process context. - const char* resource_attrs[] = { + + // Make sure to leave host.name first... + const char* all_resource_attrs[] = { "host.name", hostname_value.c_str(), "container.id", container_id.c_str(), nullptr, }; + // ...so that we can omit it when it's not available. + const char** resource_attrs = + hostname_ ? all_resource_attrs : all_resource_attrs + 2; + const char* extra_attrs[] = { "datadog.process_tags", process_tags_joined.c_str(), diff --git a/test/test_otel_process_ctx.cpp b/test/test_otel_process_ctx.cpp index d80426bd3..fd9a51d40 100644 --- a/test/test_otel_process_ctx.cpp +++ b/test/test_otel_process_ctx.cpp @@ -107,3 +107,30 @@ OTEL_CTX_TEST("Tracer construction publishes OTel process context") { } #endif } + +OTEL_CTX_TEST("host.name is omitted when report_hostname is false") { +#ifndef __linux__ + SUCCEED("OpenTelemetry process context is Linux-only"); +#else + TracerConfig config; + config.service = "otel-ctx-svc"; + config.report_hostname = false; + config.collector = std::make_shared(); + config.logger = std::make_shared(); + + auto finalized = finalize_config(config); + REQUIRE(finalized); + + Tracer tracer{*finalized}; + auto read_result = otel_process_ctx_read(); + REQUIRE(read_result.success); + + // Sanity check that context is not empty + CHECK(std::string(read_result.data.service_name) == "otel-ctx-svc"); + + const auto resource = to_map(read_result.data.resource_attributes); + CHECK(resource.count("host.name") == 0); + + REQUIRE(otel_process_ctx_read_drop(&read_result)); +#endif +} From ab0b053f04cebf2eca9297d46521d95e1c3da863 Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Fri, 19 Jun 2026 12:27:19 +0000 Subject: [PATCH 15/18] Add missing `static` declaration --- src/datadog/otel_process_ctx.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/datadog/otel_process_ctx.cpp b/src/datadog/otel_process_ctx.cpp index aff2de53b..9056e11dc 100644 --- a/src/datadog/otel_process_ctx.cpp +++ b/src/datadog/otel_process_ctx.cpp @@ -827,7 +827,7 @@ static otel_process_ctx_result otel_process_ctx_encode_protobuf_payload(char **o data_out->telemetry_sdk_name != NULL; } - void otel_process_ctx_read_data_drop(otel_process_ctx_data data) { + static void otel_process_ctx_read_data_drop(otel_process_ctx_data data) { if (data.deployment_environment_name) free((void *)data.deployment_environment_name); if (data.service_instance_id) free((void *)data.service_instance_id); if (data.service_name) free((void *)data.service_name); From 2984054d6dec233a4f9cab3d5596a9c741f78df2 Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Fri, 19 Jun 2026 12:31:25 +0000 Subject: [PATCH 16/18] Minor: Fix wording in comment to use infinitive --- include/datadog/tracer.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/datadog/tracer.h b/include/datadog/tracer.h index 692139116..a1c851414 100644 --- a/include/datadog/tracer.h +++ b/include/datadog/tracer.h @@ -65,7 +65,7 @@ class Tracer { Tracer(const FinalizedTracerConfig& config, const std::shared_ptr& generator); - // Drops all state, including process discovery and process context. + // Drop all state, including process discovery and process context. ~Tracer(); // Create a new trace and return the root span of the trace. Optionally From c5615244c7dfab8e1c3a1281128dc4e0b8376dc9 Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Fri, 19 Jun 2026 15:38:27 +0000 Subject: [PATCH 17/18] Introduce move-only RAII guard and mutex to ensure correctness Before this change, Tracer had a user-declared destructor that called otel_process_ctx_drop_current() directly. That caused two problems: 1. Declaring ~Tracer() suppresses the implicit move constructor and move assignment, while leaving copy operations in place. Tracer became accidentally copyable and non-movable -- the exact wrong shape for a class that owns a process-wide singleton. Callers like nginx-datadog that return Tracer by value silently bound to the copy ctor, and the source's destructor then dropped the global context out from under the live copy. 2. otel_process_ctx_publish / otel_process_ctx_drop_current are documented as not thread-safe. Concurrent construction or destruction of Tracers across threads would race on the global published_state. This change introduces OtelCtxGuard, a small RAII type that owns the published OpenTelemetry process context, and a publish_otel_process_ctx() factory that returns one. Tracer now holds the guard via std::unique_ptr, which gives us: - Move-only Tracer. Move ops are implicitly defaulted (the previous explicit ~Tracer() body is gone) and copy ops are implicitly deleted through the unique_ptr member. A failed or skipped publish is represented as a null guard, so the destructor is correctly a no-op in that case. - Symmetric publish/drop encapsulation. Both directions through the C API go through OtelCtxGuard, and tracer.cpp no longer references otel_process_ctx_publish / otel_process_ctx_drop_current directly. - Thread safety. A mutex internal to the guard serializes publish and drop, so concurrent Tracer construction/destruction across threads is safe. Multi-instance behavior is "last writer wins": the C API holds at most one published context per process, and any guard destruction drops whatever is current. This matches the common case (one Tracer per process) and is documented at the top of otel_process_ctx_guard.h. --- BUILD.bazel | 2 ++ CMakeLists.txt | 1 + include/datadog/tracer.h | 12 +++++++- src/datadog/otel_process_ctx_guard.cpp | 39 ++++++++++++++++++++++++++ src/datadog/otel_process_ctx_guard.h | 32 +++++++++++++++++++++ src/datadog/tracer.cpp | 14 ++++----- 6 files changed, 90 insertions(+), 10 deletions(-) create mode 100644 src/datadog/otel_process_ctx_guard.cpp create mode 100644 src/datadog/otel_process_ctx_guard.h diff --git a/BUILD.bazel b/BUILD.bazel index db9e0e655..11dfc5c7b 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -41,6 +41,8 @@ cc_library( "src/datadog/null_logger.h", "src/datadog/otel_process_ctx.cpp", "src/datadog/otel_process_ctx.h", + "src/datadog/otel_process_ctx_guard.cpp", + "src/datadog/otel_process_ctx_guard.h", "src/datadog/parse_util.cpp", "src/datadog/parse_util.h", "src/datadog/platform_util.h", diff --git a/CMakeLists.txt b/CMakeLists.txt index a892ad0a4..63a291040 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -195,6 +195,7 @@ target_sources(dd-trace-cpp-objects src/datadog/logger.cpp src/datadog/msgpack.cpp src/datadog/otel_process_ctx.cpp + src/datadog/otel_process_ctx_guard.cpp src/datadog/parse_util.cpp src/datadog/propagation_style.cpp src/datadog/random.cpp diff --git a/include/datadog/tracer.h b/include/datadog/tracer.h index a1c851414..6f058dd6a 100644 --- a/include/datadog/tracer.h +++ b/include/datadog/tracer.h @@ -33,6 +33,7 @@ class TraceSampler; class SpanSampler; class IDGenerator; class InMemoryFile; +class OtelCtxGuard; class Tracer { std::shared_ptr logger_; @@ -51,6 +52,8 @@ class Tracer { // read to determine if the process is instrumented with a tracer and to // retrieve relevant tracing information. std::shared_ptr metadata_file_; + // Owns the published OpenTelemetry process context, if any. + std::unique_ptr otel_guard_; Baggage::Options baggage_opts_; bool baggage_injection_enabled_; bool baggage_extraction_enabled_; @@ -65,9 +68,16 @@ class Tracer { Tracer(const FinalizedTracerConfig& config, const std::shared_ptr& generator); - // Drop all state, including process discovery and process context. ~Tracer(); + // Move-only. The otel context guarded by OtelCtxGuard is a process-wide + // singleton; duplicating ownership would lead to spurious drops, so copies + // are disallowed. + Tracer(Tracer&&) noexcept; + Tracer& operator=(Tracer&&) noexcept; + Tracer(const Tracer&) = delete; + Tracer& operator=(const Tracer&) = delete; + // Create a new trace and return the root span of the trace. Optionally // specify a `config` indicating the attributes of the root span. Span create_span(); diff --git a/src/datadog/otel_process_ctx_guard.cpp b/src/datadog/otel_process_ctx_guard.cpp new file mode 100644 index 000000000..2f7ac78d5 --- /dev/null +++ b/src/datadog/otel_process_ctx_guard.cpp @@ -0,0 +1,39 @@ +#include "otel_process_ctx_guard.h" + +#include + +#include +#include + +namespace datadog { +namespace tracing { +namespace { + +std::mutex& otel_ctx_mutex() { + static std::mutex m; + return m; +} + +} // namespace + +OtelCtxGuard::~OtelCtxGuard() { + std::lock_guard lock(otel_ctx_mutex()); + otel_process_ctx_drop_current(); +} + +std::unique_ptr publish_otel_process_ctx( + const otel_process_ctx_data& data, Logger& logger) { + std::lock_guard lock(otel_ctx_mutex()); + const auto result = otel_process_ctx_publish(&data); + if (!result.success) { + logger.log_error([&](std::ostream& log) { + log << "Failed to publish OpenTelemetry process context: " + << result.error_message; + }); + return nullptr; + } + return std::make_unique(); +} + +} // namespace tracing +} // namespace datadog diff --git a/src/datadog/otel_process_ctx_guard.h b/src/datadog/otel_process_ctx_guard.h new file mode 100644 index 000000000..e35c26023 --- /dev/null +++ b/src/datadog/otel_process_ctx_guard.h @@ -0,0 +1,32 @@ +#pragma once + +#include + +#include "otel_process_ctx.h" + +namespace datadog { +namespace tracing { + +class Logger; + +// RAII handle for a published OpenTelemetry process context. Destroying the +// guard drops the process-wide context. +// +// Multi-instance behavior: the OTel context is a per-process singleton. +// A successful publish replaces any previously-published context, and +// destroying any guard drops whatever is current (last writer wins style). +// +// This global state is serialized via an internal mutex so this class is +// thread-safe. +class OtelCtxGuard { + public: + ~OtelCtxGuard(); +}; + +// Publish the OTel process context. Returns a guard whose destructor drops +// the context, or nullptr on failure (errors are logged via `logger`). +std::unique_ptr publish_otel_process_ctx( + const otel_process_ctx_data& data, Logger& logger); + +} // namespace tracing +} // namespace datadog diff --git a/src/datadog/tracer.cpp b/src/datadog/tracer.cpp index bf72f9592..f00166ea1 100644 --- a/src/datadog/tracer.cpp +++ b/src/datadog/tracer.cpp @@ -22,7 +22,7 @@ #include "hex.h" #include "json.hpp" #include "msgpack.h" -#include "otel_process_ctx.h" +#include "otel_process_ctx_guard.h" #include "platform_util.h" #include "random.h" #include "root_session_id.h" @@ -121,7 +121,9 @@ Tracer::Tracer(const FinalizedTracerConfig& config, store_config(process_tags); } -Tracer::~Tracer() { otel_process_ctx_drop_current(); } +Tracer::~Tracer() = default; +Tracer::Tracer(Tracer&&) noexcept = default; +Tracer& Tracer::operator=(Tracer&&) noexcept = default; std::string Tracer::config() const { // clang-format off @@ -231,13 +233,7 @@ void Tracer::store_config( otel_data.extra_attributes = extra_attrs; otel_data.thread_ctx_config = nullptr; - const auto otel_result = otel_process_ctx_publish(&otel_data); - if (!otel_result.success) { - logger_->log_error([&](std::ostream& log) { - log << "Failed to publish OpenTelemetry process context: " - << otel_result.error_message; - }); - } + otel_guard_ = publish_otel_process_ctx(otel_data, *logger_); #endif } From 0575cdea1ae78c42054f5eb970a5ee47d52fc6db Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Fri, 19 Jun 2026 15:50:46 +0000 Subject: [PATCH 18/18] Tighten tests for moving the tracer instance --- test/test_tracer.cpp | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/test/test_tracer.cpp b/test/test_tracer.cpp index 72231115f..9c4a7da49 100644 --- a/test/test_tracer.cpp +++ b/test/test_tracer.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include "matchers.h" @@ -1882,8 +1883,16 @@ TEST_TRACER("heterogeneous extraction") { REQUIRE(writer.items == test_case.expected_injected_headers); } -TEST_TRACER("move semantics") { - // Verify that `Tracer` can be moved. +TEST_TRACER("move-only semantics") { + static_assert(std::is_move_constructible::value, + "Tracer must be move-constructible"); + static_assert(std::is_move_assignable::value, + "Tracer must be move-assignable"); + static_assert(!std::is_copy_constructible::value, + "Tracer must not be copy-constructible"); + static_assert(!std::is_copy_assignable::value, + "Tracer must not be copy-assignable"); + TracerConfig config; config.service = "testsvc"; config.logger = std::make_shared(); @@ -1891,11 +1900,11 @@ TEST_TRACER("move semantics") { auto finalized_config = finalize_config(config); REQUIRE(finalized_config); - Tracer tracer1{*finalized_config}; - // This must compile. + // The moved-into Tracer must remain usable. + Tracer tracer1{*finalized_config}; Tracer tracer2{std::move(tracer1)}; - (void)tracer2; + { auto span = tracer2.create_span(); } } TEST_TRACER("APM tracing disabled") {