WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit 552ae2b

Browse files
authored
Fix API Security sampling in standalone mode (#10165)
What Does This Do Fix the API security sampling in standalone mode (APM tracing disabled). Since we decided that, at the end of the request, if the pre-sample chooses to retain the trace we will add the asm.keep and propagation tags for ASM in standalone mode, we also need to keep that same decision in the sample method. This avoids inconsistencies in concurrent threads under high load scenarios where the post-processor might be delayed. Added a bit of extra complexity to get more detailed logs through logSamplingDecision. This should make it easier to troubleshoot future sampling issues Motivation API Security standalone system tests were failing intermittently in CI with _sampling_priority_v1 set to 2, in traces that should be not retained due to API Security sampling Related with APPSEC-57815 Additional Notes tested in https://github.com/DataDog/system-tests/actions/runs/20127087811/job/57783265656
1 parent 766474f commit 552ae2b

File tree

2 files changed

+189
-2
lines changed

2 files changed

+189
-2
lines changed

dd-java-agent/appsec/src/main/java/com/datadog/appsec/api/security/ApiSecuritySamplerImpl.java

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import datadog.trace.api.Config;
55
import datadog.trace.api.time.SystemTimeSource;
66
import datadog.trace.api.time.TimeSource;
7+
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
8+
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
79
import java.util.Deque;
810
import java.util.concurrent.ConcurrentHashMap;
911
import java.util.concurrent.ConcurrentLinkedDeque;
@@ -73,8 +75,15 @@ public boolean preSampleRequest(final @Nonnull AppSecRequestContext ctx) {
7375
return false;
7476
}
7577
if (counter.tryAcquire()) {
76-
log.debug("API security sampling is required for this request (presampled)");
7778
ctx.setKeepOpenForApiSecurityPostProcessing(true);
79+
if (!Config.get().isApmTracingEnabled()) {
80+
boolean sampled = updateApiAccessIfExpired(hash);
81+
if (sampled) {
82+
logSamplingDecision("preSampleRequest", hash);
83+
}
84+
return sampled;
85+
}
86+
logSamplingDecision("preSampleRequest", hash);
7887
return true;
7988
}
8089
return false;
@@ -91,7 +100,11 @@ public boolean sampleRequest(AppSecRequestContext ctx) {
91100
// This should never happen, it should have been short-circuited before.
92101
return false;
93102
}
94-
return updateApiAccessIfExpired(hash);
103+
boolean sampled = Config.get().isApmTracingEnabled() ? updateApiAccessIfExpired(hash) : true;
104+
if (sampled) {
105+
logSamplingDecision("sampleRequest", hash);
106+
}
107+
return sampled;
95108
}
96109

97110
@Override
@@ -168,4 +181,23 @@ private long computeApiHash(final String route, final String method, final int s
168181
result = 31 * result + statusCode;
169182
return result;
170183
}
184+
185+
private void logSamplingDecision(final String method, final long hash) {
186+
if (!log.isDebugEnabled()) {
187+
return;
188+
}
189+
AgentSpan activeSpan = AgentTracer.get().activeSpan();
190+
191+
if (activeSpan != null) {
192+
log.debug(
193+
"API security sampling decision in {}: hash={}, traceId={}, spanId={}",
194+
method,
195+
hash,
196+
activeSpan.getTraceId(),
197+
activeSpan.getSpanId());
198+
} else {
199+
log.debug(
200+
"API security sampling decision in {}: hash={}, traceId=null, spanId=null", method, hash);
201+
}
202+
}
171203
}

dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/api/security/ApiSecuritySamplerTest.groovy

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,161 @@ class ApiSecuritySamplerTest extends DDSpecification {
209209
}
210210
}
211211

212+
void 'preSampleRequest with tracing disabled updates access map immediately'() {
213+
given:
214+
injectSysConfig('dd.apm.tracing.enabled', 'false')
215+
rebuildConfig()
216+
def ctx = createContext('route1', 'GET', 200)
217+
final timeSource = new ControllableTimeSource()
218+
timeSource.set(0)
219+
final long expirationTimeInMs = 10_000
220+
def sampler = new ApiSecuritySamplerImpl(10, expirationTimeInMs, timeSource)
221+
222+
when: 'first request is presampled with tracing disabled'
223+
def preSampled = sampler.preSampleRequest(ctx)
224+
225+
then: 'request is sampled and access map is updated immediately'
226+
preSampled
227+
sampler.accessMap.size() == 1
228+
sampler.accessMap.containsKey(ctx.getApiSecurityEndpointHash())
229+
230+
when: 'second request for same endpoint is attempted'
231+
def ctx2 = createContext('route1', 'GET', 200)
232+
sampler.releaseOne()
233+
def preSampled2 = sampler.preSampleRequest(ctx2)
234+
235+
then: 'second request is not sampled because endpoint was already updated in first preSampleRequest'
236+
!preSampled2
237+
}
238+
239+
void 'sampleRequest with tracing disabled returns true without updating access map'() {
240+
given:
241+
injectSysConfig('dd.apm.tracing.enabled', 'false')
242+
rebuildConfig()
243+
def ctx = createContext('route1', 'GET', 200)
244+
ctx.setApiSecurityEndpointHash(42L)
245+
ctx.setKeepOpenForApiSecurityPostProcessing(true)
246+
final timeSource = new ControllableTimeSource()
247+
timeSource.set(0)
248+
final long expirationTimeInMs = 10_000
249+
def sampler = new ApiSecuritySamplerImpl(10, expirationTimeInMs, timeSource)
250+
251+
when: 'sampleRequest is called with tracing disabled'
252+
def sampled = sampler.sampleRequest(ctx)
253+
254+
then: 'request is sampled without updating access map'
255+
sampled
256+
sampler.accessMap.size() == 0
257+
}
258+
259+
void 'preSampleRequest with tracing enabled does not update access map immediately'() {
260+
given:
261+
injectSysConfig('dd.apm.tracing.enabled', 'true')
262+
rebuildConfig()
263+
def ctx = createContext('route1', 'GET', 200)
264+
final timeSource = new ControllableTimeSource()
265+
timeSource.set(0)
266+
final long expirationTimeInMs = 10_000
267+
def sampler = new ApiSecuritySamplerImpl(10, expirationTimeInMs, timeSource)
268+
269+
when: 'request is presampled with tracing enabled'
270+
def preSampled = sampler.preSampleRequest(ctx)
271+
272+
then: 'request is sampled but access map is NOT updated yet'
273+
preSampled
274+
sampler.accessMap.size() == 0
275+
276+
when: 'sampleRequest is called to finalize sampling'
277+
def sampled = sampler.sampleRequest(ctx)
278+
279+
then: 'access map is updated in sampleRequest'
280+
sampled
281+
sampler.accessMap.size() == 1
282+
sampler.accessMap.containsKey(ctx.getApiSecurityEndpointHash())
283+
}
284+
285+
void 'sampleRequest with tracing enabled updates access map'() {
286+
given:
287+
injectSysConfig('dd.apm.tracing.enabled', 'true')
288+
rebuildConfig()
289+
def ctx = createContext('route1', 'GET', 200)
290+
ctx.setApiSecurityEndpointHash(42L)
291+
ctx.setKeepOpenForApiSecurityPostProcessing(true)
292+
final timeSource = new ControllableTimeSource()
293+
timeSource.set(0)
294+
final long expirationTimeInMs = 10_000
295+
def sampler = new ApiSecuritySamplerImpl(10, expirationTimeInMs, timeSource)
296+
297+
when: 'sampleRequest is called with tracing enabled'
298+
def sampled = sampler.sampleRequest(ctx)
299+
300+
then: 'request is sampled and access map is updated'
301+
sampled
302+
sampler.accessMap.size() == 1
303+
sampler.accessMap.containsKey(42L)
304+
305+
when: 'second request for same endpoint is made'
306+
def sampled2 = sampler.sampleRequest(ctx)
307+
308+
then: 'second request is not sampled'
309+
!sampled2
310+
}
311+
312+
void 'concurrent requests with tracing disabled do not see expired state'() {
313+
given:
314+
injectSysConfig('dd.apm.tracing.enabled', 'false')
315+
rebuildConfig()
316+
def ctx1 = createContext('route1', 'GET', 200)
317+
def ctx2 = createContext('route1', 'GET', 200)
318+
final timeSource = new ControllableTimeSource()
319+
timeSource.set(0)
320+
final long expirationTimeInMs = 10_000
321+
def sampler = new ApiSecuritySamplerImpl(10, expirationTimeInMs, timeSource)
322+
323+
when: 'first request is presampled'
324+
def preSampled1 = sampler.preSampleRequest(ctx1)
325+
326+
then: 'first request is sampled and access map is updated immediately'
327+
preSampled1
328+
ctx1.getApiSecurityEndpointHash() != null
329+
sampler.accessMap.size() == 1
330+
331+
when: 'concurrent second request tries to presample same endpoint'
332+
sampler.releaseOne()
333+
def preSampled2 = sampler.preSampleRequest(ctx2)
334+
335+
then: 'second request is not sampled because endpoint is already in access map'
336+
!preSampled2
337+
}
338+
339+
void 'full flow with tracing disabled updates map only in preSampleRequest'() {
340+
given:
341+
injectSysConfig('dd.apm.tracing.enabled', 'false')
342+
rebuildConfig()
343+
def ctx = createContext('route1', 'GET', 200)
344+
final timeSource = new ControllableTimeSource()
345+
timeSource.set(0)
346+
final long expirationTimeInMs = 10_000
347+
def sampler = new ApiSecuritySamplerImpl(10, expirationTimeInMs, timeSource)
348+
349+
when: 'request goes through full sampling flow with tracing disabled'
350+
def preSampled = sampler.preSampleRequest(ctx)
351+
352+
then: 'preSampleRequest returns true and updates access map'
353+
preSampled
354+
sampler.accessMap.size() == 1
355+
def hash = ctx.getApiSecurityEndpointHash()
356+
sampler.accessMap.containsKey(hash)
357+
358+
when: 'sampleRequest is called'
359+
def sampled = sampler.sampleRequest(ctx)
360+
361+
then: 'sampleRequest returns true without modifying access map'
362+
sampled
363+
sampler.accessMap.size() == 1
364+
sampler.accessMap.get(hash) == 0L // Still has the value from preSampleRequest
365+
}
366+
212367
private static AppSecRequestContext createContext(final String route, final String method, int statusCode) {
213368
final AppSecRequestContext ctx = new AppSecRequestContext()
214369
ctx.setRoute(route)

0 commit comments

Comments
 (0)