11package com .datadog .debugger .probe ;
22
33import static com .datadog .debugger .probe .LogProbe .Capture .toLimits ;
4+ import static com .datadog .debugger .probe .LogProbe .CoordinatedSamplingState .Status .DROP ;
5+ import static com .datadog .debugger .probe .LogProbe .CoordinatedSamplingState .Status .EMIT ;
46import static datadog .trace .api .debugger .DebuggerMetricCollector .SkippedReason .RATE_LIMIT ;
57import static java .lang .String .format ;
68
2426import com .squareup .moshi .JsonAdapter ;
2527import com .squareup .moshi .JsonReader ;
2628import com .squareup .moshi .JsonWriter ;
29+ import datadog .context .Context ;
30+ import datadog .context .ContextKey ;
2731import datadog .trace .api .Config ;
2832import datadog .trace .api .CorrelationIdentifier ;
2933import datadog .trace .api .DDTraceId ;
5256import java .util .List ;
5357import java .util .Map ;
5458import java .util .Objects ;
59+ import java .util .Set ;
60+ import java .util .concurrent .ConcurrentHashMap ;
5561import java .util .concurrent .atomic .AtomicInteger ;
5662import java .util .function .Consumer ;
5763import org .slf4j .Logger ;
@@ -62,7 +68,8 @@ public class LogProbe extends ProbeDefinition implements Sampled, CapturedContex
6268 private static final Logger LOGGER = LoggerFactory .getLogger (LogProbe .class );
6369 private static final Limits LIMITS = new Limits (1 , 3 , 8192 , 5 );
6470 private static final int LOG_MSG_LIMIT = 8192 ;
65-
71+ private static final ContextKey <CoordinatedSamplingState > SAMPLING_KEY =
72+ ContextKey .named ("debugger_sampling" );
6673 public static final int CAPTURING_PROBE_BUDGET = 10 ;
6774 public static final int NON_CAPTURING_PROBE_BUDGET = 1000 ;
6875
@@ -511,7 +518,7 @@ public InstrumentationResult.Status instrument(
511518 public boolean isReadyToCapture () {
512519 if (!hasCondition ()) {
513520 // we are sampling here to avoid creating CapturedContext when the sampling result is negative
514- boolean sampled = ProbeRateLimiter . tryProbe (sampler , isFullSnapshot () );
521+ boolean sampled = trySample (sampler );
515522 if (!sampled ) {
516523 DebuggerAgent .getSink ().skipSnapshot (id , RATE_LIMIT );
517524 }
@@ -581,15 +588,29 @@ private void sample(LogStatus logStatus, MethodLocation methodLocation) {
581588 if (!MethodLocation .isSame (methodLocation , evaluateAt )) {
582589 return ;
583590 }
584- // if condition has error and no capture Snapshot, the error is reported using errorSampler
585- // at 1/s rate instead of the log template one
586- Sampler localSampler =
587- logStatus .hasConditionErrors && !isFullSnapshot () ? errorSampler : sampler ;
588- boolean sampled =
589- !logStatus .getDebugSessionStatus ().isDisabled ()
590- && ProbeRateLimiter .tryProbe (localSampler , isFullSnapshot ());
591+ DebugSessionStatus debugSessionStatus = logStatus .getDebugSessionStatus ();
592+ if (debugSessionStatus .isDisabled ()) {
593+ return ;
594+ }
595+ boolean sampled ;
596+ if (debugSessionStatus .isActive ()) {
597+ // LogStatus.shouldSend() emits unconditionally when the debug session is ACTIVE, so this
598+ // probe must not record a coordinated decision that shouldSend() would ignore anyway.
599+ // Instead, make sure the shared trace-level state lets the other full-snapshot probes on
600+ // this trace emit too, rather than leaving/recording a DROP that would suppress them.
601+ sampled = true ;
602+ if (isFullSnapshot () && useCoordinatedSampling ()) {
603+ forceCoordinatedEmit ();
604+ }
605+ } else {
606+ // if condition has error and no capture Snapshot, the error is reported using errorSampler
607+ // at 1/s rate instead of the log template one
608+ Sampler localSampler =
609+ logStatus .hasConditionErrors && !isFullSnapshot () ? errorSampler : sampler ;
610+ sampled = trySample (localSampler );
611+ }
591612 logStatus .setSampled (sampled );
592- if (!sampled && ! logStatus . getDebugSessionStatus (). isDisabled () ) {
613+ if (!sampled ) {
593614 DebuggerAgent .getSink ().skipSnapshot (id , RATE_LIMIT );
594615 }
595616 }
@@ -642,6 +663,104 @@ public void commit(
642663 }
643664 }
644665
666+ /**
667+ * Holds the once-per-trace coordinated sampling decision shared by all full-snapshot probes on a
668+ * given local root span, so that either all of them emit or none of them do.
669+ */
670+ static class CoordinatedSamplingState {
671+ /** Outcome of the first probe's sampling decision for the trace, cached on the root span. */
672+ enum Status {
673+ /** The trace was not sampled; every probe sharing this state must not emit. */
674+ DROP ,
675+ /** The trace was sampled; probes sharing this state may emit, once each. */
676+ EMIT
677+ }
678+
679+ private final Set <String > emittedProbeIds ;
680+ private final Status status ;
681+
682+ CoordinatedSamplingState (Status status ) {
683+ this .status = status ;
684+ if (status == EMIT ) {
685+ emittedProbeIds = ConcurrentHashMap .newKeySet ();
686+ } else {
687+ emittedProbeIds = Collections .emptySet ();
688+ }
689+ }
690+
691+ boolean tryEmit (String probeEncodedId ) {
692+ return status == EMIT && emittedProbeIds .add (probeEncodedId );
693+ }
694+ }
695+
696+ /**
697+ * Whether this probe shares the trace-wide {@link CoordinatedSamplingState} with other
698+ * full-snapshot probes on the same local root span, so they all emit together or not at all.
699+ * Probes with their own independent sampling flow (e.g. {@link ExceptionProbe}) must not
700+ * participate, or their unrelated decision would leak into (and be leaked into by) ordinary
701+ * snapshot probes sharing the same trace.
702+ */
703+ protected boolean useCoordinatedSampling () {
704+ return true ;
705+ }
706+
707+ private boolean trySample (Sampler sampler ) {
708+ if (!isFullSnapshot ()) {
709+ return ProbeRateLimiter .tryProbe (sampler , false );
710+ }
711+ if (!useCoordinatedSampling ()) {
712+ return ProbeRateLimiter .tryProbe (sampler , true );
713+ }
714+
715+ AgentSpan localRootSpan = getActiveLocalRootSpan ();
716+ if (localRootSpan == null ) {
717+ return ProbeRateLimiter .tryProbe (sampler , true );
718+ }
719+
720+ CoordinatedSamplingState state = Context .from (localRootSpan ).get (SAMPLING_KEY );
721+ if (state == null ) {
722+ synchronized (localRootSpan ) {
723+ Context traceContext = Context .from (localRootSpan );
724+ state = traceContext .get (SAMPLING_KEY );
725+ if (state == null ) {
726+ boolean sampled = ProbeRateLimiter .tryProbe (sampler , true );
727+ state = new CoordinatedSamplingState (sampled ? EMIT : DROP );
728+ traceContext .with (SAMPLING_KEY , state ).attachTo (localRootSpan );
729+ }
730+ }
731+ }
732+ return state .tryEmit (getProbeId ().getEncodedId ());
733+ }
734+
735+ /**
736+ * Ensures the trace-level coordinated sampling state allows emission, upgrading a previously
737+ * cached DROP (recorded by an ordinary probe before this debug session became active) so that
738+ * every full-snapshot probe on this trace gets a chance to emit.
739+ */
740+ private void forceCoordinatedEmit () {
741+ AgentSpan localRootSpan = getActiveLocalRootSpan ();
742+ if (localRootSpan == null ) {
743+ return ;
744+ }
745+ synchronized (localRootSpan ) {
746+ Context traceContext = Context .from (localRootSpan );
747+ CoordinatedSamplingState state = traceContext .get (SAMPLING_KEY );
748+ if (state == null || state .status != EMIT ) {
749+ traceContext .with (SAMPLING_KEY , new CoordinatedSamplingState (EMIT )).attachTo (localRootSpan );
750+ }
751+ }
752+ }
753+
754+ private AgentSpan getActiveLocalRootSpan () {
755+ TracerAPI tracer = AgentTracer .get ();
756+ AgentSpan activeSpan = tracer != null ? tracer .activeSpan () : null ;
757+ if (activeSpan == null || activeSpan == AgentTracer .noopSpan ()) {
758+ return null ;
759+ }
760+ AgentSpan localRootSpan = activeSpan .getLocalRootSpan ();
761+ return localRootSpan != AgentTracer .noopSpan () ? localRootSpan : null ;
762+ }
763+
645764 protected Snapshot createSnapshot () {
646765 int maxDepth = capture != null ? capture .maxReferenceDepth : -1 ;
647766 return new Snapshot (Thread .currentThread (), this , maxDepth );
@@ -652,8 +771,9 @@ protected boolean fillSnapshot(
652771 CapturedContext exitContext ,
653772 List <CapturedContext .CapturedThrowable > caughtExceptions ,
654773 Snapshot snapshot ) {
655- LogStatus entryStatus = convertStatus (entryContext .getStatus (probeId .getEncodedId ()));
656- LogStatus exitStatus = convertStatus (exitContext .getStatus (probeId .getEncodedId ()));
774+ String probeEncodedId = getProbeId ().getEncodedId ();
775+ LogStatus entryStatus = convertStatus (entryContext .getStatus (probeEncodedId ));
776+ LogStatus exitStatus = convertStatus (exitContext .getStatus (probeEncodedId ));
657777 String message = null ;
658778 switch (evaluateAt ) {
659779 case ENTRY :
@@ -829,7 +949,7 @@ protected void commitSnapshot(Snapshot snapshot, DebuggerSink sink) {
829949
830950 @ Override
831951 public void commit (CapturedContext lineContext , int line ) {
832- LogStatus status = (LogStatus ) lineContext .getStatus (probeId .getEncodedId ());
952+ LogStatus status = (LogStatus ) lineContext .getStatus (getProbeId () .getEncodedId ());
833953 if (status == null ) {
834954 return ;
835955 }
0 commit comments