diff --git a/process-context/README.md b/process-context/README.md new file mode 100644 index 0000000..ca4413c --- /dev/null +++ b/process-context/README.md @@ -0,0 +1,5 @@ +# OpenTelemetry Process Context (Under development) + +This folder contains experimental implementations for the specification proposed in https://github.com/open-telemetry/opentelemetry-specification/pull/4719 . + +The process context OTEP introduces a standard mechanism for OpenTelemetry SDKs to publish process-level resource attributes for access by out-of-process readers such as the OpenTelemetry eBPF Profiler. diff --git a/process-context/c-and-cpp/.gitignore b/process-context/c-and-cpp/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/process-context/c-and-cpp/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/process-context/c-and-cpp/CMakeLists.txt b/process-context/c-and-cpp/CMakeLists.txt new file mode 100644 index 0000000..ed480f7 --- /dev/null +++ b/process-context/c-and-cpp/CMakeLists.txt @@ -0,0 +1,111 @@ +cmake_minimum_required(VERSION 3.10) +project(otel_process_ctx C CXX) + +# Set C standard +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) + +# Set C++ standard for C++ variants +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Note: We build here with both a C compiler as well as a C++ compiler because this implementation is expected to be +# used with either, and we want to make sure we build cleanly on either (so we don't accidentally add C-isms that C++ +# doesn't like, or vice-versa) + +# Compiler flags +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -O2 -fPIC -ggdb3") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -O2 -fPIC -ggdb3") + +# Source files +set(SOURCES otel_process_ctx.c) +configure_file(otel_process_ctx.c otel_process_ctx.cpp COPYONLY) +set(SOURCES_CXX ${CMAKE_CURRENT_BINARY_DIR}/otel_process_ctx.cpp) + +# Create shared library +add_library(otel_process_ctx SHARED ${SOURCES}) +set_target_properties(otel_process_ctx PROPERTIES + OUTPUT_NAME "otel_process_ctx" +) + +# Create static library +add_library(otel_process_ctx_static STATIC ${SOURCES}) +set_target_properties(otel_process_ctx_static PROPERTIES + OUTPUT_NAME "otel_process_ctx" +) + +# Create shared noop library +add_library(otel_process_ctx_noop SHARED ${SOURCES}) +set_target_properties(otel_process_ctx_noop PROPERTIES + OUTPUT_NAME "otel_process_ctx_noop" +) +target_compile_definitions(otel_process_ctx_noop PRIVATE OTEL_PROCESS_CTX_NOOP=1) + +# Create static noop library +add_library(otel_process_ctx_noop_static STATIC ${SOURCES}) +set_target_properties(otel_process_ctx_noop_static PROPERTIES + OUTPUT_NAME "otel_process_ctx_noop" +) +target_compile_definitions(otel_process_ctx_noop_static PRIVATE OTEL_PROCESS_CTX_NOOP=1) + +# Create C++ shared library +add_library(otel_process_ctx_cpp SHARED ${SOURCES_CXX}) +set_target_properties(otel_process_ctx_cpp PROPERTIES + OUTPUT_NAME "otel_process_ctx_cpp" + CXX_STANDARD 11 + CXX_STANDARD_REQUIRED ON +) + +# Create C++ static library +add_library(otel_process_ctx_cpp_static STATIC ${SOURCES_CXX}) +set_target_properties(otel_process_ctx_cpp_static PROPERTIES + OUTPUT_NAME "otel_process_ctx_cpp" + CXX_STANDARD 11 + CXX_STANDARD_REQUIRED ON +) + +# Create C++ shared noop library +add_library(otel_process_ctx_cpp_noop SHARED ${SOURCES_CXX}) +set_target_properties(otel_process_ctx_cpp_noop PROPERTIES + OUTPUT_NAME "otel_process_ctx_cpp_noop" + CXX_STANDARD 11 + CXX_STANDARD_REQUIRED ON +) +target_compile_definitions(otel_process_ctx_cpp_noop PRIVATE OTEL_PROCESS_CTX_NOOP=1) + +# Create C++ static noop library +add_library(otel_process_ctx_cpp_noop_static STATIC ${SOURCES_CXX}) +set_target_properties(otel_process_ctx_cpp_noop_static PROPERTIES + OUTPUT_NAME "otel_process_ctx_cpp_noop" + CXX_STANDARD 11 + CXX_STANDARD_REQUIRED ON +) +target_compile_definitions(otel_process_ctx_cpp_noop_static PRIVATE OTEL_PROCESS_CTX_NOOP=1) + +# Set include directories +target_include_directories(otel_process_ctx PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_include_directories(otel_process_ctx_static PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_include_directories(otel_process_ctx_noop PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_include_directories(otel_process_ctx_noop_static PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_include_directories(otel_process_ctx_cpp PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_include_directories(otel_process_ctx_cpp_static PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_include_directories(otel_process_ctx_cpp_noop PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_include_directories(otel_process_ctx_cpp_noop_static PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +# Create example executable +add_executable(example_ctx example_ctx.c) +target_link_libraries(example_ctx otel_process_ctx) + +# Create example noop executable +add_executable(example_ctx_noop example_ctx.c) +target_link_libraries(example_ctx_noop otel_process_ctx_noop) + +# Install rules +install(TARGETS otel_process_ctx otel_process_ctx_static otel_process_ctx_noop otel_process_ctx_noop_static + otel_process_ctx_cpp otel_process_ctx_cpp_static otel_process_ctx_cpp_noop otel_process_ctx_cpp_noop_static + example_ctx example_ctx_noop + LIBRARY DESTINATION lib + ARCHIVE DESTINATION lib + RUNTIME DESTINATION bin +) +install(FILES otel_process_ctx.h DESTINATION include) diff --git a/process-context/c-and-cpp/README.md b/process-context/c-and-cpp/README.md new file mode 100644 index 0000000..a875cb6 --- /dev/null +++ b/process-context/c-and-cpp/README.md @@ -0,0 +1,310 @@ +# OpenTelemetry Process Context - C and C++ reference implementation + +This is a reference implementation of the [OpenTelemetry Process Context specification](https://github.com/open-telemetry/opentelemetry-specification/pull/4719/) for C and C++. + +## What is it? + +The OpenTelemetry Process Context specification defines a standard mechanism for OpenTelemetry SDKs to publish process-level resource attributes (such as service name, version, environment, etc.) in a way that can be read by out-of-process consumers, such as the [OpenTelemetry eBPF Profiler](https://github.com/open-telemetry/opentelemetry-ebpf-profiler). + +This reference implementation provides: + +- **A simple API** (`otel_process_ctx.h`) for publishing process context data +- **Linux-only implementation** using anonymous memory mappings with the `OTEL_CTX` signature (with a no-op fallback for other operating systems) +- **C and C++ compatibility** - the same header works for both languages + +## How to Build + +Use the provided build script (uses CMake): + +```bash +./build.sh +``` + +This will create a `build/` directory containing: +- `libotel_process_ctx.so` - Shared library (C) +- `libotel_process_ctx.a` - Static library (C) +- `libotel_process_ctx_cpp.so` - Shared library (C++) +- `libotel_process_ctx_cpp.a` - Static library (C++) +- `libotel_process_ctx_noop.so` - Shared no-op library (C) +- `libotel_process_ctx_noop.a` - Static no-op library (C) +- `libotel_process_ctx_cpp_noop.so` - Shared no-op library (C++) +- `libotel_process_ctx_cpp_noop.a` - Static no-op library (C++) +- `example_ctx` - Example program +- `example_ctx_noop` - Example program (no-op variant) + +(The "cpp" versions of the library are built with the C++ compiler instead of the C compiler to make sure we remain compatible on both) + +### Build Requirements + +- CMake 3.10 or newer +- GCC or Clang compiler with C11 and C++11 support + +### Build Options + +The implementation supports a no-op mode via compile-time definition: + +- **`OTEL_PROCESS_CTX_NOOP`** - Compiles no-op versions of all functions (useful for non-Linux platforms or when the feature is not needed) +- **`OTEL_PROCESS_CTX_NO_READ`** - Disables read support (reduces binary size if reading is not needed) + +These can be set in your own build system as needed. + +## File Descriptions + +### Core Implementation Files + +- **`otel_process_ctx.h`** - Main header file with the complete C API. Include this in your application. Contains the `otel_process_ctx_data` struct and functions for publishing, dropping, and reading process contexts. + +- **`otel_process_ctx.c`** - C implementation of the process context. Contains the core logic for creating anonymous memory mappings, encoding data as Protocol Buffers, and managing the context lifecycle. + +- **`otel_process_ctx.cpp`** - C++ implementation (identical to `.c` but compiled as C++). This ensures the code works correctly when compiled with a C++ compiler. + +### Example and Tools + +- **`example_ctx.c`** - Example program demonstrating how to use the API. Shows publishing, reading, updating, and forking scenarios. Can run in "keep-running" mode for testing with the dump script. + +- **`otel_process_ctx_dump.sh`** - Bash script to inspect a published process context from outside the process. Takes a PID as argument and dumps the context structure and payload. Useful for validation and debugging. Linux-only. + +### Build Configuration + +- **`CMakeLists.txt`** - CMake build configuration. Defines all library variants (C/C++, shared/static, normal/no-op) and the example program. + +- **`build.sh`** - Simple build script that invokes CMake and make. + +### Protocol Buffers Definitions + +- **`resource.proto`** - OpenTelemetry Resource protobuf definition (extracted from OpenTelemetry protocol) +- **`common.proto`** - OpenTelemetry common types protobuf definition (AnyValue, KeyValue, etc.) + +These proto files are included for reference and for use with `protoc` when decoding the payload with the dump script. The implementation includes its own minimal protobuf encoder/decoder and does not depend on the protobuf library. + +## How to Use from C + +### Basic Usage + +1. **Include the header:** + +```c +#include "otel_process_ctx.h" +``` + +2. **Prepare your context data:** + +```c +otel_process_ctx_data data = { + .deployment_environment_name = "production", + .service_instance_id = "123e4567-e89b-12d3-a456-426614174000", + .service_name = "my-service", + .service_version = "1.2.3", + .telemetry_sdk_language = "c", + .telemetry_sdk_version = "0.1.0", + .telemetry_sdk_name = "example-c", + .resources = NULL // Optional additional key-value pairs +}; +``` + +3. **Publish the context:** + +```c +otel_process_ctx_result result = otel_process_ctx_publish(&data); +if (!result.success) { + fprintf(stderr, "Failed to publish context: %s\n", result.error_message); + return 1; +} +``` + +4. **Drop the context when done:** + +```c +if (!otel_process_ctx_drop_current()) { + fprintf(stderr, "Failed to drop context\n"); +} +``` + +### Adding Custom Resources + +You can add custom resource attributes using the `resources` field: + +```c +const char *custom_resources[] = { + "custom.key1", "value1", + "custom.key2", "value2", + NULL // Must be NULL-terminated +}; + +otel_process_ctx_data data = { + // ... other fields ... + .resources = custom_resources +}; +``` + +### Reading the Current Context (for debugging) + +```c +#ifndef OTEL_PROCESS_CTX_NO_READ +otel_process_ctx_read_result result = otel_process_ctx_read(); +if (result.success) { + printf("Service: %s\n", result.data.service_name); + printf("Version: %s\n", result.data.service_version); + + // Don't forget to free the allocated strings + otel_process_ctx_read_drop(&result); +} else { + fprintf(stderr, "Failed to read: %s\n", result.error_message); +} +#endif +``` + +## How to Use from C++ + +The header is C++-compatible using `extern "C"` linkage. Usage is identical to C. + +## Important API Notes + +### Thread Safety + +- `otel_process_ctx_publish()` and `otel_process_ctx_drop_current()` are **NOT thread-safe**. Only call these from a single thread. +- `otel_process_ctx_read()` is thread-safe for reading, but assumes no concurrent mutations. + +### Fork Safety + +- The memory mapping is marked with `MADV_DONTFORK`, so it does **not** propagate to child processes. +- After forking, the child process should call `otel_process_ctx_publish()` again with updated data (especially a new `service_instance_id`). +- The child process can optionally call `otel_process_ctx_drop_current()` to clean up inherited memory allocations (the payload buffer). + +### String Requirements + +All strings in `otel_process_ctx_data` must be: +- Non-NULL +- UTF-8 encoded +- No longer than 4096 bytes (per key/value) + +Empty strings are allowed. + +## Validation with otel_process_ctx_dump.sh + +The `otel_process_ctx_dump.sh` script allows you to inspect a published process context from outside the process. This is useful for: +- Verifying that your application correctly publishes context +- Debugging context data +- Understanding the on-disk format + +### Usage + +1. **Run your application** (or the example): + +```bash +./build/example_ctx --keep-running +``` + +This will print the PID and keep running. + +2. **Dump the context** (requires root/sudo for reading `/proc//mem`): + +```bash +sudo ./otel_process_ctx_dump.sh +``` + +### Example Output + +``` +Found OTEL context for PID 267023 +Start address: 756f28ce1000 +00000000 4f 54 45 4c 5f 43 54 58 02 00 00 00 0b 68 55 47 |OTEL_CTX.....hUG| +00000010 70 24 7d 18 50 01 00 00 a0 82 6d 7e 6a 5f 00 00 |p$}.P.....m~j_..| +00000020 +Parsed struct: + otel_process_ctx_signature : "OTEL_CTX" + otel_process_ctx_version : 2 + otel_process_ctx_published_at_ns : 1764606693650819083 (2025-12-01 16:31:33 GMT) + otel_process_payload_size : 336 + otel_process_payload : 0x00005f6a7e6d82a0 +Payload dump (336 bytes): +00000000 0a 25 0a 1b 64 65 70 6c 6f 79 6d 65 6e 74 2e 65 |.%..deployment.e| +00000010 6e 76 69 72 6f 6e 6d 65 6e 74 2e 6e 61 6d 65 12 |nvironment.name.| +... +``` + +If `protoc` is installed and the proto files are available, the script will also decode the payload: + +``` +Protobuf decode: +attributes { + key: "deployment.environment.name" + value { + string_value: "prod" + } +} +attributes { + key: "service.instance.id" + value { + string_value: "123d8444-2c7e-46e3-89f6-6217880f7123" + } +} +attributes { + key: "service.name" + value { + string_value: "my-service" + } +} +... +``` + +### Requirements for the Dump Script + +- Bash shell +- Root/sudo access (to read `/proc//mem`) +- Standard Linux utilities: `dd`, `hexdump`, `base64`, `date` +- Optional: `protoc` for Protocol Buffers decoding + +## Platform Support + +- **Linux only**: This implementation uses Linux-specific features (anonymous memory mappings, `prctl`, `/proc` filesystem). +- **Kernel version**: Works best on Linux 5.17+ (which supports named anonymous mappings), but includes fallback support for older kernels. +- **No-op mode**: On non-Linux platforms or when `OTEL_PROCESS_CTX_NOOP=1` is defined, all functions become no-ops. + +## Example Application + +The `example_ctx.c` program demonstrates the complete lifecycle: + +1. **Initial publish**: Publishes a context with initial data +2. **Read and verify**: Reads back the context to verify it was published correctly +3. **Update**: Updates the context with new data +4. **Fork test**: Forks a child process that publishes its own context with a different `service_instance_id` +5. **Cleanup**: Both parent and child drop their contexts before exiting + +Run the example: + +```bash +./build/example_ctx +``` + +Or run it continuously for testing with the dump script: + +```bash +./build/example_ctx --keep-running +``` + +## Integration Guidelines + +When integrating this into an OpenTelemetry SDK or application: + +1. **Call publish early**: Call `otel_process_ctx_publish()` as early as possible in your application lifecycle, once you have the service configuration available. + +2. **Update on configuration changes**: If your service attributes change at runtime, call `otel_process_ctx_publish()` again with the updated data. + +3. **Handle forks**: After forking, update the `service_instance_id` and call `otel_process_ctx_publish()` in the child process. + +4. **Clean up on exit**: Call `otel_process_ctx_drop_current()` during shutdown (optional but recommended for clean exit). + +5. **Check result codes**: Always check the `success` field of the result and log errors appropriately. + +6. **Consider no-op builds**: For non-Linux platforms, use the no-op variant or define `OTEL_PROCESS_CTX_NOOP=1`. + +## Specification Compliance + +This implementation follows the [OpenTelemetry Process Context specification](https://github.com/open-telemetry/opentelemetry-specification/pull/4719/). Key aspects: + +- Uses the `OTEL_CTX` signature for discoverability +- Stores data in version 2 format (packed struct + protobuf payload) +- Encodes resource attributes using OpenTelemetry Protocol Buffers Resource format +- Supports both standard semantic conventions and custom resource attributes +- Provides proper memory ordering guarantees for concurrent readers diff --git a/process-context/c-and-cpp/build.sh b/process-context/c-and-cpp/build.sh new file mode 100755 index 0000000..f8d373f --- /dev/null +++ b/process-context/c-and-cpp/build.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -e + +# Create build directory +mkdir -p build +cd build + +# Configure and build +cmake .. +make -j$(nproc) + +echo "Build complete! Libraries are in the build/ directory:" +echo " - libotel_process_ctx.so (shared library)" +echo " - libotel_process_ctx.a (static library)" +echo " - (Check folder for more variants)" +echo "And the example program is in the build/ directory:" +echo " - example_ctx" diff --git a/process-context/c-and-cpp/common.proto b/process-context/c-and-cpp/common.proto new file mode 100644 index 0000000..7f9ffab --- /dev/null +++ b/process-context/c-and-cpp/common.proto @@ -0,0 +1,129 @@ +// Copyright 2019, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package opentelemetry.proto.common.v1; + +option csharp_namespace = "OpenTelemetry.Proto.Common.V1"; +option java_multiple_files = true; +option java_package = "io.opentelemetry.proto.common.v1"; +option java_outer_classname = "CommonProto"; +option go_package = "go.opentelemetry.io/proto/otlp/common/v1"; + +// Represents any type of attribute value. AnyValue may contain a +// primitive value such as a string or integer or it may contain an arbitrary nested +// object containing arrays, key-value lists and primitives. +message AnyValue { + // The value is one of the listed fields. It is valid for all values to be unspecified + // in which case this AnyValue is considered to be "empty". + oneof value { + string string_value = 1; + bool bool_value = 2; + int64 int_value = 3; + double double_value = 4; + ArrayValue array_value = 5; + KeyValueList kvlist_value = 6; + bytes bytes_value = 7; + } +} + +// ArrayValue is a list of AnyValue messages. We need ArrayValue as a message +// since oneof in AnyValue does not allow repeated fields. +message ArrayValue { + // Array of values. The array may be empty (contain 0 elements). + repeated AnyValue values = 1; +} + +// KeyValueList is a list of KeyValue messages. We need KeyValueList as a message +// since `oneof` in AnyValue does not allow repeated fields. Everywhere else where we need +// a list of KeyValue messages (e.g. in Span) we use `repeated KeyValue` directly to +// avoid unnecessary extra wrapping (which slows down the protocol). The 2 approaches +// are semantically equivalent. +message KeyValueList { + // A collection of key/value pairs of key-value pairs. The list may be empty (may + // contain 0 elements). + // + // The keys MUST be unique (it is not allowed to have more than one + // value with the same key). + // The behavior of software that receives duplicated keys can be unpredictable. + repeated KeyValue values = 1; +} + +// Represents a key-value pair that is used to store Span attributes, Link +// attributes, etc. +message KeyValue { + // The key name of the pair. + string key = 1; + + // The value of the pair. + AnyValue value = 2; +} + +// InstrumentationScope is a message representing the instrumentation scope information +// such as the fully qualified name and version. +message InstrumentationScope { + // A name denoting the Instrumentation scope. + // An empty instrumentation scope name means the name is unknown. + string name = 1; + + // Defines the version of the instrumentation scope. + // An empty instrumentation scope version means the version is unknown. + string version = 2; + + // Additional attributes that describe the scope. [Optional]. + // Attribute keys MUST be unique (it is not allowed to have more than one + // attribute with the same key). + // The behavior of software that receives duplicated keys can be unpredictable. + repeated KeyValue attributes = 3; + + // The number of attributes that were discarded. Attributes + // can be discarded because their keys are too long or because there are too many + // attributes. If this value is 0, then no attributes were dropped. + uint32 dropped_attributes_count = 4; +} + +// A reference to an Entity. +// Entity represents an object of interest associated with produced telemetry: e.g spans, metrics, profiles, or logs. +// +// Status: [Development] +message EntityRef { + // The Schema URL, if known. This is the identifier of the Schema that the entity data + // is recorded in. To learn more about Schema URL see + // https://opentelemetry.io/docs/specs/otel/schemas/#schema-url + // + // This schema_url applies to the data in this message and to the Resource attributes + // referenced by id_keys and description_keys. + // TODO: discuss if we are happy with this somewhat complicated definition of what + // the schema_url applies to. + // + // This field obsoletes the schema_url field in ResourceMetrics/ResourceSpans/ResourceLogs. + string schema_url = 1; + + // Defines the type of the entity. MUST not change during the lifetime of the entity. + // For example: "service" or "host". This field is required and MUST not be empty + // for valid entities. + string type = 2; + + // Attribute Keys that identify the entity. + // MUST not change during the lifetime of the entity. The Id must contain at least one attribute. + // These keys MUST exist in the containing {message}.attributes. + repeated string id_keys = 3; + + // Descriptive (non-identifying) attribute keys of the entity. + // MAY change over the lifetime of the entity. MAY be empty. + // These attribute keys are not part of entity's identity. + // These keys MUST exist in the containing {message}.attributes. + repeated string description_keys = 4; +} \ No newline at end of file diff --git a/process-context/c-and-cpp/example_ctx.c b/process-context/c-and-cpp/example_ctx.c new file mode 100644 index 0000000..a3dfc3b --- /dev/null +++ b/process-context/c-and-cpp/example_ctx.c @@ -0,0 +1,193 @@ +// 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 +#include +#include +#include +#include +#include +#include +#include + +#include "otel_process_ctx.h" + +double burn_cpu(void) { + double x = 0.0; + for (int i = 0; i < 1000000; i++) { + x = (x+i)*(x+i); + } + return x; +} + +void burn_cpu_forever(void) { + // avoid compiler optimization + volatile double x = 0.0; + while (true) { + x += burn_cpu(); + } +} + +void burn_cpu_for(int seconds) { + // avoid compiler optimization + volatile double x = 0.0; + time_t t0 = time(NULL); + while (time(NULL) - t0 < seconds) { + x += burn_cpu(); + } + printf("Press enter to continue...\n"); + getchar(); +} + +bool read_and_print_ctx(const char* prefix) { + otel_process_ctx_read_result result = otel_process_ctx_read(); + + if (!result.success) { + fprintf(stderr, "Failed to read context: %s\n", result.error_message); + return false; + } + + printf( + "%s (for pid %d): service=%s, instance=%s, env=%s, version=%s, sdk=%s/%s/%s", + prefix, + getpid(), + result.data.service_name, + result.data.service_instance_id, + result.data.deployment_environment_name, + result.data.service_version, + result.data.telemetry_sdk_name, + result.data.telemetry_sdk_language, + result.data.telemetry_sdk_version + ); + + if (result.data.resources && result.data.resources[0] != NULL) { + printf(", resources="); + for (int i = 0; result.data.resources[i] != NULL; i += 2) { + if (i > 0) printf(","); + printf("%s=%s", result.data.resources[i], result.data.resources[i + 1]); + } + } else { + printf(", resources=(none)"); + } + printf("\n"); + + otel_process_ctx_read_drop(&result); + return true; +} + +const char *resources[] = { + "resource.key1", "resource.value1", + "resource.key2", "resource.value2", + NULL +}; + +int update_and_fork(void) { + printf("Burning CPU for 5 seconds...\n"); + burn_cpu_for(5); + printf("Updating...\n"); + + otel_process_ctx_data update_data = { + .deployment_environment_name = "staging", + .service_instance_id = "456d8444-2c7e-46e3-89f6-6217880f7456", + .service_name = "my-service-updated", + .service_version = "7.8.9", + .telemetry_sdk_language = "c", + .telemetry_sdk_version = "1.2.3", + .telemetry_sdk_name = "example_ctx.c", + .resources = resources + }; + + otel_process_ctx_result result = otel_process_ctx_publish(&update_data); + if (!result.success) { + fprintf(stderr, "Failed to update: %s\n", result.error_message); + return 1; + } + + if (!read_and_print_ctx("Updated")) return 1; + + printf("Forking...\n"); + + if (fork() == 0) { + printf("[child] Calling update in child...\n"); + burn_cpu_for(5); + + otel_process_ctx_data child_data = { + .deployment_environment_name = "staging", + .service_instance_id = "789d8444-2c7e-46e3-89f6-6217880f7789", + .service_name = "my-service-forked", + .service_version = "10.11.12", + .telemetry_sdk_language = "c", + .telemetry_sdk_version = "1.2.3", + .telemetry_sdk_name = "example_ctx.c", + .resources = NULL + }; + + result = otel_process_ctx_publish(&child_data); + if (!result.success) { + fprintf(stderr, "[child] Failed to update: %s\n", result.error_message); + return 1; + } + + if (!read_and_print_ctx("[child] Updated")) return 1; + + burn_cpu_for(5); + + if (!otel_process_ctx_drop_current()) { + fprintf(stderr, "[child] Failed to drop process context\n"); + return 1; + } + + return 0; + } + + wait(NULL); + + if (!otel_process_ctx_drop_current()) { + fprintf(stderr, "Failed to drop process context\n"); + return 1; + } + + return 0; +} + + +int main(int argc, char* argv[]) { + bool keep_running = false; + + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--keep-running") == 0) { + keep_running = true; + } else { + fprintf(stderr, "Unknown argument: %s\n", argv[i]); + fprintf(stderr, "Usage: %s [--keep-running]\n", argv[0]); + return 1; + } + } + + otel_process_ctx_data data = { + .deployment_environment_name = "prod", + .service_instance_id = "123d8444-2c7e-46e3-89f6-6217880f7123", + .service_name = "my-service", + .service_version = "4.5.6", + .telemetry_sdk_language = "c", + .telemetry_sdk_version = "1.2.3", + .telemetry_sdk_name = "example_ctx.c", + .resources = resources + }; + + otel_process_ctx_result result = otel_process_ctx_publish(&data); + if (!result.success) { + fprintf(stderr, "Failed to publish: %s\n", result.error_message); + return 1; + } + + if (!read_and_print_ctx("Published")) return 1; + + if (keep_running) { + printf("Continuing forever, to exit press ctrl+c...\n"); + printf("TIP: You can now `sudo ./otel_process_ctx_dump.sh %d` to see the context\n", getpid()); + burn_cpu_forever(); + } else { + return update_and_fork(); + } +} diff --git a/process-context/c-and-cpp/otel_process_ctx.c b/process-context/c-and-cpp/otel_process_ctx.c new file mode 100644 index 0000000..8221b7b --- /dev/null +++ b/process-context/c-and-cpp/otel_process_ctx.c @@ -0,0 +1,621 @@ +// 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 + +#ifdef __cplusplus + #include + using std::atomic_thread_fence; + using std::memory_order_seq_cst; +#else + #include +#endif +#include +#include +#include +#include +#include +#include +#include +#include + +#define ADD_QUOTES_HELPER(x) #x +#define ADD_QUOTES(x) ADD_QUOTES_HELPER(x) +#define KEY_VALUE_LIMIT 4096 +#define UINT14_MAX 16383 + +#ifndef PR_SET_VMA + #define PR_SET_VMA 0x53564d41 + #define PR_SET_VMA_ANON_NAME 0 +#endif + +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, + .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_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 + +/** + * 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_ctx_published_at_ns; // Always > 0, timestamp from when the context was published in nanoseconds since epoch + 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_encode_protobuf_payload(char **out, uint32_t *out_size, otel_process_ctx_data data); + +// We use a mapping size of 2 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 ssize_t size_for_mapping(void) { + long page_size_bytes = sysconf(_SC_PAGESIZE); + if (page_size_bytes < 4096) { + return -1; + } + return page_size_bytes * 2; +} + +static uint64_t time_now_ns(void) { + struct timespec ts; + if (clock_gettime(CLOCK_REALTIME, &ts) == -1) return 0; + return ts.tv_sec * 1000000000ULL + ts.tv_nsec; +} + +// 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) { + // 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__) ")"}; + } + + // Step: Determine size for mapping + ssize_t 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_protobuf_payload(&published_state.payload, &payload_size, *data); + if (!result.success) return result; + + // Step: Create the mapping + 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(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__) ")"}; + } + } + + // Step: Populate the mapping + // The payload and any extra fields must come first and not be reordered with the signature by the compiler. + + uint64_t published_at_ns = time_now_ns(); + if (published_at_ns == 0) { + return (otel_process_ctx_result) {.success = false, .error_message = "Failed to get current time (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"}; + } + + *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 = 2, + .otel_process_payload_size = payload_size, + .otel_process_ctx_published_at_ns = published_at_ns, + .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(published_state.mapping->otel_process_ctx_signature, "OTEL_CTX", sizeof(published_state.mapping->otel_process_ctx_signature)); + + // 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(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: 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") == -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 look for the "OTEL_CTX" signature in the memory itself, + // after observing the mapping has the expected number of pages and permissions. + // 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 (state.mapping != NULL && state.mapping != MAP_FAILED && getpid() == state.publisher_pid) { + ssize_t mapping_size = size_for_mapping(); + success = mapping_size > 0 && munmap(state.mapping, mapping_size) == 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; +} + +// 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 +} + +// 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, const char *key, const char *value) { + write_protobuf_tag(ptr, 1); // Resource.attributes (field 1) + 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); +} + +// Encode the payload as protobuf bytes. +// +// This method implements an extremely compact but limited protobuf encoder for the Resource message. +// It encodes all fields as 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 resources_pairs_size = 0; + if (data.resources != NULL) { + validation_result = validate_and_calculate_protobuf_payload_size(&resources_pairs_size, data.resources); + if (!validation_result.success) return validation_result; + } + + size_t total_size = pairs_size + resources_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; + + for (size_t i = 0; pairs[i * 2] != NULL; i++) { + write_attribute(&ptr, pairs[i * 2], pairs[i * 2 + 1]); + } + + for (size_t i = 0; data.resources != NULL && data.resources[i * 2] != NULL; i++) { + write_attribute(&ptr, data.resources[i * 2], data.resources[i * 2 + 1]); + } + + *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 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'; + + // Validate expected permission + if (strstr(line, " r--p ") == NULL) return false; + + // Validate expected context size + int64_t start, end; + if (sscanf(line, "%" PRIx64 "-%" PRIx64, &start, &end) != 2) return false; + if (start == 0 || end == 0 || end <= start) return false; + if ((end - start) != size_for_mapping()) return false; + + // Check if the mapping is named + if (memcmp(line + (line_len - name_len), "[anon:OTEL_CTX]", name_len) == 0) return true; + + // To support legacy kernels and those with naming mappings disabled, let's check the mapping for the expected signature + + void *addr = parse_mapping_start(line); + if (addr == NULL) return false; + + // Read 8 bytes at the address using process_vm_readv (to avoid any issues with concurrency/races) + char buffer[8]; + struct iovec local[] = {{.iov_base = buffer, .iov_len = sizeof(buffer)}}; + struct iovec remote[] = {{.iov_base = addr, .iov_len = sizeof(buffer)}}; + + ssize_t bytes_read = process_vm_readv(getpid(), local, 1, remote, 1, 0); + if (bytes_read != sizeof(buffer)) return false; + + return memcmp(buffer, "OTEL_CTX", sizeof(buffer)) == 0; + } + + 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)) { + if (is_otel_process_ctx_mapping(line)) { + 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 + } + + // 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; + + size_t resource_index = 0; + size_t resource_capacity = 201; // Allocate space for 100 pairs + NULL terminator entry + data_out->resources = (const char **) calloc(resource_capacity, sizeof(char *)); + if (data_out->resources == NULL) return false; + + while (ptr < end_ptr) { + uint8_t field_number; + if (!read_protobuf_tag(&ptr, end_ptr, &field_number) || field_number != 1) 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; + + bool key_found = false; + bool value_found = false; + + // Parse KeyValue + while (ptr < kv_end) { + uint8_t kv_field; + if (!read_protobuf_tag(&ptr, kv_end, &kv_field)) return false; + + if (kv_field == 1) { // KeyValue.key + if (!read_protobuf_string(&ptr, kv_end, 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, kv_end, &_any_len)) return false; + uint8_t any_field; + if (!read_protobuf_tag(&ptr, kv_end, &any_field)) return false; + + if (any_field == 1) { // AnyValue.string_value + if (!read_protobuf_string(&ptr, kv_end, value_buffer)) return false; + value_found = true; + } + } + } + + if (!key_found || !value_found) return false; + + char *value = strdup(value_buffer); + if (!value) return false; + + // Dispatch based on key + if (strcmp(key_buffer, "deployment.environment.name") == 0) { data_out->deployment_environment_name = value; } + else if (strcmp(key_buffer, "service.instance.id") == 0) { data_out->service_instance_id = value; } + else if (strcmp(key_buffer, "service.name") == 0) { data_out->service_name = value; } + else if (strcmp(key_buffer, "service.version") == 0) { data_out->service_version = value; } + else if (strcmp(key_buffer, "telemetry.sdk.language") == 0) { data_out->telemetry_sdk_language = value; } + else if (strcmp(key_buffer, "telemetry.sdk.version") == 0) { data_out->telemetry_sdk_version = value; } + else if (strcmp(key_buffer, "telemetry.sdk.name") == 0) { data_out->telemetry_sdk_name = value; } + else { + char *key = strdup(key_buffer); + + if (!key || resource_index + 2 >= resource_capacity) { + free(key); + free(value); + return false; + } + data_out->resources[resource_index] = key; + data_out->resources[resource_index + 1] = value; + resource_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.resources) { + for (int i = 0; data.resources[i] != NULL; i++) free((void *)data.resources[i]); + free((void *)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__) ")", .data = empty_data}; + } + + if (strncmp(mapping->otel_process_ctx_signature, "OTEL_CTX", 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/process-context/c-and-cpp/otel_process_ctx.h b/process-context/c-and-cpp/otel_process_ctx.h new file mode 100644 index 0000000..44ce5bc --- /dev/null +++ b/process-context/c-and-cpp/otel_process_ctx.h @@ -0,0 +1,135 @@ +// 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 0 +#define OTEL_PROCESS_CTX_VERSION_MINOR 1 +#define OTEL_PROCESS_CTX_VERSION_PATCH 0 +#define OTEL_PROCESS_CTX_VERSION_STRING "0.1.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/pull/4719/) + * + * 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`. + * 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 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). + const char **resources; +} 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 diff --git a/process-context/c-and-cpp/otel_process_ctx_dump.sh b/process-context/c-and-cpp/otel_process_ctx_dump.sh new file mode 100755 index 0000000..b521af3 --- /dev/null +++ b/process-context/c-and-cpp/otel_process_ctx_dump.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +set -euo pipefail + +# 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. + +# otel_process_ctx_dump.sh +# Usage: ./otel_process_ctx_dump.sh +# +# Reads the OTEL process context mapping for a given PID, parses the struct, +# and dumps the payload as well. + +check_otel_signature() { + # Check that the first 8 bytes are "OTEL_CTX" + [ "$(printf '%s' "$1" | base64 -d | dd bs=1 count=8 status=none)" = "OTEL_CTX" ] +} + +if [ "$(uname -s)" != "Linux" ]; then + echo "Error: this script only supports Linux." >&2 + exit 1 +fi + +pid="${1:-}" +if ! [[ "$pid" =~ ^[0-9]+$ ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +# Try to use the name of the mapping to find the context first +line="$(grep -F '[anon:OTEL_CTX]' "/proc/$pid/maps" | head -n 1 || true)" + +if [ -n "$line" ]; then + start_addr="${line%%-*}" + found_via="named mapping" +else + # Fallback: scan for anonymous mappings that could be OTEL_CTX + while IFS= read -r line; do + # Parse start-end addresses and check if line contains required characteristics + if [[ "$line" =~ "00:00 0" ]] && \ + [[ "$line" =~ r--p ]] && \ + [[ "$line" =~ ^([0-9a-f]+)-([0-9a-f]+) ]]; then + + start_addr_hex="${BASH_REMATCH[1]}" + end_addr_hex="${BASH_REMATCH[2]}" + size=$((16#$end_addr_hex - 16#$start_addr_hex)) + + # Check if size is 8192 bytes and verify signature + if [ "$size" -eq 8192 ]; then + candidate_data_b64="$(dd if="/proc/$pid/mem" bs=1 count=8 skip=$((16#$start_addr_hex)) status=none 2>/dev/null | base64 -w0)" + if check_otel_signature "$candidate_data_b64"; then + start_addr="$start_addr_hex" + found_via="mapping not named" + break + fi + fi + fi + done < "/proc/$pid/maps" + + if [ -z "${start_addr:-}" ]; then + echo "No OTEL_CTX context found." >&2 + exit 1 + fi +fi + +echo "Found OTEL context for PID $pid ($found_via)" +echo "Start address: $start_addr" + +# Read struct otel_process_ctx_mapping, encode as base64 so we can safely store it in a shell variable. +# (Bash variables cannot hold NUL bytes, so raw binary causes issues) +data_b64="$(dd if="/proc/$pid/mem" bs=1 count=32 skip=$((16#$start_addr)) status=none | base64 -w0)" + +# Pretty-print otel_process_ctx_mapping +printf '%s' "$data_b64" | base64 -d | hexdump -C + +# Check that the first 8 bytes are "OTEL_CTX" +check_otel_signature "$data_b64" + +# Extract fields from otel_process_ctx_mapping +signature="$( + printf '%s' "$data_b64" | base64 -d | dd bs=1 count=8 status=none +)" +version="$( + printf '%s' "$data_b64" | base64 -d | dd bs=1 skip=8 count=4 status=none | od -An -t u4 | tr -d ' ' +)" +payload_size="$( + printf '%s' "$data_b64" | base64 -d | dd bs=1 skip=12 count=4 status=none | od -An -t u4 | tr -d ' ' +)" +published_at_ns="$( + printf '%s' "$data_b64" | base64 -d | dd bs=1 skip=16 count=8 status=none | od -An -t u8 | tr -d ' ' +)" +payload_ptr_hex="$( + printf '%s' "$data_b64" | base64 -d | dd bs=1 skip=24 count=8 status=none | od -An -t x8 | tr -d ' ' +)" + +echo "Parsed struct:" +echo " otel_process_ctx_signature : \"$signature\"" +echo " otel_process_ctx_version : $version" +# Convert nanoseconds to seconds for date command +published_at_s=$((published_at_ns / 1000000000)) +published_at_pretty="$(date -d "@$published_at_s" '+%Y-%m-%d %H:%M:%S %Z')" +echo " otel_process_payload_size : $payload_size" +echo " otel_process_ctx_published_at_ns : $published_at_ns ($published_at_pretty)" +echo " otel_process_payload : 0x$payload_ptr_hex" + +echo "Payload dump ($payload_size bytes):" +dd if="/proc/$pid/mem" bs=1 count="$payload_size" skip=$((16#$payload_ptr_hex)) status=none | hexdump -C + +if command -v protoc >/dev/null 2>&1; then + echo "Protobuf decode:" + dd if="/proc/$pid/mem" bs=1 count="$payload_size" skip=$((16#$payload_ptr_hex)) status=none | protoc --decode=opentelemetry.proto.resource.v1.Resource resource.proto common.proto +else + echo + echo "protoc not available - skipping protobuf decode" +fi diff --git a/process-context/c-and-cpp/resource.proto b/process-context/c-and-cpp/resource.proto new file mode 100644 index 0000000..c81884b --- /dev/null +++ b/process-context/c-and-cpp/resource.proto @@ -0,0 +1,45 @@ +// Copyright 2019, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package opentelemetry.proto.resource.v1; + +import "common.proto"; + +option csharp_namespace = "OpenTelemetry.Proto.Resource.V1"; +option java_multiple_files = true; +option java_package = "io.opentelemetry.proto.resource.v1"; +option java_outer_classname = "ResourceProto"; +option go_package = "go.opentelemetry.io/proto/otlp/resource/v1"; + +// Resource information. +message Resource { + // Set of attributes that describe the resource. + // Attribute keys MUST be unique (it is not allowed to have more than one + // attribute with the same key). + // The behavior of software that receives duplicated keys can be unpredictable. + repeated opentelemetry.proto.common.v1.KeyValue attributes = 1; + + // The number of dropped attributes. If the value is 0, then + // no attributes were dropped. + uint32 dropped_attributes_count = 2; + + // Set of entities that participate in this Resource. + // + // Note: keys in the references MUST exist in attributes of this message. + // + // Status: [Development] + repeated opentelemetry.proto.common.v1.EntityRef entity_refs = 3; +}