Skip to content

Commit 580f347

Browse files
committed
Fix Exception Replay with Lambda proxy classes
If JVM is started with -XX:+ShowhiddenFrames lambda proxy classes dynamically generated are shown in the stacktraces which may be used in the fingerprinting for Exception Replay. The proxy class generated contains an id that is different for each loading of the class. Upon re-transformation this id is changing which led to a different fingerprint for the same stacktrace which will trigger a new instrumentation and a re-transformation. And again new fingerprint... We are fixing this by filtering out lambda proxy classes if detected.
1 parent cb1d8ed commit 580f347

File tree

4 files changed

+79
-1
lines changed

4 files changed

+79
-1
lines changed

dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/util/ClassNameFiltering.java

+8-1
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
import datadog.trace.util.ClassNameTrie;
77
import java.util.Collections;
88
import java.util.Set;
9+
import java.util.regex.Pattern;
910

1011
/** A class to filter out classes based on their package name. */
1112
public class ClassNameFiltering implements ClassNameFilter {
13+
private static final Pattern LAMBDA_PROXY_CLASS_PATTERN = Pattern.compile(".*\\$\\$Lambda.*/.*");
1214

1315
private final ClassNameTrie includeTrie;
1416
private final ClassNameTrie excludeTrie;
@@ -33,7 +35,12 @@ public ClassNameFiltering(Set<String> excludes, Set<String> includes) {
3335
}
3436

3537
public boolean isExcluded(String className) {
36-
return includeTrie.apply(className) < 0 && excludeTrie.apply(className) > 0;
38+
return (includeTrie.apply(className) < 0 && excludeTrie.apply(className) > 0)
39+
|| isLambdaProxyClass(className);
40+
}
41+
42+
static boolean isLambdaProxyClass(String className) {
43+
return LAMBDA_PROXY_CLASS_PATTERN.matcher(className).matches();
3744
}
3845

3946
public static ClassNameFiltering allowAll() {

dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/util/ClassNameFilteringTest.java

+21
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,25 @@ public void testExcludeDefaults(String input) {
9292
new ClassNameFiltering(ThirdPartyLibraries.INSTANCE.getThirdPartyLibraries(config));
9393
assertTrue(classNameFiltering.isExcluded(input));
9494
}
95+
96+
@Test
97+
void lambdaProxyClasses() {
98+
// jdk8: at
99+
// datadog.smoketest.debugger.ServerDebuggerTestApplication$$Lambda$231/1770027171.apply(<Unknown>:1000008)
100+
// jdk11: at
101+
// datadog.smoketest.debugger.ServerDebuggerTestApplication$$Lambda$262/0x0000000800467040.apply(Unknown Source)
102+
// jdk17: at
103+
// datadog.smoketest.debugger.ServerDebuggerTestApplication$$Lambda$303/0x00000008013dd1f8.apply(Unknown Source)
104+
// jdk21: at
105+
// datadog.smoketest.debugger.ServerDebuggerTestApplication$$Lambda/0x000000b801392c58.apply(Unknown Source)
106+
assertTrue(
107+
ClassNameFiltering.isLambdaProxyClass(
108+
"datadog.smoketest.debugger.ServerDebuggerTestApplication$$Lambda$231/1770027171"));
109+
assertTrue(
110+
ClassNameFiltering.isLambdaProxyClass(
111+
"datadog.smoketest.debugger.ServerDebuggerTestApplication$$Lambda$262/0x0000000800467040"));
112+
assertTrue(
113+
ClassNameFiltering.isLambdaProxyClass(
114+
"at datadog.smoketest.debugger.ServerDebuggerTestApplication$$Lambda/0x000000b801392c58"));
115+
}
95116
}

dd-smoke-tests/debugger-integration-tests/src/main/java/datadog/smoketest/debugger/ServerDebuggerTestApplication.java

+16
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import java.util.HashMap;
66
import java.util.Map;
77
import java.util.function.Consumer;
8+
import java.util.function.Function;
89
import okhttp3.HttpUrl;
910
import okhttp3.MediaType;
1011
import okhttp3.OkHttpClient;
@@ -138,6 +139,8 @@ private static void runTracedMethod(String arg) {
138139
tracedMethodWithException(42, "foobar", 3.42, map, "var1", "var2", "var3");
139140
} else if ("deepOops".equals(arg)) {
140141
tracedMethodWithDeepException1(42, "foobar", 3.42, map, "var1", "var2", "var3");
142+
} else if ("lambdaOops".equals(arg)) {
143+
tracedMethodWithLambdaException(42, "foobar", 3.42, map, "var1", "var2", "var3");
141144
} else {
142145
tracedMethod(42, "foobar", 3.42, map, "var1", "var2", "var3");
143146
}
@@ -215,6 +218,19 @@ private static void tracedMethodWithDeepException5(
215218
tracedMethodWithException(argInt, argStr, argDouble, argMap, argVar);
216219
}
217220

221+
private static void tracedMethodWithLambdaException(
222+
int argInt, String argStr, double argDouble, Map<String, String> argMap, String... argVar) {
223+
throw toRuntimeException("lambdaOops");
224+
}
225+
226+
private static RuntimeException toRuntimeException(String msg) {
227+
return toException(RuntimeException::new, msg);
228+
}
229+
230+
private static <S extends Throwable> S toException(Function<String, S> constructor, String msg) {
231+
return constructor.apply(msg);
232+
}
233+
218234
private static class AppDispatcher extends Dispatcher {
219235
private final ServerDebuggerTestApplication app;
220236

dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/ExceptionDebuggerIntegrationTest.java

+34
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,40 @@ void test5CapturedFrames() throws Exception {
237237
});
238238
}
239239

240+
@Test
241+
@DisplayName("testLambdaHiddenFrames")
242+
@DisabledIf(value = "datadog.trace.api.Platform#isJ9", disabledReason = "HotSpot specific test")
243+
void testLambdaHiddenFrames() throws Exception {
244+
additionalJvmArgs.add("-XX:+UnlockDiagnosticVMOptions");
245+
additionalJvmArgs.add("-XX:+ShowHiddenFrames");
246+
appUrl = startAppAndAndGetUrl();
247+
execute(appUrl, TRACED_METHOD_NAME, "lambdaOops"); // instrumenting first exception
248+
waitForInstrumentation(appUrl);
249+
execute(appUrl, TRACED_METHOD_NAME, "lambdaOops"); // collecting snapshots and sending them
250+
registerTraceListener(this::receiveExceptionReplayTrace);
251+
registerSnapshotListener(this::receiveSnapshot);
252+
processRequests(
253+
() -> {
254+
if (snapshotIdTags.isEmpty()) {
255+
return false;
256+
}
257+
String snapshotId0 = snapshotIdTags.get(0);
258+
if (traceReceived && snapshotReceived && snapshots.containsKey(snapshotId0)) {
259+
Snapshot snapshot = snapshots.get(snapshotId0);
260+
assertNotNull(snapshot);
261+
assertEquals(
262+
"lambdaOops",
263+
snapshot.getCaptures().getReturn().getCapturedThrowable().getMessage());
264+
assertEquals(
265+
"datadog.smoketest.debugger.ServerDebuggerTestApplication.tracedMethodWithLambdaException",
266+
snapshot.getStack().get(0).getFunction());
267+
assertFullMethodCaptureArgs(snapshot.getCaptures().getReturn());
268+
return true;
269+
}
270+
return false;
271+
});
272+
}
273+
240274
private void resetSnapshotsAndTraces() {
241275
resetTraceListener();
242276
traceReceived = false;

0 commit comments

Comments
 (0)