Skip to content

Commit bfb879a

Browse files
jbachorikclaude
andcommitted
Force JNI path for context writes in OTEL mode
On Java < 17, ThreadContext.put() uses the ByteBuffer path which writes directly to the Context struct, bypassing ContextApi::set(). In OTEL mode this means the OTEP record is never written. Fix: use JNI when otelMode is active (production path). For tests that restart the profiler with different storage modes in the same JVM, add writeOtelContext/readOtelContext static helpers that bypass the cached ThreadContext and go through ContextApi JNI directly. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent 2000f0b commit bfb879a

3 files changed

Lines changed: 42 additions & 14 deletions

File tree

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,11 @@ Java_com_datadoghq_profiler_ThreadContext_setContext0(JNIEnv* env, jclass unused
572572
return Contexts::checksum(spanId, rootSpanId);
573573
}
574574

575+
extern "C" DLLEXPORT void JNICALL
576+
Java_com_datadoghq_profiler_ThreadContext_setContextOtel0(JNIEnv* env, jclass unused, jlong traceIdHigh, jlong traceIdLow, jlong spanId) {
577+
ContextApi::setOtel(traceIdHigh, traceIdLow, spanId);
578+
}
579+
575580
// Legacy API: writes directly to Context.tags[] regardless of storage mode.
576581
// In OTEL mode, writeCurrentContext() in flightRecorder.cpp reads both Context.tags[]
577582
// and the OTEP attrs_data, so tags set via this path are still recorded in JFR.

ddprof-lib/src/main/java/com/datadoghq/profiler/ThreadContext.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,19 @@ public long put(long spanId, long rootSpanId) {
161161
return (useJNI || otelMode) ? setContext0(spanId, rootSpanId) : putContextJava(spanId, rootSpanId);
162162
}
163163

164+
/**
165+
* Sets trace context with full 128-bit W3C trace ID support.
166+
* In OTEL mode, all three values are written to the OTEP record.
167+
* In profiler mode, traceIdHigh is ignored (Datadog uses 64-bit trace IDs).
168+
*
169+
* @param spanId The span ID
170+
* @param traceIdHigh Upper 64 bits of the 128-bit trace ID
171+
* @param traceIdLow Lower 64 bits of the 128-bit trace ID (rootSpanId in Datadog)
172+
*/
173+
public void put(long spanId, long traceIdHigh, long traceIdLow) {
174+
setContextOtel0(traceIdHigh, traceIdLow, spanId);
175+
}
176+
164177
public void putCustom(int offset, int value) {
165178
if (offset >= MAX_CUSTOM_SLOTS) {
166179
throw new IllegalArgumentException("Invalid offset: " + offset + " (max " + MAX_CUSTOM_SLOTS + ")");
@@ -215,6 +228,7 @@ public boolean setContextAttribute(int keyIndex, String value) {
215228
}
216229

217230
private static native long setContext0(long spanId, long rootSpanId);
231+
private static native void setContextOtel0(long traceIdHigh, long traceIdLow, long spanId);
218232
private static native void setContextSlot0(int offset, int value);
219233
private static native boolean setContextAttribute0(int keyIndex, String value);
220234

@@ -245,4 +259,16 @@ public boolean setContextAttribute(int keyIndex, String value) {
245259
public static long[] readOtelContext() {
246260
return getContext0();
247261
}
262+
263+
/**
264+
* Writes context via the native ContextApi, bypassing the cached otelMode flag.
265+
* Primarily intended for testing where the profiler may be restarted with a
266+
* different storage mode while the ThreadContext instance is cached in ThreadLocal.
267+
*
268+
* @param spanId The span ID
269+
* @param rootSpanId The root span ID (trace ID low bits for OTEL)
270+
*/
271+
public static void writeOtelContext(long spanId, long rootSpanId) {
272+
setContext0(spanId, rootSpanId);
273+
}
248274
}

ddprof-test/src/test/java/com/datadoghq/profiler/context/OtelContextStorageModeTest.java

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,13 @@ public void testOtelStorageModeContext() throws Exception {
9696
profiler.execute(String.format("start,cpu=1ms,ctxstorage=otel,jfr,file=%s", jfrFile.toAbsolutePath()));
9797
profilerStarted = true;
9898

99-
// Set context — writes to OTEP #4947 TLS record
99+
// Write and read context directly via JNI, bypassing cached ThreadContext.
100+
// ThreadContext caches otelMode at construction time; if a profiler-mode test
101+
// ran first on this thread, the cached flag would route writes to the wrong storage.
100102
long spanId = 0xAAAABBBBCCCCDDDDL;
101103
long rootSpanId = 0x1111222233334444L;
102-
profiler.setContext(spanId, rootSpanId);
104+
ThreadContext.writeOtelContext(spanId, rootSpanId);
103105

104-
// Verify context round-trips through JNI, bypassing cached ThreadContext
105-
// (ThreadContext caches otelMode at construction time; if a profiler-mode test
106-
// ran first on this thread, the cached flag would be wrong)
107106
long[] ctx = ThreadContext.readOtelContext();
108107
assertNotNull(ctx, "Context should be readable in OTEL mode");
109108
assertEquals(spanId, ctx[0], "SpanId should match in OTEL mode");
@@ -121,7 +120,7 @@ public void testOtelModeStartsOnAnyPlatform() throws Exception {
121120
profilerStarted = true;
122121

123122
// Context operations should not crash
124-
profiler.setContext(0x123L, 0x456L);
123+
ThreadContext.writeOtelContext(0x123L, 0x456L);
125124
}
126125

127126
/**
@@ -134,11 +133,9 @@ public void testOtelModeClearContext() throws Exception {
134133
profiler.execute(String.format("start,cpu=1ms,ctxstorage=otel,jfr,file=%s", jfrFile.toAbsolutePath()));
135134
profilerStarted = true;
136135

137-
// Set context
138-
profiler.setContext(0xDEADBEEFL, 0xCAFEBABEL);
139-
140-
// Clear context
141-
profiler.setContext(0, 0);
136+
// Set and clear context via direct JNI (bypasses cached ThreadContext)
137+
ThreadContext.writeOtelContext(0xDEADBEEFL, 0xCAFEBABEL);
138+
ThreadContext.writeOtelContext(0, 0);
142139

143140
// Verify context is cleared (use direct JNI read, not cached ThreadContext)
144141
long[] ctx = ThreadContext.readOtelContext();
@@ -164,10 +161,10 @@ public void testOtelModeCustomAttributes() throws Exception {
164161
// Register attribute keys
165162
OTelContext.getInstance().registerAttributeKeys("http.route", "db.system");
166163

167-
// Set context first (trace/span)
164+
// Set context first (trace/span) via direct JNI
168165
long spanId = 0xAAAABBBBCCCCDDDDL;
169166
long rootSpanId = 0x1111222233334444L;
170-
profiler.setContext(spanId, rootSpanId);
167+
ThreadContext.writeOtelContext(spanId, rootSpanId);
171168

172169
// Set custom attribute via new API
173170
ThreadContext ctx = profiler.getThreadContext();
@@ -214,7 +211,7 @@ public void testOtelModeAttributeOverflow() throws Exception {
214211

215212
OTelContext.getInstance().registerAttributeKeys("k0", "k1", "k2", "k3", "k4");
216213

217-
profiler.setContext(0x1L, 0x2L);
214+
ThreadContext.writeOtelContext(0x1L, 0x2L);
218215

219216
ThreadContext ctx = profiler.getThreadContext();
220217

0 commit comments

Comments
 (0)