Commit 866d7c8
authored
feat: otel thread ctx FFI (#1915)
# What does this PR do?
This PR adds a basic FFI for the OTel thread-level context feature: create a new context, attach, detach, and update in place.
We also make `ThreadContextRecord` public, or at least exposed in the FFI. The rationale is that:
1. it's imposed by the spec, so it should not be a liability regarding breaking changes: we can't really touch it anyway.
2. as mentioned in the doc of the FFI, there's a potential for SDK updating themselves the contexts without going through libdatadog at all after publication. In this usage mode, the export of the C struct `ThreadContextRecord` is a way to document its expected memory layout.
<details>
<summary>Generated C header</summary>
```c
// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/
// SPDX-License-Identifier: Apache-2.0
#ifndef DDOG_OTEL_THREAD_CTX_H
#define DDOG_OTEL_THREAD_CTX_H
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
/**
* In-memory layout of a thread-level context.
*
* **CAUTION**: The structure MUST match exactly the OTel thread-level context specification.
* It is read by external, out-of-process code. Do not re-order fields or modify in any way,
* unless you know exactly what you're doing.
*
* # Synchronization
*
* Readers are async-signal handlers. The writer is always stopped while a reader runs.
* Sharing memory with a signal handler still requires some form of synchronization, which is
* achieved through atomics and compiler fence, using `valid` and/or the TLS slot as
* synchronization points.
*
* - The writer stores `valid = 0` *before* modifying fields in-place, guarded by a fence.
* - The writer stores `valid = 1` *after* all fields are populated, guarded by a fence.
* - `valid` starts at `1` on construction and is never set to `0` except during an in-place
* update.
*/
typedef struct ddog_ThreadContextRecord {
/**
* Trace identifier; all-zeroes means "no trace".
*/
uint8_t trace_id[16];
/**
* Span identifier.
*/
uint8_t span_id[8];
/**
* Whether the record is ready/consistent. Always set to `1` except during in-place update
* of the current record.
*/
uint8_t valid;
uint8_t _reserved;
/**
* Number of populated bytes in `attrs_data`.
*/
uint16_t attrs_data_size;
/**
* Packed variable-length key-value records.
*
* It's a contiguous list of blocks with layout:
*
* 1. 1-byte `key_index`
* 2. 1-byte `val_len`
* 3. `val_len` bytes of a string value.
*
* # Size
*
* Currently, we always allocate the max recommended size. This potentially wastes a few
* hundred bytes per thread, but it guarantees that we can modify the context in-place
* without (re)allocation in the hot path. Having a hybrid scheme (starting smaller and
* resizing up a few times) is not out of the question.
*/
uint8_t attrs_data[ddog_MAX_ATTRS_DATA_SIZE];
} ddog_ThreadContextRecord;
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
/**
* Allocate and initialise a new thread context.
*
* Returns a non-null owned handle that must eventually be released with
* `ddog_otel_thread_ctx_free`.
*/
struct ddog_ThreadContextRecord *ddog_otel_thread_ctx_new(const uint8_t (*trace_id)[16],
const uint8_t (*span_id)[8],
const uint8_t (*local_root_span_id)[8]);
/**
* Free an owned thread context.
*
* # Safety
*
* `ctx` must be a valid non-null pointer obtained from `ddog_otel_thread_ctx_new` or
* `ddog_otel_thread_ctx_detach`, and must not be used after this call. In particular, `ctx`
* must not be currently attached to a thread.
*/
void ddog_otel_thread_ctx_free(struct ddog_ThreadContextRecord *ctx);
/**
* Attach `ctx` to the current thread. Returns the previously attached context if any, or null
* otherwise.
*
* # Safety
*
* `ctx` must be a valid non-null pointer obtained from this API. Ownership of `ctx` is
* transferred to the TLS slot: the caller must not drop `ctx` while it is still actively
* attached.
*
* ## In-place update
*
* The preferred method to update the thread context in place is [ddog_otel_thread_ctx_update].
*
* If calling into native code is too costly, it is possible to update an attached context
* directly in-memory without going through libdatadog (contexts are guaranteed to have a
* stable address through their lifetime). **HOWEVER, IF DOING SO, PLEASE BE VERY CAUTIOUS OF
* THE FOLLOWING POINTS**:
*
* 1. The update process requires a [seqlock](https://en.wikipedia.org/wiki/Seqlock)-like
* pattern: [ThreadContextRecord::valid] must be first set to `0` before the update and set
* to `1` again at the end. Additionally, depending on your language's memory model, you
* might need specific synchronization primitives (compiler fences, atomics, etc.), since
* the context can be read by an asynchronous signal handler at any point in time. See the
* [Otel thread context
* specification](open-telemetry/opentelemetry-specification#4947)
* for more details.
* 2. Only update the context from the thread it's attached to. Contexts are designed to be
* attached, written to and read from on the same thread (whether from signal code or
* program code). Thus, they are NOT thread-safe. Given the current specification, I don't
* think it's possible to safely update an attached context from a different thread, since
* the signal handler doesn't assume the context can be written to concurrently from another
* thread.
*/
struct ddog_ThreadContextRecord *ddog_otel_thread_ctx_attach(struct ddog_ThreadContextRecord *ctx);
/**
* Remove the currently attached context from the TLS slot.
*
* Returns the detached context (caller now owns it and must release it with
* `ddog_otel_thread_ctx_free`), or null if the slot was empty.
*/
struct ddog_ThreadContextRecord *ddog_otel_thread_ctx_detach(void);
/**
* Update the currently attached context in-place.
*
* If no context is currently attached, one is created and attached, equivalent to calling
* `ddog_otel_thread_ctx_new` followed by `ddog_otel_thread_ctx_attach`.
*/
void ddog_otel_thread_ctx_update(const uint8_t (*trace_id)[16],
const uint8_t (*span_id)[8],
const uint8_t (*local_root_span_id)[8]);
#ifdef __cplusplus
} // extern "C"
#endif // __cplusplus
#endif /* DDOG_OTEL_THREAD_CTX_H */
```
</details>
# Motivation
OTel thread-level context has been implemented in #1791 in order to provide better interop with the OTel eBPF profiler. The first user is supposed to be dd-trace-rs, but it turns out the dotnet SDK people are interested in using it as well (and eventually other non-Rust SDKs will use it and thus require an FFI).
# Additional Notes
N/A
# How to test the change?
There's a test to check that the TLS symbol is properly handled. For real usage, we plan to check when integrating in dotnet (or whichever is the first SDK to use it).
Co-authored-by: yann.hamdaoui <[email protected]>1 parent c713122 commit 866d7c8
11 files changed
Lines changed: 357 additions & 13 deletions
File tree
- .github
- libdd-otel-thread-ctx-ffi
- src
- tests
- libdd-otel-thread-ctx/src
- tools/docker
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
48 | 48 | | |
49 | 49 | | |
50 | 50 | | |
| 51 | + | |
51 | 52 | | |
52 | 53 | | |
53 | 54 | | |
| |||
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
| 19 | + | |
19 | 20 | | |
20 | 21 | | |
21 | 22 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
0 commit comments