Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6b02ea9
Drop APM traces when APM tracing is disabled and keep LLM Observabili…
ncybul Sep 11, 2026
5e30ea9
Clarify why the tracer-disabled and drop-all-traces branches exist
ncybul Sep 14, 2026
5923093
Drop the LLMObs smoke tests and cover the behavior in SamplerTest
ncybul Sep 14, 2026
6f19b69
Merge branch 'master' into nicole.cybul/llmobs-apm-tracing-disabled
ncybul Sep 15, 2026
9179365
Ignore the upstream sampling priority when APM tracing is disabled an…
ncybul Sep 16, 2026
80e580e
Start LLM Observability when CI Visibility installs the tracer
ncybul Sep 16, 2026
fe764e7
Dispatch the secondary queue before a flush completes
ncybul Sep 18, 2026
a2a2a57
Keep the standalone ASM trickle when AppSec activates at runtime
ncybul Sep 18, 2026
a153f2d
Let the APM traces drop override a manual.keep when APM tracing is di…
ncybul Sep 25, 2026
5a9d467
Keep the APM traces drop from overriding a Data Jobs keep
ncybul Sep 25, 2026
16e1082
Clarify that the CI Visibility carve-out only guarantees a live tracer
ncybul Sep 25, 2026
6a1ee87
Test that sampled-out traces still deliver LLMObs spans
ncybul Oct 9, 2026
b154556
Match the extraction priority reset to TraceCollector's product check
ncybul Oct 9, 2026
ec10186
Shorten comments to the why
ncybul Oct 9, 2026
c0b43f1
Test canAvoidSamplingPriorityLock with Data Jobs and APM tracing enabled
ncybul Oct 9, 2026
56f612a
Keep only one standalone ASM trace per minute when traces share a mil…
ncybul Oct 13, 2026
c8b2677
Merge branch 'master' into nicole.cybul/llmobs-apm-tracing-disabled
ncybul Oct 13, 2026
a946fca
Merge branch 'master' into nicole.cybul/llmobs-apm-tracing-disabled
ncybul Oct 13, 2026
1163902
Merge branch 'master' into nicole.cybul/llmobs-apm-tracing-disabled
ncybul Oct 14, 2026
2f3dea3
Stub isApmTracingEnabled in W3COtelTraceStateContinuationTest mocks
ncybul Oct 14, 2026
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 @@ -42,6 +42,14 @@ public static void start(Instrumentation inst, SharedCommunicationObjects sco) {
return;
}

// LLMObs spans need a real tracer (buildSpan() returns null on the no-op one), so keep the
// no-op SDK when none is installed. Mirrors TracerInstaller.installGlobalTracer, which still
// installs one for CI Visibility.
if (!config.isTraceEnabled() && !config.isCiVisibilityEnabled()) {
Comment thread
ncybul marked this conversation as resolved.
LOGGER.debug("LLM Observability is disabled: no tracer is installed (dd.trace.enabled)");
return;
}

sco.createRemaining(config);

String mlApp = config.getLlmObsMlApp();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package datadog.trace.llmobs;

import static datadog.trace.api.config.CiVisibilityConfig.CIVISIBILITY_ENABLED;
import static datadog.trace.api.config.LlmObsConfig.LLMOBS_ENABLED;
import static datadog.trace.api.config.TraceInstrumentationConfig.TRACE_ENABLED;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;

import datadog.trace.api.llmobs.LLMObs;
import datadog.trace.api.llmobs.noop.NoOpLLMObsSpan;
import datadog.trace.test.junit.utils.config.WithConfig;
import datadog.trace.test.util.DDJavaSpecification;
import org.junit.jupiter.api.Test;

class LLMObsSystemTest extends DDJavaSpecification {

/**
* LLM Observability spans are backed by tracer spans, so the subsystem must stay off when tracing
* is disabled. Starting it would install the real SDK implementation on top of the no-op tracer,
* whose {@code buildSpan()} returns null, and every span-starting call would throw.
*/
@WithConfig(key = LLMOBS_ENABLED, value = "true")
@WithConfig(key = TRACE_ENABLED, value = "false")
@Test
void staysNoOpWhenTracingIsDisabled() {
// The null SharedCommunicationObjects doubles as an assertion that start() returns before
// touching it: any use would throw.
LLMObsSystem.start(null, null);

assertSame(
NoOpLLMObsSpan.INSTANCE, LLMObs.startLLMSpan("span", "model", "provider", null, null));
}

/**
* CI Visibility installs a {@code CoreTracer} even with tracing disabled (see {@code
* TracerInstaller#installGlobalTracer}), so the guard above must not fire in that combination —
* the tracer LLM Observability needs does exist.
*/
@WithConfig(key = LLMOBS_ENABLED, value = "true")
@WithConfig(key = TRACE_ENABLED, value = "false")
@WithConfig(key = CIVISIBILITY_ENABLED, value = "true")
@Test
void startsWhenCiVisibilityInstallsTheTracer() {
// Reaching the null SharedCommunicationObjects is the assertion: start() only dereferences it
// after the guard, so the NPE proves the subsystem was not short-circuited.
assertThrows(NullPointerException.class, () -> LLMObsSystem.start(null, null));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package datadog.trace.common.sampling;

import datadog.trace.api.sampling.PrioritySampling;
import datadog.trace.api.sampling.SamplingMechanism;
import datadog.trace.bootstrap.ActiveSubsystems;
import datadog.trace.core.CoreSpan;
import java.time.Clock;

/**
* Drops APM traces for {@code dd.apm.tracing.enabled=false}. If Remote Configuration activates
* AppSec at runtime, falls back to the standalone ASM trickle of 1 APM trace per minute, so it
* checks {@link ActiveSubsystems#APPSEC_ACTIVE} per trace.
*/
public class ApmTracingDisabledSampler implements Sampler, PrioritySampler {

private final AsmStandaloneSampler asmStandaloneSampler;
private final ForcePrioritySampler dropSampler;

public ApmTracingDisabledSampler(final Clock clock) {
this.asmStandaloneSampler = new AsmStandaloneSampler(clock);
this.dropSampler =
new ForcePrioritySampler(PrioritySampling.SAMPLER_DROP, SamplingMechanism.DEFAULT);
}

@Override
public <T extends CoreSpan<T>> boolean sample(final T span) {
// Both delegates keep sending the trace to the agent so it can collect stats on dropped traces.
return isAsmActive() ? asmStandaloneSampler.sample(span) : dropSampler.sample(span);
}

@Override
public <T extends CoreSpan<T>> void setSamplingPriority(final T span) {
if (isAsmActive()) {
asmStandaloneSampler.setSamplingPriority(span);
} else {
dropSampler.setSamplingPriority(span);
}
}

private static boolean isAsmActive() {
return ActiveSubsystems.APPSEC_ACTIVE;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,12 @@ public <T extends CoreSpan<T>> void setSamplingPriority(final T span) {

private boolean shouldSample() {
long now = clock.millis();
return lastSampleTime.updateAndGet(
lastTime -> now - lastTime >= RATE_IN_MILLISECONDS ? now : lastTime)
== now;
// Compare against the time before the update, not after: two traces landing in the same
// millisecond as the last sample would both see the updated value equal to now and both be
// kept, sending 2 APM traces a minute instead of 1.
long lastSample =
lastSampleTime.getAndUpdate(
lastTime -> now - lastTime >= RATE_IN_MILLISECONDS ? now : lastTime);
return now - lastSample >= RATE_IN_MILLISECONDS;
Comment thread
ncybul marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,17 @@ public static Sampler forConfig(
@Nullable final RateByServiceTraceSampler agentSampler) {
Sampler sampler;
if (config != null) {
if (!config.isApmTracingEnabled() && isAsmEnabled(config)) {
log.debug("APM is disabled. Only 1 trace per minute will be sent.");
return new AsmStandaloneSampler(Clock.systemUTC());
if (!config.isApmTracingEnabled()) {
if (isAsmEnabled(config)) {
Comment thread
ncybul marked this conversation as resolved.
log.debug(
"APM tracing is disabled, but ASM is enabled. Only 1 APM trace per minute will be sent.");
return new AsmStandaloneSampler(Clock.systemUTC());
Comment thread
ncybul marked this conversation as resolved.
}
// Only ASM needs a trickle of APM traces, so drop them all unless AppSec is activated
// later by Remote Configuration.
log.debug(
"APM tracing is disabled. APM traces will be dropped, unless AppSec is activated at runtime.");
return new ApmTracingDisabledSampler(Clock.systemUTC());
Comment thread
ncybul marked this conversation as resolved.
}
final Map<String, String> serviceRules = config.getTraceSamplingServiceRules();
final Map<String, String> operationRules = config.getTraceSamplingOperationRules();
Expand Down Expand Up @@ -133,6 +141,7 @@ public static Sampler forConfig(
return sampler;
}

/** Whether any Application Security product is on at startup. */
private static boolean isAsmEnabled(Config config) {
return config.getAppSecActivation() == ProductActivation.FULLY_ENABLED
|| config.getIastActivation() == ProductActivation.FULLY_ENABLED
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ MessagePassingBlockingQueue<Object> getPrimaryQueue() {
return primaryQueue;
}

@VisibleForTesting
MessagePassingBlockingQueue<Object> getSecondaryQueue() {
return secondaryQueue;
}

private static MessagePassingBlockingQueue<Object> createQueue(int capacity) {
return Queues.mpscBlockingConsumerArrayQueue(capacity);
}
Expand Down Expand Up @@ -195,6 +200,9 @@ public void onEvent(Object event) {
// TODO populate `_sample_rate` metric in a way that accounts for lost/dropped traces
payloadDispatcher.addTrace(trace);
} else if (event instanceof FlushEvent) {
// The flush marker only goes to the primary queue, so drain the secondary queue too:
// sampled-out traces can carry LLM Observability spans that must still be sent.
consumeFromSecondaryQueue();
Comment thread
ncybul marked this conversation as resolved.
Comment thread
ncybul marked this conversation as resolved.
payloadDispatcher.flush();
((FlushEvent) event).sync();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import datadog.trace.api.DDSpanId;
import datadog.trace.api.DDTraceId;
import datadog.trace.api.Functions;
import datadog.trace.api.ProductTraceSource;
import datadog.trace.api.TagMap;
import datadog.trace.api.TraceConfig;
import datadog.trace.api.TracePropagationStyle;
Expand Down Expand Up @@ -72,6 +73,7 @@ public abstract class ContextInterpreter implements AgentPropagation.KeyClassifi
private final boolean requestHeaderTagsCommaAllowed;
private final int baggageMaxItems;
private final int baggageMaxBytes;
private final boolean apmTracingEnabled;

protected static final boolean LOG_EXTRACT_HEADER_NAMES = Config.get().isLogExtractHeaderNames();
private static final DDCache<String, String> CACHE = DDCaches.newFixedSizeCache(64);
Expand All @@ -89,6 +91,7 @@ protected ContextInterpreter(Config config) {
this.requestHeaderTagsCommaAllowed = config.isRequestHeaderTagsCommaAllowed();
this.baggageMaxItems = config.getTraceBaggageMaxItems();
this.baggageMaxBytes = config.getTraceBaggageMaxBytes();
this.apmTracingEnabled = config.isApmTracingEnabled();
}

final TagMap.Ledger tagLedger() {
Expand Down Expand Up @@ -345,6 +348,17 @@ private TagContext.HttpHeaders getHeaders() {
}

private int samplingPriorityOrDefault(DDTraceId traceId, int samplingPriority) {
if (!apmTracingEnabled
Comment thread
ncybul marked this conversation as resolved.
&& (propagationTags == null
|| !ProductTraceSource.isProductMarked(
propagationTags.getTraceSource(),
ProductTraceSource.ASM,
ProductTraceSource.AI_GUARD))) {
// With APM tracing disabled, don't inherit an upstream APM decision: let the local sampler
// set the priority. Same products as TraceCollector.setSamplingPriorityIfNecessary. Needed
// when Data Jobs is enabled, where the drop can't override an already-set priority.
return PrioritySampling.UNSET;
Comment thread
ncybul marked this conversation as resolved.
}
return samplingPriority == PrioritySampling.UNSET || DDTraceId.ZERO.equals(traceId)
? defaultSamplingPriority()
: samplingPriority;
Expand Down
Loading
Loading