Skip to content

Commit 8849603

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. Add JavaProfiler.resetThreadContext() to flush the cached ThreadLocal so tests that switch storage modes between profiler restarts get a fresh ThreadContext with the correct otelMode flag. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent 2000f0b commit 8849603

4 files changed

Lines changed: 48 additions & 36 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/JavaProfiler.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,4 +360,13 @@ public ThreadContext getThreadContext() {
360360
public static native void testlog(String msg);
361361

362362
public static native void dumpContext();
363+
364+
/**
365+
* Resets the cached ThreadContext for the current thread.
366+
* The next call to {@link #getThreadContext()} or {@link #setContext(long, long)}
367+
* will re-create it, picking up the current storage mode.
368+
*/
369+
public void resetThreadContext() {
370+
tlsContextStorage.remove();
371+
}
363372
}

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

Lines changed: 14 additions & 11 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

@@ -234,15 +248,4 @@ public boolean setContextAttribute(int keyIndex, String value) {
234248
* @return array with [spanId, rootSpanId], or null on error
235249
*/
236250
private static native long[] getContext0();
237-
238-
/**
239-
* Reads context via the native ContextApi, bypassing the cached otelMode flag.
240-
* Primarily intended for testing where the profiler may be restarted with a
241-
* different storage mode while the ThreadContext instance is cached in ThreadLocal.
242-
*
243-
* @return array with [spanId, rootSpanId], or null on error
244-
*/
245-
public static long[] readOtelContext() {
246-
return getContext0();
247-
}
248251
}

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

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -96,18 +96,16 @@ 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+
// Reset cached ThreadContext so it picks up the current OTEL mode
100+
profiler.resetThreadContext();
101+
100102
long spanId = 0xAAAABBBBCCCCDDDDL;
101103
long rootSpanId = 0x1111222233334444L;
102104
profiler.setContext(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)
107-
long[] ctx = ThreadContext.readOtelContext();
108-
assertNotNull(ctx, "Context should be readable in OTEL mode");
109-
assertEquals(spanId, ctx[0], "SpanId should match in OTEL mode");
110-
assertEquals(rootSpanId, ctx[1], "RootSpanId should match in OTEL mode");
106+
ThreadContext ctx = profiler.getThreadContext();
107+
assertEquals(spanId, ctx.getSpanId(), "SpanId should match in OTEL mode");
108+
assertEquals(rootSpanId, ctx.getRootSpanId(), "RootSpanId should match in OTEL mode");
111109
}
112110

113111
/**
@@ -120,6 +118,8 @@ public void testOtelModeStartsOnAnyPlatform() throws Exception {
120118
profiler.execute(String.format("start,cpu=1ms,ctxstorage=otel,jfr,file=%s", jfrFile.toAbsolutePath()));
121119
profilerStarted = true;
122120

121+
profiler.resetThreadContext();
122+
123123
// Context operations should not crash
124124
profiler.setContext(0x123L, 0x456L);
125125
}
@@ -134,19 +134,14 @@ public void testOtelModeClearContext() throws Exception {
134134
profiler.execute(String.format("start,cpu=1ms,ctxstorage=otel,jfr,file=%s", jfrFile.toAbsolutePath()));
135135
profilerStarted = true;
136136

137-
// Set context
138-
profiler.setContext(0xDEADBEEFL, 0xCAFEBABEL);
137+
profiler.resetThreadContext();
139138

140-
// Clear context
139+
profiler.setContext(0xDEADBEEFL, 0xCAFEBABEL);
141140
profiler.setContext(0, 0);
142141

143-
// Verify context is cleared (use direct JNI read, not cached ThreadContext)
144-
long[] ctx = ThreadContext.readOtelContext();
145-
// After clear, readContextDirect returns zeros (OTEL record is invalidated)
146-
long clearedSpanId = (ctx != null) ? ctx[0] : 0;
147-
long clearedRootSpanId = (ctx != null) ? ctx[1] : 0;
148-
assertEquals(0, clearedSpanId, "SpanId should be zero after clear");
149-
assertEquals(0, clearedRootSpanId, "RootSpanId should be zero after clear");
142+
ThreadContext ctx = profiler.getThreadContext();
143+
assertEquals(0, ctx.getSpanId(), "SpanId should be zero after clear");
144+
assertEquals(0, ctx.getRootSpanId(), "RootSpanId should be zero after clear");
150145
}
151146

152147
/**
@@ -164,24 +159,22 @@ public void testOtelModeCustomAttributes() throws Exception {
164159
// Register attribute keys
165160
OTelContext.getInstance().registerAttributeKeys("http.route", "db.system");
166161

167-
// Set context first (trace/span)
162+
profiler.resetThreadContext();
163+
168164
long spanId = 0xAAAABBBBCCCCDDDDL;
169165
long rootSpanId = 0x1111222233334444L;
170166
profiler.setContext(spanId, rootSpanId);
171167

172-
// Set custom attribute via new API
173168
ThreadContext ctx = profiler.getThreadContext();
174169
boolean result = ctx.setContextAttribute(0, "GET /api/users");
175170
assertTrue(result, "setContextAttribute should succeed");
176171

177172
result = ctx.setContextAttribute(1, "postgresql");
178173
assertTrue(result, "setContextAttribute for second key should succeed");
179174

180-
// Verify trace context is still intact (use direct JNI read)
181-
long[] context = ThreadContext.readOtelContext();
182-
assertNotNull(context, "Context should be readable after setAttribute");
183-
assertEquals(spanId, context[0], "SpanId should match after setAttribute");
184-
assertEquals(rootSpanId, context[1], "RootSpanId should match after setAttribute");
175+
// Verify trace context is still intact
176+
assertEquals(spanId, ctx.getSpanId(), "SpanId should match after setAttribute");
177+
assertEquals(rootSpanId, ctx.getRootSpanId(), "RootSpanId should match after setAttribute");
185178
}
186179

187180
/**
@@ -214,6 +207,8 @@ public void testOtelModeAttributeOverflow() throws Exception {
214207

215208
OTelContext.getInstance().registerAttributeKeys("k0", "k1", "k2", "k3", "k4");
216209

210+
profiler.resetThreadContext();
211+
217212
profiler.setContext(0x1L, 0x2L);
218213

219214
ThreadContext ctx = profiler.getThreadContext();

0 commit comments

Comments
 (0)