Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,6 @@ public int hashCode() {
public String toString() {
return String.format(
"CodeOriginProbe{probeId=%s, entrySpanProbe=%s, signature=%s, where=%s, location=%s}",
probeId, entrySpanProbe, signature, where, location);
getProbeId(), entrySpanProbe, signature, where, location);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.datadog.debugger.probe;

import static com.datadog.debugger.probe.LogProbe.Capture.toLimits;
import static com.datadog.debugger.probe.LogProbe.CoordinatedSamplingState.Status.DROP;
import static com.datadog.debugger.probe.LogProbe.CoordinatedSamplingState.Status.EMIT;
import static datadog.trace.api.debugger.DebuggerMetricCollector.SkippedReason.RATE_LIMIT;
import static java.lang.String.format;

Expand All @@ -24,6 +26,8 @@
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
import datadog.context.Context;
import datadog.context.ContextKey;
import datadog.trace.api.Config;
import datadog.trace.api.CorrelationIdentifier;
import datadog.trace.api.DDTraceId;
Expand Down Expand Up @@ -52,6 +56,8 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import org.slf4j.Logger;
Expand All @@ -62,7 +68,8 @@ public class LogProbe extends ProbeDefinition implements Sampled, CapturedContex
private static final Logger LOGGER = LoggerFactory.getLogger(LogProbe.class);
private static final Limits LIMITS = new Limits(1, 3, 8192, 5);
private static final int LOG_MSG_LIMIT = 8192;

private static final ContextKey<CoordinatedSamplingState> SAMPLING_KEY =
ContextKey.named("debugger_sampling");
public static final int CAPTURING_PROBE_BUDGET = 10;
public static final int NON_CAPTURING_PROBE_BUDGET = 1000;

Expand Down Expand Up @@ -511,7 +518,7 @@ public InstrumentationResult.Status instrument(
public boolean isReadyToCapture() {
if (!hasCondition()) {
// we are sampling here to avoid creating CapturedContext when the sampling result is negative
boolean sampled = ProbeRateLimiter.tryProbe(sampler, isFullSnapshot());
boolean sampled = trySample(sampler);
if (!sampled) {
DebuggerAgent.getSink().skipSnapshot(id, RATE_LIMIT);
}
Expand Down Expand Up @@ -585,9 +592,7 @@ private void sample(LogStatus logStatus, MethodLocation methodLocation) {
// at 1/s rate instead of the log template one
Sampler localSampler =
logStatus.hasConditionErrors && !isFullSnapshot() ? errorSampler : sampler;
boolean sampled =
!logStatus.getDebugSessionStatus().isDisabled()
&& ProbeRateLimiter.tryProbe(localSampler, isFullSnapshot());
boolean sampled = !logStatus.getDebugSessionStatus().isDisabled() && trySample(localSampler);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude active debug-session probes from shared sampling

When a full-snapshot probe tagged for an active debug session reaches this path, such as a conditioned probe, trySample can cache DROP even though LogStatus.shouldSend() later emits that probe unconditionally because the trigger already sampled the session. Every ordinary snapshot probe later in the same local trace then observes DROP and is suppressed, producing the fragmented snapshot set this coordination is meant to prevent; conversely, an active probe still emits after an ordinary probe cached DROP. Bypass or update the coordinated state for active-session probes rather than recording a decision that shouldSend() ignores.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude active debug-session probes from the shared decision

When a full-snapshot probe tagged for an active debug session reaches this path (for example, a conditioned probe), trySample can cache DROP even though LogStatus.shouldSend() later emits that probe unconditionally because the trigger already sampled the session. Every ordinary snapshot probe later in the same local trace then observes DROP and is suppressed, producing exactly the fragmented snapshot set this coordination is meant to prevent; if an ordinary probe cached DROP first, the active probe still emits with the same inconsistency. Bypass or update the coordinated state for active-session probes rather than recording a decision that shouldSend() ignores.

Useful? React with 👍 / 👎.

logStatus.setSampled(sampled);
if (!sampled && !logStatus.getDebugSessionStatus().isDisabled()) {
DebuggerAgent.getSink().skipSnapshot(id, RATE_LIMIT);
Expand Down Expand Up @@ -642,6 +647,71 @@ public void commit(
}
}

/**
* Holds the once-per-trace coordinated sampling decision shared by all full-snapshot probes on
* a given local root span, so that either all of them emit or none of them do.
*/
static class CoordinatedSamplingState {
/** Outcome of the first probe's sampling decision for the trace, cached on the root span. */
enum Status {
/** The trace was not sampled; every probe sharing this state must not emit. */
DROP,
/** The trace was sampled; probes sharing this state may emit, once each. */
EMIT
}

private final Set<String> emittedProbeIds;
private final Status status;

CoordinatedSamplingState(Status status) {
this.status = status;
if (status == EMIT) {
emittedProbeIds = ConcurrentHashMap.newKeySet();
} else {
emittedProbeIds = Collections.emptySet();
}
}

boolean tryEmit(String probeEncodedId) {
return status == EMIT && emittedProbeIds.add(probeEncodedId);
}
}

private boolean trySample(Sampler sampler) {
if (!isFullSnapshot()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Exclude exception probes from shared sampling

One feature can stop the other feature or let it emit without its own sampling decision.

Assertion details
  • Input: Enable exception debugging and an ordinary snapshot probe on the same local root span.
  • Expected: Exception probes must use their exception sampling flow. They must not share the coordinated state of ordinary snapshot probes.
  • Actual: The first full-snapshot probe stores its decision on the local root span. An ExceptionProbe uses the same state because it extends LogProbe and is a full-snapshot probe.

Was this helpful? React 👍 or 👎
🤖 Datadog Autotest · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest · Open Bits AI session

return ProbeRateLimiter.tryProbe(sampler, false);
}

AgentSpan localRootSpan = getActiveLocalRootSpan();
if (localRootSpan == null) {
return ProbeRateLimiter.tryProbe(sampler, true);
}

CoordinatedSamplingState state = Context.from(localRootSpan).get(SAMPLING_KEY);
Comment thread
jpbempel marked this conversation as resolved.
Comment thread
jpbempel marked this conversation as resolved.
if (state == null) {
synchronized (localRootSpan) {
Context traceContext = Context.from(localRootSpan);
state = traceContext.get(SAMPLING_KEY);
if (state == null) {
boolean sampled = ProbeRateLimiter.tryProbe(sampler, true);
state = new CoordinatedSamplingState(sampled ? EMIT : DROP);
traceContext.with(SAMPLING_KEY, state).attachTo(localRootSpan);
}
}
}
return state.tryEmit(getProbeId().getEncodedId());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve snapshot rate caps for coordinated followers

When an EMIT trace reaches two or more full-snapshot probes, only the first probe invokes ProbeRateLimiter.tryProbe; every later probe returns true here without consulting either its own sampler or GLOBAL_SNAPSHOT_SAMPLER. This lets followers exceed their documented snapshotsPerSecond maximum when probes have different rates, and it lets the configured global snapshot cap be exceeded by up to the number of matching probes per trace. Use a coordinated group decision that still enforces the applicable per-probe and global caps rather than bypassing both samplers for followers.

Useful? React with 👍 / 👎.

}

private AgentSpan getActiveLocalRootSpan() {
TracerAPI tracer = AgentTracer.get();
AgentSpan activeSpan = tracer != null ? tracer.activeSpan() : null;
if (activeSpan == null || activeSpan == AgentTracer.noopSpan()) {
return null;
}
AgentSpan localRootSpan = activeSpan.getLocalRootSpan();
return localRootSpan != AgentTracer.noopSpan() ? localRootSpan : null;
}

protected Snapshot createSnapshot() {
int maxDepth = capture != null ? capture.maxReferenceDepth : -1;
return new Snapshot(Thread.currentThread(), this, maxDepth);
Expand All @@ -652,8 +722,9 @@ protected boolean fillSnapshot(
CapturedContext exitContext,
List<CapturedContext.CapturedThrowable> caughtExceptions,
Snapshot snapshot) {
LogStatus entryStatus = convertStatus(entryContext.getStatus(probeId.getEncodedId()));
LogStatus exitStatus = convertStatus(exitContext.getStatus(probeId.getEncodedId()));
String probeEncodedId = getProbeId().getEncodedId();
LogStatus entryStatus = convertStatus(entryContext.getStatus(probeEncodedId));
LogStatus exitStatus = convertStatus(exitContext.getStatus(probeEncodedId));
String message = null;
switch (evaluateAt) {
case ENTRY:
Expand Down Expand Up @@ -829,7 +900,7 @@ protected void commitSnapshot(Snapshot snapshot, DebuggerSink sink) {

@Override
public void commit(CapturedContext lineContext, int line) {
LogStatus status = (LogStatus) lineContext.getStatus(probeId.getEncodedId());
LogStatus status = (LogStatus) lineContext.getStatus(getProbeId().getEncodedId());
if (status == null) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public abstract class ProbeDefinition implements ProbeImplementation {
protected final String language;
protected final String id;
protected final int version;
protected transient ProbeId probeId;
private transient ProbeId probeId;
protected final Tag[] tags;
protected final Map<String, String> tagMap = new HashMap<>();
protected final Where where;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,10 +273,11 @@ public void commit(
CapturedContext entryContext,
CapturedContext exitContext,
List<CapturedContext.CapturedThrowable> caughtExceptions) {
String probeEncodedId = getProbeId().getEncodedId();
CapturedContext.Status status =
evaluateAt == MethodLocation.EXIT
? exitContext.getStatus(probeId.getEncodedId())
: entryContext.getStatus(probeId.getEncodedId());
? exitContext.getStatus(probeEncodedId)
: entryContext.getStatus(probeEncodedId);
if (status == null) {
return;
}
Expand All @@ -287,7 +288,7 @@ public void commit(

@Override
public void commit(CapturedContext lineContext, int line) {
CapturedContext.Status status = lineContext.getStatus(probeId.getEncodedId());
CapturedContext.Status status = lineContext.getStatus(getProbeId().getEncodedId());
if (status == null) {
return;
}
Expand Down Expand Up @@ -319,7 +320,7 @@ private void decorateTags(SpanDecorationStatus status) {
}
if (!tagsToDecorate.isEmpty()) {
// only send EMITTING status if we set at least one tag
DebuggerAgent.getSink().getProbeStatusSink().addEmitting(probeId);
DebuggerAgent.getSink().getProbeStatusSink().addEmitting(getProbeId());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ private boolean evaluateCondition(CapturedContext capture) {
Duration timeout = Duration.ofMillis(Config.get().getDynamicInstrumentationEvalTimeout());
return probeCondition.execute(capture, TimeoutChecker.create(Config.get(), timeout));
} catch (Exception ex) {
DebuggerAgent.getSink().getProbeStatusSink().addError(probeId, ex);
DebuggerAgent.getSink().getProbeStatusSink().addError(getProbeId(), ex);
return false;
} finally {
LOGGER.debug(
Expand Down Expand Up @@ -199,7 +199,7 @@ public String toString() {
language,
location,
probeCondition,
probeId,
getProbeId(),
sampling,
tagMap,
Arrays.toString(tags),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.params.provider.Arguments.arguments;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
Expand Down Expand Up @@ -444,15 +445,6 @@ public void multiProbeSameMethod() throws IOException, URISyntaxException {
assertCaptureReturnValue(snapshot1.getCaptures().getReturn(), "int", "31");
}

private List<Snapshot> assertSnapshots(
TestSnapshotListener listener, int expectedCount, ProbeId... probeIds) {
assertEquals(expectedCount, listener.snapshots.size());
for (int i = 0; i < probeIds.length; i++) {
assertEquals(probeIds[i].getId(), listener.snapshots.get(i).getProbe().getId());
}
return listener.snapshots;
}

@Test
public void catchBlock() throws IOException, URISyntaxException {
final String CLASS_NAME = "CapturedSnapshot02";
Expand Down Expand Up @@ -908,9 +900,14 @@ public void fieldExtractorDepth1() throws IOException, URISyntaxException {
@Test
public void fieldExtractorDuplicateUnionDepth() throws IOException, URISyntaxException {
final String CLASS_NAME = "CapturedSnapshot04";
LogProbe.Builder builder = createProbeBuilder(PROBE_ID, CLASS_NAME, "createSimpleData", "()");
LogProbe probe1 = builder.capture(0, 100, 50, Limits.DEFAULT_FIELD_COUNT).build();
LogProbe probe2 = builder.capture(3, 100, 50, Limits.DEFAULT_FIELD_COUNT).build();
LogProbe probe1 =
createProbeBuilder(PROBE_ID1, CLASS_NAME, "createSimpleData", "()")
.capture(0, 100, 50, Limits.DEFAULT_FIELD_COUNT)
.build();
LogProbe probe2 =
createProbeBuilder(PROBE_ID2, CLASS_NAME, "createSimpleData", "()")
.capture(3, 100, 50, Limits.DEFAULT_FIELD_COUNT)
.build();
TestSnapshotListener listener = installProbes(probe1, probe2);
Class<?> testClass = compileAndLoadClass(CLASS_NAME);
int result = Reflect.onClass(testClass).call("main", "").get();
Expand Down Expand Up @@ -2330,7 +2327,8 @@ public void enumConstructorArgs() throws IOException, URISyntaxException {
final String CLASS_NAME = "com.datadog.debugger.CapturedSnapshot23";
final String ENUM_CLASS = CLASS_NAME + "$MyEnum";
TestSnapshotListener listener =
installProbes(createMethodProbe(PROBE_ID, ENUM_CLASS, "<init>", null));
installProbes(
createProbeBuilder(PROBE_ID, ENUM_CLASS, "<init>", null).sampling(10).build());
Class<?> testClass = compileAndLoadClass(CLASS_NAME);
int result = Reflect.onClass(testClass).call("main", "").get();
assertEquals(2, result);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import com.datadog.debugger.probe.Sampled;
import com.datadog.debugger.sink.DebuggerSink;
import com.datadog.debugger.sink.ProbeStatusSink;
import com.datadog.debugger.sink.Snapshot;
import com.datadog.debugger.util.MoshiHelper;
import com.datadog.debugger.util.MoshiSnapshotTestHelper;
import com.datadog.debugger.util.SerializerWithLimits;
Expand Down Expand Up @@ -160,6 +161,15 @@ private static Collection<?> getCollection(CapturedContext.CapturedValue capture
}
}

protected List<Snapshot> assertSnapshots(
TestSnapshotListener listener, int expectedCount, ProbeId... probeIds) {
assertEquals(expectedCount, listener.snapshots.size());
for (int i = 0; i < probeIds.length; i++) {
assertEquals(probeIds[i].getId(), listener.snapshots.get(i).getProbe().getId());
}
return listener.snapshots;
}

protected void assertCaptureFields(
CapturedContext context, String name, String typeName, Map<Object, Object> expectedMap) {
CapturedContext.CapturedValue field = getFields(context.getArguments().get("this")).get(name);
Expand Down
Loading
Loading