Skip to content

Commit 1e473e3

Browse files
authored
Capture exception snapshots only once an hour (#6983)
* Capture exception snapshots only once an hour Keep a timestamp associated to the fingerprint to know when last time we capture snapshots for this exception. Hardcoded once an hour will probably make it a config token for this later * Instant.MIN remove comment * use seconds instead of millisecs with Instant.MIN
1 parent 7461cf4 commit 1e473e3

6 files changed

Lines changed: 103 additions & 11 deletions

File tree

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,15 @@
2828
import java.lang.ref.WeakReference;
2929
import java.nio.file.Path;
3030
import java.nio.file.Paths;
31+
import java.time.Duration;
3132
import java.util.zip.ZipOutputStream;
3233
import org.slf4j.Logger;
3334
import org.slf4j.LoggerFactory;
3435

3536
/** Debugger agent implementation */
3637
public class DebuggerAgent {
3738
private static final Logger LOGGER = LoggerFactory.getLogger(DebuggerAgent.class);
39+
public static final Duration EXCEPTION_CAPTURE_INTERVAL = Duration.ofHours(1);
3840
private static ConfigurationPoller configurationPoller;
3941
private static DebuggerSink sink;
4042
private static String agentVersion;
@@ -81,7 +83,8 @@ public static synchronized void run(
8183
DebuggerContext.initTracer(new DebuggerTracer(debuggerSink.getProbeStatusSink()));
8284
if (config.isDebuggerExceptionEnabled()) {
8385
DefaultExceptionDebugger defaultExceptionDebugger =
84-
new DefaultExceptionDebugger(configurationUpdater, classNameFiltering);
86+
new DefaultExceptionDebugger(
87+
configurationUpdater, classNameFiltering, EXCEPTION_CAPTURE_INTERVAL);
8588
DebuggerContext.initExceptionDebugger(defaultExceptionDebugger);
8689
}
8790
if (config.isDebuggerInstrumentTheWorld()) {

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import datadog.trace.bootstrap.debugger.DebuggerContext;
1313
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
1414
import datadog.trace.util.AgentTaskScheduler;
15+
import java.time.Duration;
1516
import java.util.List;
1617
import org.slf4j.Logger;
1718
import org.slf4j.LoggerFactory;
@@ -32,8 +33,13 @@ public class DefaultExceptionDebugger implements DebuggerContext.ExceptionDebugg
3233
private final ClassNameFiltering classNameFiltering;
3334

3435
public DefaultExceptionDebugger(
35-
ConfigurationUpdater configurationUpdater, ClassNameFiltering classNameFiltering) {
36-
this(new ExceptionProbeManager(classNameFiltering), configurationUpdater, classNameFiltering);
36+
ConfigurationUpdater configurationUpdater,
37+
ClassNameFiltering classNameFiltering,
38+
Duration captureInterval) {
39+
this(
40+
new ExceptionProbeManager(classNameFiltering, captureInterval),
41+
configurationUpdater,
42+
classNameFiltering);
3743
}
3844

3945
DefaultExceptionDebugger(
@@ -64,6 +70,7 @@ public void handleException(Throwable t, AgentSpan span) {
6470
return;
6571
}
6672
processSnapshotsAndSetTags(t, span, state, innerMostException);
73+
exceptionProbeManager.updateLastCapture(fingerprint);
6774
} else {
6875
if (exceptionProbeManager.createProbesForException(innerMostException.getStackTrace())) {
6976
AgentTaskScheduler.INSTANCE.execute(() -> applyExceptionConfiguration(fingerprint));

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

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@
77
import com.datadog.debugger.util.ExceptionHelper;
88
import com.datadog.debugger.util.WeakIdentityHashMap;
99
import datadog.trace.bootstrap.debugger.ProbeId;
10+
import java.time.Clock;
11+
import java.time.Duration;
12+
import java.time.Instant;
13+
import java.time.temporal.ChronoUnit;
1014
import java.util.ArrayList;
1115
import java.util.Collection;
1216
import java.util.Collections;
1317
import java.util.List;
1418
import java.util.Map;
15-
import java.util.Set;
1619
import java.util.UUID;
1720
import java.util.concurrent.ConcurrentHashMap;
1821
import org.slf4j.Logger;
@@ -22,15 +25,28 @@
2225
public class ExceptionProbeManager {
2326
private static final Logger LOGGER = LoggerFactory.getLogger(ExceptionProbeManager.class);
2427

25-
private final Set<String> fingerprints = ConcurrentHashMap.newKeySet();
28+
private final Map<String, Instant> fingerprints = new ConcurrentHashMap<>();
2629
private final Map<String, ExceptionProbe> probes = new ConcurrentHashMap<>();
2730
private final ClassNameFiltering classNameFiltering;
2831
// FIXME: if this becomes a bottleneck, find a way to make it concurrent weak identity hashmap
2932
private final Map<Throwable, ThrowableState> snapshotsByThrowable =
3033
Collections.synchronizedMap(new WeakIdentityHashMap<>());
34+
private final long captureIntervalS;
35+
private final Clock clock;
3136

32-
public ExceptionProbeManager(ClassNameFiltering classNameFiltering) {
37+
public ExceptionProbeManager(ClassNameFiltering classNameFiltering, Duration captureInterval) {
38+
this(classNameFiltering, captureInterval, Clock.systemUTC());
39+
}
40+
41+
ExceptionProbeManager(ClassNameFiltering classNameFiltering) {
42+
this(classNameFiltering, Duration.ofHours(1), Clock.systemUTC());
43+
}
44+
45+
ExceptionProbeManager(
46+
ClassNameFiltering classNameFiltering, Duration captureInterval, Clock clock) {
3347
this.classNameFiltering = classNameFiltering;
48+
this.captureIntervalS = captureInterval.getSeconds();
49+
this.clock = clock;
3450
}
3551

3652
public ClassNameFiltering getClassNameFiltering() {
@@ -62,7 +78,7 @@ public boolean createProbesForException(StackTraceElement[] stackTraceElements)
6278
}
6379

6480
void addFingerprint(String fingerprint) {
65-
fingerprints.add(fingerprint);
81+
fingerprints.put(fingerprint, Instant.MIN);
6682
}
6783

6884
private static ExceptionProbe createMethodProbe(
@@ -73,15 +89,23 @@ private static ExceptionProbe createMethodProbe(
7389
}
7490

7591
public boolean isAlreadyInstrumented(String fingerprint) {
76-
return fingerprints.contains(fingerprint);
92+
return fingerprints.containsKey(fingerprint);
7793
}
7894

7995
public Collection<ExceptionProbe> getProbes() {
8096
return probes.values();
8197
}
8298

8399
public boolean shouldCaptureException(String fingerprint) {
84-
return fingerprints.contains(fingerprint);
100+
return shouldCaptureException(fingerprint, clock);
101+
}
102+
103+
boolean shouldCaptureException(String fingerprint, Clock clock) {
104+
Instant lastCapture = fingerprints.get(fingerprint);
105+
if (lastCapture == null) {
106+
return false;
107+
}
108+
return ChronoUnit.SECONDS.between(lastCapture, Instant.now(clock)) >= captureIntervalS;
85109
}
86110

87111
public void addSnapshot(Snapshot snapshot) {
@@ -104,6 +128,14 @@ public ThrowableState getSateByThrowable(Throwable throwable) {
104128
return snapshotsByThrowable.get(throwable);
105129
}
106130

131+
public void updateLastCapture(String fingerprint) {
132+
updateLastCapture(fingerprint, clock);
133+
}
134+
135+
void updateLastCapture(String fingerprint, Clock clock) {
136+
fingerprints.put(fingerprint, Instant.now(clock));
137+
}
138+
107139
public static class ThrowableState {
108140
private final String exceptionId;
109141
private List<Snapshot> snapshots;

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ class DefaultExceptionDebuggerTest {
5858
public void setUp() {
5959
configurationUpdater = mock(ConfigurationUpdater.class);
6060
classNameFiltering = new ClassNameFiltering(emptySet());
61-
exceptionDebugger = new DefaultExceptionDebugger(configurationUpdater, classNameFiltering);
61+
exceptionDebugger =
62+
new DefaultExceptionDebugger(configurationUpdater, classNameFiltering, Duration.ofHours(1));
6263
listener = new TestSnapshotListener(createConfig(), mock(ProbeStatusSink.class));
6364
DebuggerAgentHelper.injectSink(listener);
6465
}

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@
3535
import datadog.trace.core.CoreTracer;
3636
import java.lang.instrument.ClassFileTransformer;
3737
import java.lang.instrument.Instrumentation;
38+
import java.time.Clock;
3839
import java.time.Duration;
40+
import java.time.Instant;
3941
import java.util.Map;
4042
import java.util.Set;
4143
import java.util.stream.Collectors;
@@ -216,6 +218,35 @@ public void recursive() throws Exception {
216218
assertEquals(1, globalSampler.getCallCount());
217219
}
218220

221+
@Test
222+
public void captureOncePerHour() throws Exception {
223+
Config config = createConfig();
224+
Clock clockMock = mock(Clock.class);
225+
when(clockMock.instant()).thenReturn(Instant.now());
226+
ExceptionProbeManager exceptionProbeManager =
227+
new ExceptionProbeManager(classNameFiltering, Duration.ofHours(1), clockMock);
228+
TestSnapshotListener listener =
229+
setupExceptionDebugging(config, exceptionProbeManager, classNameFiltering);
230+
final String CLASS_NAME = "com.datadog.debugger.CapturedSnapshot20";
231+
Class<?> testClass = compileAndLoadClass(CLASS_NAME);
232+
// instrument RuntimeException stacktrace
233+
String fingerprint0 = callMethodThrowingRuntimeException(testClass);
234+
assertWithTimeout(
235+
() -> exceptionProbeManager.isAlreadyInstrumented(fingerprint0), Duration.ofSeconds(30));
236+
// generate snapshots RuntimeException
237+
callMethodThrowingRuntimeException(testClass);
238+
assertEquals(1, listener.snapshots.size());
239+
listener.snapshots.clear();
240+
// second call, no snapshot should be generated
241+
callMethodThrowingRuntimeException(testClass);
242+
assertEquals(0, listener.snapshots.size());
243+
// Fast-forward 1 hour
244+
when(clockMock.instant()).thenReturn(Instant.now().plus(Duration.ofMinutes(61)));
245+
// second call, snapshot should be generated
246+
callMethodThrowingRuntimeException(testClass);
247+
assertEquals(1, listener.snapshots.size());
248+
}
249+
219250
private static void assertExceptionMsg(String expectedMsg, Snapshot snapshot) {
220251
assertEquals(
221252
expectedMsg, snapshot.getCaptures().getReturn().getCapturedThrowable().getMessage());

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
import com.datadog.debugger.probe.ExceptionProbe;
88
import com.datadog.debugger.util.ClassNameFiltering;
99
import datadog.trace.api.Config;
10+
import java.time.Clock;
11+
import java.time.Duration;
12+
import java.time.Instant;
1013
import java.util.Collections;
1114
import java.util.stream.Collectors;
1215
import java.util.stream.Stream;
@@ -41,7 +44,7 @@ void instrumentSingleFrame() {
4144
ExceptionProbeManager exceptionProbeManager = new ExceptionProbeManager(classNameFiltering);
4245

4346
String fingerprint = Fingerprinter.fingerprint(exception, classNameFiltering);
44-
assertEquals("d2e9d63e304d95f6435d77bf4d0d387521591e550be21d432339a14ee1cb40", fingerprint);
47+
assertEquals("1c27b291764c9d387fb85247bb7c2711f885aadfbf2f64fed34b2e0c64c5a2", fingerprint);
4548
exceptionProbeManager.createProbesForException(exception.getStackTrace());
4649
assertEquals(1, exceptionProbeManager.getProbes().size());
4750
ExceptionProbe exceptionProbe = exceptionProbeManager.getProbes().iterator().next();
@@ -70,4 +73,19 @@ void filterAllFrames() {
7073
exceptionProbeManager.createProbesForException(exception.getStackTrace());
7174
assertEquals(0, exceptionProbeManager.getProbes().size());
7275
}
76+
77+
@Test
78+
void lastCapture() {
79+
ClassNameFiltering classNameFiltering = ClassNameFiltering.allowAll();
80+
ExceptionProbeManager exceptionProbeManager = new ExceptionProbeManager(classNameFiltering);
81+
RuntimeException exception = new RuntimeException("test");
82+
String fingerprint = Fingerprinter.fingerprint(exception, classNameFiltering);
83+
exceptionProbeManager.addFingerprint(fingerprint);
84+
assertTrue(exceptionProbeManager.shouldCaptureException(fingerprint));
85+
exceptionProbeManager.updateLastCapture(fingerprint);
86+
assertFalse(exceptionProbeManager.shouldCaptureException(fingerprint));
87+
Clock clock =
88+
Clock.fixed(Instant.now().plus(Duration.ofMinutes(61)), Clock.systemUTC().getZone());
89+
assertTrue(exceptionProbeManager.shouldCaptureException(fingerprint, clock));
90+
}
7391
}

0 commit comments

Comments
 (0)