From df68e4d2cb967655ea87c1f9ace179d36797644a Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Fri, 22 Aug 2025 11:37:21 +0100 Subject: [PATCH 1/4] [PROF-12372] Hack for experimental support for publishing process context for fullhost profiler **What does this PR do?** This PR imports the experimental code from https://github.com/DataDog/fullhost-code-hotspots-wip/pull/2 (Datadog-internal repo for now!) into Ruby so we can test out the code on Ruby apps easily. See that original PR for more details. **Motivation:** I'm opening this draft PR so I can add this to test apps in our reliability environment a bit more easily. **Additional Notes:** Since this is a quick hack, I piggy-backed on the existing process discovery support, rather than separating it out too much. **How to test the change?** The PR linked above has a script that can be used to read the data. Here's how it looks for me: ``` $ sudo otel_process_ctx_dump.sh 205306 Found OTEL context for PID 205306 Start address: 7f0f25efd000 00000000 4f 54 45 4c 5f 43 54 58 01 00 00 00 93 00 00 00 |OTEL_CTX........| 00000010 60 8a 1c 91 9c 5c 00 00 |`....\..| 00000018 Parsed struct: otel_process_ctx_signature : "OTEL_CTX" otel_process_ctx_version : 1 otel_process_payload_size : 147 otel_process_payload : 0x00005c9c911c8a60 Payload dump (147 bytes): 00000000 de 00 03 da 00 0c 73 65 72 76 69 63 65 2e 6e 61 |......service.na| 00000010 6d 65 da 00 11 74 65 73 74 2d 73 65 72 76 69 63 |me...test-servic| 00000020 65 2d 6e 61 6d 65 da 00 13 73 65 72 76 69 63 65 |e-name...service| 00000030 2e 69 6e 73 74 61 6e 63 65 2e 69 64 da 00 24 64 |.instance.id..$d| 00000040 35 30 63 33 33 63 30 2d 31 37 39 33 2d 34 37 63 |50c33c0-1793-47c| 00000050 30 2d 61 38 63 62 2d 65 38 36 62 38 62 36 61 35 |0-a8cb-e86b8b6a5| 00000060 38 62 36 da 00 1b 64 65 70 6c 6f 79 6d 65 6e 74 |8b6...deployment| 00000070 2e 65 6e 76 69 72 6f 6e 6d 65 6e 74 2e 6e 61 6d |.environment.nam| 00000080 65 da 00 0f 74 68 69 73 2d 69 73 2d 74 68 65 2d |e...this-is-the-| 00000090 65 6e 76 |env| 00000093 ``` --- ext/libdatadog_api/otel_process_ctx.c | 371 +++++++++++++++++++++++++ ext/libdatadog_api/otel_process_ctx.h | 115 ++++++++ ext/libdatadog_api/process_discovery.c | 31 +++ 3 files changed, 517 insertions(+) create mode 100644 ext/libdatadog_api/otel_process_ctx.c create mode 100644 ext/libdatadog_api/otel_process_ctx.h diff --git a/ext/libdatadog_api/otel_process_ctx.c b/ext/libdatadog_api/otel_process_ctx.c new file mode 100644 index 00000000000..bd0c10f82ac --- /dev/null +++ b/ext/libdatadog_api/otel_process_ctx.c @@ -0,0 +1,371 @@ +#include "otel_process_ctx.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define ADD_QUOTES_HELPER(x) #x +#define ADD_QUOTES(x) ADD_QUOTES_HELPER(x) + +#ifndef PR_SET_VMA + #define PR_SET_VMA 0x53564d41 + #define PR_SET_VMA_ANON_NAME 0 +#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" + // TODO: Is version useful? Should we just get rid of it? + uint32_t otel_process_ctx_version; // Always > 0, incremented when the data structure changes + // TODO: Is size useful? Should we just get rid of it? + uint32_t otel_process_payload_size; // Always > 0, size of storage + // TODO: Should we just inline the data in the mapping itself? + char *otel_process_payload; // Always non-null, points to the storage for the data; expected to be a msgpack map of string key/value pairs, null-terminated +} otel_process_ctx_mapping; + +/** + * The full state of a published process context. + * + * This is returned as an opaque type to the caller. + * + * It is used to store the all data for the process context and that needs to be kept around while the context is published. + */ +struct otel_process_ctx_state { + // 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; +}; + +static otel_process_ctx_result otel_process_ctx_encode_payload(char **out, uint32_t *out_size, otel_process_ctx_data data); + +// The `volatile` isn't strictly needed here but saves on a few casts below. +static void otel_process_ctx_state_drop(volatile otel_process_ctx_state *state) { + free(state->payload); + free((void *) state); +} + +// 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(otel_process_ctx_data data) { + volatile otel_process_ctx_state *state = calloc(1, sizeof(otel_process_ctx_state)); + if (!state) { + return (otel_process_ctx_result) {.success = false, .error_message = "Failed to allocate state (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } + + state->publisher_pid = getpid(); + + // 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_payload((char **)&state->payload, &payload_size, data); + if (!result.success) { + otel_process_ctx_state_drop(state); + return result; + } + + // Step: Create the mapping + otel_process_ctx_mapping *mapping = + mmap(NULL, sizeof(otel_process_ctx_mapping), PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapping == MAP_FAILED) { + otel_process_ctx_state_drop(state); + 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(mapping, sizeof(otel_process_ctx_mapping), MADV_DONTFORK) == -1) { + otel_process_ctx_state_drop(state); + + if (munmap(mapping, sizeof(otel_process_ctx_mapping)) == -1) { + return (otel_process_ctx_result) {.success = false, .error_message = "Failed to unmap mapping (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } else { + return (otel_process_ctx_result) {.success = false, .error_message = "Failed to setup MADV_DONTFORK (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } + } + + // (Store the mapping in the `volatile` state and stop using the local variable to force ordering below) + state->mapping = mapping; + mapping = NULL; + + // Step: Populate the mapping + // The payload and any extra fields must come first and not be reordered with the signature by the compiler. + // (In this implementation we guarantee this because `state` is declared `volatile`.) + *state->mapping = (otel_process_ctx_mapping) { + .otel_process_ctx_version = 1, + .otel_process_payload_size = payload_size, + .otel_process_payload = state->payload + }; + + // Step: Populate the signature into the mapping + // The signature must come last and not be reordered with the fields above by the compiler. After this step, external readers + // can read the signature and know that the payload is ready to be read. + memcpy(state->mapping->otel_process_ctx_signature, "OTEL_CTX", sizeof(state->mapping->otel_process_ctx_signature)); + + // TODO: Do we like this and want to keep it? + // Optional step: Change permissions on the mapping to only read permission + // We've observed the combination of anonymous mapping + single page + read-only permission is not very common, + // so this is left as a hint for when running on older kernels and the naming the mapping feature below isn't available. + // For modern kernels, doing this is harmless so we do it unconditionally. + if (mprotect(state->mapping, sizeof(otel_process_ctx_mapping), PROT_READ) == -1) { + otel_process_ctx_state_drop(state); + + if (munmap(state->mapping, sizeof(otel_process_ctx_mapping)) == -1) { + return (otel_process_ctx_result) {.success = false, .error_message = "Failed to unmap mapping (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } else { + return (otel_process_ctx_result) {.success = false, .error_message = "Failed to change permissions on mapping (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } + } + + // Step: 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, state->mapping, sizeof(otel_process_ctx_mapping), "OTEL_CTX") == -1) { + // Naming an anonymous mapping is a Linux 5.17+ feature. On earlier versions, this method call can fail. Thus it's OK + // for this to fail because: + // 1. Things that hook on prctl are still able to see this call, even though it's not supported (TODO: Confirm this is actually the case) + // 2. As a fallback, on older kernels, it's possible to scan the mappings and look for the "OTEL_CTX" signature in the memory itself, + // after observing the mapping has the expected size and permissions. + } + + // All done! + + return (otel_process_ctx_result) {.success = true, .published_context = (otel_process_ctx_state *) state}; +} + +otel_process_ctx_result otel_process_ctx_update(otel_process_ctx_result *previous, otel_process_ctx_data data) { + if (!otel_process_ctx_drop(previous)) { + return (otel_process_ctx_result) {.success = false, .error_message = "Failed to drop previous context (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } + + return otel_process_ctx_publish(data); +} + +bool otel_process_ctx_drop(otel_process_ctx_result *previous) { + if (!previous || !previous->success || !previous->published_context) { + return false; + } + + // 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 (getpid() == previous->published_context->publisher_pid) { + if (munmap(previous->published_context->mapping, sizeof(otel_process_ctx_mapping)) == -1) { + return false; + } + } + + otel_process_ctx_state_drop(previous->published_context); + previous->published_context = NULL; + + // Just to be nice to the caller, reset these as well + previous->success = false; + previous->error_message = "Context dropped"; + + return true; +} + +// TODO: The serialization format is still under discussion and is not considered stable yet. +// +// Encode the payload as a msgpack map of string key/value pairs. +// +// This method implements an extremely compact but limited msgpack encoder. This encoder supports only encoding a single +// flat key-value map where every key and value is a string. +// For extra compact code, it uses only a "map 16" encoding format with only "str 16" strings, rather than attempting to +// use some of the other encoding alternatives. +static otel_process_ctx_result otel_process_ctx_encode_payload(char **out, uint32_t *out_size, otel_process_ctx_data data) { + const char *pairs[][2] = { + {"service.name", data.service_name}, + {"service.instance.id", data.service_instance_id}, + {"deployment.environment.name", data.deployment_environment_name} + }; + + const size_t num_pairs = sizeof(pairs) / sizeof(pairs[0]); + + // Validate + calculate size of payload + size_t total_size = 1 + 2; // map 16 header (1 byte + 2 bytes for count) + for (size_t i = 0; i < num_pairs; i++) { + size_t key_len = strlen(pairs[i][0]); + if (pairs[i][1] == NULL) { + return (otel_process_ctx_result) {.success = false, .error_message = "Value in otel_process_ctx_data is NULL (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } + size_t value_len = strlen(pairs[i][1]); + if (value_len > INT16_MAX) { + // Keys are hardcoded above so we know they have a valid length + return (otel_process_ctx_result) {.success = false, .error_message = "Length of value in otel_process_ctx_data exceeds INT16_MAX limit (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } + total_size += 1 + 2 + key_len; // str 16 for key + total_size += 1 + 2 + value_len; // str 16 for value + } + + char *encoded = 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; + + // Write map 16 header (0xde) followed by count + *ptr++ = 0xde; + *ptr++ = (num_pairs >> 8) & 0xFF; // high byte of count + *ptr++ = num_pairs & 0xFF; // low byte of count + + for (size_t i = 0; i < num_pairs; i++) { + size_t key_len = strlen(pairs[i][0]); + size_t value_len = strlen(pairs[i][1]); + + // Write key as str 16 + *ptr++ = 0xda; + *ptr++ = (key_len >> 8) & 0xFF; // high byte of length + *ptr++ = key_len & 0xFF; // low byte of length + memcpy(ptr, pairs[i][0], key_len); + ptr += key_len; + + // Write value as str 16 + *ptr++ = 0xda; + *ptr++ = (value_len >> 8) & 0xFF; // high byte of length + *ptr++ = value_len & 0xFF; // low byte of length + memcpy(ptr, pairs[i][1], value_len); + ptr += value_len; + } + + *out = encoded; + *out_size = (uint32_t) total_size; + + return (otel_process_ctx_result) {.success = true }; +} + +#ifndef OTEL_PROCESS_CTX_NO_READ + // Note: The below parsing code is only for otel_process_ctx_read and is only provided for debugging + // and testing purposes. + + static bool is_otel_process_ctx_mapping(char *line) { + size_t name_len = sizeof("[anon:OTEL_CTX]") - 1; + size_t line_len = strlen(line); + if (line_len < name_len) return false; + if (line[line_len-1] == '\n') line[--line_len] = '\0'; + return memcmp(line + (line_len - name_len), "[anon:OTEL_CTX]", name_len) == 0; + } + + 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]; + void *result = NULL; + + FILE *fp = fopen("/proc/self/maps", "r"); + if (!fp) return result; + + while (fgets(line, sizeof(line), fp)) { + if (is_otel_process_ctx_mapping(line)) { + result = parse_mapping_start(line); + break; + } + } + + fclose(fp); + return (otel_process_ctx_mapping *) result; + } + + // Simplified msgpack decoder to match the exact encoder above. If the msgpack string doesn't match the encoder, this will + // return false. + static bool otel_process_ctx_decode_payload(char *payload, otel_process_ctx_data *data_out) { + char *ptr = payload; + + // Check map 16 header (0xde) + if ((unsigned char)*ptr++ != 0xde) return false; + + // Read count (2 bytes, big endian) + uint16_t count = ((uint8_t)*ptr << 8) | (uint8_t)*(ptr + 1); + ptr += 2; + + // We expect exactly 3 pairs + if (count != 3) return false; + + // Initialize output data + data_out->service_name = NULL; + data_out->service_instance_id = NULL; + data_out->deployment_environment_name = NULL; + + // Decode each key-value pair + for (int i = 0; i < count; i++) { + // Check str 16 header for key (0xda) + if ((unsigned char)*ptr++ != 0xda) return false; + + // Read key length (2 bytes, big endian) + uint16_t key_len = ((uint8_t)*ptr << 8) | (uint8_t)*(ptr + 1); + ptr += 2; + + // Get pointer to key (not null-terminated) + char *key_not_terminated = ptr; + ptr += key_len; + + // Check str 16 header for value (0xda) + if ((unsigned char)*ptr++ != 0xda) return false; + + // Read value length (2 bytes, big endian) + uint16_t value_len = ((uint8_t)*ptr << 8) | (uint8_t)*(ptr + 1); + ptr += 2; + + // Read value + char *value = malloc(value_len + 1); + if (!value) return false; + memcpy(value, ptr, value_len); + value[value_len] = '\0'; + ptr += value_len; + + // Assign to appropriate field based on key + if (key_len == strlen("service.name") && memcmp(key_not_terminated, "service.name", strlen("service.name")) == 0) { + data_out->service_name = value; + } else if (key_len == strlen("service.instance.id") && memcmp(key_not_terminated, "service.instance.id", strlen("service.instance.id")) == 0) { + data_out->service_instance_id = value; + } else if (key_len == strlen("deployment.environment.name") && memcmp(key_not_terminated, "deployment.environment.name", strlen("deployment.environment.name")) == 0) { + data_out->deployment_environment_name = value; + } else { + // Unknown key, clean up and fail + free(value); + return false; + } + } + + // Verify all required fields were found + return data_out->service_name != NULL && + data_out->service_instance_id != NULL && + data_out->deployment_environment_name != NULL; + } + + 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__) ")"}; + } + + if (strncmp(mapping->otel_process_ctx_signature, "OTEL_CTX", sizeof(mapping->otel_process_ctx_signature)) != 0 || mapping->otel_process_ctx_version != 1) { + return (otel_process_ctx_read_result) {.success = false, .error_message = "Invalid OTEL_CTX signature or version (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } + + otel_process_ctx_data data = {0}; + + if (!otel_process_ctx_decode_payload(mapping->otel_process_payload, &data)) { + return (otel_process_ctx_read_result) {.success = false, .error_message = "Failed to decode payload (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } + + return (otel_process_ctx_read_result) {.success = true, .data = data}; + } +#endif // OTEL_PROCESS_CTX_NO_READ diff --git a/ext/libdatadog_api/otel_process_ctx.h b/ext/libdatadog_api/otel_process_ctx.h new file mode 100644 index 00000000000..117b9bce052 --- /dev/null +++ b/ext/libdatadog_api/otel_process_ctx.h @@ -0,0 +1,115 @@ +#pragma once + +#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. (TODO Link) + * + * This reference implementation is Linux-only, as the specification currently only covers Linux. + */ + +/** + * 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` or + * `otel_process_ctx_update`. 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/service/#service-name + char *service_name; + // https://opentelemetry.io/docs/specs/semconv/registry/attributes/service/#service-instance-id + char *service_instance_id; + // https://opentelemetry.io/docs/specs/semconv/registry/attributes/deployment/#deployment-environment-name + char *deployment_environment_name; +} otel_process_ctx_data; + +/** + * Opaque type representing the state of a published process context. + * + * Internally useful for dropping the context and any memory allocations related to it. + */ +typedef struct otel_process_ctx_state otel_process_ctx_state; + +typedef struct { + bool success; + const char *error_message; // Static strings only, non-NULL if success is false + otel_process_ctx_state *published_context; // Non-NULL if success is true +} 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). + * + * @param data The data to publish. This data is copied into the context and only needs to be valid for the duration of + * the call. + * @return The result of the operation. + */ +otel_process_ctx_result otel_process_ctx_publish(otel_process_ctx_data data); + +/** + * Replaces the previous OpenTelemetry process context with the given data. + * + * This API is usually called 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`) + * + * @param previous The previous context. This context is dropped before the new one is installed. + * 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). + * Must not be `NULL`. + * @param data The data to publish. This data is copied into the context and only needs to be valid for the duration of + * the call. + * @return The result of the operation. + */ +otel_process_ctx_result otel_process_ctx_update(otel_process_ctx_result *previous, otel_process_ctx_data data); + +/** + * Drops the previous OpenTelemetry process context. + * + * @param previous The previous context to drop. This API can be called in a fork of the process that published the + * previous context, to clean memory allocations related to the parent's context (even though the + * context is not carried over into forked processes). + * Must not be `NULL`. + * @return `true` if the context was successfully dropped, `false` otherwise. + */ +bool otel_process_ctx_drop(otel_process_ctx_result *previous); + +#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 allocated using `malloc` and the + * caller is responsible for `free`ing 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); +#endif + +#ifdef __cplusplus +} +#endif diff --git a/ext/libdatadog_api/process_discovery.c b/ext/libdatadog_api/process_discovery.c index 7b701afa197..9225872fc6f 100644 --- a/ext/libdatadog_api/process_discovery.c +++ b/ext/libdatadog_api/process_discovery.c @@ -4,6 +4,7 @@ #include #include "datadog_ruby_common.h" +#include "otel_process_ctx.h" static VALUE _native_store_tracer_metadata(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _self); static VALUE _native_to_rb_int(DDTRACE_UNUSED VALUE _self, VALUE tracer_memfd); @@ -36,6 +37,8 @@ void process_discovery_init(VALUE core_module) { rb_define_singleton_method(process_discovery_class, "_native_close_tracer_memfd", _native_close_tracer_memfd, 2); } +static otel_process_ctx_result otel_ctx = {0}; + static VALUE _native_store_tracer_metadata(int argc, VALUE *argv, VALUE self) { VALUE logger; VALUE options; @@ -60,6 +63,21 @@ static VALUE _native_store_tracer_metadata(int argc, VALUE *argv, VALUE self) { ENFORCE_TYPE(service_env, T_STRING); ENFORCE_TYPE(service_version, T_STRING); + otel_process_ctx_data otel_ctx_data = { + .service_name = RSTRING_PTR(service_name), + .service_instance_id = RSTRING_PTR(runtime_id), + .deployment_environment_name = RSTRING_PTR(service_env), + }; + otel_ctx = !otel_ctx.success ? + otel_process_ctx_publish(otel_ctx_data) : + otel_process_ctx_update(&otel_ctx, otel_ctx_data); + + if (otel_ctx.success) { + rb_funcall(logger, rb_intern("info"), 1, rb_str_new2("OTEL process context publish successful")); + } else { + rb_funcall(logger, rb_intern("info"), 1, rb_sprintf("OTEL process context publish failed: %s", otel_ctx.error_message)); + } + ddog_Result_TracerMemfdHandle result = ddog_store_tracer_metadata( (uint8_t) NUM2UINT(schema_version), char_slice_from_ruby_string(runtime_id), @@ -95,6 +113,19 @@ static VALUE _native_to_rb_int(DDTRACE_UNUSED VALUE _self, VALUE tracer_memfd) { static VALUE _native_close_tracer_memfd(DDTRACE_UNUSED VALUE _self, VALUE tracer_memfd, VALUE logger) { int *fd; TypedData_Get_Struct(tracer_memfd, int, &tracer_memfd_type, fd); + + if (otel_ctx.success) { + bool result = otel_process_ctx_drop(&otel_ctx); + otel_ctx.success = false; + if (!result) { + rb_funcall(logger, rb_intern("info"), 1, rb_str_new2("OTEL process context drop failed")); + } else { + rb_funcall(logger, rb_intern("info"), 1, rb_str_new2("OTEL process context drop successful")); + } + } else { + rb_funcall(logger, rb_intern("info"), 1, rb_str_new2("OTEL process context drop no-op")); + } + if (*fd == -1) { rb_funcall(logger, rb_intern("debug"), 1, rb_sprintf("The tracer configuration memory file descriptor has already been closed")); return Qnil; From c90f267da1dc9cfb32bb3bbbd3dae9b2700f487f Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Fri, 22 Aug 2025 14:38:18 +0100 Subject: [PATCH 2/4] Hack: Make sure runtime-id is correct on forks and reconfiguration The semantics for `shutdown!` on reconfiguration are a bit different from what I expected so the code to drop the context wasn't quite correct. In particular, when reconfiguration happens, the new component gets started before the older one shuts down; this is fine generally but since the context is a singleton it means my approach of dropping on `shutdown!` was not correct as it was tearing down the context after updating it. As a simplification, let's never drop the context. + Also fix updating the runtime-id on forks; the process discovery module actually is incorrect as it wasn't handling this, and so I did a bit of a heavy-handed thingy to fix. --- ext/libdatadog_api/process_discovery.c | 43 +++++++++++++++----------- lib/datadog/core/process_discovery.rb | 17 ++++++++++ sig/datadog/core/process_discovery.rbs | 9 ++++++ 3 files changed, 51 insertions(+), 18 deletions(-) diff --git a/ext/libdatadog_api/process_discovery.c b/ext/libdatadog_api/process_discovery.c index 9225872fc6f..1a7bd50590d 100644 --- a/ext/libdatadog_api/process_discovery.c +++ b/ext/libdatadog_api/process_discovery.c @@ -27,6 +27,27 @@ static const rb_data_type_t tracer_memfd_type = { .flags = RUBY_TYPED_FREE_IMMEDIATELY }; +static otel_process_ctx_result otel_ctx = {0}; + +static VALUE _native_publish_otel_ctx_on_fork(VALUE _self, VALUE service_name, VALUE runtime_id, VALUE service_env, VALUE logger) { + otel_process_ctx_data otel_ctx_data = { + .service_name = RSTRING_PTR(service_name), + .service_instance_id = RSTRING_PTR(runtime_id), + .deployment_environment_name = RSTRING_PTR(service_env), + }; + otel_ctx = !otel_ctx.success ? + otel_process_ctx_publish(otel_ctx_data) : + otel_process_ctx_update(&otel_ctx, otel_ctx_data); + + if (otel_ctx.success) { + rb_funcall(logger, rb_intern("info"), 1, rb_str_new2("OTEL process context publish successful")); + return Qtrue; + } else { + rb_funcall(logger, rb_intern("info"), 1, rb_sprintf("OTEL process context publish failed: %s", otel_ctx.error_message)); + return Qfalse; + } +} + void process_discovery_init(VALUE core_module) { VALUE process_discovery_class = rb_define_class_under(core_module, "ProcessDiscovery", rb_cObject); VALUE tracer_memfd_class = rb_define_class_under(process_discovery_class, "TracerMemfd", rb_cObject); @@ -35,10 +56,9 @@ void process_discovery_init(VALUE core_module) { rb_define_singleton_method(process_discovery_class, "_native_store_tracer_metadata", _native_store_tracer_metadata, -1); rb_define_singleton_method(process_discovery_class, "_native_to_rb_int", _native_to_rb_int, 1); rb_define_singleton_method(process_discovery_class, "_native_close_tracer_memfd", _native_close_tracer_memfd, 2); + rb_define_singleton_method(process_discovery_class, "_native_publish_otel_ctx_on_fork", _native_publish_otel_ctx_on_fork, 4); } -static otel_process_ctx_result otel_ctx = {0}; - static VALUE _native_store_tracer_metadata(int argc, VALUE *argv, VALUE self) { VALUE logger; VALUE options; @@ -63,20 +83,7 @@ static VALUE _native_store_tracer_metadata(int argc, VALUE *argv, VALUE self) { ENFORCE_TYPE(service_env, T_STRING); ENFORCE_TYPE(service_version, T_STRING); - otel_process_ctx_data otel_ctx_data = { - .service_name = RSTRING_PTR(service_name), - .service_instance_id = RSTRING_PTR(runtime_id), - .deployment_environment_name = RSTRING_PTR(service_env), - }; - otel_ctx = !otel_ctx.success ? - otel_process_ctx_publish(otel_ctx_data) : - otel_process_ctx_update(&otel_ctx, otel_ctx_data); - - if (otel_ctx.success) { - rb_funcall(logger, rb_intern("info"), 1, rb_str_new2("OTEL process context publish successful")); - } else { - rb_funcall(logger, rb_intern("info"), 1, rb_sprintf("OTEL process context publish failed: %s", otel_ctx.error_message)); - } + _native_publish_otel_ctx_on_fork(Qnil, service_name, runtime_id, service_env, logger); ddog_Result_TracerMemfdHandle result = ddog_store_tracer_metadata( (uint8_t) NUM2UINT(schema_version), @@ -114,7 +121,7 @@ static VALUE _native_close_tracer_memfd(DDTRACE_UNUSED VALUE _self, VALUE tracer int *fd; TypedData_Get_Struct(tracer_memfd, int, &tracer_memfd_type, fd); - if (otel_ctx.success) { + /*if (otel_ctx.success) { bool result = otel_process_ctx_drop(&otel_ctx); otel_ctx.success = false; if (!result) { @@ -124,7 +131,7 @@ static VALUE _native_close_tracer_memfd(DDTRACE_UNUSED VALUE _self, VALUE tracer } } else { rb_funcall(logger, rb_intern("info"), 1, rb_str_new2("OTEL process context drop no-op")); - } + }*/ if (*fd == -1) { rb_funcall(logger, rb_intern("debug"), 1, rb_sprintf("The tracer configuration memory file descriptor has already been closed")); diff --git a/lib/datadog/core/process_discovery.rb b/lib/datadog/core/process_discovery.rb index 204840f52a1..9dd6e836b38 100644 --- a/lib/datadog/core/process_discovery.rb +++ b/lib/datadog/core/process_discovery.rb @@ -1,11 +1,15 @@ # frozen_string_literal: true require 'datadog/core/process_discovery/tracer_memfd' +require 'datadog/core/utils/at_fork_monkey_patch' +require 'datadog/core/utils/only_once' module Datadog module Core # Class used to store tracer metadata in a native file descriptor. class ProcessDiscovery + ACTIVATE_FORKING_PATCH = Core::Utils::OnlyOnce.new + def self.get_and_store_metadata(settings, logger) if (libdatadog_api_failure = Datadog::Core::LIBDATADOG_API_FAILURE) logger.debug("Cannot enable process discovery: #{libdatadog_api_failure}") @@ -14,6 +18,19 @@ def self.get_and_store_metadata(settings, logger) metadata = get_metadata(settings) memfd = _native_store_tracer_metadata(logger, **metadata) memfd.logger = logger if memfd + + ACTIVATE_FORKING_PATCH.run do + Datadog::Core::Utils::AtForkMonkeyPatch.at_fork(:child) do + settings = Datadog.configuration + Datadog::Core::ProcessDiscovery._native_publish_otel_ctx_on_fork( + settings.service || '', + Core::Environment::Identity.id, + settings.env || '', + Datadog.logger, + ) + end + end + memfd end diff --git a/sig/datadog/core/process_discovery.rbs b/sig/datadog/core/process_discovery.rbs index 61675c59e0d..14188833a9e 100644 --- a/sig/datadog/core/process_discovery.rbs +++ b/sig/datadog/core/process_discovery.rbs @@ -1,6 +1,8 @@ module Datadog module Core class ProcessDiscovery + ACTIVATE_FORKING_PATCH: Datadog::Core::Utils::OnlyOnce + # defined in C. struct containing int fd. class TracerMemfd end @@ -19,6 +21,13 @@ module Datadog def self._native_close_tracer_memfd: (TracerMemfd tracer_memfd, Datadog::Core::Logger logger) -> void + def self._native_publish_otel_ctx_on_fork: ( + String service_name, + String runtime_id, + String service_env, + Datadog::Core::Logger logger + ) -> bool + def self.get_and_store_metadata: ( Datadog::Core::Configuration::Settings settings, Datadog::Core::Logger logger From f047c4f39a924bb20319a09345d0ba6553e1d647 Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Fri, 29 Aug 2025 09:33:19 +0100 Subject: [PATCH 3/4] Import latest version of process context library Taken from https://github.com/DataDog/fullhost-code-hotspots-wip/tree/b33673d801b85a6c38fa0e9f1a139cb246737ce8/lang-exp/anonmapping-clib The ruby side of the code has not been updated yet; I'll do it in the next commit. --- ext/libdatadog_api/otel_process_ctx.c | 393 +++++++++++++++++--------- ext/libdatadog_api/otel_process_ctx.h | 108 ++++--- 2 files changed, 334 insertions(+), 167 deletions(-) diff --git a/ext/libdatadog_api/otel_process_ctx.c b/ext/libdatadog_api/otel_process_ctx.c index bd0c10f82ac..d1cd08c1822 100644 --- a/ext/libdatadog_api/otel_process_ctx.c +++ b/ext/libdatadog_api/otel_process_ctx.c @@ -1,7 +1,13 @@ #include "otel_process_ctx.h" -#include #include +#ifdef __cplusplus + #include + using std::atomic_thread_fence; + using std::memory_order_seq_cst; +#else + #include +#endif #include #include #include @@ -18,6 +24,42 @@ #define PR_SET_VMA_ANON_NAME 0 #endif +static const otel_process_ctx_data empty_data = { + .deployment_environment_name = NULL, + .host_name = NULL, + .service_instance_id = NULL, + .service_name = NULL, + .service_version = NULL, + .telemetry_sdk_language = NULL, + .telemetry_sdk_version = NULL, + .telemetry_sdk_name = NULL, + .resources = 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(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 + /** * The process context data that's written into the published anonymous mapping. * @@ -40,7 +82,7 @@ typedef struct __attribute__((packed, aligned(8))) { * * It is used to store the all data for the process context and that needs to be kept around while the context is published. */ -struct otel_process_ctx_state { +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 @@ -48,136 +90,170 @@ struct otel_process_ctx_state { 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_encode_payload(char **out, uint32_t *out_size, otel_process_ctx_data data); -// The `volatile` isn't strictly needed here but saves on a few casts below. -static void otel_process_ctx_state_drop(volatile otel_process_ctx_state *state) { - free(state->payload); - free((void *) state); +// We use a mapping size of 3 pages explicitly as a hint when running on legacy kernels that don't support the +// PR_SET_VMA_ANON_NAME prctl call; see below for more details. +static long size_for_mapping(void) { + long page_size_bytes = sysconf(_SC_PAGESIZE); + if (page_size_bytes < 4096) { + return -1; + } + return page_size_bytes * 2; } // 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(otel_process_ctx_data data) { - volatile otel_process_ctx_state *state = calloc(1, sizeof(otel_process_ctx_state)); - if (!state) { - return (otel_process_ctx_result) {.success = false, .error_message = "Failed to allocate state (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; +otel_process_ctx_result otel_process_ctx_publish(const otel_process_ctx_data *data) { + // Step: Drop any previous context it 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__) ")"}; } - state->publisher_pid = getpid(); + // Step: Determine size for mapping + long mapping_size = size_for_mapping(); + if (mapping_size == -1) { + return (otel_process_ctx_result) {.success = false, .error_message = "Failed to get page size (" __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. + if (!data) return (otel_process_ctx_result) {.success = false, .error_message = "otel_process_ctx_data is NULL (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; uint32_t payload_size = 0; - otel_process_ctx_result result = otel_process_ctx_encode_payload((char **)&state->payload, &payload_size, data); - if (!result.success) { - otel_process_ctx_state_drop(state); - return result; - } + otel_process_ctx_result result = otel_process_ctx_encode_payload(&published_state.payload, &payload_size, *data); + if (!result.success) return result; // Step: Create the mapping - otel_process_ctx_mapping *mapping = - mmap(NULL, sizeof(otel_process_ctx_mapping), PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); - if (mapping == MAP_FAILED) { - otel_process_ctx_state_drop(state); + published_state.publisher_pid = getpid(); // This allows us to detect in forks that we shouldn't touch the mapping + 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) { + otel_process_ctx_drop_current(); 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(mapping, sizeof(otel_process_ctx_mapping), MADV_DONTFORK) == -1) { - otel_process_ctx_state_drop(state); - - if (munmap(mapping, sizeof(otel_process_ctx_mapping)) == -1) { - return (otel_process_ctx_result) {.success = false, .error_message = "Failed to unmap mapping (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; - } else { + 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 previous context (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; } } - // (Store the mapping in the `volatile` state and stop using the local variable to force ordering below) - state->mapping = mapping; - mapping = NULL; - // Step: Populate the mapping // The payload and any extra fields must come first and not be reordered with the signature by the compiler. - // (In this implementation we guarantee this because `state` is declared `volatile`.) - *state->mapping = (otel_process_ctx_mapping) { + *published_state.mapping = (otel_process_ctx_mapping) { + .otel_process_ctx_signature = {0}, // Set in "Step: Populate the signature into the mapping" below .otel_process_ctx_version = 1, .otel_process_payload_size = payload_size, - .otel_process_payload = state->payload + .otel_process_payload = published_state.payload }; + // Step: Synchronization - Mapping has been filled and is missing signature + // Make sure the initialization of the mapping + payload above does not get reordered with setting the signature below. Setting + // the signature is what tells an outside reader that the context is fully published. + atomic_thread_fence(memory_order_seq_cst); + // Step: Populate the signature into the mapping // The signature must come last and not be reordered with the fields above by the compiler. After this step, external readers // can read the signature and know that the payload is ready to be read. - memcpy(state->mapping->otel_process_ctx_signature, "OTEL_CTX", sizeof(state->mapping->otel_process_ctx_signature)); + memcpy(published_state.mapping->otel_process_ctx_signature, "OTEL_CTX", sizeof(published_state.mapping->otel_process_ctx_signature)); - // TODO: Do we like this and want to keep it? - // Optional step: Change permissions on the mapping to only read permission - // We've observed the combination of anonymous mapping + single page + read-only permission is not very common, + // Step: Change permissions on the mapping to only read permission + // We've observed the combination of anonymous mapping + a given number of pages + read-only permission is not very common, // so this is left as a hint for when running on older kernels and the naming the mapping feature below isn't available. // For modern kernels, doing this is harmless so we do it unconditionally. - if (mprotect(state->mapping, sizeof(otel_process_ctx_mapping), PROT_READ) == -1) { - otel_process_ctx_state_drop(state); - - if (munmap(state->mapping, sizeof(otel_process_ctx_mapping)) == -1) { - return (otel_process_ctx_result) {.success = false, .error_message = "Failed to unmap mapping (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; - } else { + if (mprotect(published_state.mapping, mapping_size, PROT_READ) == -1) { + if (otel_process_ctx_drop_current()) { return (otel_process_ctx_result) {.success = false, .error_message = "Failed to change permissions on mapping (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } else { + return (otel_process_ctx_result) {.success = false, .error_message = "Failed to drop previous context (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; } } // Step: 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, state->mapping, sizeof(otel_process_ctx_mapping), "OTEL_CTX") == -1) { + if (prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, published_state.mapping, mapping_size, "OTEL_CTX") == -1) { // Naming an anonymous mapping is a Linux 5.17+ feature. On earlier versions, this method call can fail. Thus it's OK // for this to fail because: // 1. Things that hook on prctl are still able to see this call, even though it's not supported (TODO: Confirm this is actually the case) // 2. As a fallback, on older kernels, it's possible to scan the mappings and look for the "OTEL_CTX" signature in the memory itself, - // after observing the mapping has the expected size and permissions. + // after observing the mapping has the expected number of pages and permissions. } // All done! - return (otel_process_ctx_result) {.success = true, .published_context = (otel_process_ctx_state *) state}; + return (otel_process_ctx_result) {.success = true, .error_message = NULL}; } -otel_process_ctx_result otel_process_ctx_update(otel_process_ctx_result *previous, otel_process_ctx_data data) { - if (!otel_process_ctx_drop(previous)) { - return (otel_process_ctx_result) {.success = false, .error_message = "Failed to drop previous context (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; +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); + + // 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 (state.mapping != NULL && state.mapping != MAP_FAILED && getpid() == state.publisher_pid) { + long mapping_size = size_for_mapping(); + if (mapping_size == -1 || munmap(published_state.mapping, mapping_size) == -1) return false; } - return otel_process_ctx_publish(data); + // 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. + if (state.payload) free(state.payload); + + return true; } -bool otel_process_ctx_drop(otel_process_ctx_result *previous) { - if (!previous || !previous->success || !previous->published_context) { - return false; +static otel_process_ctx_result validate_and_calculate_payload_size(size_t *out_pairs_size, size_t *out_num_pairs, 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_num_pairs = num_entries / 2; - // 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 (getpid() == previous->published_context->publisher_pid) { - if (munmap(previous->published_context->mapping, sizeof(otel_process_ctx_mapping)) == -1) { - return false; + *out_pairs_size = 0; + for (size_t i = 0; i < *out_num_pairs; i++) { + size_t key_len = strlen(pairs[i * 2]); + if (key_len > INT16_MAX) { + return (otel_process_ctx_result) {.success = false, .error_message = "Length of key in otel_process_ctx_data exceeds INT16_MAX limit (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; } + size_t value_len = strlen(pairs[i * 2 + 1]); + if (value_len > INT16_MAX) { + return (otel_process_ctx_result) {.success = false, .error_message = "Length of value in otel_process_ctx_data exceeds INT16_MAX limit (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } + *out_pairs_size += 1 + 2 + key_len; // str 16 for key + *out_pairs_size += 1 + 2 + value_len; // str 16 for value } - otel_process_ctx_state_drop(previous->published_context); - previous->published_context = NULL; - - // Just to be nice to the caller, reset these as well - previous->success = false; - previous->error_message = "Context dropped"; + return (otel_process_ctx_result) {.success = true, .error_message = NULL}; +} - return true; +static void write_msgpack_string(char **ptr, const char *str) { + size_t len = strlen(str); + // Write str 16 header + *(*ptr)++ = 0xda; + *(*ptr)++ = (len >> 8) & 0xFF; // high byte of length + *(*ptr)++ = len & 0xFF; // low byte of length + memcpy(*ptr, str, len); + *ptr += len; } // TODO: The serialization format is still under discussion and is not considered stable yet. +// Comments **very** welcome: Should we use JSON instead? Or protobuf? // // Encode the payload as a msgpack map of string key/value pairs. // @@ -186,31 +262,36 @@ bool otel_process_ctx_drop(otel_process_ctx_result *previous) { // For extra compact code, it uses only a "map 16" encoding format with only "str 16" strings, rather than attempting to // use some of the other encoding alternatives. static otel_process_ctx_result otel_process_ctx_encode_payload(char **out, uint32_t *out_size, otel_process_ctx_data data) { - const char *pairs[][2] = { - {"service.name", data.service_name}, - {"service.instance.id", data.service_instance_id}, - {"deployment.environment.name", data.deployment_environment_name} + const char *pairs[] = { + "deployment.environment.name", data.deployment_environment_name, + "host.name", data.host_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 }; - const size_t num_pairs = sizeof(pairs) / sizeof(pairs[0]); + size_t num_pairs = 0, pairs_size = 0; + otel_process_ctx_result validation_result = validate_and_calculate_payload_size(&pairs_size, &num_pairs, (char **) pairs); + if (!validation_result.success) return validation_result; - // Validate + calculate size of payload - size_t total_size = 1 + 2; // map 16 header (1 byte + 2 bytes for count) - for (size_t i = 0; i < num_pairs; i++) { - size_t key_len = strlen(pairs[i][0]); - if (pairs[i][1] == NULL) { - return (otel_process_ctx_result) {.success = false, .error_message = "Value in otel_process_ctx_data is NULL (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; - } - size_t value_len = strlen(pairs[i][1]); - if (value_len > INT16_MAX) { - // Keys are hardcoded above so we know they have a valid length - return (otel_process_ctx_result) {.success = false, .error_message = "Length of value in otel_process_ctx_data exceeds INT16_MAX limit (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; - } - total_size += 1 + 2 + key_len; // str 16 for key - total_size += 1 + 2 + value_len; // str 16 for value + size_t resources_pairs_size = 0, resources_num_pairs = 0; + if (data.resources != NULL) { + validation_result = validate_and_calculate_payload_size(&resources_pairs_size, &resources_num_pairs, data.resources); + if (!validation_result.success) return validation_result; + } + + size_t total_pairs = num_pairs + resources_num_pairs; + size_t total_size = pairs_size + resources_pairs_size + 1 + 2; // map 16 header (1 byte + 2 bytes for count) + + if (total_pairs > INT16_MAX) { + return (otel_process_ctx_result) {.success = false, .error_message = "Total number of pairs exceeds INT16_MAX limit (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; } - char *encoded = calloc(total_size, 1); + 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__) ")"}; } @@ -218,32 +299,25 @@ static otel_process_ctx_result otel_process_ctx_encode_payload(char **out, uint3 // Write map 16 header (0xde) followed by count *ptr++ = 0xde; - *ptr++ = (num_pairs >> 8) & 0xFF; // high byte of count - *ptr++ = num_pairs & 0xFF; // low byte of count + *ptr++ = (total_pairs >> 8) & 0xFF; // high byte of count + *ptr++ = total_pairs & 0xFF; // low byte of count for (size_t i = 0; i < num_pairs; i++) { - size_t key_len = strlen(pairs[i][0]); - size_t value_len = strlen(pairs[i][1]); - - // Write key as str 16 - *ptr++ = 0xda; - *ptr++ = (key_len >> 8) & 0xFF; // high byte of length - *ptr++ = key_len & 0xFF; // low byte of length - memcpy(ptr, pairs[i][0], key_len); - ptr += key_len; - - // Write value as str 16 - *ptr++ = 0xda; - *ptr++ = (value_len >> 8) & 0xFF; // high byte of length - *ptr++ = value_len & 0xFF; // low byte of length - memcpy(ptr, pairs[i][1], value_len); - ptr += value_len; + write_msgpack_string(&ptr, pairs[i * 2]); // Write key + write_msgpack_string(&ptr, pairs[i * 2 + 1]); // Write value + } + + if (data.resources != NULL) { + for (size_t i = 0; i < resources_num_pairs; i++) { + write_msgpack_string(&ptr, data.resources[i * 2]); // Write key + write_msgpack_string(&ptr, data.resources[i * 2 + 1]); // Write value + } } *out = encoded; *out_size = (uint32_t) total_size; - return (otel_process_ctx_result) {.success = true }; + return (otel_process_ctx_result) {.success = true, .error_message = NULL}; } #ifndef OTEL_PROCESS_CTX_NO_READ @@ -267,20 +341,20 @@ static otel_process_ctx_result otel_process_ctx_encode_payload(char **out, uint3 static otel_process_ctx_mapping *try_finding_mapping(void) { char line[8192]; - void *result = NULL; + otel_process_ctx_mapping *result = NULL; FILE *fp = fopen("/proc/self/maps", "r"); if (!fp) return result; while (fgets(line, sizeof(line), fp)) { if (is_otel_process_ctx_mapping(line)) { - result = parse_mapping_start(line); + result = (otel_process_ctx_mapping *)parse_mapping_start(line); break; } } fclose(fp); - return (otel_process_ctx_mapping *) result; + return result; } // Simplified msgpack decoder to match the exact encoder above. If the msgpack string doesn't match the encoder, this will @@ -295,13 +369,25 @@ static otel_process_ctx_result otel_process_ctx_encode_payload(char **out, uint3 uint16_t count = ((uint8_t)*ptr << 8) | (uint8_t)*(ptr + 1); ptr += 2; - // We expect exactly 3 pairs - if (count != 3) return false; + // We expect at least 8 pairs (the standard fields) + if (count < 8) return false; // Initialize output data - data_out->service_name = NULL; - data_out->service_instance_id = NULL; data_out->deployment_environment_name = NULL; + data_out->host_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; + data_out->resources = NULL; + + // Allocate resources array with space for all pairs as a simplification (2 entries per pair + 1 for NULL terminator) + data_out->resources = (char **) calloc(count * 2 + 1, sizeof(char *)); + if (!data_out->resources) return false; + + int resources_index = 0; // Decode each key-value pair for (int i = 0; i < count; i++) { @@ -324,48 +410,101 @@ static otel_process_ctx_result otel_process_ctx_encode_payload(char **out, uint3 ptr += 2; // Read value - char *value = malloc(value_len + 1); + char *value = (char *) calloc(value_len + 1, 1); if (!value) return false; memcpy(value, ptr, value_len); value[value_len] = '\0'; ptr += value_len; // Assign to appropriate field based on key - if (key_len == strlen("service.name") && memcmp(key_not_terminated, "service.name", strlen("service.name")) == 0) { - data_out->service_name = value; + if (key_len == strlen("deployment.environment.name") && memcmp(key_not_terminated, "deployment.environment.name", strlen("deployment.environment.name")) == 0) { + data_out->deployment_environment_name = value; + } else if (key_len == strlen("host.name") && memcmp(key_not_terminated, "host.name", strlen("host.name")) == 0) { + data_out->host_name = value; } else if (key_len == strlen("service.instance.id") && memcmp(key_not_terminated, "service.instance.id", strlen("service.instance.id")) == 0) { data_out->service_instance_id = value; - } else if (key_len == strlen("deployment.environment.name") && memcmp(key_not_terminated, "deployment.environment.name", strlen("deployment.environment.name")) == 0) { - data_out->deployment_environment_name = value; + } else if (key_len == strlen("service.name") && memcmp(key_not_terminated, "service.name", strlen("service.name")) == 0) { + data_out->service_name = value; + } else if (key_len == strlen("service.version") && memcmp(key_not_terminated, "service.version", strlen("service.version")) == 0) { + data_out->service_version = value; + } else if (key_len == strlen("telemetry.sdk.language") && memcmp(key_not_terminated, "telemetry.sdk.language", strlen("telemetry.sdk.language")) == 0) { + data_out->telemetry_sdk_language = value; + } else if (key_len == strlen("telemetry.sdk.version") && memcmp(key_not_terminated, "telemetry.sdk.version", strlen("telemetry.sdk.version")) == 0) { + data_out->telemetry_sdk_version = value; + } else if (key_len == strlen("telemetry.sdk.name") && memcmp(key_not_terminated, "telemetry.sdk.name", strlen("telemetry.sdk.name")) == 0) { + data_out->telemetry_sdk_name = value; } else { - // Unknown key, clean up and fail - free(value); - return false; + // Unknown key, put it into resources + char *key = (char *) calloc(key_len + 1, 1); + if (!key) { + free(value); + return false; + } + memcpy(key, key_not_terminated, key_len); + key[key_len] = '\0'; + + data_out->resources[resources_index++] = key; + data_out->resources[resources_index++] = value; } } // Verify all required fields were found - return data_out->service_name != NULL && + return data_out->deployment_environment_name != NULL && + data_out->host_name != NULL && data_out->service_instance_id != NULL && - data_out->deployment_environment_name != 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(data.deployment_environment_name); + if (data.host_name) free(data.host_name); + if (data.service_instance_id) free(data.service_instance_id); + if (data.service_name) free(data.service_name); + if (data.service_version) free(data.service_version); + if (data.telemetry_sdk_language) free(data.telemetry_sdk_language); + if (data.telemetry_sdk_version) free(data.telemetry_sdk_version); + if (data.telemetry_sdk_name) free(data.telemetry_sdk_name); + if (data.resources) { + for (int i = 0; data.resources[i] != NULL; i++) free(data.resources[i]); + free(data.resources); + } } 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__) ")"}; + 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", sizeof(mapping->otel_process_ctx_signature)) != 0 || mapping->otel_process_ctx_version != 1) { - return (otel_process_ctx_read_result) {.success = false, .error_message = "Invalid OTEL_CTX signature or version (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + 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 = {0}; + otel_process_ctx_data data = empty_data; if (!otel_process_ctx_decode_payload(mapping->otel_process_payload, &data)) { - return (otel_process_ctx_read_result) {.success = false, .error_message = "Failed to decode payload (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + 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, .data = 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; + + // Free allocated strings in the data + otel_process_ctx_read_data_drop(result->data); + + // Reset the result to empty state + *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/ext/libdatadog_api/otel_process_ctx.h b/ext/libdatadog_api/otel_process_ctx.h index 117b9bce052..fbb1b0a3025 100644 --- a/ext/libdatadog_api/otel_process_ctx.h +++ b/ext/libdatadog_api/otel_process_ctx.h @@ -1,5 +1,10 @@ #pragma once +#define OTEL_PROCESS_CTX_VERSION_MAJOR 0 +#define OTEL_PROCESS_CTX_VERSION_MINOR 0 +#define OTEL_PROCESS_CTX_VERSION_PATCH 5 +#define OTEL_PROCESS_CTX_VERSION_STRING "0.0.5" + #ifdef __cplusplus extern "C" { #endif @@ -13,83 +18,98 @@ extern "C" { * process-level context sharing specification. (TODO Link) * * 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. */ /** * 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` or - * `otel_process_ctx_update`. Strings will be copied into the 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 + * * Non-NULL * * UTF-8 encoded * * Not longer than INT16_MAX bytes * * Strings MAY be: * * Empty + * + * The below fields map to usual datadog attributes as follows (TODO: Remove this once we share the header publicly) + * * deployment_environment_name -> env + * * host_name -> hostname + * * service_instance_id -> runtime-id + * * service_name -> service + * * service_version -> version + * * telemetry_sdk_language -> tracer_language + * * telemetry_sdk_version -> tracer_version + * * telemetry_sdk_name -> name of library (e.g. dd-trace-java) */ typedef struct { - // https://opentelemetry.io/docs/specs/semconv/registry/attributes/service/#service-name - char *service_name; - // https://opentelemetry.io/docs/specs/semconv/registry/attributes/service/#service-instance-id - char *service_instance_id; // https://opentelemetry.io/docs/specs/semconv/registry/attributes/deployment/#deployment-environment-name char *deployment_environment_name; + // https://opentelemetry.io/docs/specs/semconv/registry/attributes/host/#host-name + char *host_name; + // https://opentelemetry.io/docs/specs/semconv/registry/attributes/service/#service-instance-id + char *service_instance_id; + // https://opentelemetry.io/docs/specs/semconv/registry/attributes/service/#service-name + char *service_name; + // https://opentelemetry.io/docs/specs/semconv/registry/attributes/service/#service-version + char *service_version; + // https://opentelemetry.io/docs/specs/semconv/registry/attributes/telemetry/#telemetry-sdk-language + char *telemetry_sdk_language; + // https://opentelemetry.io/docs/specs/semconv/registry/attributes/telemetry/#telemetry-sdk-version + char *telemetry_sdk_version; + // https://opentelemetry.io/docs/specs/semconv/registry/attributes/telemetry/#telemetry-sdk-name + char *telemetry_sdk_name; + // Additional key/value pairs as resources https://opentelemetry.io/docs/specs/otel/resource/sdk/ + // Can be NULL if no resources 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). + char **resources; } otel_process_ctx_data; -/** - * Opaque type representing the state of a published process context. - * - * Internally useful for dropping the context and any memory allocations related to it. - */ -typedef struct otel_process_ctx_state otel_process_ctx_state; +/** 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_state *published_context; // Non-NULL if success is true } 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. * - * @param data The data to publish. This data is copied into the context and only needs to be valid for the duration of - * the call. - * @return The result of the operation. - */ -otel_process_ctx_result otel_process_ctx_publish(otel_process_ctx_data data); - -/** - * Replaces the previous OpenTelemetry process context with the given data. - * - * This API is usually called when: + * 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`) * - * @param previous The previous context. This context is dropped before the new one is installed. - * 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). - * Must not be `NULL`. - * @param data The data to publish. This data is copied into the context and only needs to be valid for the duration of - * the call. + * 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_update(otel_process_ctx_result *previous, otel_process_ctx_data data); +otel_process_ctx_result otel_process_ctx_publish(const otel_process_ctx_data *data); /** - * Drops the previous OpenTelemetry process context. + * 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). * - * @param previous The previous context to drop. This API can be called in a fork of the process that published the - * previous context, to clean memory allocations related to the parent's context (even though the - * context is not carried over into forked processes). - * Must not be `NULL`. - * @return `true` if the context was successfully dropped, `false` otherwise. + * @return `true` if the context was successfully dropped or no context existed, `false` otherwise. */ -bool otel_process_ctx_drop(otel_process_ctx_result *previous); +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; @@ -100,14 +120,22 @@ bool otel_process_ctx_drop(otel_process_ctx_result *previous); /** * Reads the current OpenTelemetry process context, if any. * - * Useful for debugging and testing purposes. Underlying returned strings in `data` are allocated using `malloc` and the - * caller is responsible for `free`ing them. + * 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 From de461446289018008f968f6c3f65b764f3b6277b Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Fri, 29 Aug 2025 09:44:14 +0100 Subject: [PATCH 4/4] Update APIs to match latest process context library --- ext/libdatadog_api/process_discovery.c | 27 ++++++++++++++------------ sig/datadog/core/process_discovery.rbs | 7 +++++-- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/ext/libdatadog_api/process_discovery.c b/ext/libdatadog_api/process_discovery.c index 1a7bd50590d..2d7e248e16c 100644 --- a/ext/libdatadog_api/process_discovery.c +++ b/ext/libdatadog_api/process_discovery.c @@ -27,23 +27,26 @@ static const rb_data_type_t tracer_memfd_type = { .flags = RUBY_TYPED_FREE_IMMEDIATELY }; -static otel_process_ctx_result otel_ctx = {0}; - -static VALUE _native_publish_otel_ctx_on_fork(VALUE _self, VALUE service_name, VALUE runtime_id, VALUE service_env, VALUE logger) { +static VALUE _native_publish_otel_ctx_on_fork(VALUE _self, VALUE env, VALUE hostname, VALUE runtime_id, VALUE service, VALUE version, VALUE tracer_version, VALUE logger) { otel_process_ctx_data otel_ctx_data = { - .service_name = RSTRING_PTR(service_name), + .deployment_environment_name = RSTRING_PTR(env), + .host_name = RSTRING_PTR(hostname), .service_instance_id = RSTRING_PTR(runtime_id), - .deployment_environment_name = RSTRING_PTR(service_env), + .service_name = RSTRING_PTR(service), + .service_version = RSTRING_PTR(version), + .telemetry_sdk_language = (char *) "ruby", + .telemetry_sdk_version = RSTRING_PTR(tracer_version), + .telemetry_sdk_name = (char *) "dd-trace-rb", + .resources = NULL, // TODO: Not supported yet for Ruby }; - otel_ctx = !otel_ctx.success ? - otel_process_ctx_publish(otel_ctx_data) : - otel_process_ctx_update(&otel_ctx, otel_ctx_data); - if (otel_ctx.success) { + otel_process_ctx_result result = otel_process_ctx_publish(&otel_ctx_data); + + if (result.success) { rb_funcall(logger, rb_intern("info"), 1, rb_str_new2("OTEL process context publish successful")); return Qtrue; } else { - rb_funcall(logger, rb_intern("info"), 1, rb_sprintf("OTEL process context publish failed: %s", otel_ctx.error_message)); + rb_funcall(logger, rb_intern("info"), 1, rb_sprintf("OTEL process context publish failed: %s", result.error_message)); return Qfalse; } } @@ -56,7 +59,7 @@ void process_discovery_init(VALUE core_module) { rb_define_singleton_method(process_discovery_class, "_native_store_tracer_metadata", _native_store_tracer_metadata, -1); rb_define_singleton_method(process_discovery_class, "_native_to_rb_int", _native_to_rb_int, 1); rb_define_singleton_method(process_discovery_class, "_native_close_tracer_memfd", _native_close_tracer_memfd, 2); - rb_define_singleton_method(process_discovery_class, "_native_publish_otel_ctx_on_fork", _native_publish_otel_ctx_on_fork, 4); + rb_define_singleton_method(process_discovery_class, "_native_publish_otel_ctx_on_fork", _native_publish_otel_ctx_on_fork, 7); } static VALUE _native_store_tracer_metadata(int argc, VALUE *argv, VALUE self) { @@ -83,7 +86,7 @@ static VALUE _native_store_tracer_metadata(int argc, VALUE *argv, VALUE self) { ENFORCE_TYPE(service_env, T_STRING); ENFORCE_TYPE(service_version, T_STRING); - _native_publish_otel_ctx_on_fork(Qnil, service_name, runtime_id, service_env, logger); + _native_publish_otel_ctx_on_fork(Qnil, service_env, hostname, runtime_id, service_name, service_version, tracer_version, logger); ddog_Result_TracerMemfdHandle result = ddog_store_tracer_metadata( (uint8_t) NUM2UINT(schema_version), diff --git a/sig/datadog/core/process_discovery.rbs b/sig/datadog/core/process_discovery.rbs index 14188833a9e..2044c1df5d8 100644 --- a/sig/datadog/core/process_discovery.rbs +++ b/sig/datadog/core/process_discovery.rbs @@ -22,9 +22,12 @@ module Datadog def self._native_close_tracer_memfd: (TracerMemfd tracer_memfd, Datadog::Core::Logger logger) -> void def self._native_publish_otel_ctx_on_fork: ( - String service_name, + String env, + String hostname, String runtime_id, - String service_env, + String service, + String version, + String tracer_version, Datadog::Core::Logger logger ) -> bool