Skip to content

Commit 88a03d5

Browse files
jbachorikclaude
andcommitted
Add OTEP #4947 OTEL-compatible context storage with custom attributes
Add alternative context storage mode (ctxstorage=otel) that exposes trace context via an OTEP #4947-compliant TLS pointer discoverable by external profilers through the ELF dynsym table. Each thread gets a 640-byte OtelThreadContextRecord with W3C trace_id, span_id, and custom attributes encoded in attrs_data[612]. A ContextApi abstraction routes reads/writes to the appropriate storage backend. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent 715b5db commit 88a03d5

21 files changed

Lines changed: 2054 additions & 23 deletions

ddprof-lib/src/main/cpp/arguments.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,15 @@ Error Arguments::parse(const char *args) {
374374
}
375375
}
376376

377+
CASE("ctxstorage")
378+
if (value != NULL) {
379+
if (strcmp(value, "otel") == 0) {
380+
_context_storage = CTX_STORAGE_OTEL;
381+
} else {
382+
_context_storage = CTX_STORAGE_PROFILER;
383+
}
384+
}
385+
377386
DEFAULT()
378387
if (_unknown_arg == NULL)
379388
_unknown_arg = arg;

ddprof-lib/src/main/cpp/arguments.h

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,16 @@ enum Clock {
9292
CLK_MONOTONIC
9393
};
9494

95+
/**
96+
* Context storage mode for trace/span context.
97+
* Both modes are async-signal safe. The profiler mode is production-proven;
98+
* OTEP mode enables interop with external profilers but is newer.
99+
*/
100+
enum ContextStorageMode {
101+
CTX_STORAGE_PROFILER, // Default: TLS-based storage
102+
CTX_STORAGE_OTEL // OTEP #4947 TLS pointer storage
103+
};
104+
95105
// Keep this in sync with JfrSync.java
96106
enum EventMask {
97107
EM_CPU = 1,
@@ -189,6 +199,7 @@ class Arguments {
189199
bool _lightweight;
190200
bool _enable_method_cleanup;
191201
bool _remote_symbolication; // Enable remote symbolication for native frames
202+
ContextStorageMode _context_storage; // Context storage mode (profiler TLS or OTEL TLS pointer)
192203

193204
Arguments(bool persistent = false)
194205
: _buf(NULL),
@@ -223,7 +234,8 @@ class Arguments {
223234
_wallclock_sampler(ASGCT),
224235
_lightweight(false),
225236
_enable_method_cleanup(true),
226-
_remote_symbolication(false) {}
237+
_remote_symbolication(false),
238+
_context_storage(CTX_STORAGE_PROFILER) {}
227239

228240
~Arguments();
229241

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
/*
2+
* Copyright 2026, Datadog, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include "context_api.h"
18+
#include "context.h"
19+
#include "guards.h"
20+
#include "otel_context.h"
21+
#include "otel_process_ctx.h"
22+
#include "profiler.h"
23+
#include "thread.h"
24+
#include <cstring>
25+
26+
// Static member initialization
27+
ContextStorageMode ContextApi::_mode = CTX_STORAGE_PROFILER;
28+
bool ContextApi::_initialized = false;
29+
char* ContextApi::_attribute_keys[MAX_ATTRIBUTE_KEYS] = {};
30+
int ContextApi::_attribute_key_count = 0;
31+
32+
bool ContextApi::initialize(const Arguments& args) {
33+
if (__atomic_load_n(&_initialized, __ATOMIC_ACQUIRE)) {
34+
return true;
35+
}
36+
37+
ContextStorageMode mode = args._context_storage;
38+
if (mode == CTX_STORAGE_OTEL) {
39+
if (!OtelContexts::initialize()) {
40+
// Fall back to profiler storage if OTEL init fails
41+
mode = CTX_STORAGE_PROFILER;
42+
__atomic_store_n(&_mode, mode, __ATOMIC_RELEASE);
43+
__atomic_store_n(&_initialized, true, __ATOMIC_RELEASE);
44+
return true;
45+
}
46+
}
47+
48+
__atomic_store_n(&_mode, mode, __ATOMIC_RELEASE);
49+
__atomic_store_n(&_initialized, true, __ATOMIC_RELEASE);
50+
return true;
51+
}
52+
53+
// Called from Profiler::stop() which is single-threaded — no racing with initialize().
54+
void ContextApi::shutdown() {
55+
if (!__atomic_load_n(&_initialized, __ATOMIC_ACQUIRE)) {
56+
return;
57+
}
58+
59+
if (__atomic_load_n(&_mode, __ATOMIC_ACQUIRE) == CTX_STORAGE_OTEL) {
60+
OtelContexts::shutdown();
61+
}
62+
63+
// Free registered attribute keys (null first, free later for signal safety)
64+
int count = _attribute_key_count;
65+
_attribute_key_count = 0;
66+
for (int i = 0; i < count; i++) {
67+
char* key = _attribute_keys[i];
68+
_attribute_keys[i] = nullptr;
69+
free(key);
70+
}
71+
72+
__atomic_store_n(&_mode, CTX_STORAGE_PROFILER, __ATOMIC_RELEASE);
73+
__atomic_store_n(&_initialized, false, __ATOMIC_RELEASE);
74+
}
75+
76+
bool ContextApi::isInitialized() {
77+
return __atomic_load_n(&_initialized, __ATOMIC_ACQUIRE);
78+
}
79+
80+
ContextStorageMode ContextApi::getMode() {
81+
return __atomic_load_n(&_mode, __ATOMIC_ACQUIRE);
82+
}
83+
84+
/**
85+
* Initialize OTel TLS for the current thread on first use.
86+
* Allocates a per-thread OtelThreadContextRecord and caches it in ProfiledThread.
87+
* Must be called with signals blocked to prevent musl TLS deadlock.
88+
*/
89+
static OtelThreadContextRecord* initializeOtelTls(ProfiledThread* thrd) {
90+
// Block profiling signals during first TLS access (same pattern as context_tls_v1)
91+
SignalBlocker blocker;
92+
93+
// Allocate one record per thread — freed in ProfiledThread destructor/releaseFromBuffer
94+
OtelThreadContextRecord* record = new OtelThreadContextRecord();
95+
record->valid = 0;
96+
record->_reserved = 0;
97+
record->attrs_data_size = 0;
98+
99+
// First access to custom_labels_current_set_v2 triggers TLS init
100+
custom_labels_current_set_v2 = nullptr;
101+
102+
// Cache in ProfiledThread for signal-safe cross-thread access
103+
thrd->markOtelContextInitialized(record);
104+
105+
return record;
106+
}
107+
108+
void ContextApi::set(u64 span_id, u64 root_span_id) {
109+
// Datadog tracer-profiler context uses localRootSpanId (64 bits) for trace
110+
// correlation, so trace_id_high is zero here. The 128-bit put() overload
111+
// supports full W3C trace IDs when needed.
112+
setOtel(0, root_span_id, span_id);
113+
}
114+
115+
void ContextApi::setOtel(u64 trace_id_high, u64 trace_id_low, u64 span_id) {
116+
ContextStorageMode mode = __atomic_load_n(&_mode, __ATOMIC_ACQUIRE);
117+
118+
if (mode == CTX_STORAGE_OTEL) {
119+
// All-zero IDs = context detachment per OTEP — use NULL pointer
120+
if (trace_id_high == 0 && trace_id_low == 0 && span_id == 0) {
121+
OtelContexts::clear();
122+
return;
123+
}
124+
125+
// Ensure TLS + record are initialized on first use
126+
ProfiledThread* thrd = ProfiledThread::current();
127+
if (thrd == nullptr) return;
128+
129+
if (!thrd->isOtelContextInitialized()) {
130+
initializeOtelTls(thrd);
131+
}
132+
133+
OtelContexts::set(trace_id_high, trace_id_low, span_id);
134+
} else {
135+
// Profiler mode: use existing TLS
136+
Context& ctx = Contexts::get();
137+
138+
__atomic_store_n(&ctx.checksum, 0ULL, __ATOMIC_RELEASE);
139+
__atomic_store_n(&ctx.spanId, span_id, __ATOMIC_RELAXED);
140+
__atomic_store_n(&ctx.rootSpanId, trace_id_low, __ATOMIC_RELAXED);
141+
142+
u64 newChecksum = Contexts::checksum(span_id, trace_id_low);
143+
__atomic_store_n(&ctx.checksum, newChecksum, __ATOMIC_RELEASE);
144+
}
145+
}
146+
147+
bool ContextApi::get(u64& span_id, u64& root_span_id) {
148+
ContextStorageMode mode = __atomic_load_n(&_mode, __ATOMIC_ACQUIRE);
149+
150+
if (mode == CTX_STORAGE_OTEL) {
151+
u64 trace_high, trace_low;
152+
if (OtelContexts::get(trace_high, trace_low, span_id)) {
153+
root_span_id = trace_low;
154+
return true;
155+
}
156+
return false;
157+
} else {
158+
Context& ctx = Contexts::get();
159+
u64 checksum1 = __atomic_load_n(&ctx.checksum, __ATOMIC_ACQUIRE);
160+
span_id = __atomic_load_n(&ctx.spanId, __ATOMIC_RELAXED);
161+
root_span_id = __atomic_load_n(&ctx.rootSpanId, __ATOMIC_RELAXED);
162+
return checksum1 != 0 && checksum1 == Contexts::checksum(span_id, root_span_id);
163+
}
164+
}
165+
166+
167+
void ContextApi::clear() {
168+
ContextStorageMode mode = __atomic_load_n(&_mode, __ATOMIC_ACQUIRE);
169+
170+
if (mode == CTX_STORAGE_OTEL) {
171+
OtelContexts::clear();
172+
} else {
173+
set(0, 0);
174+
}
175+
}
176+
177+
bool ContextApi::setAttribute(uint8_t key_index, const char* value, uint8_t value_len) {
178+
ContextStorageMode mode = __atomic_load_n(&_mode, __ATOMIC_ACQUIRE);
179+
180+
if (mode == CTX_STORAGE_OTEL) {
181+
// Ensure TLS + record are initialized on first use
182+
ProfiledThread* thrd = ProfiledThread::current();
183+
if (thrd == nullptr) return false;
184+
185+
if (!thrd->isOtelContextInitialized()) {
186+
initializeOtelTls(thrd);
187+
}
188+
189+
// Pre-register the value string in the Dictionary from this JNI thread.
190+
// The signal handler (writeCurrentContext) will later call bounded_lookup
191+
// to find the encoding — by pre-registering here, the signal handler
192+
// only does a read (no malloc), which is async-signal-safe.
193+
Profiler::instance()->contextValueMap()->bounded_lookup(
194+
value, value_len, 1 << 16);
195+
196+
return OtelContexts::setAttribute(key_index, value, value_len);
197+
} else {
198+
// Profiler mode: register the string and write encoding to Context.tags[]
199+
if (key_index >= DD_TAGS_CAPACITY) return false;
200+
201+
u32 encoding = Profiler::instance()->contextValueMap()->bounded_lookup(
202+
value, value_len, 1 << 16);
203+
if (encoding == INT_MAX) return false;
204+
205+
Context& ctx = Contexts::get();
206+
ctx.tags[key_index].value = encoding;
207+
return true;
208+
}
209+
}
210+
211+
// Called from JNI during setup, before profiling starts.
212+
void ContextApi::registerAttributeKeys(const char** keys, int count) {
213+
// Free any previously registered keys (null first, free later)
214+
int oldCount = _attribute_key_count;
215+
_attribute_key_count = 0;
216+
char* oldKeys[MAX_ATTRIBUTE_KEYS];
217+
for (int i = 0; i < oldCount; i++) {
218+
oldKeys[i] = _attribute_keys[i];
219+
_attribute_keys[i] = nullptr;
220+
}
221+
for (int i = 0; i < oldCount; i++) {
222+
free(oldKeys[i]);
223+
}
224+
225+
_attribute_key_count = count < MAX_ATTRIBUTE_KEYS ? count : MAX_ATTRIBUTE_KEYS;
226+
for (int i = 0; i < _attribute_key_count; i++) {
227+
_attribute_keys[i] = strdup(keys[i]);
228+
}
229+
230+
// If in OTEL mode, re-publish process context with thread_ctx_config
231+
ContextStorageMode mode = __atomic_load_n(&_mode, __ATOMIC_ACQUIRE);
232+
if (mode == CTX_STORAGE_OTEL) {
233+
// Build NULL-terminated key array for the process context config
234+
const char* key_ptrs[MAX_ATTRIBUTE_KEYS + 1];
235+
for (int i = 0; i < _attribute_key_count; i++) {
236+
key_ptrs[i] = _attribute_keys[i];
237+
}
238+
key_ptrs[_attribute_key_count] = nullptr;
239+
240+
otel_thread_ctx_config_data config = {
241+
.schema_version = "tlsdesc_v1_dev",
242+
.attribute_key_map = key_ptrs,
243+
};
244+
245+
#ifndef OTEL_PROCESS_CTX_NO_READ
246+
// Re-publish the process context with thread_ctx_config.
247+
// Guard: otel_process_ctx_read() is only available when the read API is compiled in.
248+
// We need to read the current context and re-publish with the config
249+
otel_process_ctx_read_result read_result = otel_process_ctx_read();
250+
if (read_result.success) {
251+
otel_process_ctx_data data = {
252+
.deployment_environment_name = read_result.data.deployment_environment_name,
253+
.service_instance_id = read_result.data.service_instance_id,
254+
.service_name = read_result.data.service_name,
255+
.service_version = read_result.data.service_version,
256+
.telemetry_sdk_language = read_result.data.telemetry_sdk_language,
257+
.telemetry_sdk_version = read_result.data.telemetry_sdk_version,
258+
.telemetry_sdk_name = read_result.data.telemetry_sdk_name,
259+
.resource_attributes = read_result.data.resource_attributes,
260+
.extra_attributes = read_result.data.extra_attributes,
261+
.thread_ctx_config = &config,
262+
};
263+
264+
otel_process_ctx_publish(&data);
265+
otel_process_ctx_read_drop(&read_result);
266+
}
267+
#endif
268+
}
269+
}
270+
271+
const char* ContextApi::getAttributeKey(int index) {
272+
if (index < 0 || index >= _attribute_key_count) {
273+
return nullptr;
274+
}
275+
return _attribute_keys[index];
276+
}
277+
278+
int ContextApi::getAttributeKeyCount() {
279+
return _attribute_key_count;
280+
}

0 commit comments

Comments
 (0)