Skip to content

Commit 3584e51

Browse files
authored
Fix Max captured frames for Exception Replay (#8856)
For recursive frames we were sending snapshot as long as the frame are the same. frame capture was relying on the fact they were different. Now we are limiting the snapshot sent to config parameter
1 parent 7dfc057 commit 3584e51

File tree

7 files changed

+82
-16
lines changed

7 files changed

+82
-16
lines changed

dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerAgent.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,8 @@ public static void startExceptionReplay() {
212212
configurationUpdater,
213213
classNameFilter,
214214
Duration.ofSeconds(config.getDebuggerExceptionCaptureInterval()),
215-
config.getDebuggerMaxExceptionPerSecond());
215+
config.getDebuggerMaxExceptionPerSecond(),
216+
config.getDebuggerExceptionMaxCapturedFrames());
216217
DebuggerContext.initExceptionDebugger(exceptionDebugger);
217218
LOGGER.info("Started Exception Replay");
218219
}

dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/exception/DefaultExceptionDebugger.java

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,28 +38,33 @@ public class DefaultExceptionDebugger implements DebuggerContext.ExceptionDebugg
3838
private final ConfigurationUpdater configurationUpdater;
3939
private final ClassNameFilter classNameFiltering;
4040
private final CircuitBreaker circuitBreaker;
41+
private final int maxCapturedFrames;
4142

4243
public DefaultExceptionDebugger(
4344
ConfigurationUpdater configurationUpdater,
4445
ClassNameFilter classNameFiltering,
4546
Duration captureInterval,
46-
int maxExceptionPerSecond) {
47+
int maxExceptionPerSecond,
48+
int maxCapturedFrames) {
4749
this(
4850
new ExceptionProbeManager(classNameFiltering, captureInterval),
4951
configurationUpdater,
5052
classNameFiltering,
51-
maxExceptionPerSecond);
53+
maxExceptionPerSecond,
54+
maxCapturedFrames);
5255
}
5356

5457
DefaultExceptionDebugger(
5558
ExceptionProbeManager exceptionProbeManager,
5659
ConfigurationUpdater configurationUpdater,
5760
ClassNameFilter classNameFiltering,
58-
int maxExceptionPerSecond) {
61+
int maxExceptionPerSecond,
62+
int maxCapturedFrames) {
5963
this.exceptionProbeManager = exceptionProbeManager;
6064
this.configurationUpdater = configurationUpdater;
6165
this.classNameFiltering = classNameFiltering;
6266
this.circuitBreaker = new CircuitBreaker(maxExceptionPerSecond, Duration.ofSeconds(1));
67+
this.maxCapturedFrames = maxCapturedFrames;
6368
}
6469

6570
@Override
@@ -91,7 +96,8 @@ public void handleException(Throwable t, AgentSpan span) {
9196
LOGGER.debug("Unable to find state for throwable: {}", innerMostException.toString());
9297
return;
9398
}
94-
processSnapshotsAndSetTags(t, span, state, chainedExceptionsList, fingerprint);
99+
processSnapshotsAndSetTags(
100+
t, span, state, chainedExceptionsList, fingerprint, maxCapturedFrames);
95101
exceptionProbeManager.updateLastCapture(fingerprint);
96102
} else {
97103
// climb up the exception chain to find the first exception that has instrumented frames
@@ -128,7 +134,8 @@ private static void processSnapshotsAndSetTags(
128134
AgentSpan span,
129135
ThrowableState state,
130136
List<Throwable> chainedExceptions,
131-
String fingerprint) {
137+
String fingerprint,
138+
int maxCapturedFrames) {
132139
if (span.getTag(DD_DEBUG_ERROR_EXCEPTION_ID) != null) {
133140
LOGGER.debug("Clear previous frame tags");
134141
// already set for this span, clear the frame tags
@@ -142,7 +149,8 @@ private static void processSnapshotsAndSetTags(
142149
}
143150
boolean snapshotAssigned = false;
144151
List<Snapshot> snapshots = state.getSnapshots();
145-
for (int i = 0; i < snapshots.size(); i++) {
152+
int maxSnapshotSize = Math.min(snapshots.size(), maxCapturedFrames);
153+
for (int i = 0; i < maxSnapshotSize; i++) {
146154
Snapshot snapshot = snapshots.get(i);
147155
Throwable currentEx = chainedExceptions.get(snapshot.getChainedExceptionIdx());
148156
int[] mapping = createThrowableMapping(currentEx, t);

dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/exception/DefaultExceptionDebuggerTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public void setUp() {
6767
new HashSet<>(singletonList("com.datadog.debugger.exception.ThirdPartyCode")));
6868
exceptionDebugger =
6969
new DefaultExceptionDebugger(
70-
configurationUpdater, classNameFiltering, Duration.ofHours(1), 100);
70+
configurationUpdater, classNameFiltering, Duration.ofHours(1), 100, 3);
7171
listener = new TestSnapshotListener(createConfig(), mock(ProbeStatusSink.class));
7272
DebuggerAgentHelper.injectSink(listener);
7373
}
@@ -275,7 +275,7 @@ public void nestedExceptionFullThirdParty() {
275275
public void filteringOutErrors() {
276276
ExceptionProbeManager manager = mock(ExceptionProbeManager.class);
277277
exceptionDebugger =
278-
new DefaultExceptionDebugger(manager, configurationUpdater, classNameFiltering, 100);
278+
new DefaultExceptionDebugger(manager, configurationUpdater, classNameFiltering, 100, 3);
279279
exceptionDebugger.handleException(new AssertionError("test"), mock(AgentSpan.class));
280280
verify(manager, times(0)).isAlreadyInstrumented(any());
281281
}

dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/exception/ExceptionProbeInstrumentationTest.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
import com.datadog.debugger.agent.ClassesToRetransformFinder;
1818
import com.datadog.debugger.agent.Configuration;
1919
import com.datadog.debugger.agent.ConfigurationUpdater;
20-
import com.datadog.debugger.agent.DebuggerAgent;
2120
import com.datadog.debugger.agent.DebuggerAgentHelper;
2221
import com.datadog.debugger.agent.DebuggerTransformer;
2322
import com.datadog.debugger.agent.JsonSnapshotSerializer;
@@ -99,7 +98,10 @@ public void before() {
9998
ProbeRateLimiter.setSamplerSupplier(rate -> rate < 101 ? probeSampler : globalSampler);
10099
ProbeRateLimiter.setGlobalSnapshotRate(1000);
101100
// to activate the call to DebuggerContext.handleException
102-
DebuggerAgent.startExceptionReplay();
101+
DebuggerContext.ProductConfigUpdater mockProductConfigUpdater =
102+
mock(DebuggerContext.ProductConfigUpdater.class);
103+
when(mockProductConfigUpdater.isExceptionReplayEnabled()).thenReturn(true);
104+
DebuggerContext.initProductConfigUpdater(mockProductConfigUpdater);
103105
setFieldInConfig(Config.get(), "debuggerExceptionEnabled", true);
104106
setFieldInConfig(Config.get(), "dynamicInstrumentationClassFileDumpEnabled", true);
105107
}
@@ -224,7 +226,8 @@ public void recursive() throws Exception {
224226
callMethodFiboException(testClass); // generate snapshots
225227
Map<String, Set<String>> probeIdsByMethodName =
226228
extractProbeIdsByMethodName(exceptionProbeManager);
227-
assertEquals(10, listener.snapshots.size());
229+
// limited by Config::getDebuggerExceptionMaxCapturedFrames
230+
assertEquals(3, listener.snapshots.size());
228231
Snapshot snapshot0 = listener.snapshots.get(0);
229232
assertProbeId(probeIdsByMethodName, "fiboException", snapshot0.getProbe().getId());
230233
assertEquals(
@@ -234,8 +237,6 @@ public void recursive() throws Exception {
234237
assertEquals("2", getValue(snapshot1.getCaptures().getReturn().getArguments().get("n")));
235238
Snapshot snapshot2 = listener.snapshots.get(2);
236239
assertEquals("3", getValue(snapshot2.getCaptures().getReturn().getArguments().get("n")));
237-
Snapshot snapshot9 = listener.snapshots.get(9);
238-
assertEquals("10", getValue(snapshot9.getCaptures().getReturn().getArguments().get("n")));
239240
// sampling happens only once ont he first snapshot then forced for coordinated sampling
240241
assertEquals(1, probeSampler.getCallCount());
241242
assertEquals(1, globalSampler.getCallCount());
@@ -382,7 +383,7 @@ private TestSnapshotListener setupExceptionDebugging(
382383
DebuggerContext.initValueSerializer(new JsonSnapshotSerializer());
383384
DefaultExceptionDebugger exceptionDebugger =
384385
new DefaultExceptionDebugger(
385-
exceptionProbeManager, configurationUpdater, classNameFiltering, 100);
386+
exceptionProbeManager, configurationUpdater, classNameFiltering, 100, 3);
386387
DebuggerContext.initExceptionDebugger(exceptionDebugger);
387388
configurationUpdater.accept(REMOTE_CONFIG, definitions);
388389
return listener;

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ private static void runTracedMethod(String arg) {
153153
tracedMethodWithDeepException1(42, "foobar", 3.42, map, "var1", "var2", "var3");
154154
} else if ("lambdaOops".equals(arg)) {
155155
tracedMethodWithLambdaException(42, "foobar", 3.42, map, "var1", "var2", "var3");
156+
} else if ("recursiveOops".equals(arg)) {
157+
tracedMethodWithRecursiveException(42, "foobar", 3.42, map, "var1", "var2", "var3");
156158
} else {
157159
tracedMethod(42, "foobar", 3.42, map, "var1", "var2", "var3");
158160
}
@@ -239,6 +241,15 @@ private static void tracedMethodWithLambdaException(
239241
throw toRuntimeException("lambdaOops");
240242
}
241243

244+
private static void tracedMethodWithRecursiveException(
245+
int argInt, String argStr, double argDouble, Map<String, String> argMap, String... argVar) {
246+
if (argInt > 0) {
247+
tracedMethodWithRecursiveException(argInt - 8, argStr, argDouble, argMap, argVar);
248+
} else {
249+
throw new RuntimeException("recursiveOops");
250+
}
251+
}
252+
242253
private static RuntimeException toRuntimeException(String msg) {
243254
return toException(RuntimeException::new, msg);
244255
}

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,51 @@ void test5CapturedFrames() throws Exception {
237237
});
238238
}
239239

240+
@Test
241+
@DisplayName("test3CapturedRecursiveFrames")
242+
@DisabledIf(
243+
value = "datadog.trace.api.Platform#isJ9",
244+
disabledReason = "we cannot get local variable debug info")
245+
void test3CapturedRecursiveFrames() throws Exception {
246+
appUrl = startAppAndAndGetUrl();
247+
execute(appUrl, TRACED_METHOD_NAME, "recursiveOops"); // instrumenting first exception
248+
waitForInstrumentation(appUrl);
249+
execute(appUrl, TRACED_METHOD_NAME, "recursiveOops"); // 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+
if (traceReceived
258+
&& snapshotReceived
259+
&& snapshots.containsKey(snapshotIdTags.get(0))
260+
&& snapshots.containsKey(snapshotIdTags.get(1))
261+
&& snapshots.containsKey(snapshotIdTags.get(2))) {
262+
assertEquals(3, snapshotIdTags.size());
263+
assertEquals(3, snapshots.size());
264+
// snapshot 0
265+
assertRecursiveSnapshot(snapshots.get(snapshotIdTags.get(0)));
266+
// snapshot 1
267+
assertRecursiveSnapshot(snapshots.get(snapshotIdTags.get(1)));
268+
// snapshot 2
269+
assertRecursiveSnapshot(snapshots.get(snapshotIdTags.get(2)));
270+
return true;
271+
}
272+
return false;
273+
});
274+
}
275+
276+
private static void assertRecursiveSnapshot(Snapshot snapshot) {
277+
assertNotNull(snapshot);
278+
assertEquals(
279+
"recursiveOops", snapshot.getCaptures().getReturn().getCapturedThrowable().getMessage());
280+
assertEquals(
281+
"datadog.smoketest.debugger.ServerDebuggerTestApplication.tracedMethodWithRecursiveException",
282+
snapshot.getStack().get(0).getFunction());
283+
}
284+
240285
@Test
241286
@DisplayName("testLambdaHiddenFrames")
242287
@DisabledIf(value = "datadog.trace.api.Platform#isJ9", disabledReason = "HotSpot specific test")

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ void testDynamicInstrumentationEnablementWithLineProbe() throws Exception {
4545
LogProbe probe =
4646
LogProbe.builder()
4747
.probeId(LINE_PROBE_ID1)
48-
.where("ServerDebuggerTestApplication.java", 301)
48+
.where("ServerDebuggerTestApplication.java", 312)
4949
.build();
5050
setCurrentConfiguration(createConfig(probe));
5151
waitForFeatureStarted(appUrl, "Dynamic Instrumentation");

0 commit comments

Comments
 (0)