diff --git a/dd-trace-core/src/main/java/datadog/trace/common/metrics/Aggregator.java b/dd-trace-core/src/main/java/datadog/trace/common/metrics/Aggregator.java index 27f81943555..4c54eaf8b42 100644 --- a/dd-trace-core/src/main/java/datadog/trace/common/metrics/Aggregator.java +++ b/dd-trace-core/src/main/java/datadog/trace/common/metrics/Aggregator.java @@ -87,6 +87,10 @@ void resetCoreHandlers(HealthMetrics healthMetrics, CardinalityLimitReporter rep aggregates.resetCoreHandlers(healthMetrics, reporter); } + AggregateTable aggregates() { + return aggregates; + } + @Override public void run() { Thread currentThread = Thread.currentThread(); diff --git a/dd-trace-core/src/main/java/datadog/trace/common/metrics/ClientStatsAggregator.java b/dd-trace-core/src/main/java/datadog/trace/common/metrics/ClientStatsAggregator.java index 8a0c69cbed0..858aa041b2c 100644 --- a/dd-trace-core/src/main/java/datadog/trace/common/metrics/ClientStatsAggregator.java +++ b/dd-trace-core/src/main/java/datadog/trace/common/metrics/ClientStatsAggregator.java @@ -21,6 +21,7 @@ import datadog.communication.ddagent.SharedCommunicationObjects; import datadog.trace.api.Config; import datadog.trace.api.WellKnownTags; +import datadog.trace.api.internal.VisibleForTesting; import datadog.trace.bootstrap.instrumentation.api.InstrumentationTags; import datadog.trace.common.metrics.SignalItem.ReportSignal; import datadog.trace.common.writer.ddagent.DDAgentApi; @@ -309,6 +310,10 @@ TimeUnit reportingIntervalTimeUnit() { return reportingIntervalTimeUnit; } + Aggregator aggregator() { + return aggregator; + } + @Override public void start() { sink.register(this); @@ -738,4 +743,9 @@ public void run(ClientStatsAggregator target) { target.report(); } } + + @VisibleForTesting + boolean isEmpty() { + return inbox.isEmpty(); + } } diff --git a/dd-trace-core/src/main/java/datadog/trace/common/sampling/RateByServiceTraceSampler.java b/dd-trace-core/src/main/java/datadog/trace/common/sampling/RateByServiceTraceSampler.java index 073dd7ed4ba..0940469c61c 100644 --- a/dd-trace-core/src/main/java/datadog/trace/common/sampling/RateByServiceTraceSampler.java +++ b/dd-trace-core/src/main/java/datadog/trace/common/sampling/RateByServiceTraceSampler.java @@ -2,6 +2,7 @@ import datadog.trace.api.cache.DDCache; import datadog.trace.api.cache.DDCaches; +import datadog.trace.api.internal.VisibleForTesting; import datadog.trace.api.sampling.PrioritySampling; import datadog.trace.api.sampling.SamplingMechanism; import datadog.trace.api.time.SystemTimeSource; @@ -77,6 +78,16 @@ private > String getSpanEnv(final T span) { return span.getTag("env", ""); } + @VisibleForTesting + double sampleRateFor(String env, String service) { + return serviceRates.getSampler(env, service).getSampleRate(); + } + + @VisibleForTesting + double fallbackSampleRate() { + return serviceRates.getFallbackSampler().getSampleRate(); + } + static boolean shouldCap(double oldRate, double newRate) { return oldRate != 0 && newRate > oldRate * MAX_RATE_INCREASE_FACTOR; } diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/ClientStatsAggregatorTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/ClientStatsAggregatorTest.groovy deleted file mode 100644 index cd37d7487f4..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/ClientStatsAggregatorTest.groovy +++ /dev/null @@ -1,1760 +0,0 @@ -package datadog.trace.common.metrics - -import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND -import datadog.trace.bootstrap.instrumentation.api.InstrumentationTags -import static java.util.concurrent.TimeUnit.MILLISECONDS -import static java.util.concurrent.TimeUnit.SECONDS - -import datadog.communication.ddagent.DDAgentFeaturesDiscovery -import datadog.trace.api.WellKnownTags -import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString -import datadog.trace.core.CoreSpan -import datadog.trace.core.monitor.HealthMetrics -import datadog.trace.test.util.DDSpecification -import java.util.concurrent.CompletableFuture -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeoutException -import java.util.function.Supplier -import spock.lang.Shared - -class ClientStatsAggregatorTest extends DDSpecification { - - static Set empty = new HashSet<>() - - static final int HTTP_OK = 200 - - @Shared - long reportingInterval = 1 - @Shared - int queueSize = 256 - - def "should ignore traces with no measured spans"() { - setup: - Sink sink = Mock(Sink) - DDAgentFeaturesDiscovery features = Mock(DDAgentFeaturesDiscovery) - features.supportsMetrics() >> true - WellKnownTags wellKnownTags = new WellKnownTags("runtimeid", "hostname", "env", "service", "version", "language") - ClientStatsAggregator aggregator = new ClientStatsAggregator( - wellKnownTags, - empty, - AdditionalTagsSchema.EMPTY, - features, - HealthMetrics.NO_OP, - sink, - 10, - queueSize, - 1, - MILLISECONDS, false - ) - aggregator.start() - - aggregator.publish([new SimpleSpan("", "", "", "", false, false, false, 0, 0, HTTP_OK)]) - when: - reportAndWaitUntilEmpty(aggregator) - then: - 0 * sink._ - - cleanup: - aggregator.close() - } - - def "should ignore traces with ignored resource names"() { - setup: - String ignoredResourceName = "foo" - Sink sink = Mock(Sink) - DDAgentFeaturesDiscovery features = Mock(DDAgentFeaturesDiscovery) - features.supportsMetrics() >> true - WellKnownTags wellKnownTags = new WellKnownTags("runtimeid", "hostname", "env", "service", "version", "language") - ClientStatsAggregator aggregator = new ClientStatsAggregator( - wellKnownTags, - [ignoredResourceName].toSet(), - AdditionalTagsSchema.EMPTY, - features, - HealthMetrics.NO_OP, - sink, - 10, - queueSize, - 1, - MILLISECONDS, false - ) - aggregator.start() - - when: "publish ignored resource names" - aggregator.publish([new SimpleSpan("", "", ignoredResourceName, "", true, true, false, 0, 0, HTTP_OK)]) - aggregator.publish([ - new SimpleSpan("", "", UTF8BytesString.create(ignoredResourceName), "", true, true, false, 0, 0, HTTP_OK) - ]) - aggregator.publish([ - new SimpleSpan("", "", ignoredResourceName, "", true, true, false, 0, 0, HTTP_OK), - new SimpleSpan("", "", - "measured, not ignored, but child of ignored, so should be ignored", "", true, true, false, 0, 0, HTTP_OK) - ]) - reportAndWaitUntilEmpty(aggregator) - then: - 0 * sink._ - - cleanup: - aggregator.close() - } - - def "should be resilient to null resource names"() { - setup: - MetricWriter writer = Mock(MetricWriter) - Sink sink = Stub(Sink) - DDAgentFeaturesDiscovery features = Mock(DDAgentFeaturesDiscovery) - features.supportsMetrics() >> true - features.peerTags() >> [] - ClientStatsAggregator aggregator = new ClientStatsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, 10, queueSize, reportingInterval, SECONDS, false) - aggregator.start() - - when: - CountDownLatch latch = new CountDownLatch(1) - aggregator.publish([ - new SimpleSpan("service", "operation", null, "type", false, true, false, 0, 100, HTTP_OK) - .setTag(SPAN_KIND, "baz") - ]) - aggregator.report() - def latchTriggered = latch.await(2, SECONDS) - - then: - latchTriggered - 1 * writer.startBucket(1, _, _) - 1 * writer.add({ - AggregateEntryTestUtils.equals(it,AggregateEntryTestUtils.of( - null, - "service", - "operation", - null, - "type", - HTTP_OK, - false, - false, - "baz", - [], - null, - null, - null - )) - }) >> { AggregateEntry e -> - assert e.getHitCount() == 1 && e.getTopLevelCount() == 1 && e.getDuration() == 100 - } - 1 * writer.finishBucket() >> { latch.countDown() } - - cleanup: - aggregator.close() - } - - def "unmeasured top level spans have metrics computed"() { - setup: - MetricWriter writer = Mock(MetricWriter) - Sink sink = Stub(Sink) - DDAgentFeaturesDiscovery features = Mock(DDAgentFeaturesDiscovery) - features.supportsMetrics() >> true - features.peerTags() >> [] - ClientStatsAggregator aggregator = new ClientStatsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, 10, queueSize, reportingInterval, SECONDS, false) - aggregator.start() - - when: - CountDownLatch latch = new CountDownLatch(1) - aggregator.publish([ - new SimpleSpan("service", "operation", "resource", "type", false, true, false, 0, 100, HTTP_OK) - .setTag(SPAN_KIND, "baz") - ]) - aggregator.report() - def latchTriggered = latch.await(2, SECONDS) - - then: - latchTriggered - 1 * writer.startBucket(1, _, _) - 1 * writer.add({ - AggregateEntryTestUtils.equals(it,AggregateEntryTestUtils.of( - "resource", - "service", - "operation", - null, - "type", - HTTP_OK, - false, - false, - "baz", - [], - null, - null, - null - )) - }) >> { AggregateEntry e -> - assert e.getHitCount() == 1 && e.getTopLevelCount() == 1 && e.getDuration() == 100 - } - 1 * writer.finishBucket() >> { latch.countDown() } - - cleanup: - aggregator.close() - } - - def "should compute stats for span kind #kind"() { - setup: - MetricWriter writer = Mock(MetricWriter) - Sink sink = Stub(Sink) - DDAgentFeaturesDiscovery features = Mock(DDAgentFeaturesDiscovery) - features.supportsMetrics() >> true - features.peerTags() >> [] - ClientStatsAggregator aggregator = new ClientStatsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, 10, queueSize, reportingInterval, SECONDS, true) - aggregator.start() - - when: - CountDownLatch latch = new CountDownLatch(1) - def span = new SimpleSpan("service", "operation", "resource", "type", false, false, false, 0, 100, HTTP_OK) - .setTag(SPAN_KIND, kind) - if (httpMethod != null) { - span.setTag("http.method", httpMethod) - } - if (httpEndpoint != null) { - span.setTag("http.endpoint", httpEndpoint) - } - aggregator.publish([span]) - aggregator.report() - def latchTriggered = latch.await(2, SECONDS) - - then: - latchTriggered == statsComputed - (statsComputed ? 1 : 0) * writer.startBucket(1, _, _) - (statsComputed ? 1 : 0) * writer.add({ - AggregateEntryTestUtils.equals(it, - AggregateEntryTestUtils.of( - "resource", - "service", - "operation", - null, - "type", - HTTP_OK, - false, - false, - kind, - [], - httpMethod, - httpEndpoint, - null - )) - }) >> { AggregateEntry e -> - assert e.getHitCount() == 1 && e.getTopLevelCount() == 0 && e.getDuration() == 100 - } - (statsComputed ? 1 : 0) * writer.finishBucket() >> { latch.countDown() } - - cleanup: - aggregator.close() - - where: - kind | httpMethod | httpEndpoint | statsComputed - "client" | null | null | true - "producer" | null | null | true - "consumer" | null | null | true - UTF8BytesString.create("server") | null | null | true - "internal" | null | null | false - null | null | null | false - "server" | "GET" | "/api/users/:id" | true - "server" | "POST" | "/api/orders" | true - "server" | "DELETE" | "/api/products/:id" | true - "client" | "GET" | "/external/api" | true - } - - def "should create separate buckets for distinct peer tag values"() { - // Peer-tag NAMES are configured per-tracer and stable for the duration of a trace publish; - // peer-tag VALUES vary per-span. Two spans with the same names but different values should - // produce two distinct aggregate buckets. - setup: - MetricWriter writer = Mock(MetricWriter) - Sink sink = Stub(Sink) - DDAgentFeaturesDiscovery features = Mock(DDAgentFeaturesDiscovery) - features.supportsMetrics() >> true - features.peerTags() >> ["country", "georegion"] - ClientStatsAggregator aggregator = new ClientStatsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, 10, queueSize, reportingInterval, SECONDS, false) - aggregator.start() - - when: - CountDownLatch latch = new CountDownLatch(1) - aggregator.publish([ - new SimpleSpan("service", "operation", "resource", "type", true, false, false, 0, 100, HTTP_OK) - .setTag(SPAN_KIND, "client").setTag("country", "france").setTag("georegion", "europe"), - new SimpleSpan("service", "operation", "resource", "type", true, false, false, 0, 100, HTTP_OK) - .setTag(SPAN_KIND, "client").setTag("country", "germany").setTag("georegion", "europe") - ]) - aggregator.report() - def latchTriggered = latch.await(2, SECONDS) - - then: - latchTriggered - 1 * writer.startBucket(2, _, _) - 1 * writer.add({ - AggregateEntryTestUtils.equals(it, - AggregateEntryTestUtils.of( - "resource", - "service", - "operation", - null, - "type", - HTTP_OK, - false, - false, - "client", - [UTF8BytesString.create("country:france"), UTF8BytesString.create("georegion:europe")], - null, - null, - null - )) - }) >> { AggregateEntry e -> - assert e.getHitCount() == 1 && e.getTopLevelCount() == 0 && e.getDuration() == 100 - } - 1 * writer.add({ - AggregateEntryTestUtils.equals(it, - AggregateEntryTestUtils.of( - "resource", - "service", - "operation", - null, - "type", - HTTP_OK, - false, - false, - "client", - [UTF8BytesString.create("country:germany"), UTF8BytesString.create("georegion:europe")], - null, - null, - null - )) - }) >> { AggregateEntry e -> - assert e.getHitCount() == 1 && e.getTopLevelCount() == 0 && e.getDuration() == 100 - } - 1 * writer.finishBucket() >> { latch.countDown() } - - cleanup: - aggregator.close() - } - - def "should aggregate the right peer tags for kind #kind"() { - setup: - MetricWriter writer = Mock(MetricWriter) - Sink sink = Stub(Sink) - DDAgentFeaturesDiscovery features = Mock(DDAgentFeaturesDiscovery) - features.supportsMetrics() >> true - features.peerTags() >> ["peer.hostname", "_dd.base_service"] - ClientStatsAggregator aggregator = new ClientStatsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, 10, queueSize, reportingInterval, SECONDS, false) - aggregator.start() - - when: - CountDownLatch latch = new CountDownLatch(1) - aggregator.publish([ - new SimpleSpan("service", "operation", "resource", "type", true, false, false, 0, 100, HTTP_OK) - .setTag(SPAN_KIND, kind).setTag("peer.hostname", "localhost").setTag("_dd.base_service", UTF8BytesString.create("test")) - ]) - aggregator.report() - def latchTriggered = latch.await(2, SECONDS) - - then: - latchTriggered - 1 * writer.startBucket(1, _, _) - 1 * writer.add({ - AggregateEntryTestUtils.equals(it, - AggregateEntryTestUtils.of( - "resource", - "service", - "operation", - null, - "type", - HTTP_OK, - false, - false, - kind, - expectedPeerTags, - null, - null, - null - )) - }) >> { AggregateEntry e -> - assert e.getHitCount() == 1 && e.getTopLevelCount() == 0 && e.getDuration() == 100 - } - 1 * writer.finishBucket() >> { latch.countDown() } - - cleanup: - aggregator.close() - - where: - kind | expectedPeerTags - "client" | [UTF8BytesString.create("peer.hostname:localhost"), UTF8BytesString.create("_dd.base_service:test")] - "internal" | [UTF8BytesString.create("_dd.base_service:test")] - "server" | [] - } - - def "measured spans do not contribute to top level count"() { - setup: - MetricWriter writer = Mock(MetricWriter) - Sink sink = Stub(Sink) - DDAgentFeaturesDiscovery features = Mock(DDAgentFeaturesDiscovery) - features.supportsMetrics() >> true - features.peerTags() >> [] - ClientStatsAggregator aggregator = new ClientStatsAggregator(empty, features, HealthMetrics.NO_OP, - sink, writer, 10, queueSize, reportingInterval, SECONDS, false) - aggregator.start() - - when: - CountDownLatch latch = new CountDownLatch(1) - aggregator.publish([ - new SimpleSpan("service", "operation", "resource", "type", measured, topLevel, false, 0, 100, HTTP_OK) - .setTag(SPAN_KIND, "baz") - ]) - aggregator.report() - def latchTriggered = latch.await(2, SECONDS) - - then: - latchTriggered - 1 * writer.startBucket(1, _, _) - 1 * writer.add({ - AggregateEntryTestUtils.equals(it,AggregateEntryTestUtils.of( - "resource", - "service", - "operation", - null, - "type", - HTTP_OK, - false, - false, - "baz", - [], - null, - null, - null - )) - }) >> { AggregateEntry e -> - assert e.getHitCount() == 1 && e.getTopLevelCount() == topLevelCount && e.getDuration() == 100 - } - 1 * writer.finishBucket() >> { latch.countDown() } - - cleanup: - aggregator.close() - - where: - measured | topLevel | topLevelCount - true | false | 0 - true | true | 1 - false | true | 1 - } - - def "aggregate repetitive spans"() { - setup: - MetricWriter writer = Mock(MetricWriter) - Sink sink = Stub(Sink) - DDAgentFeaturesDiscovery features = Mock(DDAgentFeaturesDiscovery) - features.supportsMetrics() >> true - features.peerTags() >> [] - ClientStatsAggregator aggregator = new ClientStatsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, 10, queueSize, reportingInterval, SECONDS, false) - long duration = 100 - List trace = [ - new SimpleSpan("service", "operation", "resource", "type", true, false, false, 0, duration, HTTP_OK).setTag(SPAN_KIND, "baz"), - new SimpleSpan("service1", "operation1", "resource1", "type", false, false, false, 0, 0, HTTP_OK).setTag(SPAN_KIND, "baz"), - new SimpleSpan("service2", "operation2", "resource2", "type", true, false, false, 0, duration * 2, HTTP_OK).setTag(SPAN_KIND, "baz") - ] - aggregator.start() - - - when: - CountDownLatch latch = new CountDownLatch(1) - for (int i = 0; i < count; ++i) { - aggregator.publish(trace) - } - aggregator.report() - def latchTriggered = latch.await(2, SECONDS) - - then: "metrics should be conflated" - latchTriggered - 1 * writer.finishBucket() >> { latch.countDown() } - 1 * writer.startBucket(2, _, SECONDS.toNanos(reportingInterval)) - 1 * writer.add({ - AggregateEntryTestUtils.equals(it,AggregateEntryTestUtils.of( - "resource", - "service", - "operation", - null, - "type", - HTTP_OK, - false, - false, - "baz", - [], - null, - null, - null - )) - }) >> { AggregateEntry e -> - assert e.getHitCount() == count && e.getDuration() == count * duration - } - 1 * writer.add({ - AggregateEntryTestUtils.equals(it,AggregateEntryTestUtils.of( - "resource2", - "service2", - "operation2", - null, - "type", - HTTP_OK, - false, - false, - "baz", - [], - null, - null, - null - )) - }) >> { AggregateEntry e -> - assert e.getHitCount() == count && e.getDuration() == count * duration * 2 - } - - cleanup: - aggregator.close() - - where: - count << [10, 100] - } - - def "aggregate spans with same HTTP endpoint together, separate different endpoints"() { - setup: - MetricWriter writer = Mock(MetricWriter) - Sink sink = Stub(Sink) - DDAgentFeaturesDiscovery features = Mock(DDAgentFeaturesDiscovery) - features.supportsMetrics() >> true - features.peerTags() >> [] - ClientStatsAggregator aggregator = new ClientStatsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, 10, queueSize, reportingInterval, SECONDS, true) - aggregator.start() - - when: "publish multiple spans with same endpoint" - CountDownLatch latch = new CountDownLatch(1) - int count = 5 - long duration = 100 - for (int i = 0; i < count; ++i) { - aggregator.publish([ - new SimpleSpan("service", "operation", "resource", "type", true, false, false, 0, duration, HTTP_OK) - .setTag(SPAN_KIND, "server") - .setTag("http.method", "GET") - .setTag("http.endpoint", "/api/users/:id") - ]) - } - aggregator.report() - def latchTriggered = latch.await(2, SECONDS) - - then: "should aggregate into single metric" - latchTriggered - 1 * writer.startBucket(1, _, _) - 1 * writer.add({ - AggregateEntryTestUtils.equals(it,AggregateEntryTestUtils.of( - "resource", - "service", - "operation", - null, - "type", - HTTP_OK, - false, - false, - "server", - [], - "GET", - "/api/users/:id", - null - )) - }) >> { AggregateEntry e -> - assert e.getHitCount() == count && e.getDuration() == count * duration - } - 1 * writer.finishBucket() >> { latch.countDown() } - - when: "publish spans with different endpoints" - CountDownLatch latch2 = new CountDownLatch(1) - aggregator.publish([ - new SimpleSpan("service", "operation", "resource", "type", true, false, false, 0, duration, HTTP_OK) - .setTag(SPAN_KIND, "server") - .setTag("http.method", "GET") - .setTag("http.endpoint", "/api/users/:id"), - new SimpleSpan("service", "operation", "resource", "type", true, false, false, 0, duration * 2, HTTP_OK) - .setTag(SPAN_KIND, "server") - .setTag("http.method", "GET") - .setTag("http.endpoint", "/api/orders/:id"), - new SimpleSpan("service", "operation", "resource", "type", true, false, false, 0, duration * 3, HTTP_OK) - .setTag(SPAN_KIND, "server") - .setTag("http.method", "POST") - .setTag("http.endpoint", "/api/users/:id") - ]) - aggregator.report() - def latchTriggered2 = latch2.await(2, SECONDS) - - then: "should create separate metrics for each endpoint/method combination" - latchTriggered2 - 1 * writer.startBucket(3, _, _) - 1 * writer.add({ - AggregateEntryTestUtils.equals(it,AggregateEntryTestUtils.of( - "resource", - "service", - "operation", - null, - "type", - HTTP_OK, - false, - false, - "server", - [], - "GET", - "/api/users/:id", - null - )) - }) >> { AggregateEntry e -> - assert e.getHitCount() == 1 && e.getDuration() == duration - } - 1 * writer.add({ - AggregateEntryTestUtils.equals(it,AggregateEntryTestUtils.of( - "resource", - "service", - "operation", - null, - "type", - HTTP_OK, - false, - false, - "server", - [], - "GET", - "/api/orders/:id", - null - )) - }) >> { AggregateEntry e -> - assert e.getHitCount() == 1 && e.getDuration() == duration * 2 - } - 1 * writer.add({ - AggregateEntryTestUtils.equals(it,AggregateEntryTestUtils.of( - "resource", - "service", - "operation", - null, - "type", - HTTP_OK, - false, - false, - "server", - [], - "POST", - "/api/users/:id", - null - )) - }) >> { AggregateEntry e -> - assert e.getHitCount() == 1 && e.getDuration() == duration * 3 - } - 1 * writer.finishBucket() >> { latch2.countDown() } - - cleanup: - aggregator.close() - } - - def "create separate metrics for different HTTP method/endpoint/status combinations"() { - setup: - MetricWriter writer = Mock(MetricWriter) - Sink sink = Stub(Sink) - DDAgentFeaturesDiscovery features = Mock(DDAgentFeaturesDiscovery) - features.supportsMetrics() >> true - features.peerTags() >> [] - ClientStatsAggregator aggregator = new ClientStatsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, 10, queueSize, reportingInterval, SECONDS, true) - aggregator.start() - - when: "publish spans with different combinations" - CountDownLatch latch = new CountDownLatch(1) - long duration = 100 - aggregator.publish([ - // Same endpoint, different methods - new SimpleSpan("service", "operation", "resource", "type", true, false, false, 0, duration, 200) - .setTag(SPAN_KIND, "server") - .setTag("http.method", "GET") - .setTag("http.endpoint", "/api/users/:id"), - new SimpleSpan("service", "operation", "resource", "type", true, false, false, 0, duration * 2, 200) - .setTag(SPAN_KIND, "server") - .setTag("http.method", "POST") - .setTag("http.endpoint", "/api/users/:id"), - // Same method/endpoint, different status - new SimpleSpan("service", "operation", "resource", "type", true, false, false, 0, duration * 3, 404) - .setTag(SPAN_KIND, "server") - .setTag("http.method", "GET") - .setTag("http.endpoint", "/api/users/:id"), - // Different endpoint - new SimpleSpan("service", "operation", "resource", "type", true, false, false, 0, duration * 4, 200) - .setTag(SPAN_KIND, "server") - .setTag("http.method", "GET") - .setTag("http.endpoint", "/api/orders/:id") - ]) - aggregator.report() - def latchTriggered = latch.await(2, SECONDS) - - then: "should create 4 separate metrics" - latchTriggered - 1 * writer.startBucket(4, _, _) - 1 * writer.add({ - AggregateEntryTestUtils.equals(it,AggregateEntryTestUtils.of( - "resource", - "service", - "operation", - null, - "type", - 200, - false, - false, - "server", - [], - "GET", - "/api/users/:id", - null - )) - }) >> { AggregateEntry e -> - assert e.getHitCount() == 1 && e.getDuration() == duration - } - 1 * writer.add({ - AggregateEntryTestUtils.equals(it,AggregateEntryTestUtils.of( - "resource", - "service", - "operation", - null, - "type", - 200, - false, - false, - "server", - [], - "POST", - "/api/users/:id", - null - )) - }) >> { AggregateEntry e -> - assert e.getHitCount() == 1 && e.getDuration() == duration * 2 - } - 1 * writer.add({ - AggregateEntryTestUtils.equals(it,AggregateEntryTestUtils.of( - "resource", - "service", - "operation", - null, - "type", - 404, - false, - false, - "server", - [], - "GET", - "/api/users/:id", - null - )) - }) >> { AggregateEntry e -> - assert e.getHitCount() == 1 && e.getDuration() == duration * 3 - } - 1 * writer.add({ - AggregateEntryTestUtils.equals(it,AggregateEntryTestUtils.of( - "resource", - "service", - "operation", - null, - "type", - 200, - false, - false, - "server", - [], - "GET", - "/api/orders/:id", - null - )) - }) >> { AggregateEntry e -> - assert e.getHitCount() == 1 && e.getDuration() == duration * 4 - } - 1 * writer.finishBucket() >> { latch.countDown() } - - cleanup: - aggregator.close() - } - - def "handle spans without HTTP endpoint tags for backward compatibility"() { - setup: - MetricWriter writer = Mock(MetricWriter) - Sink sink = Stub(Sink) - DDAgentFeaturesDiscovery features = Mock(DDAgentFeaturesDiscovery) - features.supportsMetrics() >> true - features.peerTags() >> [] - ClientStatsAggregator aggregator = new ClientStatsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, 10, queueSize, reportingInterval, SECONDS, true) - aggregator.start() - - when: "publish spans with and without HTTP tags" - CountDownLatch latch = new CountDownLatch(1) - long duration = 100 - aggregator.publish([ - // Span without HTTP tags (legacy behavior) - new SimpleSpan("service", "operation", "resource", "type", true, false, false, 0, duration, 200) - .setTag(SPAN_KIND, "server"), - // Span with HTTP tags (new behavior) - new SimpleSpan("service", "operation", "resource", "type", true, false, false, 0, duration * 2, 200) - .setTag(SPAN_KIND, "server") - .setTag("http.method", "GET") - .setTag("http.endpoint", "/api/users/:id") - ]) - aggregator.report() - def latchTriggered = latch.await(2, SECONDS) - - then: "should create separate metric keys for spans with and without HTTP tags" - latchTriggered - 1 * writer.startBucket(2, _, _) - 1 * writer.add({ - AggregateEntryTestUtils.equals(it,AggregateEntryTestUtils.of( - "resource", - "service", - "operation", - null, - "type", - 200, - false, - false, - "server", - [], - null, - null, - null - )) - }) >> { AggregateEntry e -> - assert e.getHitCount() == 1 && e.getDuration() == duration - } - 1 * writer.add({ - AggregateEntryTestUtils.equals(it,AggregateEntryTestUtils.of( - "resource", - "service", - "operation", - null, - "type", - 200, - false, - false, - "server", - [], - "GET", - "/api/users/:id", - null - )) - }) >> { AggregateEntry e -> - assert e.getHitCount() == 1 && e.getDuration() == duration * 2 - } - 1 * writer.finishBucket() >> { latch.countDown() } - - cleanup: - aggregator.close() - } - - def "gather the service name source when the span is published"() { - setup: - MetricWriter writer = Mock(MetricWriter) - Sink sink = Stub(Sink) - DDAgentFeaturesDiscovery features = Mock(DDAgentFeaturesDiscovery) - features.supportsMetrics() >> true - features.peerTags() >> [] - ClientStatsAggregator aggregator = new ClientStatsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, 10, queueSize, reportingInterval, SECONDS, false) - aggregator.start() - - when: "publish spans with different service name source" - CountDownLatch latch = new CountDownLatch(1) - long duration = 100 - aggregator.publish([ - new SimpleSpan("service", "operation", "resource", "type", true, true, false, 0, duration, 200, false, 0, "source") - .setTag(SPAN_KIND, "server"), - new SimpleSpan("service", "operation", "resource", "type", true, true, false, 0, duration, 200, false, 0, null) - .setTag(SPAN_KIND, "server"), - new SimpleSpan("service", "operation", "resource", "type", true, true, false, 0, duration, 200, false, 0, "source") - .setTag(SPAN_KIND, "server") - ]) - aggregator.report() - def latchTriggered = latch.await(2, SECONDS) - - then: "should create the different metric keys for spans with and without sources" - latchTriggered - 1 * writer.startBucket(2, _, _) - 1 * writer.add({ - AggregateEntryTestUtils.equals(it,AggregateEntryTestUtils.of( - "resource", - "service", - "operation", - "source", - "type", - 200, - false, - false, - "server", - [], - null, - null, - null - )) - }) >> { AggregateEntry e -> - assert e.getHitCount() == 2 && e.getDuration() == 2 * duration - } - 1 * writer.add({ - AggregateEntryTestUtils.equals(it,AggregateEntryTestUtils.of( - "resource", - "service", - "operation", - null, - "type", - 200, - false, - false, - "server", - [], - null, - null, - null - )) - }) >> { AggregateEntry e -> - assert e.getHitCount() == 1 && e.getDuration() == duration - } - 1 * writer.finishBucket() >> { latch.countDown() } - - cleanup: - aggregator.close() - } - - def "new aggregates beyond size limit are dropped when no stale entries can be evicted"() { - // The table only evicts entries with hitCount == 0 to make room. When all entries are live - // (all have been recorded against), an over-cap insert drops the new key rather than evicting - // an established one. This protects the data we've already collected from a burst of new keys. - setup: - int maxAggregates = 10 - MetricWriter writer = Mock(MetricWriter) - Sink sink = Stub(Sink) - DDAgentFeaturesDiscovery features = Mock(DDAgentFeaturesDiscovery) - features.supportsMetrics() >> true - features.peerTags() >> [] - ClientStatsAggregator aggregator = new ClientStatsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, maxAggregates, queueSize, reportingInterval, SECONDS, false) - long duration = 100 - aggregator.start() - - when: - CountDownLatch latch = new CountDownLatch(1) - for (int i = 0; i < 11; ++i) { - aggregator.publish([ - new SimpleSpan("service" + i, "operation", "resource", "type", false, true, false, 0, duration, HTTP_OK) - .setTag(SPAN_KIND, "baz") - ]) - } - aggregator.report() - def latchTriggered = latch.await(2, SECONDS) - - then: "the established service0..service9 are reported; service10 is dropped" - latchTriggered - 1 * writer.startBucket(10, _, SECONDS.toNanos(reportingInterval)) - for (int i = 0; i < 10; ++i) { - def expected = AggregateEntryTestUtils.of( - "resource", - "service" + i, - "operation", - null, - "type", - HTTP_OK, - false, - false, - "baz", - [], - null, - null, - null) - 1 * writer.add({ AggregateEntryTestUtils.equals(it, expected) }) >> { AggregateEntry e -> - assert e.getHitCount() == 1 && e.getDuration() == duration - } - } - 0 * writer.add({ - AggregateEntryTestUtils.equals(it,AggregateEntryTestUtils.of( - "resource", - "service10", - "operation", - null, - "type", - HTTP_OK, - false, - false, - "baz", - [], - null, - null, - null - )) - }) - 1 * writer.finishBucket() >> { latch.countDown() } - - cleanup: - aggregator.close() - } - - def "should report dropped aggregate to health metrics on LRU eviction"() { - setup: - int maxAggregates = 10 - MetricWriter writer = Mock(MetricWriter) - Sink sink = Stub(Sink) - DDAgentFeaturesDiscovery features = Mock(DDAgentFeaturesDiscovery) - features.supportsMetrics() >> true - features.peerTags() >> [] - HealthMetrics healthMetrics = Mock(HealthMetrics) - ClientStatsAggregator aggregator = new ClientStatsAggregator(empty, - features, healthMetrics, sink, writer, maxAggregates, queueSize, reportingInterval, SECONDS, false) - long duration = 100 - aggregator.start() - - when: - CountDownLatch latch = new CountDownLatch(1) - for (int i = 0; i < maxAggregates + 1; ++i) { - aggregator.publish([ - new SimpleSpan("service" + i, "operation", "resource", "type", false, true, false, 0, duration, HTTP_OK) - .setTag(SPAN_KIND, "baz") - ]) - } - aggregator.report() - def latchTriggered = latch.await(2, SECONDS) - - then: - latchTriggered - 1 * writer.finishBucket() >> { latch.countDown() } - 1 * healthMetrics.onStatsAggregateDropped() - - cleanup: - aggregator.close() - } - - def "should not report dropped aggregate when evicted entry was already flushed"() { - setup: - int maxAggregates = 5 - MetricWriter writer = Mock(MetricWriter) - Sink sink = Stub(Sink) - DDAgentFeaturesDiscovery features = Mock(DDAgentFeaturesDiscovery) - features.supportsMetrics() >> true - features.peerTags() >> [] - HealthMetrics healthMetrics = Mock(HealthMetrics) - ClientStatsAggregator aggregator = new ClientStatsAggregator(empty, - features, healthMetrics, sink, writer, maxAggregates, queueSize, reportingInterval, SECONDS, false) - aggregator.start() - - when: "fill cache and flush — entries are cleared (hitCount=0) but stay in the LRU" - CountDownLatch latch1 = new CountDownLatch(1) - for (int i = 0; i < maxAggregates; ++i) { - aggregator.publish([ - new SimpleSpan("service" + i, "operation", "resource", "type", false, true, false, 0, 100, HTTP_OK) - .setTag(SPAN_KIND, "baz") - ]) - } - aggregator.report() - latch1.await(2, SECONDS) - - then: - 1 * writer.finishBucket() >> { latch1.countDown() } - - when: "publish new distinct spans — LRU evicts the cleared entries before the next report" - CountDownLatch latch2 = new CountDownLatch(1) - for (int i = maxAggregates; i < maxAggregates * 2; ++i) { - aggregator.publish([ - new SimpleSpan("service" + i, "operation", "resource", "type", false, true, false, 0, 100, HTTP_OK) - .setTag(SPAN_KIND, "baz") - ]) - } - aggregator.report() - latch2.await(2, SECONDS) - - then: "no drop metric because all evicted entries had hitCount=0 (already reported)" - 1 * writer.finishBucket() >> { latch2.countDown() } - 0 * healthMetrics.onStatsAggregateDropped() - - cleanup: - aggregator.close() - } - - def "aggregate not updated in reporting interval not reported"() { - setup: - int maxAggregates = 10 - MetricWriter writer = Mock(MetricWriter) - Sink sink = Stub(Sink) - DDAgentFeaturesDiscovery features = Mock(DDAgentFeaturesDiscovery) - features.supportsMetrics() >> true - features.peerTags() >> [] - ClientStatsAggregator aggregator = new ClientStatsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, maxAggregates, queueSize, reportingInterval, SECONDS, false) - long duration = 100 - aggregator.start() - - when: - CountDownLatch latch = new CountDownLatch(1) - for (int i = 0; i < 5; ++i) { - aggregator.publish([ - new SimpleSpan("service" + i, "operation", "resource", "type", false, true, false, 0, duration, HTTP_OK) - .setTag(SPAN_KIND, "baz") - ]) - } - aggregator.report() - def latchTriggered = latch.await(2, SECONDS) - - then: "all aggregates should be reported" - latchTriggered - 1 * writer.startBucket(5, _, SECONDS.toNanos(reportingInterval)) - for (int i = 0; i < 5; ++i) { - def expected = AggregateEntryTestUtils.of( - "resource", - "service" + i, - "operation", - null, - "type", - HTTP_OK, - false, - false, - "baz", - [], - null, - null, - null) - 1 * writer.add({ AggregateEntryTestUtils.equals(it, expected) }) >> { AggregateEntry e -> - assert e.getHitCount() == 1 && e.getDuration() == duration - } - } - 1 * writer.finishBucket() >> { latch.countDown() } - - when: - latch = new CountDownLatch(1) - for (int i = 1; i < 5; ++i) { - aggregator.publish([ - new SimpleSpan("service" + i, "operation", "resource", "type", false, true, false, 0, duration, HTTP_OK) - .setTag(SPAN_KIND, "baz") - ]) - } - aggregator.report() - latchTriggered = latch.await(2, SECONDS) - - then: "aggregate not updated in cycle is not reported" - latchTriggered - 1 * writer.startBucket(4, _, SECONDS.toNanos(reportingInterval)) - for (int i = 1; i < 5; ++i) { - def expected = AggregateEntryTestUtils.of( - "resource", - "service" + i, - "operation", - null, - "type", - HTTP_OK, - false, - false, - "baz", - [], - null, - null, - null) - 1 * writer.add({ AggregateEntryTestUtils.equals(it, expected) }) >> { AggregateEntry e -> - assert e.getHitCount() == 1 && e.getDuration() == duration - } - } - 0 * writer.add({ - AggregateEntryTestUtils.equals(it,AggregateEntryTestUtils.of( - "resource", - "service0", - "operation", - null, - "type", - HTTP_OK, - false, - false, - "baz", - [], - null, - null, - null - )) - }) - 1 * writer.finishBucket() >> { latch.countDown() } - - cleanup: - aggregator.close() - } - - def "when no aggregate is updated in reporting interval nothing is reported"() { - setup: - int maxAggregates = 10 - MetricWriter writer = Mock(MetricWriter) - Sink sink = Stub(Sink) - DDAgentFeaturesDiscovery features = Mock(DDAgentFeaturesDiscovery) - features.supportsMetrics() >> true - features.peerTags() >> [] - ClientStatsAggregator aggregator = new ClientStatsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, maxAggregates, queueSize, reportingInterval, SECONDS, false) - long duration = 100 - aggregator.start() - - when: - CountDownLatch latch = new CountDownLatch(1) - for (int i = 0; i < 5; ++i) { - aggregator.publish([ - new SimpleSpan("service" + i, "operation", "resource", "type", false, true, false, 0, duration, HTTP_OK) - .setTag(SPAN_KIND, "quux") - ]) - } - aggregator.report() - def latchTriggered = latch.await(2, SECONDS) - - then: "all aggregates should be reported" - latchTriggered - 1 * writer.startBucket(5, _, SECONDS.toNanos(reportingInterval)) - for (int i = 0; i < 5; ++i) { - def expected = AggregateEntryTestUtils.of( - "resource", - "service" + i, - "operation", - null, - "type", - HTTP_OK, - false, - false, - "quux", - [], - null, - null, - null) - 1 * writer.add({ AggregateEntryTestUtils.equals(it, expected) }) >> { AggregateEntry e -> - assert e.getHitCount() == 1 && e.getDuration() == duration - } - } - 1 * writer.finishBucket() >> { latch.countDown() } - - when: - reportAndWaitUntilEmpty(aggregator) - - then: "aggregate not updated in cycle is not reported" - 0 * writer.finishBucket() - 0 * writer.startBucket(_, _, _) - 0 * writer.add(_) - - cleanup: - aggregator.close() - } - - def "should report periodically"() { - setup: - int maxAggregates = 10 - MetricWriter writer = Mock(MetricWriter) - Sink sink = Stub(Sink) - DDAgentFeaturesDiscovery features = Mock(DDAgentFeaturesDiscovery) - features.supportsMetrics() >> true - features.peerTags() >> [] - ClientStatsAggregator aggregator = new ClientStatsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, maxAggregates, queueSize, 1, SECONDS, false) - long duration = 100 - aggregator.start() - - when: - CountDownLatch latch = new CountDownLatch(1) - for (int i = 0; i < 5; ++i) { - aggregator.publish([ - new SimpleSpan("service" + i, "operation", "resource", "type", false, true, false, 0, duration, HTTP_OK, true) - .setTag(SPAN_KIND, "garply") - ]) - } - def latchTriggered = latch.await(2, SECONDS) - - then: "all aggregates should be reported" - latchTriggered - 1 * writer.startBucket(5, _, SECONDS.toNanos(1)) - for (int i = 0; i < 5; ++i) { - def expected = AggregateEntryTestUtils.of( - "resource", - "service" + i, - "operation", - null, - "type", - HTTP_OK, - false, - true, - "garply", - [], - null, - null, - null) - 1 * writer.add({ AggregateEntryTestUtils.equals(it, expected) }) >> { AggregateEntry e -> - assert e.getHitCount() == 1 && e.getDuration() == duration - } - } - 1 * writer.finishBucket() >> { latch.countDown() } - - cleanup: - aggregator.close() - } - - def "should be resilient to serialization errors"() { - setup: - int maxAggregates = 10 - MetricWriter writer = Mock(MetricWriter) - Sink sink = Stub(Sink) - DDAgentFeaturesDiscovery features = Mock(DDAgentFeaturesDiscovery) - features.supportsMetrics() >> true - features.peerTags() >> [] - ClientStatsAggregator aggregator = new ClientStatsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, maxAggregates, queueSize, 1, SECONDS, false) - long duration = 100 - aggregator.start() - - when: - CountDownLatch latch = new CountDownLatch(1) - for (int i = 0; i < 5; ++i) { - aggregator.publish([ - new SimpleSpan("service" + i, "operation", "resource", "type", false, true, false, 0, duration, HTTP_OK) - ]) - } - def latchTriggered = latch.await(2, SECONDS) - - then: "writer should be reset if reporting fails" - latchTriggered - 1 * writer.startBucket(_, _, _) >> { - throw new IllegalArgumentException("something went wrong") - } - 1 * writer.reset() >> { latch.countDown() } - - cleanup: - aggregator.close() - } - - def "force flush should not block if metrics are disabled"() { - setup: - int maxAggregates = 10 - MetricWriter writer = Mock(MetricWriter) - Sink sink = Stub(Sink) - DDAgentFeaturesDiscovery features = Mock(DDAgentFeaturesDiscovery) - ClientStatsAggregator aggregator = new ClientStatsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, maxAggregates, queueSize, 1, SECONDS, false) - aggregator.start() - - when: - def flushed = aggregator.forceReport().get(10, SECONDS) - - then: - notThrown(TimeoutException) - !flushed - - cleanup: - aggregator.close() - } - - def "should start even if the agent is not available"() { - setup: - MetricWriter writer = Mock(MetricWriter) - Sink sink = Stub(Sink) - DDAgentFeaturesDiscovery features = Mock(DDAgentFeaturesDiscovery) - features.supportsMetrics() >> false - features.peerTags() >> [] - ClientStatsAggregator aggregator = new ClientStatsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, 10, queueSize, 200, MILLISECONDS, false) - final spans = [ - new SimpleSpan("service", "operation", "resource", "type", false, true, false, 0, 10, HTTP_OK) - ] - aggregator.start() - - when: - aggregator.publish(spans) - Thread.sleep(1_000) - - then: - 0 * writer._ - when: - features.supportsMetrics() >> true - aggregator.publish(spans) - Thread.sleep(1_000) - - then: - (1.._) * writer._ - - cleanup: - aggregator.close() - } - - def "force flush should wait for aggregator to start"() { - setup: - int maxAggregates = 10 - MetricWriter writer = Mock(MetricWriter) - Sink sink = Stub(Sink) - DDAgentFeaturesDiscovery features = Mock(DDAgentFeaturesDiscovery) - features.supportsMetrics() >> true - ClientStatsAggregator aggregator = new ClientStatsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, maxAggregates, queueSize, 1, SECONDS, false) - - when: - def async = CompletableFuture.supplyAsync(new Supplier() { - @Override - Boolean get() { - return aggregator.forceReport().get() - } - }) - async.get(3, SECONDS) - - then: - thrown(TimeoutException) - - when: - aggregator.start() - def flushed = async.get(3, TimeUnit.SECONDS) - - then: - notThrown(TimeoutException) - flushed - - cleanup: - aggregator.close() - } - - def "should not count partial snapshot(long running)"() { - setup: - MetricWriter writer = Mock(MetricWriter) - Sink sink = Stub(Sink) - DDAgentFeaturesDiscovery features = Mock(DDAgentFeaturesDiscovery) - features.supportsMetrics() >> true - ClientStatsAggregator aggregator = new ClientStatsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, 10, queueSize, reportingInterval, SECONDS, false) - aggregator.start() - - when: - CountDownLatch latch = new CountDownLatch(1) - aggregator.publish([ - new SimpleSpan("service", "operation", "resource", "type", true, true, false, 0, 100, HTTP_OK, true, 12345), - new SimpleSpan("service", "operation", "resource", "type", true, true, false, 0, 100, HTTP_OK, true, 0) - ]) - aggregator.report() - def latchTriggered = latch.await(2, SECONDS) - - then: - latchTriggered - 1 * writer.startBucket(1, _, _) - 1 * writer.add({ - AggregateEntryTestUtils.equals(it, - AggregateEntryTestUtils.of( - "resource", - "service", - "operation", - null, - "type", - HTTP_OK, - false, - true, - "", - [], - null, - null, - null - )) - }) >> { AggregateEntry e -> - assert e.getHitCount() == 1 && e.getTopLevelCount() == 1 && e.getDuration() == 100 - } - 1 * writer.finishBucket() >> { latch.countDown() } - - cleanup: - aggregator.close() - } - - def "should not change metric buckets when includeEndpointInMetrics is disabled"() { - setup: - MetricWriter writer = Mock(MetricWriter) - Sink sink = Stub(Sink) - DDAgentFeaturesDiscovery features = Mock(DDAgentFeaturesDiscovery) - features.supportsMetrics() >> true - features.peerTags() >> [] - ClientStatsAggregator aggregator = new ClientStatsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, 10, queueSize, reportingInterval, SECONDS, false) - aggregator.start() - - when: "publishing spans with different http.method and http.endpoint" - CountDownLatch latch = new CountDownLatch(1) - aggregator.publish([ - new SimpleSpan("service", "operation", "resource", "type", false, true, false, 0, 100, HTTP_OK) - .setTag(SPAN_KIND, "server") - .setTag("http.method", "GET") - .setTag("http.endpoint", "/api/users/:id"), - new SimpleSpan("service", "operation", "resource", "type", false, true, false, 0, 200, HTTP_OK) - .setTag(SPAN_KIND, "server") - .setTag("http.method", "POST") - .setTag("http.endpoint", "/api/orders"), - new SimpleSpan("service", "operation", "resource", "type", false, true, false, 0, 150, HTTP_OK) - .setTag(SPAN_KIND, "server") - ]) - reportAndWaitUntilEmpty(aggregator) - def latchTriggered = latch.await(2, SECONDS) - - then: "all spans should go to the same bucket (httpMethod and httpEndpoint are ignored)" - latchTriggered - 1 * writer.startBucket(1, _, _) - 1 * writer.add({ - AggregateEntryTestUtils.equals(it, - AggregateEntryTestUtils.of( - "resource", - "service", - "operation", - null, - "type", - HTTP_OK, - false, - false, - "server", - [], - null, - null, - null - )) - }) >> { AggregateEntry e -> - assert e.getHitCount() == 3 && e.getTopLevelCount() == 3 && e.getDuration() == 450 - } - 1 * writer.finishBucket() >> { latch.countDown() } - - cleanup: - aggregator.close() - } - - def "should separate metric buckets when includeEndpointInMetrics is enabled"() { - setup: - MetricWriter writer = Mock(MetricWriter) - Sink sink = Stub(Sink) - DDAgentFeaturesDiscovery features = Mock(DDAgentFeaturesDiscovery) - features.supportsMetrics() >> true - features.peerTags() >> [] - ClientStatsAggregator aggregator = new ClientStatsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, 10, queueSize, reportingInterval, SECONDS, true) - aggregator.start() - - when: "publishing spans with different http.method and http.endpoint" - CountDownLatch latch = new CountDownLatch(1) - aggregator.publish([ - new SimpleSpan("service", "operation", "resource", "type", false, true, false, 0, 100, HTTP_OK) - .setTag(SPAN_KIND, "server") - .setTag("http.method", "GET") - .setTag("http.endpoint", "/api/users/:id"), - new SimpleSpan("service", "operation", "resource", "type", false, true, false, 0, 200, HTTP_OK) - .setTag(SPAN_KIND, "server") - .setTag("http.method", "POST") - .setTag("http.endpoint", "/api/orders"), - new SimpleSpan("service", "operation", "resource", "type", false, true, false, 0, 150, HTTP_OK) - .setTag(SPAN_KIND, "server") - ]) - reportAndWaitUntilEmpty(aggregator) - def latchTriggered = latch.await(2, SECONDS) - - then: "spans should go to separate buckets based on httpMethod and httpEndpoint" - latchTriggered - 1 * writer.startBucket(3, _, _) - 1 * writer.add({ - AggregateEntryTestUtils.equals(it, - AggregateEntryTestUtils.of( - "resource", - "service", - "operation", - null, - "type", - HTTP_OK, - false, - false, - "server", - [], - "GET", - "/api/users/:id", - null - )) - }) >> { AggregateEntry e -> - assert e.getHitCount() == 1 && e.getTopLevelCount() == 1 && e.getDuration() == 100 - } - 1 * writer.add({ - AggregateEntryTestUtils.equals(it, - AggregateEntryTestUtils.of( - "resource", - "service", - "operation", - null, - "type", - HTTP_OK, - false, - false, - "server", - [], - "POST", - "/api/orders", - null - )) - }) >> { AggregateEntry e -> - assert e.getHitCount() == 1 && e.getTopLevelCount() == 1 && e.getDuration() == 200 - } - 1 * writer.add({ - AggregateEntryTestUtils.equals(it, - AggregateEntryTestUtils.of( - "resource", - "service", - "operation", - null, - "type", - HTTP_OK, - false, - false, - "server", - [], - null, - null, - null - )) - }) >> { AggregateEntry e -> - assert e.getHitCount() == 1 && e.getTopLevelCount() == 1 && e.getDuration() == 150 - } - 1 * writer.finishBucket() >> { latch.countDown() } - - cleanup: - aggregator.close() - } - - def "should include grpc status code in metric key for rpc spans"() { - setup: - MetricWriter writer = Mock(MetricWriter) - Sink sink = Stub(Sink) - DDAgentFeaturesDiscovery features = Mock(DDAgentFeaturesDiscovery) - features.supportsMetrics() >> true - features.peerTags() >> [] - ClientStatsAggregator aggregator = new ClientStatsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, 10, queueSize, reportingInterval, SECONDS, false) - aggregator.start() - - when: - CountDownLatch latch = new CountDownLatch(1) - aggregator.publish([ - new SimpleSpan("service", "grpc.server", "grpc.service/Method", "rpc", true, false, false, 0, 100, 0) - .setTag(SPAN_KIND, "server") - .setTag(InstrumentationTags.GRPC_STATUS_CODE, 0), - new SimpleSpan("service", "grpc.server", "grpc.service/Method", "rpc", true, false, false, 0, 50, 0) - .setTag(SPAN_KIND, "server") - .setTag(InstrumentationTags.GRPC_STATUS_CODE, 5), - new SimpleSpan("service", "http.request", "GET /api", "web", true, false, false, 0, 75, 200) - .setTag(SPAN_KIND, "server") - ]) - aggregator.report() - def latchTriggered = latch.await(2, SECONDS) - - then: - latchTriggered - 1 * writer.startBucket(3, _, _) - 1 * writer.add({ - AggregateEntryTestUtils.equals(it,AggregateEntryTestUtils.of( - "grpc.service/Method", - "service", - "grpc.server", - null, - "rpc", - 0, - false, - false, - "server", - [], - null, - null, - "0" - )) - }) - 1 * writer.add({ - AggregateEntryTestUtils.equals(it,AggregateEntryTestUtils.of( - "grpc.service/Method", - "service", - "grpc.server", - null, - "rpc", - 0, - false, - false, - "server", - [], - null, - null, - "5" - )) - }) - 1 * writer.add({ - AggregateEntryTestUtils.equals(it,AggregateEntryTestUtils.of( - "GET /api", - "service", - "http.request", - null, - "web", - 200, - false, - false, - "server", - [], - null, - null, - null - )) - }) - 1 * writer.finishBucket() >> { latch.countDown() } - - cleanup: - aggregator.close() - } - - def "cardinality limits reset between report cycles"() { - setup: - List cycle1Entries = [] - List cycle2Entries = [] - CountDownLatch latch1 = new CountDownLatch(1) - CountDownLatch latch2 = new CountDownLatch(1) - MetricWriter writer = Mock(MetricWriter) - Sink sink = Stub(Sink) - DDAgentFeaturesDiscovery features = Mock(DDAgentFeaturesDiscovery) - features.supportsMetrics() >> true - features.peerTags() >> [] - ClientStatsAggregator aggregator = new ClientStatsAggregator(empty, - features, HealthMetrics.NO_OP, sink, writer, 256, queueSize, reportingInterval, SECONDS, false) - aggregator.start() - - when: "publish SERVICE+1 distinct services to fill and overflow the cardinality budget" - for (int i = 0; i <= MetricCardinalityLimits.SERVICE; i++) { - aggregator.publish([new SimpleSpan("svc-$i", "op", "resource", "web", false, true, false, 0, 100, HTTP_OK)]) - } - aggregator.report() - latch1.await(2, SECONDS) - - then: "the overflow service maps to the tracer_blocked_value sentinel" - 1 * writer.startBucket(MetricCardinalityLimits.SERVICE + 1, _, _) - (1.._) * writer.add(_) >> { AggregateEntry e -> cycle1Entries << e } - 1 * writer.finishBucket() >> { latch1.countDown() } - cycle1Entries.count { it.getService().toString() == "tracer_blocked_value" } == 1 - - when: "publish the overflow service in the next cycle after the cardinality reset" - aggregator.publish([ - new SimpleSpan("svc-${MetricCardinalityLimits.SERVICE}", "op", "resource", "web", false, true, false, 0, 100, HTTP_OK) - ]) - aggregator.report() - latch2.await(2, SECONDS) - - then: "after reset the overflow service name is accepted as a real entry" - 1 * writer.startBucket(1, _, _) - 1 * writer.add(_) >> { AggregateEntry e -> cycle2Entries << e } - 1 * writer.finishBucket() >> { latch2.countDown() } - cycle2Entries[0].getService().toString() == "svc-${MetricCardinalityLimits.SERVICE}" - - cleanup: - aggregator.close() - } - - def reportAndWaitUntilEmpty(ClientStatsAggregator aggregator) { - waitUntilEmpty(aggregator) - aggregator.report() - waitUntilEmpty(aggregator) - } - - - def waitUntilEmpty(ClientStatsAggregator aggregator) { - int i = 0 - while (!aggregator.inbox.isEmpty() && i++ < 100) { - Thread.sleep(10) - } - } -} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/FootprintForkedTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/FootprintForkedTest.groovy deleted file mode 100644 index fb5bc2ed561..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/FootprintForkedTest.groovy +++ /dev/null @@ -1,164 +0,0 @@ -package datadog.trace.common.metrics - -import datadog.communication.ddagent.DDAgentFeaturesDiscovery -import datadog.metrics.api.Histograms -import datadog.metrics.impl.DDSketchHistograms -import datadog.trace.api.WellKnownTags -import datadog.trace.core.monitor.HealthMetrics -import datadog.trace.test.util.DDSpecification -import org.openjdk.jol.info.GraphLayout -import spock.lang.Requires -import spock.lang.Shared - -import java.nio.ByteBuffer -import java.util.concurrent.CountDownLatch -import java.util.concurrent.ThreadLocalRandom - -import static java.util.concurrent.TimeUnit.SECONDS - -@Requires({ - !System.getProperty("java.vendor").toUpperCase().contains("IBM") -}) -class FootprintForkedTest extends DDSpecification { - - @Shared - Random random = new Random(0) - - def setupSpec() { - // Initialize metrics-lib histograms to register the DDSketch implementation - Histograms.register(DDSketchHistograms.FACTORY) - } - - def "footprint less than 10MB"() { - setup: - CountDownLatch latch = new CountDownLatch(1) - ValidatingSink sink = new ValidatingSink(latch) - DDAgentFeaturesDiscovery features = Stub(DDAgentFeaturesDiscovery) { - it.supportsMetrics() >> true - it.peerTags() >> [] - } - ClientStatsAggregator aggregator = new ClientStatsAggregator( - new WellKnownTags("runtimeid","hostname", "env", "service", "version","language"), - [].toSet() as Set, - AdditionalTagsSchema.EMPTY, - features, - HealthMetrics.NO_OP, - sink, - 1000, - 1000, - 100, - SECONDS, - false) - // Removing the 'features' as it's a mock, and mocks are heavyweight, e.g. around 22MiB - def baseline = footprint(aggregator, features) - aggregator.start() - - when: "lots of traces are published" - String[] operations = randomNames(operationCardinality) - Map serviceNamesByOperation = scopedRandomNames(operations, servicePerOperation) - Map resourceNamesByService = scopedRandomNames(serviceNamesByOperation.values(), resourceNamesPerService) - Map typesByOperation = scopedRandomNames(operations, typesPerOperation) - int traceCount = 10_000 - int errorThreshold = (int) (errorRate * traceCount) - for (int i = 0; i < traceCount; ++i) { - String operation = operations[ThreadLocalRandom.current().nextInt(operations.length)] - String[] types = typesByOperation.get(operation) - String type = types[ThreadLocalRandom.current().nextInt(types.length)] - String[] serviceNames = serviceNamesByOperation.get(operation) - String serviceName = serviceNames[ThreadLocalRandom.current().nextInt(serviceNames.length)] - String[] resourceNames = resourceNamesByService.get(serviceName) - String resourceName = resourceNames[ThreadLocalRandom.current().nextInt(resourceNames.length)] - boolean isError = ThreadLocalRandom.current().nextInt(traceCount) < errorThreshold - aggregator.publish([ - new SimpleSpan(serviceName, operation, resourceName, type, true, true, isError, System.nanoTime(), - isError ? expDistributedNanoseconds(0.99) : expDistributedNanoseconds(0.01), 200) - ]) - } - if (!aggregator.report()) { - int attempts = 0 - while (++attempts < 10 && !aggregator.report()) { - Thread.sleep(10) - } - assert attempts < 10 - } - assert latch.await(30, SECONDS) - - then: - def after = footprint(aggregator, features) - after - baseline <= 10 * 1024 * 1024 - - cleanup: - aggregator.close() - - where: - operationCardinality | servicePerOperation | resourceNamesPerService | typesPerOperation | errorRate - 5 | 1 | 10 | 2 | 0.00 - 5 | 1 | 100 | 2 | 0.00 - 5 | 1 | 10 | 2 | 0.01 - 5 | 1 | 100 | 2 | 0.01 - 10 | 1 | 100 | 2 | 0.00 - 10 | 1 | 100 | 2 | 0.01 - } - - def randomNames(int cardinality) { - String[] things = new String[cardinality] - for (int i = 0; i < things.length; ++i) { - things[i] = UUID.randomUUID().toString() - } - return things - } - - def scopedRandomNames(String[] parents, int childCardinality) { - Map things = new HashMap<>() - for (String parent : parents) { - things.put(parent, randomNames(childCardinality)) - } - return things - } - - def scopedRandomNames(Collection parents, int childCardinality) { - Map things = new HashMap<>() - for (String[] parent : parents) { - for (String p : parent) { - things.put(p, randomNames(childCardinality)) - } - } - return things - } - - def expDistributedNanoseconds(double intensity) { - return (long)(Math.log(random.nextDouble()) / Math.log(1 - intensity) + 1) - } - - class ValidatingSink implements Sink { - - final CountDownLatch latch - - ValidatingSink(CountDownLatch latch) { - this.latch = latch - } - - @Override - void register(EventListener listener) { - } - - @Override - void accept(int messageCount, ByteBuffer buffer) { - latch.countDown() - } - } - - static long footprint(Object root, Object... excludedRootFieldInstance) { - GraphLayout layout = GraphLayout.parseInstance(root) - def size = layout.totalSize() - - excludedRootFieldInstance.each { - def excludedLayout = GraphLayout.parseInstance(it) - layout = layout.subtract(excludedLayout) - size -= excludedLayout.totalSize() - } - - println(layout.toFootprint()) - return size - } -} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/MetricsReliabilityTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/MetricsReliabilityTest.groovy deleted file mode 100644 index 26c1e463dd0..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/MetricsReliabilityTest.groovy +++ /dev/null @@ -1,173 +0,0 @@ -package datadog.trace.common.metrics - -import datadog.communication.ddagent.SharedCommunicationObjects -import datadog.trace.api.Config -import datadog.metrics.api.statsd.StatsDClient -import datadog.trace.core.monitor.HealthMetrics -import datadog.trace.core.monitor.TracerHealthMetrics -import datadog.trace.core.test.DDCoreSpecification -import datadog.trace.util.Strings - -import java.util.concurrent.CountDownLatch - -import static datadog.trace.agent.test.server.http.TestHttpServer.httpServer - -class MetricsReliabilityTest extends DDCoreSpecification { - - static class State { - boolean agentMetricsAvailable = true - int statsResponseCode = 200 - boolean receivedStats - boolean receivedClientComputedHeader - CountDownLatch latch - String hash - - def reset(agentMetricsAvailable, statsResponseCode = 200) { - this.agentMetricsAvailable = agentMetricsAvailable - this.statsResponseCode = statsResponseCode - receivedStats = false - receivedClientComputedHeader = false - latch = new CountDownLatch(1) - } - } - - static newAgent(State state) { - httpServer { - handlers { - get("/info") { - final def res = '{"version":"7.65.0","endpoints":[' + (state.agentMetricsAvailable ? '"/v0.6/stats", ' : '') + '"/v0.4/traces"], "client_drop_p0s" : true}' - state.hash = Strings.sha256(res) - response.send(res) - state.latch.countDown() - } - post("/v0.6/stats", { - state.receivedStats = true - response.status(state.statsResponseCode).send() - }) - put("/v0.4/traces", { - state.receivedClientComputedHeader = "true" == request.getHeader('Datadog-Client-Computed-Stats') - response.status(200).addHeader("Datadog-Agent-State", state.hash).send() - }) - } - } - } - - def "metrics should reliably handle momentary downgrades"() { - setup: - def state = new State() - state.reset(true) - def agent = newAgent(state) - agent.start() - def props = new Properties() - props.put("trace.agent.url", agent.getAddress().toString()) - props.put("trace.stats.computation.enabled", "true") - def config = Config.get(props) - def sharedComm = new SharedCommunicationObjects() - sharedComm.createRemaining(config) - def featuresDiscovery = sharedComm.featuresDiscovery(config) - def healthMetrics = new TracerHealthMetrics(StatsDClient.NO_OP) - def tracer = tracerBuilder().sharedCommunicationObjects(sharedComm).healthMetrics(healthMetrics).config(config).build() - when: "metrics enabled and discovery is performed" - featuresDiscovery.discover() - - then: "should support metrics" - state.latch.await() - assert featuresDiscovery.supportsMetrics() - - when: "a span is published" - tracer.startSpan("test", "test").finish() - tracer.flush() - tracer.flushMetrics() - - then: "should have sent statistics and informed the agent that we calculate the stats" - assert state.receivedClientComputedHeader - assert state.receivedStats - // 1 trace processed. 1 p0 drop No errors - assertMetrics(healthMetrics, 1, 1, 1, 0, 0) - - - when: "simulate an agent downgrade" - state.reset(false, 404) - tracer.startSpan("test", "test").finish() - tracer.flush() - tracer.flushMetrics() - - then: "a discovery should have done - we do not support anymore stats calculation" - state.latch.await() - assert !featuresDiscovery.supportsMetrics() - // 2 traces processed. 2 p0 dropped. 2 requests and 1 downgrade no errors - assertMetrics(healthMetrics, 2, 2, 2, 0, 1) - - - when: "a span is published" - state.reset(false) // we have a call to stats for the downgrade so let's reset the counter - tracer.startSpan("test", "test").finish() - tracer.flush() - tracer.flushMetrics() - - then: "should have not sent statistics and informed the agent that we don't calculate the stats anymore" - assert !state.receivedClientComputedHeader - assert !state.receivedStats - // 2 traces processed. 1 p0 dropped. 2 requests and 1 downgrade no errors - assertMetrics(healthMetrics, 2, 2, 2, 0, 1) - - when: "we detect that the agent can calculate the stats again" - state.reset(true) - featuresDiscovery.discover() - - then: "we should understand it" - state.latch.await() - assert featuresDiscovery.supportsMetrics() - - when: "a span is published" - tracer.startSpan("test", "test").finish() - tracer.flush() - tracer.flushMetrics() - - then: "we should have sent the stats and informed the agent to not calculate the stats on the trace payload" - assert state.receivedClientComputedHeader - assert state.receivedStats - // 3 traces processed. 2 p0 dropped. 3 requests and 1 downgrade no errors - assertMetrics(healthMetrics, 3, 3, 3, 0, 1) - - when: "an error occurred on the agent stats endpoint" - state.reset(true, 500) - tracer.startSpan("test", "test").finish() - tracer.flush() - tracer.flushMetrics() - - then: "the error counter is incremented" - assert state.receivedClientComputedHeader - assert state.receivedStats - // 4 traces processed. 3 p0 dropped. 4 requests and 1 downgrade - 1 error - assertMetrics(healthMetrics, 4, 4, 4, 1, 1) - - when: "the next call succeed" - state.reset(true) - tracer.startSpan("test", "test").setError(true).finish() - tracer.flush() - tracer.flushMetrics() - - then: "the request counter is incremented" - assert state.receivedClientComputedHeader - assert state.receivedStats - // 5 traces processed. 3 p0 dropped (this one is errored so it's not dropped). - // 5 requests and 1 downgrade - 1 error - assertMetrics(healthMetrics, 5, 4, 5, 1, 1) - - cleanup: - tracer.close() - agent.stop() - } - - void assertMetrics(HealthMetrics healthMetrics, int traces, int drops, int requests, int errors, int downgrades) { - def summary = healthMetrics.summary() - assert summary.contains("clientStatsRequests=$requests") - assert summary.contains("clientStatsErrors=$errors") - assert summary.contains("clientStatsDowngrades=$downgrades") - assert summary.contains("clientStatsP0DroppedSpans=$drops") - assert summary.contains("clientStatsP0DroppedTraces=$drops") - assert summary.contains("clientStatsProcessedSpans=$traces") - assert summary.contains("clientStatsProcessedTraces=$traces") - } -} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/OkHttpSinkTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/OkHttpSinkTest.groovy deleted file mode 100644 index a20fd424301..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/OkHttpSinkTest.groovy +++ /dev/null @@ -1,133 +0,0 @@ -package datadog.trace.common.metrics - -import datadog.trace.test.util.DDSpecification -import okhttp3.Call -import okhttp3.MediaType -import okhttp3.OkHttpClient -import okhttp3.Protocol -import okhttp3.Request -import okhttp3.Response -import okhttp3.ResponseBody - -import java.nio.ByteBuffer -import java.util.concurrent.CopyOnWriteArrayList -import java.util.concurrent.CountDownLatch -import java.util.concurrent.atomic.AtomicBoolean - -import static datadog.trace.common.metrics.EventListener.EventType.BAD_PAYLOAD -import static datadog.trace.common.metrics.EventListener.EventType.DOWNGRADED -import static datadog.trace.common.metrics.EventListener.EventType.ERROR -import static datadog.trace.common.metrics.EventListener.EventType.OK -import static datadog.communication.ddagent.DDAgentFeaturesDiscovery.V06_METRICS_ENDPOINT - -class OkHttpSinkTest extends DDSpecification { - - def "http status code #responseCode yields #eventType"() { - setup: - String agentUrl = "http://localhost:8126" - String path = V06_METRICS_ENDPOINT - EventListener listener = Mock(EventListener) - OkHttpClient client = Mock(OkHttpClient) - OkHttpSink sink = new OkHttpSink(client, agentUrl, path, true, false, Collections.emptyMap()) - sink.register(listener) - - when: - sink.accept(0, ByteBuffer.allocate(0)) - - then: - 1 * client.newCall(_) >> { Request request -> respond(request, responseCode) } - 1 * listener.onEvent(eventType, _) - - where: - eventType | responseCode - DOWNGRADED | 404 - ERROR | 500 - ERROR | 0 // throw - BAD_PAYLOAD | 400 - OK | 200 - OK | 201 - } - - def "degrade to async mode when agent slow to respond"() { - // metrics payloads are relatively large and we don't want to copy them, - // and we typically expect the agent to respond well within the aggregation - // window, so will send synchronously whenever possible to avoid allocating - // a copy of the payload. When the agent is slow to respond, we degrade to - // an asynchronous mode where up to 100 seconds of requests are copied and - // enqueued for sending in the background, because we don't want to lose - // them if it's possible not to. - setup: - String agentUrl = "http://localhost:8126" - String path = V06_METRICS_ENDPOINT - CountDownLatch latch = new CountDownLatch(2) - EventListener listener = new BlockingListener(latch) - OkHttpClient client = Mock(OkHttpClient) - OkHttpSink sink = new OkHttpSink(client, agentUrl, path, true, false, Collections.emptyMap()) - sink.register(listener) - AtomicBoolean first = new AtomicBoolean(true) - - when: "one slow response followed by a request" - sink.accept(1, ByteBuffer.allocate(0)) - sink.accept(1, ByteBuffer.allocate(0)) - latch.await() - then: "the second request degrades to async mode" - 2 * client.newCall(_) >> { Request request -> - if (first.compareAndSet(true, false)) { - Thread.sleep(1001) - } else { - assert sink.isInDegradedMode() - } - respond(request, 200) - } - listener.events.size() == 2 - for (EventListener.EventType eventType : listener.events) { - assert eventType == OK - } - long asyncRequests = sink.asyncRequestCount() - asyncRequests == 1 - sink.isInDegradedMode() - when: "the agent has recovered and has responded quickly once" - sink.accept(1, ByteBuffer.allocate(0)) - then: "the request was sent synchronously" - 1 * client.newCall(_) >> { Request request -> respond(request, 200) } - asyncRequests == sink.asyncRequestCount() - !sink.isInDegradedMode() - } - - def respond(Request request, int code) { - if (0 == code) { - return error(request) - } - return Mock(Call) { - it.execute() >> new Response.Builder() - .code(code) - .request(request) - .protocol(Protocol.HTTP_1_1) - .message("message") - .body(ResponseBody.create(MediaType.get("text/plain"), "message")) - .build() - } - } - - def error(Request request) { - return Mock(Call) { - it.execute() >> { throw new IOException("thrown by test") } - } - } - - class BlockingListener implements EventListener { - - private final CountDownLatch latch - private List events = new CopyOnWriteArrayList<>() - - BlockingListener(CountDownLatch latch) { - this.latch = latch - } - - @Override - void onEvent(EventType eventType, String message) { - events.add(eventType) - latch.countDown() - } - } -} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/SimpleSpan.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/SimpleSpan.groovy deleted file mode 100644 index 8cb37243790..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/SimpleSpan.groovy +++ /dev/null @@ -1,307 +0,0 @@ -package datadog.trace.common.metrics - -import datadog.trace.api.DDSpanId -import datadog.trace.api.DDTraceId -import datadog.trace.bootstrap.instrumentation.api.Tags -import datadog.trace.core.CoreSpan -import datadog.trace.core.DDSpanContext -import datadog.trace.core.MetadataConsumer -import datadog.trace.core.SpanKindFilter - -class SimpleSpan implements CoreSpan { - - private final String serviceName - private final String operationName - private final CharSequence resourceName - private final CharSequence serviceNameSource - private final String type - private final boolean measured - private final boolean topLevel - private final boolean traceRoot - private final boolean error - private final short statusCode - - private final long duration - private final long startTime - private final long longRunningVersion - - private final Map tags = [:] - - private byte spanKindOrdinal = 0 // SPAN_KIND_UNSET - - SimpleSpan( - String serviceName, - String operationName, - CharSequence resourceName, - String type, - boolean measured, - boolean topLevel, - boolean error, - long startTime, - long duration, - int statusCode, - boolean traceRoot = false, - int longRunningVersion = 0, - CharSequence serviceNameSource = null) { - this.serviceName = serviceName - this.operationName = operationName - this.resourceName = resourceName - this.serviceNameSource = serviceNameSource - this.type = type - this.measured = measured - this.topLevel = topLevel - this.traceRoot = traceRoot - this.error = error - this.startTime = startTime - this.duration = duration - this.statusCode = (short) statusCode - this.longRunningVersion = longRunningVersion - } - - @Override - SimpleSpan getLocalRootSpan() { - return this - } - - @Override - String getServiceName() { - return serviceName - } - - @Override - CharSequence getServiceNameSource() { - return serviceNameSource - } - - @Override - CharSequence getOperationName() { - return operationName - } - - @Override - CharSequence getResourceName() { - return resourceName - } - - @Override - DDTraceId getTraceId() { - return DDTraceId.ZERO - } - - @Override - long getSpanId() { - return DDSpanId.ZERO - } - - @Override - long getParentId() { - return traceRoot ? DDSpanId.ZERO : 1L - } - - @Override - long getStartTime() { - return startTime - } - - @Override - long getDurationNano() { - return duration - } - - @Override - int getError() { - return error ? 1 : 0 - } - - @Override - short getHttpStatusCode() { - return statusCode - } - - @Override - CharSequence getOrigin() { - return null - } - - @Override - SimpleSpan setMeasured(boolean measured) { - return this - } - - @Override - SimpleSpan setErrorMessage(String errorMessage) { - return this - } - - @Override - SimpleSpan addThrowable(Throwable error) { - return this - } - - @Override - SimpleSpan setTag(String tag, String value) { - return setTag(tag, (Object) value) - } - - @Override - SimpleSpan setTag(String tag, boolean value) { - return setTag(tag, (Object) value) - } - - @Override - SimpleSpan setTag(String tag, int value) { - return setTag(tag, (Object) value) - } - - @Override - SimpleSpan setTag(String tag, long value) { - return setTag(tag, (Object) value) - } - - @Override - SimpleSpan setTag(String tag, double value) { - return setTag(tag, (Object) value) - } - - @Override - SimpleSpan setTag(String tag, Number value) { - return setTag(tag, (Object) value) - } - - @Override - SimpleSpan setTag(String tag, CharSequence value) { - return setTag(tag, (Object) value) - } - - @Override - SimpleSpan setTag(String tag, Object value) { - tags.put(tag, value) - if (Tags.SPAN_KIND == tag) { - spanKindOrdinal = DDSpanContext.spanKindOrdinalOf(value == null ? null : value.toString()) - } - return this - } - - @Override - SimpleSpan removeTag(String tag) { - tags.remove(tag) - return this - } - - @Override - U getTag(CharSequence name, U defaultValue) { - def tagValue = tags.get(String.valueOf(name)) - return tagValue != null ? (U) tagValue : defaultValue - } - - @Override - U getTag(CharSequence name) { - return getTag(name, null) - } - - @Override - U unsafeGetTag(CharSequence name, U defaultValue) { - return getTag(name, defaultValue) - } - - @Override - U unsafeGetTag(CharSequence name) { - return getTag(name) - } - - @Override - boolean hasSamplingPriority() { - return false - } - - @Override - boolean isMeasured() { - return measured - } - - @Override - boolean isTopLevel() { - return topLevel - } - - @Override - boolean isForceKeep() { - return false - } - - @Override - boolean isKind(SpanKindFilter filter) { - return filter.matches(spanKindOrdinal) - } - - @Override - CharSequence getType() { - return type - } - - @Override - void processServiceTags() {} - - @Override - void processTagsAndBaggage(MetadataConsumer consumer) {} - - @Override - SimpleSpan setSamplingPriority(int samplingPriority, int samplingMechanism) { - return this - } - - @Override - SimpleSpan setSamplingPriority(int samplingPriority, CharSequence rate, double sampleRate, int samplingMechanism) { - return this - } - - @Override - SimpleSpan setSpanSamplingPriority(double rate, int limit) { - return this - } - - @Override - SimpleSpan setMetric(CharSequence name, int value) { - return this - } - - @Override - SimpleSpan setMetric(CharSequence name, long value) { - return this - } - - @Override - SimpleSpan setMetric(CharSequence name, float value) { - return this - } - - @Override - SimpleSpan setMetric(CharSequence name, double value) { - return this - } - - @Override - SimpleSpan setFlag(CharSequence name, boolean value) { - return this - } - - @Override - int samplingPriority() { - return 0 - } - - @Override - Map getMetaStruct() { - return [:] - } - - @Override - SimpleSpan setMetaStruct(String field, Object value) { - return this - } - - @Override - int getLongRunningVersion() { - return longRunningVersion - } -} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/AllSamplerTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/AllSamplerTest.groovy deleted file mode 100644 index 4345362f265..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/AllSamplerTest.groovy +++ /dev/null @@ -1,20 +0,0 @@ -package datadog.trace.common.sampling - -import datadog.trace.core.DDSpan -import datadog.trace.test.util.DDSpecification -import spock.lang.Subject - -class AllSamplerTest extends DDSpecification { - - @Subject - DDSpan span = Mock() - - private final AllSampler sampler = new AllSampler() - - def "test AllSampler"() { - expect: - for (int i = 0; i < 500; i++) { - assert sampler.sample(span) - } - } -} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/AsmStandaloneSamplerTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/AsmStandaloneSamplerTest.groovy deleted file mode 100644 index 7ec7579f77b..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/AsmStandaloneSamplerTest.groovy +++ /dev/null @@ -1,58 +0,0 @@ -package datadog.trace.common.sampling - -import datadog.trace.common.writer.ListWriter -import datadog.trace.core.test.DDCoreSpecification -import datadog.trace.api.sampling.PrioritySampling - -import java.time.Clock -import java.util.concurrent.atomic.AtomicLong - -class AsmStandaloneSamplerTest extends DDCoreSpecification{ - - def writer = new ListWriter() - - void "test setSamplingPriority"(){ - setup: - def current = new AtomicLong(System.currentTimeMillis()) - final Clock clock = Mock(Clock) { - millis() >> { - current.get() - } - } - def sampler = new AsmStandaloneSampler(clock) - def tracer = tracerBuilder().writer(writer).sampler(sampler).build() - - when: - def span1 = tracer.buildSpan("datadog", "test").start() - sampler.setSamplingPriority(span1) - - then: - 1 * clock.millis() >> { - current.updateAndGet(value -> value + 1000) - } // increment in one second - span1.getSamplingPriority() == PrioritySampling.SAMPLER_KEEP - - when: - def span2 = tracer.buildSpan("datadog", "test2").start() - sampler.setSamplingPriority(span2) - - then: - 1 * clock.millis() >> { - current.updateAndGet(value -> value + 1000) - } // increment in one second - span2.getSamplingPriority() == PrioritySampling.SAMPLER_DROP - - when: - def span3 = tracer.buildSpan("datadog", "test3").start() - sampler.setSamplingPriority(span3) - - then: "Mock one minute later" - clock.millis() >> { - current.updateAndGet(value -> value + 60000) - } // increment in one minute - span3.getSamplingPriority() == PrioritySampling.SAMPLER_KEEP - - cleanup: - tracer.close() - } -} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/DeterministicSpanSamplerTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/DeterministicSpanSamplerTest.groovy deleted file mode 100644 index b4a3632c068..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/DeterministicSpanSamplerTest.groovy +++ /dev/null @@ -1,362 +0,0 @@ -package datadog.trace.common.sampling - -import datadog.trace.core.DDSpan -import datadog.trace.test.util.DDSpecification - -class DeterministicSpanSamplerTest extends DDSpecification { - - def "test known values: #spanId"() { - given: - DeterministicSampler sampler = new DeterministicSampler.SpanSampler(0.5) - DDSpan span = Mock(DDSpan) { - getSpanId() >> spanId - } - - when: - def sampled = sampler.sample(span) - - then: - sampled == expected - - where: - expected | spanId - false | 10428415896243638596 - false | 11199607447739267382 - false | 11273630029763932141 - false | 11407674492757219439 - false | 11792151447964398879 - false | 12432680895096110463 - false | 13126262220165910460 - false | 13174268766980400525 - false | 15505210698284655633 - false | 15649472107743074779 - false | 17204678798284737396 - false | 17344948852394588913 - false | 17496662575514578077 - false | 18252401681137062077 - false | 18317291550776694829 - false | 1874068156324778273 - false | 1905388747193831650 - false | 2202916659517317514 - false | 2227583514184312746 - false | 2338498362660772719 - false | 2781055864473387780 - false | 3328451335138149956 - false | 3337066551442961397 - false | 3409814636252858217 - false | 3510942875414458836 - false | 3784560248718450071 - false | 4751997750760398084 - false | 4831389563158288344 - false | 4990765271833742716 - false | 5089134323978233018 - false | 5199948958991797301 - false | 5577006791947779410 - false | 5600924393587988459 - false | 5793183108815074904 - false | 6263450610539110790 - false | 6382800227808658932 - false | 6651414131918424343 - false | 6842348953158377901 - false | 6941261091797652072 - false | 7273596521315663110 - false | 7504504064263669287 - false | 788787457839692041 - false | 7955079406183515637 - false | 8549944162621642512 - false | 8603989663476771718 - false | 8807817071862113702 - false | 9010467728050264449 - true | 10667007354186551956 - true | 10683692646452562431 - true | 10821471013040158923 - true | 10950412492527322440 - true | 11239168150708129139 - true | 1169089424364679180 - true | 11818186001859264308 - true | 11833901312327420776 - true | 11926759511765359899 - true | 11926873763676642186 - true | 11963748953446345529 - true | 11998794077335055257 - true | 12096659438561119542 - true | 12156940908066221323 - true | 12947799971452915849 - true | 13260572831089785859 - true | 13771804148684671731 - true | 14117161486975057715 - true | 14242321332569825828 - true | 14486903973548550719 - true | 14967026985784794439 - true | 15213854965919594827 - true | 15352856648520921629 - true | 15399114114227588261 - true | 15595235597337683065 - true | 16194613440650274502 - true | 1687184559264975024 - true | 17490665426807838719 - true | 18218388313430417611 - true | 2601737961087659062 - true | 261049867304784443 - true | 2740103009342231109 - true | 2970700287221458280 - true | 3916589616287113937 - true | 4324745483838182873 - true | 4937104021912138218 - true | 5486140987150761883 - true | 5944830206637008055 - true | 6296367092202729479 - true | 6334824724549167320 - true | 6556961545928831643 - true | 6735196588112087610 - true | 7388428680384065704 - true | 8249030965139585917 - true | 837825985403119657 - true | 8505906760983331750 - true | 8674665223082153551 - true | 894385949183117216 - true | 898860202204764712 - true | 9768663798983814715 - true | 9828766684487745566 - true | 9908585559158765387 - true | 9956202364908137547 - } - - def "test sampling none: #spanId"() { - given: - DeterministicSampler sampler = new DeterministicSampler.SpanSampler(0) - DDSpan span = Mock(DDSpan) { - getSpanId() >> spanId.toLong() - } - - when: - def sampled = sampler.sample(span) - - then: - sampled == expected - - // These values are repeated from the "known values test" - // It is an arbitrary subset of all possible spanIds - where: - expected | spanId - false | 10428415896243638596 - false | 11199607447739267382 - false | 11273630029763932141 - false | 11407674492757219439 - false | 11792151447964398879 - false | 12432680895096110463 - false | 13126262220165910460 - false | 13174268766980400525 - false | 15505210698284655633 - false | 15649472107743074779 - false | 17204678798284737396 - false | 17344948852394588913 - false | 17496662575514578077 - false | 18252401681137062077 - false | 18317291550776694829 - false | 1874068156324778273 - false | 1905388747193831650 - false | 2202916659517317514 - false | 2227583514184312746 - false | 2338498362660772719 - false | 2781055864473387780 - false | 3328451335138149956 - false | 3337066551442961397 - false | 3409814636252858217 - false | 3510942875414458836 - false | 3784560248718450071 - false | 4751997750760398084 - false | 4831389563158288344 - false | 4990765271833742716 - false | 5089134323978233018 - false | 5199948958991797301 - false | 5577006791947779410 - false | 5600924393587988459 - false | 5793183108815074904 - false | 6263450610539110790 - false | 6382800227808658932 - false | 6651414131918424343 - false | 6842348953158377901 - false | 6941261091797652072 - false | 7273596521315663110 - false | 7504504064263669287 - false | 788787457839692041 - false | 7955079406183515637 - false | 8549944162621642512 - false | 8603989663476771718 - false | 8807817071862113702 - false | 9010467728050264449 - false | 10667007354186551956 - false | 10683692646452562431 - false | 10821471013040158923 - false | 10950412492527322440 - false | 11239168150708129139 - false | 1169089424364679180 - false | 11818186001859264308 - false | 11833901312327420776 - false | 11926759511765359899 - false | 11926873763676642186 - false | 11963748953446345529 - false | 11998794077335055257 - false | 12096659438561119542 - false | 12156940908066221323 - false | 12947799971452915849 - false | 13260572831089785859 - false | 13771804148684671731 - false | 14117161486975057715 - false | 14242321332569825828 - false | 14486903973548550719 - false | 14967026985784794439 - false | 15213854965919594827 - false | 15352856648520921629 - false | 15399114114227588261 - false | 15595235597337683065 - false | 16194613440650274502 - false | 1687184559264975024 - false | 17490665426807838719 - false | 18218388313430417611 - false | 2601737961087659062 - false | 261049867304784443 - false | 2740103009342231109 - false | 2970700287221458280 - false | 3916589616287113937 - false | 4324745483838182873 - false | 4937104021912138218 - false | 5486140987150761883 - false | 5944830206637008055 - false | 6296367092202729479 - false | 6334824724549167320 - false | 6556961545928831643 - false | 6735196588112087610 - false | 7388428680384065704 - false | 8249030965139585917 - false | 837825985403119657 - false | 8505906760983331750 - false | 8674665223082153551 - false | 894385949183117216 - false | 898860202204764712 - false | 9768663798983814715 - false | 9828766684487745566 - false | 9908585559158765387 - false | 9956202364908137547 - } - - def "test sampling all: #spanId"() { - given: - DeterministicSampler sampler = new DeterministicSampler.SpanSampler(1) - DDSpan span = Mock(DDSpan) { - getSpanId() >> spanId.toLong() - } - - when: - def sampled = sampler.sample(span) - - then: - sampled == expected - - // These values are repeated from the "known values test" - // It is an arbitrary subset of all possible spanIds - where: - expected | spanId - true | 10428415896243638596 - true | 11199607447739267382 - true | 11273630029763932141 - true | 11407674492757219439 - true | 11792151447964398879 - true | 12432680895096110463 - true | 13126262220165910460 - true | 13174268766980400525 - true | 15505210698284655633 - true | 15649472107743074779 - true | 17204678798284737396 - true | 17344948852394588913 - true | 17496662575514578077 - true | 18252401681137062077 - true | 18317291550776694829 - true | 1874068156324778273 - true | 1905388747193831650 - true | 2202916659517317514 - true | 2227583514184312746 - true | 2338498362660772719 - true | 2781055864473387780 - true | 3328451335138149956 - true | 3337066551442961397 - true | 3409814636252858217 - true | 3510942875414458836 - true | 3784560248718450071 - true | 4751997750760398084 - true | 4831389563158288344 - true | 4990765271833742716 - true | 5089134323978233018 - true | 5199948958991797301 - true | 5577006791947779410 - true | 5600924393587988459 - true | 5793183108815074904 - true | 6263450610539110790 - true | 6382800227808658932 - true | 6651414131918424343 - true | 6842348953158377901 - true | 6941261091797652072 - true | 7273596521315663110 - true | 7504504064263669287 - true | 788787457839692041 - true | 7955079406183515637 - true | 8549944162621642512 - true | 8603989663476771718 - true | 8807817071862113702 - true | 9010467728050264449 - true | 10667007354186551956 - true | 10683692646452562431 - true | 10821471013040158923 - true | 10950412492527322440 - true | 11239168150708129139 - true | 1169089424364679180 - true | 11818186001859264308 - true | 11833901312327420776 - true | 11926759511765359899 - true | 11926873763676642186 - true | 11963748953446345529 - true | 11998794077335055257 - true | 12096659438561119542 - true | 12156940908066221323 - true | 12947799971452915849 - true | 13260572831089785859 - true | 13771804148684671731 - true | 14117161486975057715 - true | 14242321332569825828 - true | 14486903973548550719 - true | 14967026985784794439 - true | 15213854965919594827 - true | 15352856648520921629 - true | 15399114114227588261 - true | 15595235597337683065 - true | 16194613440650274502 - true | 1687184559264975024 - true | 17490665426807838719 - true | 18218388313430417611 - true | 2601737961087659062 - true | 261049867304784443 - true | 2740103009342231109 - true | 2970700287221458280 - true | 3916589616287113937 - true | 4324745483838182873 - true | 4937104021912138218 - true | 5486140987150761883 - true | 5944830206637008055 - true | 6296367092202729479 - true | 6334824724549167320 - true | 6556961545928831643 - true | 6735196588112087610 - true | 7388428680384065704 - true | 8249030965139585917 - true | 837825985403119657 - true | 8505906760983331750 - true | 8674665223082153551 - true | 894385949183117216 - true | 898860202204764712 - true | 9768663798983814715 - true | 9828766684487745566 - true | 9908585559158765387 - true | 9956202364908137547 - } -} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/DeterministicTraceSamplerTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/DeterministicTraceSamplerTest.groovy deleted file mode 100644 index daa8240cd92..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/DeterministicTraceSamplerTest.groovy +++ /dev/null @@ -1,379 +0,0 @@ -package datadog.trace.common.sampling - -import datadog.trace.api.DDTraceId -import datadog.trace.core.DDSpan -import datadog.trace.test.util.DDSpecification - -class DeterministicTraceSamplerTest extends DDSpecification { - - def "test known values: #traceId"() { - given: - DeterministicSampler sampler = new DeterministicSampler.TraceSampler(0.5) - DDSpan span = Mock(DDSpan) { - getTraceId() >> DDTraceId.from(traceId) - } - - when: - def sampled = sampler.sample(span) - - then: - sampled == expected - - where: - expected | traceId - false | "10428415896243638596" - false | "11199607447739267382" - false | "11273630029763932141" - false | "11407674492757219439" - false | "11792151447964398879" - false | "12432680895096110463" - false | "13126262220165910460" - false | "13174268766980400525" - false | "15505210698284655633" - false | "15649472107743074779" - false | "17204678798284737396" - false | "17344948852394588913" - false | "17496662575514578077" - false | "18252401681137062077" - false | "18317291550776694829" - false | "1874068156324778273" - false | "1905388747193831650" - false | "2202916659517317514" - false | "2227583514184312746" - false | "2338498362660772719" - false | "2781055864473387780" - false | "3328451335138149956" - false | "3337066551442961397" - false | "3409814636252858217" - false | "3510942875414458836" - false | "3784560248718450071" - false | "4751997750760398084" - false | "4831389563158288344" - false | "4990765271833742716" - false | "5089134323978233018" - false | "5199948958991797301" - false | "5577006791947779410" - false | "5600924393587988459" - false | "5793183108815074904" - false | "6263450610539110790" - false | "6382800227808658932" - false | "6651414131918424343" - false | "6842348953158377901" - false | "6941261091797652072" - false | "7273596521315663110" - false | "7504504064263669287" - false | "788787457839692041" - false | "7955079406183515637" - false | "8549944162621642512" - false | "8603989663476771718" - false | "8807817071862113702" - false | "9010467728050264449" - true | "10667007354186551956" - true | "10683692646452562431" - true | "10821471013040158923" - true | "10950412492527322440" - true | "11239168150708129139" - true | "1169089424364679180" - true | "11818186001859264308" - true | "11833901312327420776" - true | "11926759511765359899" - true | "11926873763676642186" - true | "11963748953446345529" - true | "11998794077335055257" - true | "12096659438561119542" - true | "12156940908066221323" - true | "12947799971452915849" - true | "13260572831089785859" - true | "13771804148684671731" - true | "14117161486975057715" - true | "14242321332569825828" - true | "14486903973548550719" - true | "14967026985784794439" - true | "15213854965919594827" - true | "15352856648520921629" - true | "15399114114227588261" - true | "15595235597337683065" - true | "16194613440650274502" - true | "1687184559264975024" - true | "17490665426807838719" - true | "18218388313430417611" - true | "2601737961087659062" - true | "261049867304784443" - true | "2740103009342231109" - true | "2970700287221458280" - true | "3916589616287113937" - true | "4324745483838182873" - true | "4937104021912138218" - true | "5486140987150761883" - true | "5944830206637008055" - true | "6296367092202729479" - true | "6334824724549167320" - true | "6556961545928831643" - true | "6735196588112087610" - true | "7388428680384065704" - true | "8249030965139585917" - true | "837825985403119657" - true | "8505906760983331750" - true | "8674665223082153551" - true | "894385949183117216" - true | "898860202204764712" - true | "9768663798983814715" - true | "9828766684487745566" - true | "9908585559158765387" - true | "9956202364908137547" - true | "9223372036854775808" - } - - def "test sampling none: #traceId"() { - given: - DeterministicSampler sampler = new DeterministicSampler.TraceSampler(0) - DDSpan span = Mock(DDSpan) { - getTraceId() >> DDTraceId.from(traceId) - } - - when: - def sampled = sampler.sample(span) - - then: - sampled == expected - - // These values are repeated from the "known values test" - // It is an arbitrary subset of all possible traceIds - where: - expected | traceId - false | "10428415896243638596" - false | "11199607447739267382" - false | "11273630029763932141" - false | "11407674492757219439" - false | "11792151447964398879" - false | "12432680895096110463" - false | "13126262220165910460" - false | "13174268766980400525" - false | "15505210698284655633" - false | "15649472107743074779" - false | "17204678798284737396" - false | "17344948852394588913" - false | "17496662575514578077" - false | "18252401681137062077" - false | "18317291550776694829" - false | "1874068156324778273" - false | "1905388747193831650" - false | "2202916659517317514" - false | "2227583514184312746" - false | "2338498362660772719" - false | "2781055864473387780" - false | "3328451335138149956" - false | "3337066551442961397" - false | "3409814636252858217" - false | "3510942875414458836" - false | "3784560248718450071" - false | "4751997750760398084" - false | "4831389563158288344" - false | "4990765271833742716" - false | "5089134323978233018" - false | "5199948958991797301" - false | "5577006791947779410" - false | "5600924393587988459" - false | "5793183108815074904" - false | "6263450610539110790" - false | "6382800227808658932" - false | "6651414131918424343" - false | "6842348953158377901" - false | "6941261091797652072" - false | "7273596521315663110" - false | "7504504064263669287" - false | "788787457839692041" - false | "7955079406183515637" - false | "8549944162621642512" - false | "8603989663476771718" - false | "8807817071862113702" - false | "9010467728050264449" - false | "10667007354186551956" - false | "10683692646452562431" - false | "10821471013040158923" - false | "10950412492527322440" - false | "11239168150708129139" - false | "1169089424364679180" - false | "11818186001859264308" - false | "11833901312327420776" - false | "11926759511765359899" - false | "11926873763676642186" - false | "11963748953446345529" - false | "11998794077335055257" - false | "12096659438561119542" - false | "12156940908066221323" - false | "12947799971452915849" - false | "13260572831089785859" - false | "13771804148684671731" - false | "14117161486975057715" - false | "14242321332569825828" - false | "14486903973548550719" - false | "14967026985784794439" - false | "15213854965919594827" - false | "15352856648520921629" - false | "15399114114227588261" - false | "15595235597337683065" - false | "16194613440650274502" - false | "1687184559264975024" - false | "17490665426807838719" - false | "18218388313430417611" - false | "2601737961087659062" - false | "261049867304784443" - false | "2740103009342231109" - false | "2970700287221458280" - false | "3916589616287113937" - false | "4324745483838182873" - false | "4937104021912138218" - false | "5486140987150761883" - false | "5944830206637008055" - false | "6296367092202729479" - false | "6334824724549167320" - false | "6556961545928831643" - false | "6735196588112087610" - false | "7388428680384065704" - false | "8249030965139585917" - false | "837825985403119657" - false | "8505906760983331750" - false | "8674665223082153551" - false | "894385949183117216" - false | "898860202204764712" - false | "9768663798983814715" - false | "9828766684487745566" - false | "9908585559158765387" - false | "9956202364908137547" - } - - def "test sampling all: #traceId"() { - given: - DeterministicSampler sampler = new DeterministicSampler.TraceSampler(1) - DDSpan span = Mock(DDSpan) { - getTraceId() >> DDTraceId.from(traceId) - } - - when: - def sampled = sampler.sample(span) - - then: - sampled == expected - - // These values are repeated from the "known values test" - // It is an arbitrary subset of all possible traceIds - where: - expected | traceId - true | "10428415896243638596" - true | "11199607447739267382" - true | "11273630029763932141" - true | "11407674492757219439" - true | "11792151447964398879" - true | "12432680895096110463" - true | "13126262220165910460" - true | "13174268766980400525" - true | "15505210698284655633" - true | "15649472107743074779" - true | "17204678798284737396" - true | "17344948852394588913" - true | "17496662575514578077" - true | "18252401681137062077" - true | "18317291550776694829" - true | "1874068156324778273" - true | "1905388747193831650" - true | "2202916659517317514" - true | "2227583514184312746" - true | "2338498362660772719" - true | "2781055864473387780" - true | "3328451335138149956" - true | "3337066551442961397" - true | "3409814636252858217" - true | "3510942875414458836" - true | "3784560248718450071" - true | "4751997750760398084" - true | "4831389563158288344" - true | "4990765271833742716" - true | "5089134323978233018" - true | "5199948958991797301" - true | "5577006791947779410" - true | "5600924393587988459" - true | "5793183108815074904" - true | "6263450610539110790" - true | "6382800227808658932" - true | "6651414131918424343" - true | "6842348953158377901" - true | "6941261091797652072" - true | "7273596521315663110" - true | "7504504064263669287" - true | "788787457839692041" - true | "7955079406183515637" - true | "8549944162621642512" - true | "8603989663476771718" - true | "8807817071862113702" - true | "9010467728050264449" - true | "10667007354186551956" - true | "10683692646452562431" - true | "10821471013040158923" - true | "10950412492527322440" - true | "11239168150708129139" - true | "1169089424364679180" - true | "11818186001859264308" - true | "11833901312327420776" - true | "11926759511765359899" - true | "11926873763676642186" - true | "11963748953446345529" - true | "11998794077335055257" - true | "12096659438561119542" - true | "12156940908066221323" - true | "12947799971452915849" - true | "13260572831089785859" - true | "13771804148684671731" - true | "14117161486975057715" - true | "14242321332569825828" - true | "14486903973548550719" - true | "14967026985784794439" - true | "15213854965919594827" - true | "15352856648520921629" - true | "15399114114227588261" - true | "15595235597337683065" - true | "16194613440650274502" - true | "1687184559264975024" - true | "17490665426807838719" - true | "18218388313430417611" - true | "2601737961087659062" - true | "261049867304784443" - true | "2740103009342231109" - true | "2970700287221458280" - true | "3916589616287113937" - true | "4324745483838182873" - true | "4937104021912138218" - true | "5486140987150761883" - true | "5944830206637008055" - true | "6296367092202729479" - true | "6334824724549167320" - true | "6556961545928831643" - true | "6735196588112087610" - true | "7388428680384065704" - true | "8249030965139585917" - true | "837825985403119657" - true | "8505906760983331750" - true | "8674665223082153551" - true | "894385949183117216" - true | "898860202204764712" - true | "9768663798983814715" - true | "9828766684487745566" - true | "9908585559158765387" - true | "9956202364908137547" - } - - static final BigDecimal CUTOFF_FACTOR = new BigDecimal(BigInteger.valueOf(2).pow(64).subtract(BigInteger.ONE)) - - def "test cutoff calculation"() { - when: - long cutoff = DeterministicSampler.cutoff(rate / 100F) - then: - Math.abs(cutoff - new BigDecimal(rate / 100D) - .multiply(CUTOFF_FACTOR) - .toBigInteger() - .longValue() + Long.MIN_VALUE) <= 1 - - where: - rate << (0..100) - } -} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/ForcePrioritySamplerTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/ForcePrioritySamplerTest.groovy deleted file mode 100644 index c2111c7d36f..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/ForcePrioritySamplerTest.groovy +++ /dev/null @@ -1,116 +0,0 @@ -package datadog.trace.common.sampling - -import datadog.trace.api.DDTags -import datadog.trace.common.writer.ListWriter -import datadog.trace.common.writer.LoggingWriter -import datadog.trace.core.test.DDCoreSpecification -import static datadog.trace.api.sampling.PrioritySampling.* -import static datadog.trace.api.sampling.SamplingMechanism.* - -class ForcePrioritySamplerTest extends DDCoreSpecification { - - def writer = new ListWriter() - - def "force priority sampling"() { - setup: - def sampler = new ForcePrioritySampler(prioritySampling, samplingMechanism) - def tracer = tracerBuilder().writer(writer).sampler(sampler).build() - - when: - def span1 = tracer.buildSpan("datadog", "test").start() - sampler.setSamplingPriority(span1) - - then: - span1.getSamplingPriority() == expectedSampling - sampler.sample(span1) - - cleanup: - tracer.close() - - where: - prioritySampling | samplingMechanism | expectedSampling - SAMPLER_KEEP | DEFAULT | SAMPLER_KEEP - SAMPLER_DROP | DEFAULT | SAMPLER_DROP - SAMPLER_KEEP | AGENT_RATE | SAMPLER_KEEP - SAMPLER_DROP | AGENT_RATE | SAMPLER_DROP - SAMPLER_KEEP | REMOTE_AUTO_RATE | SAMPLER_KEEP - SAMPLER_DROP | REMOTE_AUTO_RATE | SAMPLER_DROP - } - - def "sampling priority set"() { - setup: - def sampler = new ForcePrioritySampler(prioritySampling, samplingMechanism) - def tracer = tracerBuilder().writer(writer).sampler(sampler).build() - - when: - def span = tracer.buildSpan("datadog", "test").start() - - then: - span.getSamplingPriority() == null - - when: - span.setTag(DDTags.SERVICE_NAME, "spock") - - then: - span.finish() - writer.waitForTraces(1) - span.getSamplingPriority() == expectedSampling - - cleanup: - tracer.close() - - where: - prioritySampling | samplingMechanism | expectedSampling - SAMPLER_KEEP | DEFAULT | SAMPLER_KEEP - SAMPLER_DROP | DEFAULT | SAMPLER_DROP - } - - def "setting forced tracing via tag"() { - when: - def sampler = new ForcePrioritySampler(SAMPLER_KEEP, DEFAULT) - def tracer = tracerBuilder().writer(new LoggingWriter()).sampler(sampler).build() - def span = tracer.buildSpan("datadog", "root").start() - if (tagName) { - span.setTag(tagName, tagValue) - } - span.finish() - - then: - span.getSamplingPriority() == expectedPriority - - cleanup: - tracer.close() - - where: - tagName | tagValue | expectedPriority - 'manual.drop' | true | USER_DROP - 'manual.keep' | true | USER_KEEP - } - - def "not setting forced tracing via tag or setting it wrong value not causing exception"() { - setup: - def sampler = new ForcePrioritySampler(SAMPLER_KEEP, DEFAULT) - def tracer = tracerBuilder().writer(new LoggingWriter()).sampler(sampler).build() - def span = tracer.buildSpan("datadog", "root").start() - if (tagName) { - span.setTag(tagName, tagValue) - } - - expect: - span.getSamplingPriority() == null - - cleanup: - span.finish() - tracer.close() - - where: - tagName | tagValue - // When no tag is set default to - null | null - // Setting to not known value - 'manual.drop' | false - 'manual.keep' | false - 'manual.drop' | 1 - 'manual.keep' | 1 - } -} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/RateByServiceTraceSamplerTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/RateByServiceTraceSamplerTest.groovy deleted file mode 100644 index 1bfb099670a..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/RateByServiceTraceSamplerTest.groovy +++ /dev/null @@ -1,417 +0,0 @@ -package datadog.trace.common.sampling - -import datadog.trace.api.DDTags -import datadog.trace.api.sampling.PrioritySampling -import datadog.trace.api.time.ControllableTimeSource -import datadog.trace.common.writer.ListWriter -import datadog.trace.common.writer.LoggingWriter -import datadog.trace.common.writer.ddagent.DDAgentApi -import datadog.trace.core.DDSpan -import datadog.trace.core.DDSpanContext -import datadog.trace.core.test.DDCoreSpecification - -class RateByServiceTraceSamplerTest extends DDCoreSpecification { - static serializer = DDAgentApi.RESPONSE_ADAPTER - - def "invalid rate -> 1"() { - setup: - RateByServiceTraceSampler serviceSampler = new RateByServiceTraceSampler() - String response = '{"rate_by_service": {"service:,env:":' + rate + '}}' - serviceSampler.onResponse("traces", serializer.fromJson(response)) - - expect: - serviceSampler.serviceRates.getSampler(RateByServiceTraceSampler.EnvAndService.FALLBACK).sampleRate == expectedRate - serviceSampler.serviceRates.getSampler("not", "found").sampleRate == expectedRate - - where: - // these values are all precisely represented in floating point - rate | expectedRate - null | 1 - 1 | 1 - 0 | 0.0 - -5 | 1 - 5 | 1 - 0.5 | 0.5 - } - - def "rate selection"() { - setup: - RateByServiceTraceSampler serviceSampler = new RateByServiceTraceSampler() - String response = '{"rate_by_service": {"service:foo,env:bar":0.8, "service:,env:":0.20}}' - serviceSampler.onResponse("traces", serializer.fromJson(response)) - - when: - def sampler = serviceSampler.serviceRates.getSampler(env, service) - - then: - sampler.sampleRate > expectedRate - 0.01 - sampler.sampleRate < expectedRate + 0.01 - - where: - service | env | expectedRate - "foo" | "bar" | 0.8 - "Foo" | "BAR" | 0.8 - "FOO" | "BAR" | 0.8 - "not" | "found" | 0.2 - "foo" | "baz" | 0.2 - "fu" | "bar" | 0.2 - } - - def "rate partial & full collisions"() { - // case insensitive equivalence -- undefined behavior, first one wins - setup: - RateByServiceTraceSampler serviceSampler = new RateByServiceTraceSampler() - String response = '{"rate_by_service": {"service:foo,env:bar":0.8, "service:FOO,env:BAR":0.2, "service:FOO,env:BAZ": 0.3, "service:quux,env:BAZ": 0.4}}' - serviceSampler.onResponse("traces", serializer.fromJson(response)) - - when: - def sampler = serviceSampler.serviceRates.getSampler(env, service) - - then: - sampler.sampleRate > expectedRate - 0.01 - sampler.sampleRate < expectedRate + 0.01 - - where: - service | env | expectedRate - "foo" | "bar" | 0.8 - "foo" | "Bar" | 0.8 - "Foo" | "BAR" | 0.8 - "FOO" | "BAR" | 0.8 - "foo" | "baz" | 0.3 - "FOO" | "BAZ" | 0.3 - "quux" | "baz" | 0.4 - } - - def "rate by service name"() { - setup: - RateByServiceTraceSampler serviceSampler = new RateByServiceTraceSampler() - def tracer = tracerBuilder().writer(new ListWriter()).build() - - when: - String response = '{"rate_by_service": {"service:spock,env:test":0.0}}' - serviceSampler.onResponse("traces", serializer.fromJson(response)) - DDSpan span1 = tracer.buildSpan("datadog", "fakeOperation") - .withServiceName("foo") - .withTag("env", "bar") - .ignoreActiveSpan().start() - - serviceSampler.setSamplingPriority(span1) - - then: - span1.getSamplingPriority() == PrioritySampling.SAMPLER_KEEP - serviceSampler.sample(span1) - - when: - // case-insensitive equivalence - undefined in spec, but implemented as first one wins - response = '{"rate_by_service": {"service:spock,env:test":1.0, "service:SPOCK,env:Test": 0.0}}' - serviceSampler.onResponse("traces", serializer.fromJson(response)) - - DDSpan span2 = tracer.buildSpan("datadog", "fakeOperation") - .withServiceName("spock") - .withTag("env", "test") - .ignoreActiveSpan().start() - serviceSampler.setSamplingPriority(span2) - - then: - span2.getSamplingPriority() == PrioritySampling.SAMPLER_KEEP - serviceSampler.sample(span2) - - cleanup: - tracer.close() - } - - def "rate by service name - case-insensitive"() { - setup: - RateByServiceTraceSampler serviceSampler = new RateByServiceTraceSampler() - def tracer = tracerBuilder().writer(new ListWriter()).build() - - when: - def response = '{"rate_by_service": {"service:spock,env:test":1.0}}' - serviceSampler.onResponse("traces", serializer.fromJson(response)) - - DDSpan span = tracer.buildSpan("datadog", "fakeOperation") - .withServiceName("SPOCK") - .withTag("env", "Test") - .ignoreActiveSpan().start() - serviceSampler.setSamplingPriority(span) - - then: - span.getSamplingPriority() == PrioritySampling.SAMPLER_KEEP - serviceSampler.sample(span) - - cleanup: - tracer.close() - } - - def "sampling priority set on context"() { - setup: - RateByServiceTraceSampler serviceSampler = new RateByServiceTraceSampler() - def tracer = tracerBuilder().writer(new ListWriter()).build() - String response = '{"rate_by_service": {"service:,env:":1.0}}' - serviceSampler.onResponse("traces", serializer.fromJson(response)) - - when: - DDSpan span = tracer.buildSpan("datadog", "fakeOperation") - .withServiceName("spock") - .withTag("env", "test") - .ignoreActiveSpan().start() - serviceSampler.setSamplingPriority(span) - - then: - // sets correctly on root span - span.getSamplingPriority() == PrioritySampling.SAMPLER_KEEP - // RateByServiceSampler must not set the sample rate - span.getTag(DDSpanContext.SAMPLE_RATE_KEY) == null - - cleanup: - tracer.close() - } - - def "sampling priority set when service later"() { - def sampler = new RateByServiceTraceSampler() - def writer = new ListWriter() - def tracer = tracerBuilder().writer(writer).sampler(sampler).build() - - sampler.onResponse("test", serializer - .fromJson('{"rate_by_service":{"service:,env:":1.0,"service:spock,env:":0.0}}')) - - when: - def span = tracer.buildSpan("datadog", "test").start() - - then: - span.getSamplingPriority() == null - - when: - span.setTag(DDTags.SERVICE_NAME, "spock") - - then: - span.finish() - writer.waitForTraces(1) - span.getSamplingPriority() == PrioritySampling.SAMPLER_DROP - - when: - span = tracer.buildSpan("datadog", "test").withTag(DDTags.SERVICE_NAME, "spock").start() - span.finish() - writer.waitForTraces(2) - - then: - span.getSamplingPriority() == PrioritySampling.SAMPLER_DROP - - cleanup: - tracer.close() - } - - def "setting forced tracing via tag"() { - when: - def sampler = new RateByServiceTraceSampler() - def tracer = tracerBuilder().writer(new LoggingWriter()).sampler(sampler).build() - def span = tracer.buildSpan("datadog", "root").start() - if (tagName) { - span.setTag(tagName, tagValue) - } - span.finish() - - then: - span.getSamplingPriority() == expectedPriority - - cleanup: - tracer.close() - - where: - tagName | tagValue | expectedPriority - 'manual.drop' | true | PrioritySampling.USER_DROP - 'manual.keep' | true | PrioritySampling.USER_KEEP - } - - def "shouldCap returns false when rate decreases or stays same"() { - expect: - !RateByServiceTraceSampler.shouldCap(0.8, 0.4) - !RateByServiceTraceSampler.shouldCap(0.5, 0.5) - !RateByServiceTraceSampler.shouldCap(0.5, 1.0) // 1.0 <= 0.5 * 2, no cap needed - } - - def "shouldCap returns false when old rate is zero"() { - expect: - !RateByServiceTraceSampler.shouldCap(0.0, 0.5) - !RateByServiceTraceSampler.shouldCap(0.0, 1.0) - } - - def "shouldCap returns true when new rate exceeds 2x old rate"() { - expect: - RateByServiceTraceSampler.shouldCap(0.1, 1.0) - RateByServiceTraceSampler.shouldCap(0.2, 0.8) - RateByServiceTraceSampler.shouldCap(0.1, 0.3) - } - - def "cappedRate returns 2x old rate"() { - expect: - RateByServiceTraceSampler.cappedRate(0.1) == 0.2 - RateByServiceTraceSampler.cappedRate(0.2) == 0.4 - RateByServiceTraceSampler.cappedRate(0.4) == 0.8 - } - - def "ramp-up caps rate increases at 2x per interval"() { - setup: - def time = new ControllableTimeSource() - time.set(1_000_000_000L) - RateByServiceTraceSampler serviceSampler = new RateByServiceTraceSampler(time) - def tolerance = 0.01 - - // Set initial rate to 0.1 - String response = '{"rate_by_service": {"service:foo,env:bar":0.1, "service:,env:":0.1}}' - serviceSampler.onResponse("traces", serializer.fromJson(response)) - - expect: - Math.abs(serviceSampler.serviceRates.getSampler("bar", "foo").sampleRate - 0.1) < tolerance - - when: "agent restart sends rate 1.0, first interval" - time.advance(RateByServiceTraceSampler.RAMP_UP_INTERVAL_NANOS) - response = '{"rate_by_service": {"service:foo,env:bar":1.0, "service:,env:":1.0}}' - serviceSampler.onResponse("traces", serializer.fromJson(response)) - - then: "rate is capped at 2x = 0.2" - Math.abs(serviceSampler.serviceRates.getSampler("bar", "foo").sampleRate - 0.2) < tolerance - Math.abs(serviceSampler.serviceRates.getFallbackSampler().sampleRate - 0.2) < tolerance - - when: "second interval" - time.advance(RateByServiceTraceSampler.RAMP_UP_INTERVAL_NANOS) - serviceSampler.onResponse("traces", serializer.fromJson(response)) - - then: "rate doubles to 0.4" - Math.abs(serviceSampler.serviceRates.getSampler("bar", "foo").sampleRate - 0.4) < tolerance - Math.abs(serviceSampler.serviceRates.getFallbackSampler().sampleRate - 0.4) < tolerance - - when: "third interval" - time.advance(RateByServiceTraceSampler.RAMP_UP_INTERVAL_NANOS) - serviceSampler.onResponse("traces", serializer.fromJson(response)) - - then: "rate doubles to 0.8" - Math.abs(serviceSampler.serviceRates.getSampler("bar", "foo").sampleRate - 0.8) < tolerance - Math.abs(serviceSampler.serviceRates.getFallbackSampler().sampleRate - 0.8) < tolerance - - when: "fourth interval" - time.advance(RateByServiceTraceSampler.RAMP_UP_INTERVAL_NANOS) - serviceSampler.onResponse("traces", serializer.fromJson(response)) - - then: "rate reaches target 1.0 (2x=1.6 > 1.0)" - Math.abs(serviceSampler.serviceRates.getSampler("bar", "foo").sampleRate - 1.0) < tolerance - Math.abs(serviceSampler.serviceRates.getFallbackSampler().sampleRate - 1.0) < tolerance - } - - def "ramp-down applies immediately"() { - setup: - def time = new ControllableTimeSource() - time.set(1_000_000_000L) - RateByServiceTraceSampler serviceSampler = new RateByServiceTraceSampler(time) - def tolerance = 0.01 - - // Set initial rate to 0.8 - String response = '{"rate_by_service": {"service:foo,env:bar":0.8, "service:,env:":0.8}}' - serviceSampler.onResponse("traces", serializer.fromJson(response)) - - when: "rate decreases to 0.2" - response = '{"rate_by_service": {"service:foo,env:bar":0.2, "service:,env:":0.2}}' - serviceSampler.onResponse("traces", serializer.fromJson(response)) - - then: "decrease is applied immediately" - Math.abs(serviceSampler.serviceRates.getSampler("bar", "foo").sampleRate - 0.2) < tolerance - Math.abs(serviceSampler.serviceRates.getFallbackSampler().sampleRate - 0.2) < tolerance - } - - def "rate increase blocked during cooldown"() { - setup: - def time = new ControllableTimeSource() - time.set(1_000_000_000L) - RateByServiceTraceSampler serviceSampler = new RateByServiceTraceSampler(time) - def tolerance = 0.01 - - // Set initial rate to 0.1 - String response = '{"rate_by_service": {"service:foo,env:bar":0.1}}' - serviceSampler.onResponse("traces", serializer.fromJson(response)) - - when: "rate jumps, first capped increase" - time.advance(RateByServiceTraceSampler.RAMP_UP_INTERVAL_NANOS) - response = '{"rate_by_service": {"service:foo,env:bar":1.0}}' - serviceSampler.onResponse("traces", serializer.fromJson(response)) - - then: "capped to 0.2" - Math.abs(serviceSampler.serviceRates.getSampler("bar", "foo").sampleRate - 0.2) < tolerance - - when: "try again immediately (within cooldown)" - serviceSampler.onResponse("traces", serializer.fromJson(response)) - - then: "rate stays at 0.2 because cooldown hasn't elapsed" - Math.abs(serviceSampler.serviceRates.getSampler("bar", "foo").sampleRate - 0.2) < tolerance - - when: "after cooldown elapsed" - time.advance(RateByServiceTraceSampler.RAMP_UP_INTERVAL_NANOS) - serviceSampler.onResponse("traces", serializer.fromJson(response)) - - then: "rate doubles to 0.4" - Math.abs(serviceSampler.serviceRates.getSampler("bar", "foo").sampleRate - 0.4) < tolerance - } - - def "cooldown not reset by blocked increase"() { - setup: - def time = new ControllableTimeSource() - time.set(1_000_000_000L) - RateByServiceTraceSampler serviceSampler = new RateByServiceTraceSampler(time) - def tolerance = 0.01 - - // Set initial low rate - String response = '{"rate_by_service": {"service:foo,env:bar":0.01}}' - serviceSampler.onResponse("traces", serializer.fromJson(response)) - - expect: - Math.abs(serviceSampler.serviceRates.getSampler("bar", "foo").sampleRate - 0.01) < tolerance - - when: "wait for cooldown, apply increase: 0.01 -> 0.02" - time.advance(RateByServiceTraceSampler.RAMP_UP_INTERVAL_NANOS) - response = '{"rate_by_service": {"service:foo,env:bar":1.0}}' - serviceSampler.onResponse("traces", serializer.fromJson(response)) - - then: "rate is capped at 2x = 0.02" - Math.abs(serviceSampler.serviceRates.getSampler("bar", "foo").sampleRate - 0.02) < tolerance - - when: "before cooldown elapses, send another increase - rate should be held and lastCapped NOT reset" - time.advance((long) (RateByServiceTraceSampler.RAMP_UP_INTERVAL_NANOS / 2)) - serviceSampler.onResponse("traces", serializer.fromJson(response)) - - then: "rate stays at 0.02 (cooldown)" - Math.abs(serviceSampler.serviceRates.getSampler("bar", "foo").sampleRate - 0.02) < tolerance - - when: "wait remaining half of cooldown from the original cap - should allow next ramp-up" - time.advance((long) (RateByServiceTraceSampler.RAMP_UP_INTERVAL_NANOS / 2)) - serviceSampler.onResponse("traces", serializer.fromJson(response)) - - then: "rate doubles to 0.04 because lastCapped was NOT reset by the blocked increase" - Math.abs(serviceSampler.serviceRates.getSampler("bar", "foo").sampleRate - 0.04) < tolerance - } - - def "not setting forced tracing via tag or setting it wrong value not causing exception"() { - setup: - def sampler = new RateByServiceTraceSampler() - def tracer = tracerBuilder().writer(new LoggingWriter()).sampler(sampler).build() - def span = tracer.buildSpan("datadog", "root").start() - if (tagName) { - span.setTag(tagName, tagValue) - } - - expect: - span.getSamplingPriority() == null - - cleanup: - span.finish() - tracer.close() - - where: - tagName | tagValue - // When no tag is set default to - null | null - // Setting to not known value - 'manual.drop' | false - 'manual.keep' | false - 'manual.drop' | 1 - 'manual.keep' | 1 - } -} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/RuleBasedSamplingTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/RuleBasedSamplingTest.groovy deleted file mode 100644 index 8d4416d6bf7..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/RuleBasedSamplingTest.groovy +++ /dev/null @@ -1,496 +0,0 @@ -package datadog.trace.common.sampling - -import datadog.trace.common.writer.ListWriter -import datadog.trace.core.DDSpan -import datadog.trace.core.test.DDCoreSpecification - -import static datadog.trace.api.config.TracerConfig.TRACE_RATE_LIMIT -import static datadog.trace.api.config.TracerConfig.TRACE_SAMPLE_RATE -import static datadog.trace.api.config.TracerConfig.TRACE_SAMPLING_RULES -import static datadog.trace.api.config.TracerConfig.TRACE_SAMPLING_OPERATION_RULES -import static datadog.trace.api.config.TracerConfig.TRACE_SAMPLING_SERVICE_RULES -import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_KEEP -import static datadog.trace.api.sampling.PrioritySampling.USER_DROP -import static datadog.trace.api.sampling.PrioritySampling.USER_KEEP - -import static datadog.trace.api.sampling.SamplingMechanism.AGENT_RATE -import static datadog.trace.api.sampling.SamplingMechanism.LOCAL_USER_RULE - - -class RuleBasedSamplingTest extends DDCoreSpecification { - def "Rule Based Sampler is not created when properties not set"() { - when: - Sampler sampler = Sampler.Builder.forConfig(new Properties()) - - then: - !(sampler instanceof RuleBasedTraceSampler) - } - - def "Rule Based Sampler is not created when just rate limit set"() { - when: - Properties properties = new Properties() - properties.setProperty(TRACE_RATE_LIMIT, "50") - Sampler sampler = Sampler.Builder.forConfig(properties) - - then: - !(sampler instanceof RuleBasedTraceSampler) - } - - def "sampling config combinations"() { - given: - Properties properties = new Properties() - if (serviceRules != null) { - properties.setProperty(TRACE_SAMPLING_SERVICE_RULES, serviceRules) - } - - if (operationRules != null) { - properties.setProperty(TRACE_SAMPLING_OPERATION_RULES, operationRules) - } - - if (defaultRate != null) { - properties.setProperty(TRACE_SAMPLE_RATE, defaultRate) - } - - properties.setProperty(TRACE_RATE_LIMIT, "50") - def tracer = tracerBuilder().writer(new ListWriter()).build() - - when: - Sampler sampler = Sampler.Builder.forConfig(properties) - - then: - sampler instanceof PrioritySampler - - when: - DDSpan span = tracer.buildSpan("datadog", "operation") - .withServiceName("service") - .withTag("env", "bar") - .ignoreActiveSpan().start() - ((PrioritySampler) sampler).setSamplingPriority(span) - - def propagationMap = span.context.propagationTags.createTagMap() - def decisionMaker = propagationMap.get('_dd.p.dm') - - def expectedDmStr = (expectedDecisionMaker == null) ? null : "-" + expectedDecisionMaker - - then: - span.getTag(RuleBasedTraceSampler.SAMPLING_RULE_RATE) == expectedRuleRate - span.getTag(RuleBasedTraceSampler.SAMPLING_LIMIT_RATE) == expectedRateLimit - span.getTag(RateByServiceTraceSampler.SAMPLING_AGENT_RATE) == expectedAgentRate - span.getSamplingPriority() == expectedPriority - decisionMaker == expectedDmStr - - cleanup: - tracer.close() - - where: - // NOTE: Using a defaultRate is still considered to be a "rule" - // decisionMaker is LOCAL_USER_RULE and expectedRuleRate is non-null - // When trace is dropped, decisionMaker isn't tracked -- e.g. null - - serviceRules | operationRules | defaultRate | expectedDecisionMaker | expectedPriority | expectedRuleRate | expectedRateLimit | expectedAgentRate - - // Matching neither passes through to rate based sampler - "xx:1" | null | null | AGENT_RATE | SAMPLER_KEEP | null | null | 1.0 - null | "xx:1" | null | AGENT_RATE | SAMPLER_KEEP | null | null | 1.0 - - // Matching neither with default rate - per spec, use of defaultRate is considered a "rule" - null | null | "1" | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - null | null | "0" | null | USER_DROP | 0 | null | null - "xx:1" | null | "1" | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - null | "xx:1" | "1" | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - "xx:1" | null | "0" | null | USER_DROP | 0 | null | null - null | "xx:1" | "0" | null | USER_DROP | 0 | null | null - - // Matching service: keep - "service:1" | null | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - "s.*:1" | null | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - ".*e:1" | null | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - - // Matching service: drop - "service:0" | null | null | null | USER_DROP | 0 | null | null - "s.*:0" | null | null | null | USER_DROP | 0 | null | null - ".*e:0" | null | null | null | USER_DROP | 0 | null | null - - // Matching service overrides default rate - "service:1" | null | "0" | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - "service:0" | null | "1" | null | USER_DROP | 0 | null | null - - // multiple services - "xxx:0,service:1" | null | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - "xxx:1,service:0" | null | null | null | USER_DROP | 0 | null | null - - // Matching operation : keep - null | "operation:1" | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - null | "o.*:1" | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - null | ".*n:1" | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - - // Matching operation: drop - null | "operation:0" | null | null | USER_DROP | 0 | null | null - null | "o.*:0" | null | null | USER_DROP | 0 | null | null - null | ".*n:0" | null | null | USER_DROP | 0 | null | null - - // Matching operation overrides default rate - null | "operation:1" | "0" | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - null | "operation:0" | "1" | null | USER_DROP | 0 | null | null - - // multiple operation combinations - null | "xxx:0,operation:1" | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - null | "xxx:1,operation:0" | null | null | USER_DROP | 0 | null | null - - // Service and operation name combinations - "service:1" | "operation:0" | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - "service:1" | "xxx:0" | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - "service:0" | "operation:1" | null | null | USER_DROP | 0 | null | null - "service:0" | "xxx:1" | null | null | USER_DROP | 0 | null | null - "xxx:0" | "operation:1" | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - "xxx:1" | "operation:0" | null | null | USER_DROP | 0 | null | null - - // There are no tests for ordering within service or operation rules because the rule order in that case is unspecified - } - - def "sampling config JSON rules combinations"() { - given: - Properties properties = new Properties() - properties.setProperty(TRACE_SAMPLING_RULES, jsonRules) - - if (defaultRate != null) { - properties.setProperty(TRACE_SAMPLE_RATE, defaultRate) - } - - properties.setProperty(TRACE_RATE_LIMIT, "50") - def tracer = tracerBuilder().writer(new ListWriter()).build() - - when: - Sampler sampler = Sampler.Builder.forConfig(properties) - - then: - sampler instanceof PrioritySampler - - when: - DDSpan span = tracer.buildSpan("datadog", "operation") - .withServiceName("service") - .withTag("env", "bar") - .withTag("tag", "foo") - .withResourceName("resource") - .ignoreActiveSpan() - .start() - ((PrioritySampler) sampler).setSamplingPriority(span) - - def propagationMap = span.context.propagationTags.createTagMap() - def decisionMaker = propagationMap.get('_dd.p.dm') - - def expectedDmStr = (expectedDecisionMaker == null) ? null : "-" + expectedDecisionMaker - - then: - span.getTag(RuleBasedTraceSampler.SAMPLING_RULE_RATE) == expectedRuleRate - span.getTag(RuleBasedTraceSampler.SAMPLING_LIMIT_RATE) == expectedRateLimit - span.getTag(RateByServiceTraceSampler.SAMPLING_AGENT_RATE) == expectedAgentRate - span.getSamplingPriority() == expectedPriority - decisionMaker == expectedDmStr - - cleanup: - tracer.close() - - where: - // NOTE: Using a defaultRate is still considered to be a "rule" - // decisionMaker is LOCAL_USER_RULE and expectedRuleRate is non-null - // When trace is dropped, decisionMaker isn't tracked -- e.g. null - - jsonRules | defaultRate | expectedDecisionMaker | expectedPriority | expectedRuleRate | expectedRateLimit | expectedAgentRate - // Matching neither passes through to rate based sampler - "[{\"service\": \"xx\", \"sample_rate\": 1}]" | null | AGENT_RATE | SAMPLER_KEEP | null | null | 1.0 - "[{\"name\": \"xx\", \"sample_rate\": 1}]" | null | AGENT_RATE | SAMPLER_KEEP | null | null | 1.0 - - // Matching neither with default rate - "[{\"sample_rate\": 1}]" | "1" | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - "[{\"sample_rate\": 0}]" | "0" | null | USER_DROP | 0 | null | null - "[]" | "0" | null | USER_DROP | 0 | null | null - "[{\"service\": \"xx\", \"sample_rate\": 1}]" | "1" | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - "[{\"name\": \"xx\", \"sample_rate\": 1}]" | "1" | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - "[{\"service\": \"xx\", \"sample_rate\": 1}]" | "0" | null | USER_DROP | 0 | null | null - "[{\"name\": \"xx\", \"sample_rate\": 1}]" | "0" | null | USER_DROP | 0 | null | null - - // Matching service: keep - "[{\"service\": \"service\", \"sample_rate\": 1}]" | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - - // Matching service: drop - "[{\"service\": \"service\", \"sample_rate\": 0}]" | null | null | USER_DROP | 0 | null | null - - // Matching service overrides default rate - "[{\"service\": \"service\", \"sample_rate\": 1}]" | "0" | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - "[{\"service\": \"service\", \"sample_rate\": 0}]" | "1" | null | USER_DROP | 0 | null | null - - // multiple services - "[{\"service\": \"xxx\", \"sample_rate\": 0}, {\"service\": \"service\", \"sample_rate\": 1}]" | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - "[{\"service\": \"xxx\", \"sample_rate\": 1}, {\"service\": \"service\", \"sample_rate\": 0}]" | null | null | USER_DROP | 0 | null | null - - // Matching operation : keep - "[{\"name\": \"operation\", \"sample_rate\": 1}]" | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - - // Matching operation: drop - "[{\"name\": \"operation\", \"sample_rate\": 0}]" | null | null | USER_DROP | 0 | null | null - - // Matching operation overrides default rate - "[{\"name\": \"operation\", \"sample_rate\": 1}]" | "0" | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - "[{\"name\": \"operation\", \"sample_rate\": 0}]" | "1" | null | USER_DROP | 0 | null | null - - // multiple operation combinations - "[{\"name\": \"xxx\", \"sample_rate\": 0}, {\"name\": \"operation\", \"sample_rate\": 1}]" | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - "[{\"name\": \"xxx\", \"sample_rate\": 1}, {\"name\": \"operation\", \"sample_rate\": 0}]" | null | null | USER_DROP | 0 | null | null - - // Matching resource : keep - "[{\"resource\": \"resource\", \"sample_rate\": 1}]" | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - - // Matching resource: drop - "[{\"resource\": \"resource\", \"sample_rate\": 0}]" | null | null | USER_DROP | 0 | null | null - - // Matching resource overrides default rate - "[{\"resource\": \"resource\", \"sample_rate\": 1}]" | "0" | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - "[{\"resource\": \"resource\", \"sample_rate\": 0}]" | "1" | null | USER_DROP | 0 | null | null - - // Multiple resource combinations - "[{\"resource\": \"xxx\", \"sample_rate\": 0}, {\"resource\": \"resource\", \"sample_rate\": 1}]" | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - "[{\"resource\": \"xxx\", \"sample_rate\": 1}, {\"resource\": \"resource\", \"sample_rate\": 0}]" | null | null | USER_DROP | 0 | null | null - - // Select matching service + operation rules - "[{\"service\": \"service\", \"sample_rate\": 1}, {\"name\": \"operation\", \"sample_rate\": 0}]" | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - "[{\"service\": \"service\", \"sample_rate\": 1}, {\"name\": \"xxx\", \"sample_rate\": 0}]" | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - "[{\"service\": \"service\", \"sample_rate\": 0}, {\"name\": \"operation\", \"sample_rate\": 1}]" | null | null | USER_DROP | 0 | null | null - "[{\"service\": \"service\", \"sample_rate\": 0}, {\"name\": \"xxx\", \"sample_rate\": 1}]" | null | null | USER_DROP | 0 | null | null - "[{\"service\": \"xxx\", \"sample_rate\": 0}, {\"name\": \"operation\", \"sample_rate\": 1}]" | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - "[{\"service\": \"xxx\", \"sample_rate\": 1}, {\"name\": \"operation\", \"sample_rate\": 0}]" | null | null | USER_DROP | 0 | null | null - - // Select matching service + operation rules - "[{\"service\": \"service\", \"name\": \"operation\", \"sample_rate\": 1}]" | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - "[{\"service\": \"service\", \"name\": \"xxx\", \"sample_rate\": 0}, {\"service\": \"service\", \"name\": \"operation\", \"sample_rate\": 1}]" | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - "[{\"service\": \"service\", \"name\": \"xxx\", \"sample_rate\": 0}, {\"service\": \"service\", \"sample_rate\": 1}]" | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - "[{\"service\": \"service\", \"name\": \"xxx\", \"sample_rate\": 0}, {\"name\": \"operation\", \"sample_rate\": 1}]" | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - "[{\"service\": \"service\", \"resource\": \"xxx\", \"sample_rate\": 0}, {\"resource\": \"resource\", \"sample_rate\": 1}]" | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - "[{\"service\": \"service\", \"name\": \"operation\", \"sample_rate\": 0}, {\"service\": \"service\", \"name\": \"operation\", \"sample_rate\": 1}]" | null | null | USER_DROP | 0 | null | null - "[{\"service\": \"service\", \"name\": \"operation\", \"sample_rate\": 0}]" | null | null | USER_DROP | 0 | null | null - - // Select matching service + resource - "[{\"service\": \"service\", \"resource\": \"xxx\", \"sample_rate\": 0}, {\"service\": \"service\", \"resource\": \"resource\", \"sample_rate\": 1}]" | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - - // Select matching service + resource + operation rules - "[{\"service\": \"service\", \"resource\": \"xxx\", \"sample_rate\": 0}, {\"service\": \"service\", \"resource\": \"resource\", \"name\": \"operation\", \"sample_rate\": 1}]" | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - - // Select matching single tag rules - "[{\"tags\": {\"env\": \"xxx\"}, \"sample_rate\": 0}, {\"tags\": {\"env\": \"bar\"}, \"sample_rate\": 1}]" | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - "[{\"tags\": {\"env\": \"*x\"}, \"sample_rate\": 0}, {\"tags\": {\"env\": \"*\"}, \"sample_rate\": 1}]" | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - "[{\"tags\": {\"env\": \"x??\"}, \"sample_rate\": 0}, {\"tags\": {\"env\": \"b?r\"}, \"sample_rate\": 1}]" | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - "[{\"tags\": {\"env\": \"x??\"}, \"sample_rate\": 1}, {\"tags\": {\"env\": \"b?r\"}, \"sample_rate\": 0}]" | null | null | USER_DROP | 0 | null | null - - // Select matching two tags rules - "[{\"tags\": {\"env\": \"xxx\"}, \"sample_rate\": 0}, {\"tags\": {\"env\": \"bar\", \"tag\": \"foo\"}, \"sample_rate\": 1}]" | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - "[{\"tags\": {\"env\": \"*x\"}, \"sample_rate\": 0}, {\"tags\": {\"env\": \"*\", \"tag\": \"*\"}, \"sample_rate\": 1}]" | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - "[{\"tags\": {\"env\": \"x??\"}, \"sample_rate\": 0}, {\"tags\": {\"env\": \"b?r\", \"tag\": \"f??\"}, \"sample_rate\": 1}]" | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - "[{\"tags\": {\"env\": \"x??\"}, \"sample_rate\": 1}, {\"tags\": {\"env\": \"b?r\", \"tag\": \"f??\"}, \"sample_rate\": 0}]" | null | null | USER_DROP | 0 | null | null - - // Select matching service + resource + operation + tag rules - "[{\"service\": \"service\", \"resource\": \"xxx\", \"tags\": {\"env\": \"x??\"}, \"sample_rate\": 0}, {\"service\": \"service\", \"resource\": \"resource\", \"name\": \"operation\", \"tags\": {\"env\": \"b?r\", \"tag\": \"f??\"}, \"sample_rate\": 1}]" | null | LOCAL_USER_RULE | USER_KEEP | 1.0 | 50 | null - } - - def "tag types test"() { - given: - def json = """[{ - "tags": {"testTag": "${tagPattern}"}, - "sample_rate": 1 - }]""" - Properties properties = new Properties() - properties.setProperty(TRACE_SAMPLING_RULES, json) - properties.setProperty(TRACE_SAMPLE_RATE, "0") - - def tracer = tracerBuilder().writer(new ListWriter()).build() - PrioritySampler sampler = (PrioritySampler)Sampler.Builder.forConfig(properties) - - when: - DDSpan span = tracer.buildSpan("datadog", "operation") - .withServiceName("service") - .withResourceName("resource") - .withTag("env", "bar") - .ignoreActiveSpan() - .start() - - span.setTag("testTag", tagValue) - - sampler.setSamplingPriority(span) - - then: - span.getSamplingPriority() == (expectedMatch ? USER_KEEP : USER_DROP) - - cleanup: - tracer.close() - - where: - tagPattern | tagValue | expectedMatch - "*" | "anything..." | true - "*" | null | false - "*" | new StringBuilder("foo") | true - "*" | object() | true - "**" | object() | true - "?" | object() | false - "*" | "foo" | true - "**" | "foo" | true - "**" | true | true - "**" | false | true - "**" | 20 | true - "**" | 20L | true - "**" | 20.1F | true - "**" | 20.1D | true - "**" | bigInteger("20") | true - "**" | bigDecimal("20.1") | true - "foo" | "foo" | true - "foo" | new StringBuilder("foo") | true - "foo" | "not-foo" | false - "ba?" | "bar" | true - "20" | 20 | true - "20" | Integer.valueOf(20) | true - "20" | 20L | true - "20" | Long.valueOf(20) | true - "20" | 20F | true - "20" | 20.1F | false - "20.*" | 20.1F | false - "20.1" | 20.1D | false - "*" | 20.1D | true - "20" | bigInteger("20") | true - "20" | bigDecimal("20") | true - "*" | bigDecimal("20.1") | true - } - - def "Prefer JSON rules over other deprecated ones"() { - setup: - def tracer = tracerBuilder().writer(new ListWriter()).build() - - when: - Properties properties = new Properties() - properties.setProperty(TRACE_SAMPLING_SERVICE_RULES, "service:0") - properties.setProperty(TRACE_SAMPLING_OPERATION_RULES, "operation:0") - properties.setProperty(TRACE_SAMPLING_RULES, "[{\"service\": \"service\", \"name\": \"operation\", \"sample_rate\": 1.0}]") - properties.setProperty(TRACE_RATE_LIMIT, "1") - Sampler sampler = Sampler.Builder.forConfig(properties) - - DDSpan span1 = tracer.buildSpan("datadog", "operation") - .withServiceName("service") - .withTag("env", "bar") - .ignoreActiveSpan().start() - - DDSpan span2 = tracer.buildSpan("datadog", "operation") - .withServiceName("service") - .withTag("env", "bar") - .ignoreActiveSpan().start() - - ((PrioritySampler) sampler).setSamplingPriority(span1) - // Span 2 should be rate limited if there isn't a >1 sec delay between these 2 lines - ((PrioritySampler) sampler).setSamplingPriority(span2) - - then: - span1.getTag(RuleBasedTraceSampler.SAMPLING_RULE_RATE) == 1.0 - span1.getTag(RuleBasedTraceSampler.SAMPLING_LIMIT_RATE) == 1.0 - span1.getTag(RateByServiceTraceSampler.SAMPLING_AGENT_RATE) == null - span1.getSamplingPriority() == USER_KEEP - - span2.getTag(RuleBasedTraceSampler.SAMPLING_RULE_RATE) == 1.0 - span2.getTag(RuleBasedTraceSampler.SAMPLING_LIMIT_RATE) == 1.0 - span2.getTag(RateByServiceTraceSampler.SAMPLING_AGENT_RATE) == null - span2.getSamplingPriority() == USER_DROP - - cleanup: - tracer.close() - } - - def "Rate limit is set for rate limited spans"() { - setup: - def tracer = tracerBuilder().writer(new ListWriter()).build() - - when: - Properties properties = new Properties() - properties.setProperty(TRACE_SAMPLING_SERVICE_RULES, "service:1") - properties.setProperty(TRACE_RATE_LIMIT, "1") - Sampler sampler = Sampler.Builder.forConfig(properties) - - DDSpan span1 = tracer.buildSpan("datadog", "operation") - .withServiceName("service") - .withTag("env", "bar") - .ignoreActiveSpan().start() - - DDSpan span2 = tracer.buildSpan("datadog", "operation") - .withServiceName("service") - .withTag("env", "bar") - .ignoreActiveSpan().start() - - ((PrioritySampler) sampler).setSamplingPriority(span1) - // Span 2 should be rate limited if there isn't a >1 sec delay between these 2 lines - ((PrioritySampler) sampler).setSamplingPriority(span2) - - then: - span1.getTag(RuleBasedTraceSampler.SAMPLING_RULE_RATE) == 1.0 - span1.getTag(RuleBasedTraceSampler.SAMPLING_LIMIT_RATE) == 1.0 - span1.getTag(RateByServiceTraceSampler.SAMPLING_AGENT_RATE) == null - span1.getSamplingPriority() == USER_KEEP - - span2.getTag(RuleBasedTraceSampler.SAMPLING_RULE_RATE) == 1.0 - span2.getTag(RuleBasedTraceSampler.SAMPLING_LIMIT_RATE) == 1.0 - span2.getTag(RateByServiceTraceSampler.SAMPLING_AGENT_RATE) == null - span2.getSamplingPriority() == USER_DROP - - cleanup: - tracer.close() - } - - def "Rate limit is set for rate limited spans (matched on different rules)"() { - setup: - def tracer = tracerBuilder().writer(new ListWriter()).build() - - when: - Properties properties = new Properties() - properties.setProperty(TRACE_SAMPLING_SERVICE_RULES, "service:1,foo:1") - properties.setProperty(TRACE_RATE_LIMIT, "1") - Sampler sampler = Sampler.Builder.forConfig(properties) - - DDSpan span1 = tracer.buildSpan("datadog", "operation") - .withServiceName("service") - .withTag("env", "bar") - .ignoreActiveSpan().start() - DDSpan span2 = tracer.buildSpan("datadog", "operation") - .withServiceName("foo") - .withTag("env", "bar") - .ignoreActiveSpan().start() - - ((PrioritySampler) sampler).setSamplingPriority(span1) - // Span 2 should be rate limited if there isn't a >1 sec delay between these 2 lines - ((PrioritySampler) sampler).setSamplingPriority(span2) - - then: - span1.getTag(RuleBasedTraceSampler.SAMPLING_RULE_RATE) == 1.0 - span1.getTag(RuleBasedTraceSampler.SAMPLING_LIMIT_RATE) == 1.0 - span1.getTag(RateByServiceTraceSampler.SAMPLING_AGENT_RATE) == null - span1.getSamplingPriority() == USER_KEEP - - span2.getTag(RuleBasedTraceSampler.SAMPLING_RULE_RATE) == 1.0 - span2.getTag(RuleBasedTraceSampler.SAMPLING_LIMIT_RATE) == 1.0 - span2.getTag(RateByServiceTraceSampler.SAMPLING_AGENT_RATE) == null - span2.getSamplingPriority() == USER_DROP - - cleanup: - tracer.close() - } - - // helper functions - to subvert codenarc - static bigInteger(str) { - return new BigInteger(str) - } - - static bigDecimal(str) { - return new BigDecimal(str) - } - - static object() { - return new Object() { - @Override - String toString() { - return 'object' - } - } - } -} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/SamplerTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/SamplerTest.groovy deleted file mode 100644 index d5dbbdf6df8..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/SamplerTest.groovy +++ /dev/null @@ -1,175 +0,0 @@ -package datadog.trace.common.sampling - -import datadog.trace.api.Config -import datadog.trace.api.sampling.PrioritySampling -import datadog.trace.common.writer.ListWriter -import datadog.trace.core.CoreTracer -import datadog.trace.core.DDSpan -import datadog.trace.test.util.DDSpecification - -class SamplerTest extends DDSpecification{ - - void "test that AsmStandaloneSampler is selected when apm tracing disabled and appsec enabled is enabled"() { - setup: - System.setProperty("dd.apm.tracing.enabled", "false") - System.setProperty("dd.appsec.enabled", "true") - Config config = new Config() - - when: - Sampler sampler = Sampler.Builder.forConfig(config, null) - - then: - sampler instanceof AsmStandaloneSampler - } - - void "test that AsmStandaloneSampler is selected when apm tracing disabled and iast enabled is enabled"() { - setup: - System.setProperty("dd.apm.tracing.enabled", "false") - System.setProperty("dd.iast.enabled", "true") - Config config = new Config() - - when: - Sampler sampler = Sampler.Builder.forConfig(config, null) - - then: - sampler instanceof AsmStandaloneSampler - } - - void "test that AsmStandaloneSampler is selected when apm tracing disabled and sca enabled is enabled"() { - setup: - System.setProperty("dd.apm.tracing.enabled", "false") - System.setProperty("dd.appsec.sca.enabled", "true") - Config config = new Config() - - when: - Sampler sampler = Sampler.Builder.forConfig(config, null) - - then: - sampler instanceof AsmStandaloneSampler - } - - void "test that AsmStandaloneSampler is not selected when apm tracing and asm not enabled"() { - setup: - System.setProperty("dd.apm.tracing.enabled", "false") - Config config = new Config() - - when: - Sampler sampler = Sampler.Builder.forConfig(config, null) - - then: - !(sampler instanceof AsmStandaloneSampler) - } - - void "test that AsmStandaloneSampler is not selected when apm tracing enabled and asm not enabled"() { - setup: - Config config = new Config() - - when: - Sampler sampler = Sampler.Builder.forConfig(config, null) - - then: - !(sampler instanceof AsmStandaloneSampler) - } - - void "test that ParentBasedAlwaysOnSampler replaces AllSampler when OTLP traces export is enabled and priority sampling is disabled"() { - setup: - System.setProperty("dd.trace.otel.exporter", "otlp") - System.setProperty("dd.priority.sampling", "false") - Config config = new Config() - - when: - Sampler sampler = Sampler.Builder.forConfig(config, null) - - then: - sampler instanceof ParentBasedAlwaysOnSampler - } - - void "test that AllSampler is selected when OTLP traces export is disabled and priority sampling is disabled"() { - setup: - System.setProperty("dd.priority.sampling", "false") - Config config = new Config() - - when: - Sampler sampler = Sampler.Builder.forConfig(config, null) - - then: - sampler instanceof AllSampler - !(sampler instanceof ParentBasedAlwaysOnSampler) - } - - void "test that trace sampling rules are respected when OTLP traces export is enabled"() { - setup: - System.setProperty("dd.trace.otel.exporter", "otlp") - System.setProperty("dd.trace.sample.rate", "0.5") - Config config = new Config() - - when: - Sampler sampler = Sampler.Builder.forConfig(config, null) - - then: - sampler instanceof RuleBasedTraceSampler - !(sampler instanceof ParentBasedAlwaysOnSampler) - } - - void "test that ParentBasedAlwaysOnSampler replaces RateByServiceTraceSampler when OTLP traces export is enabled with default priority sampling"() { - setup: - System.setProperty("dd.trace.otel.exporter", "otlp") - Config config = new Config() - - when: - Sampler sampler = Sampler.Builder.forConfig(config, null) - - then: - sampler instanceof ParentBasedAlwaysOnSampler - !(sampler instanceof RateByServiceTraceSampler) - } - - void "test that ForcePrioritySampler is respected when OTLP traces export is enabled and priority sampling is forced to keep"() { - setup: - System.setProperty("dd.trace.otel.exporter", "otlp") - System.setProperty("dd.priority.sampling.force", "keep") - Config config = new Config() - - when: - Sampler sampler = Sampler.Builder.forConfig(config, null) - - then: - sampler instanceof ForcePrioritySampler - !(sampler instanceof ParentBasedAlwaysOnSampler) - } - - void "test that ForcePrioritySampler is respected when OTLP traces export is enabled and priority sampling is forced to drop"() { - setup: - System.setProperty("dd.trace.otel.exporter", "otlp") - System.setProperty("dd.priority.sampling.force", "drop") - Config config = new Config() - - when: - Sampler sampler = Sampler.Builder.forConfig(config, null) - - then: - sampler instanceof ForcePrioritySampler - !(sampler instanceof ParentBasedAlwaysOnSampler) - } - - void "test that spans built with OTLP traces export enabled and priority sampling disabled have a non-UNSET sampling priority"() { - setup: - System.setProperty("dd.trace.otel.exporter", "otlp") - System.setProperty("dd.priority.sampling", "false") - Config config = new Config() - Sampler sampler = Sampler.Builder.forConfig(config, null) - CoreTracer tracer = CoreTracer.builder().writer(new ListWriter()).sampler(sampler).build() - - when: - DDSpan span = (DDSpan) tracer.buildSpan("datadog", "test").start() - ((PrioritySampler) sampler).setSamplingPriority(span) - - then: - span.getSamplingPriority() != null - span.getSamplingPriority() == PrioritySampling.SAMPLER_KEEP - - cleanup: - span.finish() - tracer.close() - } -} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/SingleSpanSamplerTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/SingleSpanSamplerTest.groovy deleted file mode 100644 index ac33a48cc80..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/SingleSpanSamplerTest.groovy +++ /dev/null @@ -1,214 +0,0 @@ -package datadog.trace.common.sampling - -import datadog.trace.api.Config -import datadog.trace.common.writer.ListWriter -import datadog.trace.core.DDSpan -import datadog.trace.core.test.DDCoreSpecification - -import static datadog.trace.api.config.TracerConfig.SPAN_SAMPLING_RULES -import static datadog.trace.api.config.TracerConfig.SPAN_SAMPLING_RULES_FILE -import static datadog.trace.api.config.TracerConfig.TRACE_SAMPLE_RATE -import static datadog.trace.api.sampling.SamplingMechanism.DEFAULT -import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_DROP -import static datadog.trace.api.sampling.SamplingMechanism.SPAN_SAMPLING_RATE - -class SingleSpanSamplerTest extends DDCoreSpecification { - - def "Single Span Sampler is not created when no rules provided"() { - given: - Properties properties = new Properties() - if (rules != null) { - properties.setProperty(SPAN_SAMPLING_RULES, rules) - } - - when: - SingleSpanSampler sampler = SingleSpanSampler.Builder.forConfig(Config.get(properties)) - - then: - sampler == null - - where: - rules << [ - // no rules provided - null, - "[]", - // invalid sample_rate must be between 0.0 and 1.0 - """[ { "service": "*", "name": "*", "sample_rate": 10.0 } ]""", - """[ { "service": "*", "name": "*", "sample_rate": "all" } ]""", - // invalid max_per_second value - """[ { "service": "*", "name": "*", "sample_rate": 1.0, "max_per_second": "N/A" } ]""" - ] - } - - def "Single Span Sampler set sampling priority"() { - given: - Properties properties = new Properties() - if (rules != null) { - properties.setProperty(SPAN_SAMPLING_RULES, rules) - } - def tracer = tracerBuilder().writer(new ListWriter()).build() - - when: - SingleSpanSampler sampler = SingleSpanSampler.Builder.forConfig(Config.get(properties)) - - DDSpan span = tracer.buildSpan("datadog", "operation") - .withServiceName("service") - .withTag("env", "bar") - .ignoreActiveSpan().start() as DDSpan - - then: - sampler.setSamplingPriority(span) == isFirstSampled - - span.getTag("_dd.span_sampling.mechanism") == expectedMechanism - span.getTag("_dd.span_sampling.rule_rate") == expectedRate - span.getTag("_dd.span_sampling.max_per_second") == expectedLimit - - where: - rules | isFirstSampled | expectedMechanism | expectedRate | expectedLimit - """[ { "service": "*", "name": "*", "sample_rate": 1.0 } ]""" | true | SPAN_SAMPLING_RATE | 1.0 | null - """[ { "service": "*", "name": "*", "sample_rate": 1.0, "max_per_second": 10 } ]""" | true | SPAN_SAMPLING_RATE | 1.0 | 10 - """[ { "service": "ser*", "name": "oper*", "sample_rate": 1.0, "max_per_second": 15 } ]""" | true | SPAN_SAMPLING_RATE | 1.0 | 15 - """[ { "service": "?ervice", "name": "operati?n", "sample_rate": 1.0, "max_per_second": 10 } ]""" | true | SPAN_SAMPLING_RATE | 1.0 | 10 - """[ { "service": "service", "name": "operation", "sample_rate": 1.0, "max_per_second": 5 } ]""" | true | SPAN_SAMPLING_RATE | 1.0 | 5 - """[ { "service": "service-b", "name": "*", "sample_rate": 1.0, "max_per_second": 10 } ]""" | false | null | null | null - """[ { "service": "*", "name": "*", "sample_rate": 0.0 } ]""" | false | null | null | null - """[ { "service": "*", "name": "operation-b", "sample_rate": 0.5 } ]""" | false | null | null | null - } - - def "Parent/child scenarios when the trace is dropped but individual spans are kept by the single span sampler"() { - given: - Properties properties = new Properties() - if (rules != null) { - properties.setProperty(SPAN_SAMPLING_RULES, rules) - properties.setProperty(TRACE_SAMPLE_RATE, "0") - } - def tracer = tracerBuilder().writer(new ListWriter()).build() - - when: - SingleSpanSampler sampler = SingleSpanSampler.Builder.forConfig(Config.get(properties)) - - DDSpan rootSpan = tracer.buildSpan("datadog", "web.request") - .withServiceName("webserver") - .ignoreActiveSpan().start() as DDSpan - - DDSpan childSpan = tracer.buildSpan("datadog", "web.handler") - .withServiceName("webserver") - .asChildOf(rootSpan) - .ignoreActiveSpan().start() as DDSpan - - then: - // set trace sampling priority to drop the trace - rootSpan.setSamplingPriority(SAMPLER_DROP, DEFAULT) - - // set spans sampling priority - sampler.setSamplingPriority(rootSpan) == sampleRoot - sampler.setSamplingPriority(childSpan) == sampleChild - - expect: - rootSpan.getTag("_dd.span_sampling.mechanism") == rootMechanism - childSpan.getTag("_dd.span_sampling.mechanism") == childMechanism - - where: - rules | sampleRoot | sampleChild | rootMechanism | childMechanism - """[{"service": "webserver", "name": "web.request"}]""" | true | false | SPAN_SAMPLING_RATE | null - """[{"service": "webserver", "name": "web.handler"}]""" | false | true | null | SPAN_SAMPLING_RATE - """[{"service": "webserver", "name": "web.*"}]""" | true | true | SPAN_SAMPLING_RATE | SPAN_SAMPLING_RATE - """[{"service": "other-server"}]""" | false | false | null | null - } - - def "Single Span Sampler set sampling priority with the max-per-second limit"() { - given: - Properties properties = new Properties() - if (rules != null) { - properties.setProperty(SPAN_SAMPLING_RULES, rules) - } - def tracer = tracerBuilder().writer(new ListWriter()).build() - - when: - SingleSpanSampler sampler = SingleSpanSampler.Builder.forConfig(Config.get(properties)) - - DDSpan span1 = tracer.buildSpan("datadog", "operation") - .withServiceName("service") - .withTag("env", "bar") - .ignoreActiveSpan().start() as DDSpan - - DDSpan span2 = tracer.buildSpan("datadog", "operation") - .withServiceName("service") - .withTag("env", "bar") - .ignoreActiveSpan().start() as DDSpan - - then: - sampler.setSamplingPriority(span1) == isFirstSampled - sampler.setSamplingPriority(span2) == isSecondSampled - - where: - rules | isFirstSampled | isSecondSampled - """[ { "service": "*", "name": "*", "sample_rate": 1.0, "max_per_second": 1 } ]""" | true | false - """[ { "service": "ser*", "name": "oper*", "sample_rate": 1.0, "max_per_second": 1 } ]""" | true | false - """[ { "service": "?ervice", "name": "operati?n", "sample_rate": 1.0, "max_per_second": 1 } ]""" | true | false - """[ { "service": "service", "name": "operation", "sample_rate": 1.0, "max_per_second": 1 } ]""" | true | false - """[ { "service": "service", "max_per_second": 1 } ]""" | true | false - """[ { "name": "operation", "sample_rate": 1.0, "max_per_second": 1 } ]""" | true | false - - """[ { "service": "*", "name": "*", "sample_rate": 1.0, "max_per_second": 2 } ]""" | true | true - """[ { "service": "ser*", "name": "oper*", "max_per_second": 2 } ]""" | true | true - """[ { "service": "?ervice", "name": "operati?n", "sample_rate": 1.0, "max_per_second": 2 } ]""" | true | true - } - - def "Load rules from file"() { - given: - Properties properties = new Properties() - if (rules != null) { - def rulesFile = SpanSamplingRulesFileTest.createRulesFile(rules) - properties.setProperty(SPAN_SAMPLING_RULES_FILE, rulesFile) - } - def tracer = tracerBuilder().writer(new ListWriter()).build() - - when: - SingleSpanSampler sampler = SingleSpanSampler.Builder.forConfig(Config.get(properties)) - - DDSpan span1 = tracer.buildSpan("datadog", "operation") - .withServiceName("service") - .withTag("env", "bar") - .ignoreActiveSpan().start() as DDSpan - - then: - sampler.setSamplingPriority(span1) - - where: - rules << ["""[ { "service": "*", "name": "op?ration*", "sample_rate": 1.0, "max_per_second": 1 } ]"""] - } - - def "Prefer rules in env var over rules from file"() { - given: - Properties properties = new Properties() - properties.setProperty(SPAN_SAMPLING_RULES, envVarRules) - properties.setProperty(SPAN_SAMPLING_RULES_FILE, SpanSamplingRulesFileTest.createRulesFile(fileRules)) - def tracer = tracerBuilder().writer(new ListWriter()).build() - - when: - SingleSpanSampler sampler = SingleSpanSampler.Builder.forConfig(Config.get(properties)) - - DDSpan span1 = tracer.buildSpan("datadog", "operation") - .withServiceName("service") - .withTag("env", "bar") - .ignoreActiveSpan().start() as DDSpan - - then: - sampler.setSamplingPriority(span1) == matched - - where: - envVarRules | fileRules | matched - """[ { "sample_rate": 0 } ]""" | """[ { "sample_rate": 1 } ]""" | false - """[ { "sample_rate": 1 } ]""" | """[ { "sample_rate": 0 } ]""" | true - } - - def "Throw NPE when passed list of rules is null"() { - when: - new SingleSpanSampler.RuleBasedSingleSpanSampler(null) - - then: - final NullPointerException exception = thrown() - exception.getMessage() == "SpanSamplingRules can't be null." - } -} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/SpanSamplingRulesTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/SpanSamplingRulesTest.groovy deleted file mode 100644 index 412134cf564..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/SpanSamplingRulesTest.groovy +++ /dev/null @@ -1,169 +0,0 @@ -package datadog.trace.common.sampling - -import com.squareup.moshi.Moshi -import datadog.trace.core.test.DDCoreSpecification - -import java.nio.file.Files -import java.nio.file.Path - -import static datadog.trace.api.sampling.SamplingRule.MATCH_ALL - -class SpanSamplingRulesTest extends DDCoreSpecification { - - def deserializeRules(String jsonRules) { - return SpanSamplingRules.deserialize(jsonRules) - } - - def "Deserialize empty list of Span Sampling Rules from JSON"() { - when: - def rules = deserializeRules('[]') - - then: - rules.empty - } - - def "Deserialize Span Sampling Rules from JSON"() { - when: - def rules = deserializeRules("""[ - {"service": "service-name", "name": "operation-name", "resource": "resource-name", "tags": - {"tag-name1": "tag-pattern1", - "tag-name2": "tag-pattern2"}, - "sample_rate": 0.0, "max_per_second": 10.0}, - {}, - {"service": "", "name": "", "resource": "", "tags": {}}, - {"service": null, "name": null, "resource": null, "tags": null, "sample_rate": null, "sample_rate": null, "max_per_second": null}, - - {"sample_rate": 0.25}, - {"sample_rate": 0.5}, - {"sample_rate": 0.75}, - {"sample_rate": 1}, - - {"max_per_second": 0.2}, - {"max_per_second": 1.0}, - {"max_per_second": 10}, - {"max_per_second": 10.123}, - {"max_per_second": 10000} - ]""").rules - def ruleIndex = 0 - - then: - rules.size() == 13 - - // Test a complete rule - rules[ruleIndex].service == "service-name" - rules[ruleIndex].name == "operation-name" - rules[ruleIndex].resource == "resource-name" - rules[ruleIndex].tags == ["tag-name1": "tag-pattern1", "tag-name2": "tag-pattern2"] - rules[ruleIndex].sampleRate == 0.0d - rules[ruleIndex++].maxPerSecond == 10 - - // Test default values with an empty rule - rules[ruleIndex].service == MATCH_ALL - rules[ruleIndex].name == MATCH_ALL - rules[ruleIndex].resource == MATCH_ALL - rules[ruleIndex].tags == [:] - rules[ruleIndex].sampleRate == 1d - rules[ruleIndex++].maxPerSecond == Integer.MAX_VALUE - - // Test rule with empty values - rules[ruleIndex].service == "" - rules[ruleIndex].name == "" - rules[ruleIndex].resource == "" - rules[ruleIndex].tags == [:] - rules[ruleIndex].sampleRate == 1d - rules[ruleIndex++].maxPerSecond == Integer.MAX_VALUE - - // Test rule with null values - rules[ruleIndex].service == MATCH_ALL - rules[ruleIndex].name == MATCH_ALL - rules[ruleIndex].resource == MATCH_ALL - rules[ruleIndex].tags == [:] - rules[ruleIndex].sampleRate == 1d - rules[ruleIndex++].maxPerSecond == Integer.MAX_VALUE - - // Test different sample rate values - rules[ruleIndex++].sampleRate == 0.25d - rules[ruleIndex++].sampleRate == 0.5d - rules[ruleIndex++].sampleRate == 0.75d - rules[ruleIndex++].sampleRate == 1d - - // Test different max per second values - rules[ruleIndex++].maxPerSecond == 1 - rules[ruleIndex++].maxPerSecond == 1 - rules[ruleIndex++].maxPerSecond == 10 - rules[ruleIndex++].maxPerSecond == 10 - rules[ruleIndex++].maxPerSecond == 10000 - } - - def "Skip Span Sampling Rules with invalid sample_rate values"() { - when: - def rules = deserializeRules("""[ - {"service": "usersvc", "name": "healthcheck", "sample_rate": $rate} - ]""") - - then: - rules.empty - - where: - rate << ['-0.1', '-11', '1.2', '100', '"zero"', '""'] - } - - def "Skip Span Sampling Rules with invalid max_per_second values"() { - when: - def rules = deserializeRules("""[ - {"service": "usersvc", "name": "healthcheck", "max_per_second": $limit} - ]""") - - then: - rules.empty - - where: - limit << ['0', '-11', '"zero"', '""'] - } - - def "Skip Span Sampling Rules when incorrect JSON provided"() { - when: - def rules = deserializeRules(jsonRules) - - then: - rules.empty - - where: - jsonRules << ['[', '{"service": "usersvc",}', ''] - } - - def "Render JsonRule correctly when toString() is called"() { - when: - def jsonRule = new Moshi.Builder().build().adapter(SpanSamplingRules.JsonRule).fromJson(json) - - then: - jsonRule.toString() == json - - where: - json = '{"max_per_second":"10","name":"name","resource":"resource","sample_rate":"0.5","service":"service","tags":{"a":"b","foo":"bar"}}' - } - - def "Keep only valid rules when invalid rules are present"() { - when: - def rules = SpanSamplingRules.deserialize("""[ - {"service": "usersvc", "name": "healthcheck", "sample_rate": 0.5}, - {"service": "usersvc", "name": "healthcheck2", "sample_rate": 200} - ]""") - - then: - rules.rules.size() == 1 - } -} - -class SpanSamplingRulesFileTest extends SpanSamplingRulesTest { - static createRulesFile(String rules) { - Path p = Files.createTempFile('single-span-sampling-rules', '.json') - p.toFile() << rules - return p.toString() - } - - @Override - def deserializeRules(String jsonRules) { - return SpanSamplingRules.deserializeFile(createRulesFile(jsonRules)) - } -} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/TraceSamplingRulesTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/TraceSamplingRulesTest.groovy deleted file mode 100644 index d3b0883f3cb..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/sampling/TraceSamplingRulesTest.groovy +++ /dev/null @@ -1,119 +0,0 @@ -package datadog.trace.common.sampling - -import com.squareup.moshi.Moshi -import datadog.trace.core.test.DDCoreSpecification - -import static datadog.trace.api.sampling.SamplingRule.MATCH_ALL - -class TraceSamplingRulesTest extends DDCoreSpecification { - - def "Deserialize empty list of Trace Sampling Rules from JSON"() { - when: - def rules = TraceSamplingRules.deserialize("[]") - - then: - rules.empty - } - - def "Deserialize Trace Sampling Rules from JSON"() { - when: - def rules = TraceSamplingRules.deserialize("""[ - {"service": "service-name", "name": "operation-name", "resource": "resource-name", "tags": - {"tag-name1": "tag-pattern1", - "tag-name2": "tag-pattern2"}, - "sample_rate": 0.0}, - {}, - {"service": "", "name": "", "resource": "", "tags": {}}, - {"service": null, "name": null, "resource": null, "tags": null, "sample_rate": null}, - - {"sample_rate": 0.25}, - {"sample_rate": 0.5}, - {"sample_rate": 0.75}, - {"sample_rate": 1} - ]""").rules - def ruleIndex = 0 - - then: - rules.size() == 8 - - // Test a complete rule - rules[ruleIndex].service == "service-name" - rules[ruleIndex].name == "operation-name" - rules[ruleIndex].resource == "resource-name" - rules[ruleIndex].tags == ["tag-name1": "tag-pattern1", "tag-name2": "tag-pattern2"] - rules[ruleIndex++].sampleRate == 0d - - // Test default values with an empty rule - rules[ruleIndex].service == MATCH_ALL - rules[ruleIndex].name == MATCH_ALL - rules[ruleIndex].resource == MATCH_ALL - rules[ruleIndex].tags == [:] - rules[ruleIndex++].sampleRate == 1d - - // Test rule with empty values - rules[ruleIndex].service == "" - rules[ruleIndex].name == "" - rules[ruleIndex].resource == "" - rules[ruleIndex].tags == [:] - rules[ruleIndex++].sampleRate == 1d - - // Test rule with null values - rules[ruleIndex].service == MATCH_ALL - rules[ruleIndex].name == MATCH_ALL - rules[ruleIndex].resource == MATCH_ALL - rules[ruleIndex].tags == [:] - rules[ruleIndex++].sampleRate == 1d - - // Test different sample rate values - rules[ruleIndex++].sampleRate == 0.25d - rules[ruleIndex++].sampleRate == 0.5d - rules[ruleIndex++].sampleRate == 0.75d - rules[ruleIndex++].sampleRate == 1d - } - - def "Skip Trace Sampling Rules with invalid sample rate values: #rate"() { - when: - def rules = TraceSamplingRules.deserialize("""[ - {"service": "usersvc", "name": "healthcheck", "sample_rate": $rate} - ]""") - - then: - rules.empty - - where: - rate << ['-0.1', '-11', '1.2', '100', '"zero"', '""', '{}', '[]'] - } - - def "Skip Trace Sampling Rules when incorrect JSON provided"() { - when: - def rules = TraceSamplingRules.deserialize(jsonRules) - - then: - rules.empty - - where: - jsonRules << ['[', '{"service": "usersvc",}', ''] - } - - def "Render JsonRule correctly when toString() is called"() { - when: - def jsonRule = new Moshi.Builder().build().adapter(TraceSamplingRules.JsonRule).fromJson(json) - - then: - jsonRule.toString() == json - - where: - json = '{"name":"name","resource":"resource","sample_rate":"0.5","service":"service","tags":{"a":"b","foo":"bar"}}' - } - - def "Keep only valid rules when invalid rules are present"() { - when: - def rules = TraceSamplingRules.deserialize("""[ - {"service": "usersvc", "name": "healthcheck", "sample_rate": 0.5}, - {"service": "usersvc", "name": "healthcheck", "sample_rate": 200} - ]""") - - then: - rules.rules.size() == 1 - } -} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/test/DDCoreSpecification.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/test/DDCoreSpecification.groovy deleted file mode 100644 index 5aaf7c5011a..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/test/DDCoreSpecification.groovy +++ /dev/null @@ -1,127 +0,0 @@ -package datadog.trace.core.test - - -import datadog.metrics.api.statsd.StatsDClient -import datadog.trace.api.DDSpanId -import datadog.trace.api.DDTraceId -import datadog.trace.api.datastreams.NoopPathwayContext -import datadog.trace.api.sampling.PrioritySampling -import datadog.trace.bootstrap.instrumentation.api.ProfilingContextIntegration -import datadog.trace.common.writer.ListWriter -import datadog.trace.core.CoreTracer -import datadog.trace.core.CoreTracer.CoreTracerBuilder -import datadog.trace.core.DDSpan -import datadog.trace.core.DDSpanContext -import datadog.trace.core.propagation.PropagationTags -import datadog.trace.core.tagprocessor.TagsPostProcessorFactory -import datadog.trace.test.util.DDSpecification -import datadog.trace.util.AgentTaskScheduler -import spock.lang.Shared - -import java.util.concurrent.TimeUnit - -abstract class DDCoreSpecification extends DDSpecification { - @Shared - static List unclosedTracers = [] - - - static class AutoCloseableCoreTracerBuilder extends CoreTracerBuilder { - @Override - CoreTracer build() { - def ret = super.build() - unclosedTracers.add(ret) - ret - } - } - - - protected boolean useNoopStatsDClient() { - return true - } - - protected boolean useStrictTraceWrites() { - return true - } - - @Override - void setupSpec() { - TagsPostProcessorFactory.withAddInternalTags(false) - TagsPostProcessorFactory.withAddRemoteHostname(false) - } - - @Override - void cleanupSpec() { - TagsPostProcessorFactory.reset() - } - - @Override - void cleanup() { - unclosedTracers.each { - try { - it.close() - } catch (Throwable ignored) { - } - } - unclosedTracers.clear() - AgentTaskScheduler.shutdownAndReset(10, TimeUnit.SECONDS) - } - - protected CoreTracerBuilder tracerBuilder() { - def builder = new AutoCloseableCoreTracerBuilder() - if (useNoopStatsDClient()) { - builder = builder.statsDClient(StatsDClient.NO_OP) - } - builder.strictTraceWrites(useStrictTraceWrites()) - } - - protected DDSpan buildSpan(long timestamp, CharSequence spanType, Map tags) { - return buildSpan(timestamp, spanType, PropagationTags.factory().empty(), tags, PrioritySampling.SAMPLER_KEEP, null) - } - - protected DDSpan buildSpan(long timestamp, String tag, String value, PropagationTags propagationTags) { - return buildSpan(timestamp, "fakeType", propagationTags, [(tag): value], PrioritySampling.UNSET, null) - } - - protected DDSpan buildSpan(long timestamp, - CharSequence spanType, - PropagationTags propagationTags, - Map tags, - byte prioritySampling, - Object ciVisibilityContextData) { - def tracer = tracerBuilder().writer(new ListWriter()).build() - def context = new DDSpanContext( - DDTraceId.ONE, - 1, - DDSpanId.ZERO, - null, - null, - "fakeService", - "fakeOperation", - "fakeResource", - prioritySampling, - null, - [:], - null, - false, - spanType, - 0, - tracer.traceCollectorFactory.create(DDTraceId.ONE), - null, - null, - ciVisibilityContextData, - NoopPathwayContext.INSTANCE, - false, - propagationTags, - ProfilingContextIntegration.NoOp.INSTANCE, - true, - true) - - def span = DDSpan.create("test", timestamp, context, null) - for (Map.Entry e : tags.entrySet()) { - span.setTag(e.key, e.value) - } - - tracer.close() - return span - } -} diff --git a/dd-trace-core/src/test/java/datadog/trace/common/metrics/ClientStatsAggregatorTest.java b/dd-trace-core/src/test/java/datadog/trace/common/metrics/ClientStatsAggregatorTest.java new file mode 100644 index 00000000000..36cab8b44d4 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/common/metrics/ClientStatsAggregatorTest.java @@ -0,0 +1,2796 @@ +package datadog.trace.common.metrics; + +import static java.util.Collections.emptyList; +import static java.util.Collections.emptySet; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import datadog.communication.ddagent.DDAgentFeaturesDiscovery; +import datadog.trace.api.WellKnownTags; +import datadog.trace.bootstrap.instrumentation.api.InstrumentationTags; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; +import datadog.trace.core.monitor.HealthMetrics; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.params.converter.ArgumentConversionException; +import org.junit.jupiter.params.converter.ArgumentConverter; +import org.junit.jupiter.params.converter.ConvertWith; +import org.tabletest.junit.TableTest; + +class ClientStatsAggregatorTest { + + private static final int HTTP_OK = 200; + private static final long REPORTING_INTERVAL = 1; + private static final int QUEUE_SIZE = 256; + + @Test + void shouldIgnoreTracesWithNoMeasuredSpans() throws Exception { + Sink sink = mock(Sink.class); + DDAgentFeaturesDiscovery features = mock(DDAgentFeaturesDiscovery.class); + when(features.supportsMetrics()).thenReturn(true); + WellKnownTags wellKnownTags = + new WellKnownTags("runtimeid", "hostname", "env", "service", "version", "language"); + ClientStatsAggregator aggregator = + new ClientStatsAggregator( + wellKnownTags, + emptySet(), + AdditionalTagsSchema.EMPTY, + features, + HealthMetrics.NO_OP, + sink, + 10, + QUEUE_SIZE, + 1, + MILLISECONDS, + false); + aggregator.start(); + + aggregator.publish( + Collections.singletonList( + new SimpleSpan("", "", "", "", false, false, false, 0, 0, HTTP_OK))); + + waitUntilAggregatorIsEmpty(aggregator); + clearInvocations(sink); + aggregator.forceReport().get(2, SECONDS); + + verifyNoInteractions(sink); + + aggregator.close(); + } + + @Test + void shouldIgnoreTracesWithIgnoredResourceNames() throws Exception { + String ignoredResourceName = "foo"; + Sink sink = mock(Sink.class); + DDAgentFeaturesDiscovery features = mock(DDAgentFeaturesDiscovery.class); + when(features.supportsMetrics()).thenReturn(true); + WellKnownTags wellKnownTags = + new WellKnownTags("runtimeid", "hostname", "env", "service", "version", "language"); + Set ignoredResources = Collections.singleton(ignoredResourceName); + ClientStatsAggregator aggregator = + new ClientStatsAggregator( + wellKnownTags, + ignoredResources, + AdditionalTagsSchema.EMPTY, + features, + HealthMetrics.NO_OP, + sink, + 10, + QUEUE_SIZE, + 1, + MILLISECONDS, + false); + aggregator.start(); + clearInvocations(sink); + + // publish ignored resource names + aggregator.publish( + Collections.singletonList( + new SimpleSpan("", "", ignoredResourceName, "", true, true, false, 0, 0, HTTP_OK))); + aggregator.publish( + Collections.singletonList( + new SimpleSpan( + "", + "", + UTF8BytesString.create(ignoredResourceName), + "", + true, + true, + false, + 0, + 0, + HTTP_OK))); + aggregator.publish( + Arrays.asList( + new SimpleSpan("", "", ignoredResourceName, "", true, true, false, 0, 0, HTTP_OK), + new SimpleSpan( + "", + "", + "measured, not ignored, but child of ignored, so should be ignored", + "", + true, + true, + false, + 0, + 0, + HTTP_OK))); + aggregator.forceReport().get(2, SECONDS); + + verifyNoInteractions(sink); + + aggregator.close(); + } + + @Test + void shouldBeResilientToNullResourceNames() throws Exception { + MetricWriter writer = mock(MetricWriter.class); + Sink sink = mock(Sink.class); + DDAgentFeaturesDiscovery features = mock(DDAgentFeaturesDiscovery.class); + when(features.supportsMetrics()).thenReturn(true); + when(features.peerTags()).thenReturn(emptySet()); + ClientStatsAggregator aggregator = + new ClientStatsAggregator( + emptySet(), + features, + HealthMetrics.NO_OP, + sink, + writer, + 10, + QUEUE_SIZE, + REPORTING_INTERVAL, + SECONDS, + false); + aggregator.start(); + + CountDownLatch latch = new CountDownLatch(1); + AggregateEntry expectedEntry = + AggregateEntryTestUtils.of( + null, + "service", + "operation", + null, + "type", + HTTP_OK, + false, + false, + "baz", + emptyList(), + null, + null, + null); + doAnswer( + invocation -> { + AggregateEntry e = invocation.getArgument(0); + assertTrue(AggregateEntryTestUtils.equals(e, expectedEntry)); + assertEquals(1, e.getHitCount()); + assertEquals(1, e.getTopLevelCount()); + assertEquals(100, e.getDuration()); + return null; + }) + .when(writer) + .add(any(AggregateEntry.class)); + doAnswer( + invocation -> { + latch.countDown(); + return null; + }) + .when(writer) + .finishBucket(); + + aggregator.publish( + Collections.singletonList( + new SimpleSpan( + "service", "operation", null, "type", false, true, false, 0, 100, HTTP_OK) + .setTag(Tags.SPAN_KIND, "baz"))); + aggregator.report(); + boolean latchTriggered = latch.await(2, SECONDS); + + assertTrue(latchTriggered); + verify(writer).startBucket(eq(1), anyLong(), anyLong()); + verify(writer, times(1)).add(any(AggregateEntry.class)); + verify(writer).finishBucket(); + + aggregator.close(); + } + + @Test + void unmeasuredTopLevelSpansHaveMetricsComputed() throws Exception { + MetricWriter writer = mock(MetricWriter.class); + Sink sink = mock(Sink.class); + DDAgentFeaturesDiscovery features = mock(DDAgentFeaturesDiscovery.class); + when(features.supportsMetrics()).thenReturn(true); + when(features.peerTags()).thenReturn(emptySet()); + ClientStatsAggregator aggregator = + new ClientStatsAggregator( + emptySet(), + features, + HealthMetrics.NO_OP, + sink, + writer, + 10, + QUEUE_SIZE, + REPORTING_INTERVAL, + SECONDS, + false); + aggregator.start(); + + CountDownLatch latch = new CountDownLatch(1); + AggregateEntry expectedEntry = + AggregateEntryTestUtils.of( + "resource", + "service", + "operation", + null, + "type", + HTTP_OK, + false, + false, + "baz", + emptyList(), + null, + null, + null); + doAnswer( + invocation -> { + AggregateEntry e = invocation.getArgument(0); + assertTrue(AggregateEntryTestUtils.equals(e, expectedEntry)); + assertEquals(1, e.getHitCount()); + assertEquals(1, e.getTopLevelCount()); + assertEquals(100, e.getDuration()); + return null; + }) + .when(writer) + .add(any(AggregateEntry.class)); + doAnswer( + invocation -> { + latch.countDown(); + return null; + }) + .when(writer) + .finishBucket(); + + aggregator.publish( + Collections.singletonList( + new SimpleSpan( + "service", "operation", "resource", "type", false, true, false, 0, 100, HTTP_OK) + .setTag(Tags.SPAN_KIND, "baz"))); + aggregator.report(); + boolean latchTriggered = latch.await(2, SECONDS); + + assertTrue(latchTriggered); + verify(writer).startBucket(eq(1), anyLong(), anyLong()); + verify(writer, times(1)).add(any(AggregateEntry.class)); + verify(writer).finishBucket(); + + aggregator.close(); + } + + @TableTest({ + "scenario | kind | httpMethod | httpEndpoint | statsComputed", + "client | client | | | true ", + "producer | producer | | | true ", + "consumer | consumer | | | true ", + "server (UTF8BytesString) | UTF8.server | | | true ", + "internal | internal | | | false ", + "null kind | | | | false ", + "server GET /api/users/:id | server | GET | /api/users/:id | true ", + "server POST /api/orders | server | POST | /api/orders | true ", + "server DELETE /api/products/:id | server | DELETE | /api/products/:id | true ", + "client GET /external/api | client | GET | /external/api | true " + }) + void shouldComputeStatsForSpanKind( + @ConvertWith(StringOrUTF8ByteStringConverter.class) CharSequence kind, + String httpMethod, + String httpEndpoint, + boolean statsComputed) + throws Exception { + MetricWriter writer = mock(MetricWriter.class); + Sink sink = mock(Sink.class); + DDAgentFeaturesDiscovery features = mock(DDAgentFeaturesDiscovery.class); + when(features.supportsMetrics()).thenReturn(true); + when(features.peerTags()).thenReturn(emptySet()); + ClientStatsAggregator aggregator = + new ClientStatsAggregator( + emptySet(), + features, + HealthMetrics.NO_OP, + sink, + writer, + 10, + QUEUE_SIZE, + REPORTING_INTERVAL, + SECONDS, + true); + aggregator.start(); + + CountDownLatch latch = new CountDownLatch(1); + if (statsComputed) { + AggregateEntry expectedEntry = + AggregateEntryTestUtils.of( + "resource", + "service", + "operation", + null, + "type", + HTTP_OK, + false, + false, + kind == null ? null : kind.toString(), + emptyList(), + httpMethod, + httpEndpoint, + null); + doAnswer( + invocation -> { + AggregateEntry e = invocation.getArgument(0); + assertTrue(AggregateEntryTestUtils.equals(e, expectedEntry)); + assertEquals(1, e.getHitCount()); + assertEquals(0, e.getTopLevelCount()); + assertEquals(100, e.getDuration()); + return null; + }) + .when(writer) + .add(any(AggregateEntry.class)); + doAnswer( + invocation -> { + latch.countDown(); + return null; + }) + .when(writer) + .finishBucket(); + } + + SimpleSpan span = + new SimpleSpan( + "service", "operation", "resource", "type", false, false, false, 0, 100, HTTP_OK) + .setTag(Tags.SPAN_KIND, kind); + if (httpMethod != null) { + span.setTag("http.method", httpMethod); + } + if (httpEndpoint != null) { + span.setTag("http.endpoint", httpEndpoint); + } + aggregator.publish(Collections.singletonList(span)); + aggregator.report(); + boolean latchTriggered = latch.await(2, SECONDS); + + assertEquals(statsComputed, latchTriggered); + verify(writer, times(statsComputed ? 1 : 0)).startBucket(eq(1), anyLong(), anyLong()); + verify(writer, times(statsComputed ? 1 : 0)).add(any(AggregateEntry.class)); + verify(writer, times(statsComputed ? 1 : 0)).finishBucket(); + + aggregator.close(); + } + + static class StringOrUTF8ByteStringConverter implements ArgumentConverter { + public static final String UTF_8_PREFIX = "UTF8."; + + @Override + public Object convert(Object source, ParameterContext context) + throws ArgumentConversionException { + if (source == null) { + return null; + } + String s = source.toString(); + if (s.isEmpty()) return null; + if (s.startsWith(UTF_8_PREFIX)) { + return UTF8BytesString.create(s.substring(UTF_8_PREFIX.length())); + } + return s; + } + } + + @Test + void shouldCreateSeparateBucketsForDistinctPeerTagValues() throws Exception { + // Peer-tag NAMES are configured per-tracer and stable for the duration of a trace publish; + // peer-tag VALUES vary per-span. Two spans with the same names but different values should + // produce two distinct aggregate buckets. + MetricWriter writer = mock(MetricWriter.class); + Sink sink = mock(Sink.class); + DDAgentFeaturesDiscovery features = mock(DDAgentFeaturesDiscovery.class); + when(features.supportsMetrics()).thenReturn(true); + when(features.peerTags()) + .thenReturn(new LinkedHashSet<>(Arrays.asList("country", "georegion"))); + + ClientStatsAggregator aggregator = + new ClientStatsAggregator( + emptySet(), + features, + HealthMetrics.NO_OP, + sink, + writer, + 10, + QUEUE_SIZE, + REPORTING_INTERVAL, + SECONDS, + false); + aggregator.start(); + + CountDownLatch latch = new CountDownLatch(1); + + AggregateEntry expectedFranceEntry = + AggregateEntryTestUtils.of( + "resource", + "service", + "operation", + null, + "type", + HTTP_OK, + false, + false, + "client", + Arrays.asList( + UTF8BytesString.create("country:france"), + UTF8BytesString.create("georegion:europe")), + null, + null, + null); + AggregateEntry expectedGermanyEntry = + AggregateEntryTestUtils.of( + "resource", + "service", + "operation", + null, + "type", + HTTP_OK, + false, + false, + "client", + Arrays.asList( + UTF8BytesString.create("country:germany"), + UTF8BytesString.create("georegion:europe")), + null, + null, + null); + doAnswer( + invocation -> { + AggregateEntry e = invocation.getArgument(0); + if (AggregateEntryTestUtils.equals(e, expectedFranceEntry) + || AggregateEntryTestUtils.equals(e, expectedGermanyEntry)) { + assertEquals(1, e.getHitCount()); + assertEquals(0, e.getTopLevelCount()); + assertEquals(100, e.getDuration()); + } else { + throw new AssertionError("Unexpected AggregateEntry in add(): " + e); + } + return null; + }) + .when(writer) + .add(any(AggregateEntry.class)); + doAnswer( + invocation -> { + latch.countDown(); + return null; + }) + .when(writer) + .finishBucket(); + + aggregator.publish( + Arrays.asList( + new SimpleSpan( + "service", "operation", "resource", "type", true, false, false, 0, 100, HTTP_OK) + .setTag(Tags.SPAN_KIND, "client") + .setTag("country", "france") + .setTag("georegion", "europe"), + new SimpleSpan( + "service", "operation", "resource", "type", true, false, false, 0, 100, HTTP_OK) + .setTag(Tags.SPAN_KIND, "client") + .setTag("country", "germany") + .setTag("georegion", "europe"))); + aggregator.report(); + boolean latchTriggered = latch.await(2, SECONDS); + + assertTrue(latchTriggered); + verify(writer, times(1)).startBucket(eq(2), anyLong(), anyLong()); + verify(writer, times(1)) + .add(argThat(e -> AggregateEntryTestUtils.equals(e, expectedFranceEntry))); + verify(writer, times(1)) + .add(argThat(e -> AggregateEntryTestUtils.equals(e, expectedGermanyEntry))); + verify(writer, times(1)).finishBucket(); + + aggregator.close(); + } + + @TableTest({ + "scenario | kind | expectedPeerTagStrings ", + "client | client | ['peer.hostname:localhost', '_dd.base_service:test']", + "internal | internal | ['_dd.base_service:test'] ", + "server | server | [] " + }) + void shouldAggregateTheRightPeerTagsForKind( + @ConvertWith(StringOrUTF8ByteStringConverter.class) CharSequence kind, + List expectedPeerTagStrings) + throws Exception { + List expectedPeerTags = new ArrayList<>(); + for (String tag : expectedPeerTagStrings) { + expectedPeerTags.add(UTF8BytesString.create(tag)); + } + MetricWriter writer = mock(MetricWriter.class); + Sink sink = mock(Sink.class); + DDAgentFeaturesDiscovery features = mock(DDAgentFeaturesDiscovery.class); + when(features.supportsMetrics()).thenReturn(true); + when(features.peerTags()) + .thenReturn(new LinkedHashSet<>(Arrays.asList("peer.hostname", "_dd.base_service"))); + ClientStatsAggregator aggregator = + new ClientStatsAggregator( + emptySet(), + features, + HealthMetrics.NO_OP, + sink, + writer, + 10, + QUEUE_SIZE, + REPORTING_INTERVAL, + SECONDS, + false); + aggregator.start(); + + CountDownLatch latch = new CountDownLatch(1); + AggregateEntry expectedEntry = + AggregateEntryTestUtils.of( + "resource", + "service", + "operation", + null, + "type", + HTTP_OK, + false, + false, + kind, + expectedPeerTags, + null, + null, + null); + doAnswer( + invocation -> { + AggregateEntry e = invocation.getArgument(0); + assertTrue(AggregateEntryTestUtils.equals(e, expectedEntry)); + assertEquals(1, e.getHitCount()); + assertEquals(0, e.getTopLevelCount()); + assertEquals(100, e.getDuration()); + return null; + }) + .when(writer) + .add(any(AggregateEntry.class)); + doAnswer( + invocation -> { + latch.countDown(); + return null; + }) + .when(writer) + .finishBucket(); + + aggregator.publish( + Collections.singletonList( + new SimpleSpan( + "service", "operation", "resource", "type", true, false, false, 0, 100, HTTP_OK) + .setTag(Tags.SPAN_KIND, kind) + .setTag("peer.hostname", "localhost") + .setTag("_dd.base_service", UTF8BytesString.create("test")))); + aggregator.report(); + boolean latchTriggered = latch.await(2, SECONDS); + + assertTrue(latchTriggered); + verify(writer).startBucket(eq(1), anyLong(), anyLong()); + verify(writer, times(1)).add(any(AggregateEntry.class)); + verify(writer).finishBucket(); + + aggregator.close(); + } + + @TableTest({ + "scenario | measured | topLevel | topLevelCount", + "measured no top level | true | false | 0 ", + "measured and top level | true | true | 1 ", + "not measured but top level | false | true | 1 " + }) + void measuredSpansDoNotContributeToTopLevelCount( + boolean measured, boolean topLevel, int topLevelCount) throws Exception { + MetricWriter writer = mock(MetricWriter.class); + Sink sink = mock(Sink.class); + DDAgentFeaturesDiscovery features = mock(DDAgentFeaturesDiscovery.class); + when(features.supportsMetrics()).thenReturn(true); + when(features.peerTags()).thenReturn(emptySet()); + ClientStatsAggregator aggregator = + new ClientStatsAggregator( + emptySet(), + features, + HealthMetrics.NO_OP, + sink, + writer, + 10, + QUEUE_SIZE, + REPORTING_INTERVAL, + SECONDS, + false); + aggregator.start(); + + CountDownLatch latch = new CountDownLatch(1); + AggregateEntry expectedEntry = + AggregateEntryTestUtils.of( + "resource", + "service", + "operation", + null, + "type", + HTTP_OK, + false, + false, + "baz", + emptyList(), + null, + null, + null); + doAnswer( + invocation -> { + AggregateEntry e = invocation.getArgument(0); + assertTrue(AggregateEntryTestUtils.equals(e, expectedEntry)); + assertEquals(1, e.getHitCount()); + assertEquals(topLevelCount, e.getTopLevelCount()); + assertEquals(100, e.getDuration()); + return null; + }) + .when(writer) + .add(any(AggregateEntry.class)); + doAnswer( + invocation -> { + latch.countDown(); + return null; + }) + .when(writer) + .finishBucket(); + + aggregator.publish( + Collections.singletonList( + new SimpleSpan( + "service", + "operation", + "resource", + "type", + measured, + topLevel, + false, + 0, + 100, + HTTP_OK) + .setTag(Tags.SPAN_KIND, "baz"))); + aggregator.report(); + boolean latchTriggered = latch.await(2, SECONDS); + + assertTrue(latchTriggered); + verify(writer).startBucket(eq(1), anyLong(), anyLong()); + verify(writer, times(1)).add(any(AggregateEntry.class)); + verify(writer).finishBucket(); + + aggregator.close(); + } + + @TableTest({"scenario | count", "count = 10 | 10 ", "count = 100 | 100 "}) + void aggregateRepetitiveSpans(int count) throws Exception { + MetricWriter writer = mock(MetricWriter.class); + Sink sink = mock(Sink.class); + DDAgentFeaturesDiscovery features = mock(DDAgentFeaturesDiscovery.class); + when(features.supportsMetrics()).thenReturn(true); + when(features.peerTags()).thenReturn(emptySet()); + ClientStatsAggregator aggregator = + new ClientStatsAggregator( + emptySet(), + features, + HealthMetrics.NO_OP, + sink, + writer, + 10, + QUEUE_SIZE, + REPORTING_INTERVAL, + SECONDS, + false); + long duration = 100; + List trace = + Arrays.asList( + new SimpleSpan( + "service", + "operation", + "resource", + "type", + true, + false, + false, + 0, + duration, + HTTP_OK) + .setTag(Tags.SPAN_KIND, "baz"), + new SimpleSpan( + "service1", + "operation1", + "resource1", + "type", + false, + false, + false, + 0, + 0, + HTTP_OK) + .setTag(Tags.SPAN_KIND, "baz"), + new SimpleSpan( + "service2", + "operation2", + "resource2", + "type", + true, + false, + false, + 0, + duration * 2, + HTTP_OK) + .setTag(Tags.SPAN_KIND, "baz")); + aggregator.start(); + + CountDownLatch latch = new CountDownLatch(1); + AggregateEntry expectedEntry1 = + AggregateEntryTestUtils.of( + "resource", + "service", + "operation", + null, + "type", + HTTP_OK, + false, + false, + "baz", + emptyList(), + null, + null, + null); + AggregateEntry expectedEntry2 = + AggregateEntryTestUtils.of( + "resource2", + "service2", + "operation2", + null, + "type", + HTTP_OK, + false, + false, + "baz", + emptyList(), + null, + null, + null); + doAnswer( + invocation -> { + AggregateEntry e = invocation.getArgument(0); + if (AggregateEntryTestUtils.equals(e, expectedEntry1)) { + assertEquals(count, e.getHitCount()); + assertEquals(count * duration, e.getDuration()); + } else if (AggregateEntryTestUtils.equals(e, expectedEntry2)) { + assertEquals(count, e.getHitCount()); + assertEquals(count * duration * 2, e.getDuration()); + } else { + throw new AssertionError("Unexpected AggregateEntry in add()"); + } + return null; + }) + .when(writer) + .add(any(AggregateEntry.class)); + doAnswer( + invocation -> { + latch.countDown(); + return null; + }) + .when(writer) + .finishBucket(); + + for (int i = 0; i < count; ++i) { + aggregator.publish(trace); + } + aggregator.report(); + boolean latchTriggered = latch.await(2, SECONDS); + + // metrics should be conflated + assertTrue(latchTriggered); + verify(writer).finishBucket(); + verify(writer).startBucket(eq(2), anyLong(), eq(SECONDS.toNanos(REPORTING_INTERVAL))); + verify(writer, times(1)).add(argThat(e -> AggregateEntryTestUtils.equals(e, expectedEntry1))); + verify(writer, times(1)).add(argThat(e -> AggregateEntryTestUtils.equals(e, expectedEntry2))); + + aggregator.close(); + } + + @Test + void aggregateSpansWithSameHttpEndpointTogetherSeparateDifferentEndpoints() throws Exception { + MetricWriter writer = mock(MetricWriter.class); + Sink sink = mock(Sink.class); + DDAgentFeaturesDiscovery features = mock(DDAgentFeaturesDiscovery.class); + when(features.supportsMetrics()).thenReturn(true); + when(features.peerTags()).thenReturn(emptySet()); + ClientStatsAggregator aggregator = + new ClientStatsAggregator( + emptySet(), + features, + HealthMetrics.NO_OP, + sink, + writer, + 10, + QUEUE_SIZE, + REPORTING_INTERVAL, + SECONDS, + true); + aggregator.start(); + + // Cycle 1: publish multiple spans with same endpoint + int count = 5; + long duration = 100; + CountDownLatch latch = new CountDownLatch(1); + CountDownLatch latch2 = new CountDownLatch(1); + + AggregateEntry expectedGetUsers = + AggregateEntryTestUtils.of( + "resource", + "service", + "operation", + null, + "type", + HTTP_OK, + false, + false, + "server", + emptyList(), + "GET", + "/api/users/:id", + null); + AggregateEntry expectedGetOrders = + AggregateEntryTestUtils.of( + "resource", + "service", + "operation", + null, + "type", + HTTP_OK, + false, + false, + "server", + emptyList(), + "GET", + "/api/orders/:id", + null); + AggregateEntry expectedPostUsers = + AggregateEntryTestUtils.of( + "resource", + "service", + "operation", + null, + "type", + HTTP_OK, + false, + false, + "server", + emptyList(), + "POST", + "/api/users/:id", + null); + + AtomicInteger cycle = new AtomicInteger(1); + doAnswer( + invocation -> { + AggregateEntry e = invocation.getArgument(0); + if (cycle.get() == 1) { + // should aggregate into single metric + assertTrue(AggregateEntryTestUtils.equals(e, expectedGetUsers)); + assertEquals(count, e.getHitCount()); + assertEquals(count * duration, e.getDuration()); + } else { + // separate metrics for each endpoint/method combination + if (AggregateEntryTestUtils.equals(e, expectedGetUsers)) { + assertEquals(1, e.getHitCount()); + assertEquals(duration, e.getDuration()); + } else if (AggregateEntryTestUtils.equals(e, expectedGetOrders)) { + assertEquals(1, e.getHitCount()); + assertEquals(duration * 2, e.getDuration()); + } else if (AggregateEntryTestUtils.equals(e, expectedPostUsers)) { + assertEquals(1, e.getHitCount()); + assertEquals(duration * 3, e.getDuration()); + } else { + throw new AssertionError("Unexpected AggregateEntry in cycle 2 add()"); + } + } + return null; + }) + .when(writer) + .add(any(AggregateEntry.class)); + doAnswer( + invocation -> { + cycle.incrementAndGet(); + latch.countDown(); + return null; + }) + .doAnswer( + invocation -> { + latch2.countDown(); + return null; + }) + .when(writer) + .finishBucket(); + + for (int i = 0; i < count; ++i) { + aggregator.publish( + Collections.singletonList( + new SimpleSpan( + "service", + "operation", + "resource", + "type", + true, + false, + false, + 0, + duration, + HTTP_OK) + .setTag(Tags.SPAN_KIND, "server") + .setTag("http.method", "GET") + .setTag("http.endpoint", "/api/users/:id"))); + } + aggregator.report(); + boolean latchTriggered = latch.await(2, SECONDS); + + // should aggregate into single metric + assertTrue(latchTriggered); + verify(writer, times(1)).startBucket(eq(1), anyLong(), anyLong()); + verify(writer, times(1)).add(argThat(e -> AggregateEntryTestUtils.equals(e, expectedGetUsers))); + + // publish spans with different endpoints + aggregator.publish( + Arrays.asList( + new SimpleSpan( + "service", + "operation", + "resource", + "type", + true, + false, + false, + 0, + duration, + HTTP_OK) + .setTag(Tags.SPAN_KIND, "server") + .setTag("http.method", "GET") + .setTag("http.endpoint", "/api/users/:id"), + new SimpleSpan( + "service", + "operation", + "resource", + "type", + true, + false, + false, + 0, + duration * 2, + HTTP_OK) + .setTag(Tags.SPAN_KIND, "server") + .setTag("http.method", "GET") + .setTag("http.endpoint", "/api/orders/:id"), + new SimpleSpan( + "service", + "operation", + "resource", + "type", + true, + false, + false, + 0, + duration * 3, + HTTP_OK) + .setTag(Tags.SPAN_KIND, "server") + .setTag("http.method", "POST") + .setTag("http.endpoint", "/api/users/:id"))); + aggregator.report(); + boolean latchTriggered2 = latch2.await(2, SECONDS); + + // should create separate metrics for each endpoint/method combination + assertTrue(latchTriggered2); + verify(writer, times(1)).startBucket(eq(3), anyLong(), anyLong()); + verify(writer, times(2)).add(argThat(e -> AggregateEntryTestUtils.equals(e, expectedGetUsers))); + verify(writer, times(1)) + .add(argThat(e -> AggregateEntryTestUtils.equals(e, expectedGetOrders))); + verify(writer, times(1)) + .add(argThat(e -> AggregateEntryTestUtils.equals(e, expectedPostUsers))); + verify(writer, times(2)).finishBucket(); + + aggregator.close(); + } + + @Test + void createSeparateMetricsForDifferentHttpMethodEndpointStatusCombinations() throws Exception { + MetricWriter writer = mock(MetricWriter.class); + Sink sink = mock(Sink.class); + DDAgentFeaturesDiscovery features = mock(DDAgentFeaturesDiscovery.class); + when(features.supportsMetrics()).thenReturn(true); + when(features.peerTags()).thenReturn(emptySet()); + ClientStatsAggregator aggregator = + new ClientStatsAggregator( + emptySet(), + features, + HealthMetrics.NO_OP, + sink, + writer, + 10, + QUEUE_SIZE, + REPORTING_INTERVAL, + SECONDS, + true); + aggregator.start(); + + // publish spans with different combinations + CountDownLatch latch = new CountDownLatch(1); + long duration = 100; + + AggregateEntry expectedGet200Users = + AggregateEntryTestUtils.of( + "resource", + "service", + "operation", + null, + "type", + 200, + false, + false, + "server", + emptyList(), + "GET", + "/api/users/:id", + null); + AggregateEntry expectedPost200Users = + AggregateEntryTestUtils.of( + "resource", + "service", + "operation", + null, + "type", + 200, + false, + false, + "server", + emptyList(), + "POST", + "/api/users/:id", + null); + AggregateEntry expectedGet404Users = + AggregateEntryTestUtils.of( + "resource", + "service", + "operation", + null, + "type", + 404, + false, + false, + "server", + emptyList(), + "GET", + "/api/users/:id", + null); + AggregateEntry expectedGet200Orders = + AggregateEntryTestUtils.of( + "resource", + "service", + "operation", + null, + "type", + 200, + false, + false, + "server", + emptyList(), + "GET", + "/api/orders/:id", + null); + + doAnswer( + invocation -> { + AggregateEntry e = invocation.getArgument(0); + if (AggregateEntryTestUtils.equals(e, expectedGet200Users)) { + assertEquals(1, e.getHitCount()); + assertEquals(duration, e.getDuration()); + } else if (AggregateEntryTestUtils.equals(e, expectedPost200Users)) { + assertEquals(1, e.getHitCount()); + assertEquals(duration * 2, e.getDuration()); + } else if (AggregateEntryTestUtils.equals(e, expectedGet404Users)) { + assertEquals(1, e.getHitCount()); + assertEquals(duration * 3, e.getDuration()); + } else if (AggregateEntryTestUtils.equals(e, expectedGet200Orders)) { + assertEquals(1, e.getHitCount()); + assertEquals(duration * 4, e.getDuration()); + } else { + throw new AssertionError("Unexpected AggregateEntry in add()"); + } + return null; + }) + .when(writer) + .add(any(AggregateEntry.class)); + doAnswer( + invocation -> { + latch.countDown(); + return null; + }) + .when(writer) + .finishBucket(); + + aggregator.publish( + Arrays.asList( + // Same endpoint, different methods + new SimpleSpan( + "service", + "operation", + "resource", + "type", + true, + false, + false, + 0, + duration, + 200) + .setTag(Tags.SPAN_KIND, "server") + .setTag("http.method", "GET") + .setTag("http.endpoint", "/api/users/:id"), + new SimpleSpan( + "service", + "operation", + "resource", + "type", + true, + false, + false, + 0, + duration * 2, + 200) + .setTag(Tags.SPAN_KIND, "server") + .setTag("http.method", "POST") + .setTag("http.endpoint", "/api/users/:id"), + // Same method/endpoint, different status + new SimpleSpan( + "service", + "operation", + "resource", + "type", + true, + false, + false, + 0, + duration * 3, + 404) + .setTag(Tags.SPAN_KIND, "server") + .setTag("http.method", "GET") + .setTag("http.endpoint", "/api/users/:id"), + // Different endpoint + new SimpleSpan( + "service", + "operation", + "resource", + "type", + true, + false, + false, + 0, + duration * 4, + 200) + .setTag(Tags.SPAN_KIND, "server") + .setTag("http.method", "GET") + .setTag("http.endpoint", "/api/orders/:id"))); + aggregator.report(); + boolean latchTriggered = latch.await(2, SECONDS); + + // should create 4 separate metrics + assertTrue(latchTriggered); + verify(writer).startBucket(eq(4), anyLong(), anyLong()); + verify(writer, times(1)) + .add(argThat(e -> AggregateEntryTestUtils.equals(e, expectedGet200Users))); + verify(writer, times(1)) + .add(argThat(e -> AggregateEntryTestUtils.equals(e, expectedPost200Users))); + verify(writer, times(1)) + .add(argThat(e -> AggregateEntryTestUtils.equals(e, expectedGet404Users))); + verify(writer, times(1)) + .add(argThat(e -> AggregateEntryTestUtils.equals(e, expectedGet200Orders))); + verify(writer).finishBucket(); + + aggregator.close(); + } + + @Test + void handleSpansWithoutHttpEndpointTagsForBackwardCompatibility() throws Exception { + MetricWriter writer = mock(MetricWriter.class); + Sink sink = mock(Sink.class); + DDAgentFeaturesDiscovery features = mock(DDAgentFeaturesDiscovery.class); + when(features.supportsMetrics()).thenReturn(true); + when(features.peerTags()).thenReturn(emptySet()); + ClientStatsAggregator aggregator = + new ClientStatsAggregator( + emptySet(), + features, + HealthMetrics.NO_OP, + sink, + writer, + 10, + QUEUE_SIZE, + REPORTING_INTERVAL, + SECONDS, + true); + aggregator.start(); + + // publish spans with and without HTTP tags + CountDownLatch latch = new CountDownLatch(1); + long duration = 100; + + AggregateEntry expectedNoHttpTags = + AggregateEntryTestUtils.of( + "resource", + "service", + "operation", + null, + "type", + 200, + false, + false, + "server", + emptyList(), + null, + null, + null); + AggregateEntry expectedWithHttpTags = + AggregateEntryTestUtils.of( + "resource", + "service", + "operation", + null, + "type", + 200, + false, + false, + "server", + emptyList(), + "GET", + "/api/users/:id", + null); + + doAnswer( + invocation -> { + AggregateEntry e = invocation.getArgument(0); + if (AggregateEntryTestUtils.equals(e, expectedNoHttpTags)) { + assertEquals(1, e.getHitCount()); + assertEquals(duration, e.getDuration()); + } else if (AggregateEntryTestUtils.equals(e, expectedWithHttpTags)) { + assertEquals(1, e.getHitCount()); + assertEquals(duration * 2, e.getDuration()); + } else { + throw new AssertionError("Unexpected AggregateEntry in add()"); + } + return null; + }) + .when(writer) + .add(any(AggregateEntry.class)); + doAnswer( + invocation -> { + latch.countDown(); + return null; + }) + .when(writer) + .finishBucket(); + + aggregator.publish( + Arrays.asList( + // Span without HTTP tags (legacy behavior) + new SimpleSpan( + "service", + "operation", + "resource", + "type", + true, + false, + false, + 0, + duration, + 200) + .setTag(Tags.SPAN_KIND, "server"), + // Span with HTTP tags (new behavior) + new SimpleSpan( + "service", + "operation", + "resource", + "type", + true, + false, + false, + 0, + duration * 2, + 200) + .setTag(Tags.SPAN_KIND, "server") + .setTag("http.method", "GET") + .setTag("http.endpoint", "/api/users/:id"))); + aggregator.report(); + boolean latchTriggered = latch.await(2, SECONDS); + + // should create separate metric keys for spans with and without HTTP tags + assertTrue(latchTriggered); + verify(writer).startBucket(eq(2), anyLong(), anyLong()); + verify(writer, times(1)) + .add(argThat(e -> AggregateEntryTestUtils.equals(e, expectedNoHttpTags))); + verify(writer, times(1)) + .add(argThat(e -> AggregateEntryTestUtils.equals(e, expectedWithHttpTags))); + verify(writer).finishBucket(); + + aggregator.close(); + } + + @Test + void gatherTheServiceNameSourceWhenTheSpanIsPublished() throws Exception { + MetricWriter writer = mock(MetricWriter.class); + Sink sink = mock(Sink.class); + DDAgentFeaturesDiscovery features = mock(DDAgentFeaturesDiscovery.class); + when(features.supportsMetrics()).thenReturn(true); + when(features.peerTags()).thenReturn(emptySet()); + ClientStatsAggregator aggregator = + new ClientStatsAggregator( + emptySet(), + features, + HealthMetrics.NO_OP, + sink, + writer, + 10, + QUEUE_SIZE, + REPORTING_INTERVAL, + SECONDS, + false); + aggregator.start(); + + // publish spans with different service name source + CountDownLatch latch = new CountDownLatch(1); + long duration = 100; + + AggregateEntry expectedWithSource = + AggregateEntryTestUtils.of( + "resource", + "service", + "operation", + "source", + "type", + 200, + false, + false, + "server", + emptyList(), + null, + null, + null); + AggregateEntry expectedWithoutSource = + AggregateEntryTestUtils.of( + "resource", + "service", + "operation", + null, + "type", + 200, + false, + false, + "server", + emptyList(), + null, + null, + null); + + doAnswer( + invocation -> { + AggregateEntry e = invocation.getArgument(0); + if (AggregateEntryTestUtils.equals(e, expectedWithSource)) { + assertEquals(2, e.getHitCount()); + assertEquals(2 * duration, e.getDuration()); + } else if (AggregateEntryTestUtils.equals(e, expectedWithoutSource)) { + assertEquals(1, e.getHitCount()); + assertEquals(duration, e.getDuration()); + } else { + throw new AssertionError("Unexpected AggregateEntry in add()"); + } + return null; + }) + .when(writer) + .add(any(AggregateEntry.class)); + doAnswer( + invocation -> { + latch.countDown(); + return null; + }) + .when(writer) + .finishBucket(); + + aggregator.publish( + Arrays.asList( + new SimpleSpan( + "service", + "operation", + "resource", + "type", + true, + true, + false, + 0, + duration, + 200, + false, + 0, + "source") + .setTag(Tags.SPAN_KIND, "server"), + new SimpleSpan( + "service", + "operation", + "resource", + "type", + true, + true, + false, + 0, + duration, + 200, + false, + 0, + null) + .setTag(Tags.SPAN_KIND, "server"), + new SimpleSpan( + "service", + "operation", + "resource", + "type", + true, + true, + false, + 0, + duration, + 200, + false, + 0, + "source") + .setTag(Tags.SPAN_KIND, "server"))); + aggregator.report(); + boolean latchTriggered = latch.await(2, SECONDS); + + // should create the different metric keys for spans with and without sources + assertTrue(latchTriggered); + verify(writer).startBucket(eq(2), anyLong(), anyLong()); + verify(writer, times(1)) + .add(argThat(e -> AggregateEntryTestUtils.equals(e, expectedWithSource))); + verify(writer, times(1)) + .add(argThat(e -> AggregateEntryTestUtils.equals(e, expectedWithoutSource))); + verify(writer).finishBucket(); + + aggregator.close(); + } + + @Test + void newAggregatesBeyondSizeLimitAreDroppedWhenNoStaleEntriesCanBeEvicted() throws Exception { + // The table only evicts entries with hitCount == 0 to make room. When all entries are live + // (all have been recorded against), an over-cap insert drops the new key rather than evicting + // an established one. This protects the data we've already collected from a burst of new keys. + int maxAggregates = 10; + MetricWriter writer = mock(MetricWriter.class); + Sink sink = mock(Sink.class); + DDAgentFeaturesDiscovery features = mock(DDAgentFeaturesDiscovery.class); + when(features.supportsMetrics()).thenReturn(true); + when(features.peerTags()).thenReturn(emptySet()); + ClientStatsAggregator aggregator = + new ClientStatsAggregator( + emptySet(), + features, + HealthMetrics.NO_OP, + sink, + writer, + maxAggregates, + QUEUE_SIZE, + REPORTING_INTERVAL, + SECONDS, + false); + long duration = 100; + aggregator.start(); + + CountDownLatch latch = new CountDownLatch(1); + + AggregateEntry expectedDropped = + AggregateEntryTestUtils.of( + "resource", + "service10", + "operation", + null, + "type", + HTTP_OK, + false, + false, + "baz", + emptyList(), + null, + null, + null); + doAnswer( + invocation -> { + AggregateEntry e = invocation.getArgument(0); + assertEquals(1, e.getHitCount()); + assertEquals(duration, e.getDuration()); + return null; + }) + .when(writer) + .add(any(AggregateEntry.class)); + doAnswer( + invocation -> { + latch.countDown(); + return null; + }) + .when(writer) + .finishBucket(); + + for (int i = 0; i < 11; ++i) { + aggregator.publish( + Collections.singletonList( + new SimpleSpan( + "service" + i, + "operation", + "resource", + "type", + false, + true, + false, + 0, + duration, + HTTP_OK) + .setTag(Tags.SPAN_KIND, "baz"))); + } + aggregator.report(); + boolean latchTriggered = latch.await(2, SECONDS); + + // the established service0..service9 are reported; service10 is dropped + assertTrue(latchTriggered); + verify(writer).startBucket(eq(10), anyLong(), eq(SECONDS.toNanos(REPORTING_INTERVAL))); + for (int i = 0; i < 10; ++i) { + AggregateEntry expected = + AggregateEntryTestUtils.of( + "resource", + "service" + i, + "operation", + null, + "type", + HTTP_OK, + false, + false, + "baz", + emptyList(), + null, + null, + null); + verify(writer, times(1)).add(argThat(e -> AggregateEntryTestUtils.equals(e, expected))); + } + verify(writer, never()).add(argThat(e -> AggregateEntryTestUtils.equals(e, expectedDropped))); + verify(writer).finishBucket(); + + aggregator.close(); + } + + @Test + void shouldReportDroppedAggregateToHealthMetricsOnLruEviction() throws Exception { + int maxAggregates = 10; + MetricWriter writer = mock(MetricWriter.class); + Sink sink = mock(Sink.class); + DDAgentFeaturesDiscovery features = mock(DDAgentFeaturesDiscovery.class); + when(features.supportsMetrics()).thenReturn(true); + when(features.peerTags()).thenReturn(emptySet()); + HealthMetrics healthMetrics = mock(HealthMetrics.class); + ClientStatsAggregator aggregator = + new ClientStatsAggregator( + emptySet(), + features, + healthMetrics, + sink, + writer, + maxAggregates, + QUEUE_SIZE, + REPORTING_INTERVAL, + SECONDS, + false); + long duration = 100; + aggregator.start(); + + CountDownLatch latch = new CountDownLatch(1); + doAnswer( + invocation -> { + latch.countDown(); + return null; + }) + .when(writer) + .finishBucket(); + + for (int i = 0; i < maxAggregates + 1; ++i) { + aggregator.publish( + Collections.singletonList( + new SimpleSpan( + "service" + i, + "operation", + "resource", + "type", + false, + true, + false, + 0, + duration, + HTTP_OK) + .setTag(Tags.SPAN_KIND, "baz"))); + } + aggregator.report(); + boolean latchTriggered = latch.await(2, SECONDS); + + assertTrue(latchTriggered); + verify(writer).finishBucket(); + verify(healthMetrics).onStatsAggregateDropped(); + + aggregator.close(); + } + + @Test + void shouldNotReportDroppedAggregateWhenEvictedEntryWasAlreadyFlushed() throws Exception { + int maxAggregates = 5; + MetricWriter writer = mock(MetricWriter.class); + Sink sink = mock(Sink.class); + DDAgentFeaturesDiscovery features = mock(DDAgentFeaturesDiscovery.class); + when(features.supportsMetrics()).thenReturn(true); + when(features.peerTags()).thenReturn(emptySet()); + HealthMetrics healthMetrics = mock(HealthMetrics.class); + ClientStatsAggregator aggregator = + new ClientStatsAggregator( + emptySet(), + features, + healthMetrics, + sink, + writer, + maxAggregates, + QUEUE_SIZE, + REPORTING_INTERVAL, + SECONDS, + false); + aggregator.start(); + + // fill cache and flush — entries are cleared (hitCount=0) but stay in the LRU + CountDownLatch latch1 = new CountDownLatch(1); + doAnswer( + invocation -> { + latch1.countDown(); + return null; + }) + .when(writer) + .finishBucket(); + + for (int i = 0; i < maxAggregates; ++i) { + aggregator.publish( + Collections.singletonList( + new SimpleSpan( + "service" + i, + "operation", + "resource", + "type", + false, + true, + false, + 0, + 100, + HTTP_OK) + .setTag(Tags.SPAN_KIND, "baz"))); + } + aggregator.report(); + latch1.await(2, SECONDS); + + verify(writer, times(1)).finishBucket(); + + // publish new distinct spans — LRU evicts the cleared entries before the next report + CountDownLatch latch2 = new CountDownLatch(1); + doAnswer( + invocation -> { + latch2.countDown(); + return null; + }) + .when(writer) + .finishBucket(); + + for (int i = maxAggregates; i < maxAggregates * 2; ++i) { + aggregator.publish( + Collections.singletonList( + new SimpleSpan( + "service" + i, + "operation", + "resource", + "type", + false, + true, + false, + 0, + 100, + HTTP_OK) + .setTag(Tags.SPAN_KIND, "baz"))); + } + aggregator.report(); + latch2.await(2, SECONDS); + + // no drop metric because all evicted entries had hitCount=0 (already reported) + verify(writer, times(2)).finishBucket(); + verify(healthMetrics, never()).onStatsAggregateDropped(); + + aggregator.close(); + } + + @Test + void aggregateNotUpdatedInReportingIntervalNotReported() throws Exception { + int maxAggregates = 10; + MetricWriter writer = mock(MetricWriter.class); + Sink sink = mock(Sink.class); + DDAgentFeaturesDiscovery features = mock(DDAgentFeaturesDiscovery.class); + when(features.supportsMetrics()).thenReturn(true); + when(features.peerTags()).thenReturn(emptySet()); + ClientStatsAggregator aggregator = + new ClientStatsAggregator( + emptySet(), + features, + HealthMetrics.NO_OP, + sink, + writer, + maxAggregates, + QUEUE_SIZE, + REPORTING_INTERVAL, + SECONDS, + false); + long duration = 100; + aggregator.start(); + + // first cycle + CountDownLatch latch = new CountDownLatch(1); + doAnswer( + invocation -> { + AggregateEntry e = invocation.getArgument(0); + assertEquals(1, e.getHitCount()); + assertEquals(duration, e.getDuration()); + return null; + }) + .when(writer) + .add(any(AggregateEntry.class)); + doAnswer( + invocation -> { + latch.countDown(); + return null; + }) + .when(writer) + .finishBucket(); + + for (int i = 0; i < 5; ++i) { + aggregator.publish( + Collections.singletonList( + new SimpleSpan( + "service" + i, + "operation", + "resource", + "type", + false, + true, + false, + 0, + duration, + HTTP_OK) + .setTag(Tags.SPAN_KIND, "baz"))); + } + aggregator.report(); + boolean latchTriggered = latch.await(2, SECONDS); + + // all aggregates should be reported + assertTrue(latchTriggered); + verify(writer).startBucket(eq(5), anyLong(), eq(SECONDS.toNanos(REPORTING_INTERVAL))); + for (int i = 0; i < 5; ++i) { + AggregateEntry expected = + AggregateEntryTestUtils.of( + "resource", + "service" + i, + "operation", + null, + "type", + HTTP_OK, + false, + false, + "baz", + emptyList(), + null, + null, + null); + verify(writer, times(1)).add(argThat(e -> AggregateEntryTestUtils.equals(e, expected))); + } + verify(writer, times(1)).finishBucket(); + + // second cycle - service0 not updated + CountDownLatch latch2 = new CountDownLatch(1); + doAnswer( + invocation -> { + latch2.countDown(); + return null; + }) + .when(writer) + .finishBucket(); + + for (int i = 1; i < 5; ++i) { + aggregator.publish( + Collections.singletonList( + new SimpleSpan( + "service" + i, + "operation", + "resource", + "type", + false, + true, + false, + 0, + duration, + HTTP_OK) + .setTag(Tags.SPAN_KIND, "baz"))); + } + aggregator.report(); + boolean latchTriggered2 = latch2.await(2, SECONDS); + + // aggregate not updated in cycle is not reported + assertTrue(latchTriggered2); + verify(writer).startBucket(eq(4), anyLong(), eq(SECONDS.toNanos(REPORTING_INTERVAL))); + for (int i = 1; i < 5; ++i) { + AggregateEntry expected = + AggregateEntryTestUtils.of( + "resource", + "service" + i, + "operation", + null, + "type", + HTTP_OK, + false, + false, + "baz", + emptyList(), + null, + null, + null); + verify(writer, times(2)).add(argThat(e -> AggregateEntryTestUtils.equals(e, expected))); + } + AggregateEntry expectedService0 = + AggregateEntryTestUtils.of( + "resource", + "service0", + "operation", + null, + "type", + HTTP_OK, + false, + false, + "baz", + emptyList(), + null, + null, + null); + verify(writer, times(1)).add(argThat(e -> AggregateEntryTestUtils.equals(e, expectedService0))); + verify(writer, times(2)).finishBucket(); + + aggregator.close(); + } + + @Test + void whenNoAggregateIsUpdatedInReportingIntervalNothingIsReported() throws Exception { + int maxAggregates = 10; + MetricWriter writer = mock(MetricWriter.class); + Sink sink = mock(Sink.class); + DDAgentFeaturesDiscovery features = mock(DDAgentFeaturesDiscovery.class); + when(features.supportsMetrics()).thenReturn(true); + when(features.peerTags()).thenReturn(emptySet()); + ClientStatsAggregator aggregator = + new ClientStatsAggregator( + emptySet(), + features, + HealthMetrics.NO_OP, + sink, + writer, + maxAggregates, + QUEUE_SIZE, + REPORTING_INTERVAL, + SECONDS, + false); + long duration = 100; + aggregator.start(); + + // first cycle + CountDownLatch latch = new CountDownLatch(1); + doAnswer( + invocation -> { + latch.countDown(); + return null; + }) + .when(writer) + .finishBucket(); + + for (int i = 0; i < 5; ++i) { + aggregator.publish( + Collections.singletonList( + new SimpleSpan( + "service" + i, + "operation", + "resource", + "type", + false, + true, + false, + 0, + duration, + HTTP_OK) + .setTag(Tags.SPAN_KIND, "quux"))); + } + aggregator.report(); + boolean latchTriggered = latch.await(2, SECONDS); + + // all aggregates should be reported + assertTrue(latchTriggered); + verify(writer).startBucket(eq(5), anyLong(), eq(SECONDS.toNanos(REPORTING_INTERVAL))); + for (int i = 0; i < 5; ++i) { + AggregateEntry expected = + AggregateEntryTestUtils.of( + "resource", + "service" + i, + "operation", + null, + "type", + HTTP_OK, + false, + false, + "quux", + emptyList(), + null, + null, + null); + verify(writer, times(1)).add(argThat(e -> AggregateEntryTestUtils.equals(e, expected))); + } + verify(writer, times(1)).finishBucket(); + + // second cycle - no updates at all + waitUntilAggregatorIsEmpty(aggregator); + clearInvocations(writer); + aggregator.forceReport().get(2, SECONDS); + + // =aggregate not updated in cycle is not reported + verify(writer, never()).startBucket(anyInt(), anyLong(), anyLong()); + verify(writer, never()).add(any(AggregateEntry.class)); + verify(writer, never()).finishBucket(); + + aggregator.close(); + } + + @Test + void shouldReportPeriodically() throws Exception { + int maxAggregates = 10; + MetricWriter writer = mock(MetricWriter.class); + Sink sink = mock(Sink.class); + DDAgentFeaturesDiscovery features = mock(DDAgentFeaturesDiscovery.class); + when(features.supportsMetrics()).thenReturn(true); + when(features.peerTags()).thenReturn(emptySet()); + ClientStatsAggregator aggregator = + new ClientStatsAggregator( + emptySet(), + features, + HealthMetrics.NO_OP, + sink, + writer, + maxAggregates, + QUEUE_SIZE, + 1, + SECONDS, + false); + long duration = 100; + aggregator.start(); + + CountDownLatch latch = new CountDownLatch(1); + doAnswer( + invocation -> { + AggregateEntry e = invocation.getArgument(0); + assertEquals(1, e.getHitCount()); + assertEquals(duration, e.getDuration()); + return null; + }) + .when(writer) + .add(any(AggregateEntry.class)); + doAnswer( + invocation -> { + latch.countDown(); + return null; + }) + .when(writer) + .finishBucket(); + + for (int i = 0; i < 5; ++i) { + aggregator.publish( + Collections.singletonList( + new SimpleSpan( + "service" + i, + "operation", + "resource", + "type", + false, + true, + false, + 0, + duration, + HTTP_OK, + true) + .setTag(Tags.SPAN_KIND, "garply"))); + } + boolean latchTriggered = latch.await(2, SECONDS); + + // all aggregates should be reported + assertTrue(latchTriggered); + verify(writer).startBucket(eq(5), anyLong(), eq(SECONDS.toNanos(1))); + for (int i = 0; i < 5; ++i) { + AggregateEntry expected = + AggregateEntryTestUtils.of( + "resource", + "service" + i, + "operation", + null, + "type", + HTTP_OK, + false, + true, + "garply", + emptyList(), + null, + null, + null); + verify(writer, times(1)).add(argThat(e -> AggregateEntryTestUtils.equals(e, expected))); + } + verify(writer, times(1)).finishBucket(); + + aggregator.close(); + } + + @Test + void shouldBeResilientToSerializationErrors() throws Exception { + int maxAggregates = 10; + MetricWriter writer = mock(MetricWriter.class); + Sink sink = mock(Sink.class); + DDAgentFeaturesDiscovery features = mock(DDAgentFeaturesDiscovery.class); + when(features.supportsMetrics()).thenReturn(true); + when(features.peerTags()).thenReturn(emptySet()); + ClientStatsAggregator aggregator = + new ClientStatsAggregator( + emptySet(), + features, + HealthMetrics.NO_OP, + sink, + writer, + maxAggregates, + QUEUE_SIZE, + 1, + SECONDS, + false); + long duration = 100; + aggregator.start(); + + CountDownLatch latch = new CountDownLatch(1); + doThrow(new IllegalArgumentException("something went wrong")) + .when(writer) + .startBucket(anyInt(), anyLong(), anyLong()); + doAnswer( + invocation -> { + latch.countDown(); + return null; + }) + .when(writer) + .reset(); + + for (int i = 0; i < 5; ++i) { + aggregator.publish( + Collections.singletonList( + new SimpleSpan( + "service" + i, + "operation", + "resource", + "type", + false, + true, + false, + 0, + duration, + HTTP_OK))); + } + boolean latchTriggered = latch.await(2, SECONDS); + + // writer should be reset if reporting fails + assertTrue(latchTriggered); + verify(writer).startBucket(anyInt(), anyLong(), anyLong()); + verify(writer).reset(); + + aggregator.close(); + } + + @Test + void forceFlushShouldNotBlockIfMetricsAreDisabled() throws Exception { + int maxAggregates = 10; + MetricWriter writer = mock(MetricWriter.class); + Sink sink = mock(Sink.class); + DDAgentFeaturesDiscovery features = mock(DDAgentFeaturesDiscovery.class); + ClientStatsAggregator aggregator = + new ClientStatsAggregator( + emptySet(), + features, + HealthMetrics.NO_OP, + sink, + writer, + maxAggregates, + QUEUE_SIZE, + 1, + SECONDS, + false); + aggregator.start(); + + Boolean flushed = aggregator.forceReport().get(10, SECONDS); + + assertNotNull(flushed); + assertFalse(flushed); + + aggregator.close(); + } + + @Test + void shouldStartEvenIfTheAgentIsNotAvailable() throws Exception { + MetricWriter writer = mock(MetricWriter.class); + Sink sink = mock(Sink.class); + DDAgentFeaturesDiscovery features = mock(DDAgentFeaturesDiscovery.class); + when(features.supportsMetrics()).thenReturn(false); + when(features.peerTags()).thenReturn(emptySet()); + ClientStatsAggregator aggregator = + new ClientStatsAggregator( + emptySet(), + features, + HealthMetrics.NO_OP, + sink, + writer, + 10, + QUEUE_SIZE, + 200, + MILLISECONDS, + false); + List spans = + Collections.singletonList( + new SimpleSpan( + "service", "operation", "resource", "type", false, true, false, 0, 10, HTTP_OK)); + aggregator.start(); + + // metrics not available + aggregator.publish(spans); + Thread.sleep(1_000); + + // no writer calls + verifyNoInteractions(writer); + + // re-enable metrics + when(features.supportsMetrics()).thenReturn(true); + aggregator.publish(spans); + Thread.sleep(1_000); + + // writer called at least once + verify(writer, atLeastOnce()).startBucket(anyInt(), anyLong(), anyLong()); + + aggregator.close(); + } + + @Test + void forceFlushShouldWaitForAggregatorToStart() throws Exception { + int maxAggregates = 10; + MetricWriter writer = mock(MetricWriter.class); + Sink sink = mock(Sink.class); + DDAgentFeaturesDiscovery features = mock(DDAgentFeaturesDiscovery.class); + when(features.supportsMetrics()).thenReturn(true); + ClientStatsAggregator aggregator = + new ClientStatsAggregator( + emptySet(), + features, + HealthMetrics.NO_OP, + sink, + writer, + maxAggregates, + QUEUE_SIZE, + 1, + SECONDS, + false); + + // call forceReport before start + CompletableFuture async = + CompletableFuture.supplyAsync( + () -> { + try { + return aggregator.forceReport().get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + + try { + async.get(3, SECONDS); + throw new AssertionError("Expected TimeoutException"); + } catch (TimeoutException expected) { + // expected + } + + // start aggregator + aggregator.start(); + Boolean flushed = async.get(3, TimeUnit.SECONDS); + + assertNotNull(flushed); + assertTrue(flushed); + + aggregator.close(); + } + + @Test + void shouldNotCountPartialSnapshotLongRunning() throws Exception { + MetricWriter writer = mock(MetricWriter.class); + Sink sink = mock(Sink.class); + DDAgentFeaturesDiscovery features = mock(DDAgentFeaturesDiscovery.class); + when(features.supportsMetrics()).thenReturn(true); + ClientStatsAggregator aggregator = + new ClientStatsAggregator( + emptySet(), + features, + HealthMetrics.NO_OP, + sink, + writer, + 10, + QUEUE_SIZE, + REPORTING_INTERVAL, + SECONDS, + false); + aggregator.start(); + + CountDownLatch latch = new CountDownLatch(1); + AggregateEntry expectedEntry = + AggregateEntryTestUtils.of( + "resource", + "service", + "operation", + null, + "type", + HTTP_OK, + false, + true, + "", + emptyList(), + null, + null, + null); + doAnswer( + invocation -> { + AggregateEntry e = invocation.getArgument(0); + assertTrue(AggregateEntryTestUtils.equals(e, expectedEntry)); + assertEquals(1, e.getHitCount()); + assertEquals(1, e.getTopLevelCount()); + assertEquals(100, e.getDuration()); + return null; + }) + .when(writer) + .add(any(AggregateEntry.class)); + doAnswer( + invocation -> { + latch.countDown(); + return null; + }) + .when(writer) + .finishBucket(); + + aggregator.publish( + Arrays.asList( + new SimpleSpan( + "service", + "operation", + "resource", + "type", + true, + true, + false, + 0, + 100, + HTTP_OK, + true, + 12345), + new SimpleSpan( + "service", + "operation", + "resource", + "type", + true, + true, + false, + 0, + 100, + HTTP_OK, + true, + 0))); + aggregator.report(); + boolean latchTriggered = latch.await(2, SECONDS); + + assertTrue(latchTriggered); + verify(writer).startBucket(eq(1), anyLong(), anyLong()); + verify(writer, times(1)).add(any(AggregateEntry.class)); + verify(writer).finishBucket(); + + aggregator.close(); + } + + @Test + void shouldNotChangeMetricBucketsWhenIncludeEndpointInMetricsIsDisabled() throws Exception { + MetricWriter writer = mock(MetricWriter.class); + Sink sink = mock(Sink.class); + DDAgentFeaturesDiscovery features = mock(DDAgentFeaturesDiscovery.class); + when(features.supportsMetrics()).thenReturn(true); + when(features.peerTags()).thenReturn(emptySet()); + ClientStatsAggregator aggregator = + new ClientStatsAggregator( + emptySet(), + features, + HealthMetrics.NO_OP, + sink, + writer, + 10, + QUEUE_SIZE, + REPORTING_INTERVAL, + SECONDS, + false); + aggregator.start(); + + // publishing spans with different http.method and http.endpoint + CountDownLatch latch = new CountDownLatch(1); + AggregateEntry expectedEntry = + AggregateEntryTestUtils.of( + "resource", + "service", + "operation", + null, + "type", + HTTP_OK, + false, + false, + "server", + emptyList(), + null, + null, + null); + doAnswer( + invocation -> { + AggregateEntry e = invocation.getArgument(0); + assertTrue(AggregateEntryTestUtils.equals(e, expectedEntry)); + assertEquals(3, e.getHitCount()); + assertEquals(3, e.getTopLevelCount()); + assertEquals(450, e.getDuration()); + return null; + }) + .when(writer) + .add(any(AggregateEntry.class)); + doAnswer( + invocation -> { + latch.countDown(); + return null; + }) + .when(writer) + .finishBucket(); + + aggregator.publish( + Arrays.asList( + new SimpleSpan( + "service", "operation", "resource", "type", false, true, false, 0, 100, HTTP_OK) + .setTag(Tags.SPAN_KIND, "server") + .setTag("http.method", "GET") + .setTag("http.endpoint", "/api/users/:id"), + new SimpleSpan( + "service", "operation", "resource", "type", false, true, false, 0, 200, HTTP_OK) + .setTag(Tags.SPAN_KIND, "server") + .setTag("http.method", "POST") + .setTag("http.endpoint", "/api/orders"), + new SimpleSpan( + "service", "operation", "resource", "type", false, true, false, 0, 150, HTTP_OK) + .setTag(Tags.SPAN_KIND, "server"))); + aggregator.forceReport().get(2, SECONDS); + boolean latchTriggered = latch.await(0, SECONDS); + + // all spans should go to the same bucket (httpMethod and httpEndpoint are ignored) + assertTrue(latchTriggered); + verify(writer).startBucket(eq(1), anyLong(), anyLong()); + verify(writer, times(1)).add(any(AggregateEntry.class)); + verify(writer).finishBucket(); + + aggregator.close(); + } + + @Test + void shouldSeparateMetricBucketsWhenIncludeEndpointInMetricsIsEnabled() throws Exception { + MetricWriter writer = mock(MetricWriter.class); + Sink sink = mock(Sink.class); + DDAgentFeaturesDiscovery features = mock(DDAgentFeaturesDiscovery.class); + when(features.supportsMetrics()).thenReturn(true); + when(features.peerTags()).thenReturn(emptySet()); + ClientStatsAggregator aggregator = + new ClientStatsAggregator( + emptySet(), + features, + HealthMetrics.NO_OP, + sink, + writer, + 10, + QUEUE_SIZE, + REPORTING_INTERVAL, + SECONDS, + true); + aggregator.start(); + + // publishing spans with different http.method and http.endpoint + CountDownLatch latch = new CountDownLatch(1); + AggregateEntry expectedGetUsers = + AggregateEntryTestUtils.of( + "resource", + "service", + "operation", + null, + "type", + HTTP_OK, + false, + false, + "server", + emptyList(), + "GET", + "/api/users/:id", + null); + AggregateEntry expectedPostOrders = + AggregateEntryTestUtils.of( + "resource", + "service", + "operation", + null, + "type", + HTTP_OK, + false, + false, + "server", + emptyList(), + "POST", + "/api/orders", + null); + AggregateEntry expectedNoHttp = + AggregateEntryTestUtils.of( + "resource", + "service", + "operation", + null, + "type", + HTTP_OK, + false, + false, + "server", + emptyList(), + null, + null, + null); + + doAnswer( + invocation -> { + AggregateEntry e = invocation.getArgument(0); + if (AggregateEntryTestUtils.equals(e, expectedGetUsers)) { + assertEquals(1, e.getHitCount()); + assertEquals(1, e.getTopLevelCount()); + assertEquals(100, e.getDuration()); + } else if (AggregateEntryTestUtils.equals(e, expectedPostOrders)) { + assertEquals(1, e.getHitCount()); + assertEquals(1, e.getTopLevelCount()); + assertEquals(200, e.getDuration()); + } else if (AggregateEntryTestUtils.equals(e, expectedNoHttp)) { + assertEquals(1, e.getHitCount()); + assertEquals(1, e.getTopLevelCount()); + assertEquals(150, e.getDuration()); + } else { + throw new AssertionError("Unexpected AggregateEntry in add()"); + } + return null; + }) + .when(writer) + .add(any(AggregateEntry.class)); + doAnswer( + invocation -> { + latch.countDown(); + return null; + }) + .when(writer) + .finishBucket(); + + aggregator.publish( + Arrays.asList( + new SimpleSpan( + "service", "operation", "resource", "type", false, true, false, 0, 100, HTTP_OK) + .setTag(Tags.SPAN_KIND, "server") + .setTag("http.method", "GET") + .setTag("http.endpoint", "/api/users/:id"), + new SimpleSpan( + "service", "operation", "resource", "type", false, true, false, 0, 200, HTTP_OK) + .setTag(Tags.SPAN_KIND, "server") + .setTag("http.method", "POST") + .setTag("http.endpoint", "/api/orders"), + new SimpleSpan( + "service", "operation", "resource", "type", false, true, false, 0, 150, HTTP_OK) + .setTag(Tags.SPAN_KIND, "server"))); + aggregator.forceReport().get(2, SECONDS); + boolean latchTriggered = latch.await(0, SECONDS); + + // spans should go to separate buckets based on httpMethod and httpEndpoint + assertTrue(latchTriggered); + verify(writer).startBucket(eq(3), anyLong(), anyLong()); + verify(writer, times(1)).add(argThat(e -> AggregateEntryTestUtils.equals(e, expectedGetUsers))); + verify(writer, times(1)) + .add(argThat(e -> AggregateEntryTestUtils.equals(e, expectedPostOrders))); + verify(writer, times(1)).add(argThat(e -> AggregateEntryTestUtils.equals(e, expectedNoHttp))); + verify(writer).finishBucket(); + + aggregator.close(); + } + + @Test + void shouldIncludeGrpcStatusCodeInMetricKeyForRpcSpans() throws Exception { + MetricWriter writer = mock(MetricWriter.class); + Sink sink = mock(Sink.class); + DDAgentFeaturesDiscovery features = mock(DDAgentFeaturesDiscovery.class); + when(features.supportsMetrics()).thenReturn(true); + when(features.peerTags()).thenReturn(emptySet()); + ClientStatsAggregator aggregator = + new ClientStatsAggregator( + emptySet(), + features, + HealthMetrics.NO_OP, + sink, + writer, + 10, + QUEUE_SIZE, + REPORTING_INTERVAL, + SECONDS, + false); + aggregator.start(); + + CountDownLatch latch = new CountDownLatch(1); + AggregateEntry expectedGrpcStatus0 = + AggregateEntryTestUtils.of( + "grpc.service/Method", + "service", + "grpc.server", + null, + "rpc", + 0, + false, + false, + "server", + emptyList(), + null, + null, + "0"); + AggregateEntry expectedGrpcStatus5 = + AggregateEntryTestUtils.of( + "grpc.service/Method", + "service", + "grpc.server", + null, + "rpc", + 0, + false, + false, + "server", + emptyList(), + null, + null, + "5"); + AggregateEntry expectedHttpSpan = + AggregateEntryTestUtils.of( + "GET /api", + "service", + "http.request", + null, + "web", + 200, + false, + false, + "server", + emptyList(), + null, + null, + null); + + doAnswer(invocation -> null).when(writer).add(any(AggregateEntry.class)); + doAnswer( + invocation -> { + latch.countDown(); + return null; + }) + .when(writer) + .finishBucket(); + + aggregator.publish( + Arrays.asList( + new SimpleSpan( + "service", + "grpc.server", + "grpc.service/Method", + "rpc", + true, + false, + false, + 0, + 100, + 0) + .setTag(Tags.SPAN_KIND, "server") + .setTag(InstrumentationTags.GRPC_STATUS_CODE, 0), + new SimpleSpan( + "service", + "grpc.server", + "grpc.service/Method", + "rpc", + true, + false, + false, + 0, + 50, + 0) + .setTag(Tags.SPAN_KIND, "server") + .setTag(InstrumentationTags.GRPC_STATUS_CODE, 5), + new SimpleSpan( + "service", "http.request", "GET /api", "web", true, false, false, 0, 75, 200) + .setTag(Tags.SPAN_KIND, "server"))); + aggregator.report(); + boolean latchTriggered = latch.await(2, SECONDS); + + assertTrue(latchTriggered); + verify(writer).startBucket(eq(3), anyLong(), anyLong()); + verify(writer, times(1)) + .add(argThat(e -> AggregateEntryTestUtils.equals(e, expectedGrpcStatus0))); + verify(writer, times(1)) + .add(argThat(e -> AggregateEntryTestUtils.equals(e, expectedGrpcStatus5))); + verify(writer, times(1)).add(argThat(e -> AggregateEntryTestUtils.equals(e, expectedHttpSpan))); + verify(writer).finishBucket(); + + aggregator.close(); + } + + @Test + void cardinalityLimitsResetBetweenReportCycles() throws Exception { + List cycle1Entries = new ArrayList<>(); + List cycle2Entries = new ArrayList<>(); + CountDownLatch latch1 = new CountDownLatch(1); + CountDownLatch latch2 = new CountDownLatch(1); + MetricWriter writer = mock(MetricWriter.class); + Sink sink = mock(Sink.class); + DDAgentFeaturesDiscovery features = mock(DDAgentFeaturesDiscovery.class); + when(features.supportsMetrics()).thenReturn(true); + when(features.peerTags()).thenReturn(emptySet()); + ClientStatsAggregator aggregator = + new ClientStatsAggregator( + emptySet(), + features, + HealthMetrics.NO_OP, + sink, + writer, + 256, + QUEUE_SIZE, + REPORTING_INTERVAL, + SECONDS, + false); + aggregator.start(); + + // publish SERVICE+1 distinct services to fill and overflow the cardinality budget + doAnswer( + invocation -> { + cycle1Entries.add(invocation.getArgument(0)); + return null; + }) + .when(writer) + .add(any(AggregateEntry.class)); + doAnswer( + invocation -> { + latch1.countDown(); + return null; + }) + .when(writer) + .finishBucket(); + for (int i = 0; i <= MetricCardinalityLimits.SERVICE; i++) { + aggregator.publish( + Collections.singletonList( + new SimpleSpan( + "svc-" + i, "op", "resource", "web", false, true, false, 0, 100, HTTP_OK))); + } + aggregator.report(); + latch1.await(2, SECONDS); + + // the overflow service maps to the tracer_blocked_value sentinel + verify(writer).startBucket(eq(MetricCardinalityLimits.SERVICE + 1), anyLong(), anyLong()); + verify(writer, times(1)).finishBucket(); + assertEquals( + 1, + cycle1Entries.stream() + .filter(e -> e.getService().toString().equals("tracer_blocked_value")) + .count()); + + // publish the overflow service in the next cycle after the cardinality reset + clearInvocations(writer); + doAnswer( + invocation -> { + cycle2Entries.add(invocation.getArgument(0)); + return null; + }) + .when(writer) + .add(any(AggregateEntry.class)); + doAnswer( + invocation -> { + latch2.countDown(); + return null; + }) + .when(writer) + .finishBucket(); + String overflowServiceName = "svc-" + MetricCardinalityLimits.SERVICE; + aggregator.publish( + Collections.singletonList( + new SimpleSpan( + overflowServiceName, + "op", + "resource", + "web", + false, + true, + false, + 0, + 100, + HTTP_OK))); + aggregator.report(); + latch2.await(2, SECONDS); + + // after reset the overflow service name is accepted as a real entry + verify(writer).startBucket(eq(1), anyLong(), anyLong()); + verify(writer, times(1)).add(any(AggregateEntry.class)); + verify(writer, times(1)).finishBucket(); + assertEquals(1, cycle2Entries.size()); + assertEquals(overflowServiceName, cycle2Entries.get(0).getService().toString()); + + aggregator.close(); + } + + private void waitUntilAggregatorIsEmpty(ClientStatsAggregator aggregator) + throws InterruptedException { + int i = 0; + while (!aggregator.isEmpty() && i++ < 100) { + Thread.sleep(10); + } + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/common/metrics/FootprintForkedTest.java b/dd-trace-core/src/test/java/datadog/trace/common/metrics/FootprintForkedTest.java new file mode 100644 index 00000000000..744996ac09a --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/common/metrics/FootprintForkedTest.java @@ -0,0 +1,184 @@ +package datadog.trace.common.metrics; + +import static java.util.Collections.emptySet; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; + +import datadog.communication.ddagent.DDAgentFeaturesDiscovery; +import datadog.metrics.api.Histograms; +import datadog.metrics.impl.DDSketchHistograms; +import datadog.trace.api.WellKnownTags; +import datadog.trace.core.monitor.HealthMetrics; +import java.nio.ByteBuffer; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ThreadLocalRandom; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.condition.DisabledIfSystemProperty; +import org.openjdk.jol.info.GraphLayout; +import org.tabletest.junit.TableTest; + +@DisabledIfSystemProperty(named = "java.vendor", matches = "(?i).*IBM.*") +class FootprintForkedTest { + + private static final Random RANDOM = new Random(0); + + @BeforeAll + static void setupSpec() { + // Initialize metrics-lib histograms to register the DDSketch implementation + Histograms.register(DDSketchHistograms.FACTORY); + } + + @TableTest({ + "scenario | operationCardinality | servicePerOperation | resourceNamesPerService | typesPerOperation | errorRate", + "5 ops 10 resources 0% errors | 5 | 1 | 10 | 2 | 0.00 ", + "5 ops 100 resources 0% errors | 5 | 1 | 100 | 2 | 0.00 ", + "5 ops 10 resources 1% errors | 5 | 1 | 10 | 2 | 0.01 ", + "5 ops 100 resources 1% errors | 5 | 1 | 100 | 2 | 0.01 ", + "10 ops 100 resources 0% errors | 10 | 1 | 100 | 2 | 0.00 ", + "10 ops 100 resources 1% errors | 10 | 1 | 100 | 2 | 0.01 " + }) + void footprintLessThan10MB( + int operationCardinality, + int servicePerOperation, + int resourceNamesPerService, + int typesPerOperation, + double errorRate) + throws Exception { + CountDownLatch latch = new CountDownLatch(1); + ValidatingSink sink = new ValidatingSink(latch); + DDAgentFeaturesDiscovery features = + mock(DDAgentFeaturesDiscovery.class, withSettings().stubOnly()); + when(features.supportsMetrics()).thenReturn(true); + when(features.peerTags()).thenReturn(emptySet()); + ClientStatsAggregator aggregator = + new ClientStatsAggregator( + new WellKnownTags("runtimeid", "hostname", "env", "service", "version", "language"), + emptySet(), + AdditionalTagsSchema.EMPTY, + features, + HealthMetrics.NO_OP, + sink, + 1000, + 1000, + 100, + SECONDS, + false); + // Measuring the AggregateTable directly (rather than the whole ClientStatsAggregator) avoids + // both the 'features' mock (mocks are heavyweight, e.g. around 22MiB) and the aggregator's + // background Thread, whose ThreadGroup transitively references every other live thread in the + // JVM (test runner, JUnit engine, etc.), pulling in a huge, non-deterministic object graph. + long baseline = footprint(aggregator.aggregator().aggregates()); + aggregator.start(); + try { + + // lots of traces are published + String[] operations = randomNames(operationCardinality); + Map serviceNamesByOperation = + scopedRandomNames(operations, servicePerOperation); + Map resourceNamesByService = + scopedRandomNames(serviceNamesByOperation.values(), resourceNamesPerService); + Map typesByOperation = scopedRandomNames(operations, typesPerOperation); + int traceCount = 10_000; + int errorThreshold = (int) (errorRate * traceCount); + for (int i = 0; i < traceCount; ++i) { + String operation = operations[ThreadLocalRandom.current().nextInt(operations.length)]; + String[] types = typesByOperation.get(operation); + String type = types[ThreadLocalRandom.current().nextInt(types.length)]; + String[] serviceNames = serviceNamesByOperation.get(operation); + String serviceName = serviceNames[ThreadLocalRandom.current().nextInt(serviceNames.length)]; + String[] resourceNames = resourceNamesByService.get(serviceName); + String resourceName = + resourceNames[ThreadLocalRandom.current().nextInt(resourceNames.length)]; + boolean isError = ThreadLocalRandom.current().nextInt(traceCount) < errorThreshold; + aggregator.publish( + Collections.singletonList( + new SimpleSpan( + serviceName, + operation, + resourceName, + type, + true, + true, + isError, + System.nanoTime(), + isError ? expDistributedNanoseconds(0.99) : expDistributedNanoseconds(0.01), + 200))); + } + if (!aggregator.report()) { + int attempts = 0; + while (++attempts < 10 && !aggregator.report()) { + Thread.sleep(10); + } + assertTrue(attempts < 10, "aggregator failed to report within 10 attempts"); + } + assertTrue(latch.await(30, SECONDS), "latch was not triggered within 30 seconds"); + long after = footprint(aggregator.aggregator().aggregates()); + assertTrue(after - baseline <= 10L * 1024 * 1024, "footprint growth exceeds 10MB"); + } finally { + aggregator.close(); + } + } + + private static String[] randomNames(int cardinality) { + String[] things = new String[cardinality]; + for (int i = 0; i < things.length; ++i) { + things[i] = UUID.randomUUID().toString(); + } + return things; + } + + private static Map scopedRandomNames(String[] parents, int childCardinality) { + Map things = new HashMap<>(); + for (String parent : parents) { + things.put(parent, randomNames(childCardinality)); + } + return things; + } + + private static Map scopedRandomNames( + Collection parents, int childCardinality) { + Map things = new HashMap<>(); + for (String[] parent : parents) { + for (String p : parent) { + things.put(p, randomNames(childCardinality)); + } + } + return things; + } + + private static long expDistributedNanoseconds(double intensity) { + return (long) (Math.log(RANDOM.nextDouble()) / Math.log(1 - intensity) + 1); + } + + private static long footprint(Object root) { + GraphLayout layout = GraphLayout.parseInstance(root); + System.out.println(layout.toFootprint()); + return layout.totalSize(); + } + + private static class ValidatingSink implements Sink { + + final CountDownLatch latch; + + ValidatingSink(CountDownLatch latch) { + this.latch = latch; + } + + @Override + public void register(EventListener listener) {} + + @Override + public void accept(int messageCount, ByteBuffer buffer) { + latch.countDown(); + } + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/common/metrics/MetricsReliabilityTest.java b/dd-trace-core/src/test/java/datadog/trace/common/metrics/MetricsReliabilityTest.java new file mode 100644 index 00000000000..cb204fdb426 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/common/metrics/MetricsReliabilityTest.java @@ -0,0 +1,231 @@ +package datadog.trace.common.metrics; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.communication.ddagent.DDAgentFeaturesDiscovery; +import datadog.communication.ddagent.SharedCommunicationObjects; +import datadog.metrics.api.statsd.StatsDClient; +import datadog.trace.agent.test.server.http.JavaTestHttpServer; +import datadog.trace.api.Config; +import datadog.trace.core.CoreTracer; +import datadog.trace.core.DDCoreJavaSpecification; +import datadog.trace.core.monitor.HealthMetrics; +import datadog.trace.core.monitor.TracerHealthMetrics; +import datadog.trace.util.Strings; +import java.security.NoSuchAlgorithmException; +import java.util.Properties; +import java.util.concurrent.CountDownLatch; +import org.junit.jupiter.api.Test; + +class MetricsReliabilityTest extends DDCoreJavaSpecification { + + @Test + void metricsShouldReliablyHandleMomentaryDowngrades() throws Exception { + State state = new State(); + state.reset(true); + JavaTestHttpServer agent = newAgent(state); + Properties props = new Properties(); + props.put("trace.agent.url", agent.getAddress().toString()); + props.put("trace.stats.computation.enabled", "true"); + Config config = Config.get(props); + SharedCommunicationObjects sharedComm = new SharedCommunicationObjects(); + sharedComm.createRemaining(config); + DDAgentFeaturesDiscovery featuresDiscovery = sharedComm.featuresDiscovery(config); + TracerHealthMetrics healthMetrics = new TracerHealthMetrics(StatsDClient.NO_OP); + CoreTracer tracer = + tracerBuilder() + .sharedCommunicationObjects(sharedComm) + .healthMetrics(healthMetrics) + .config(config) + .build(); + + try { + // metrics enabled and discovery is performed + featuresDiscovery.discover(); + + // should support metrics + assertTrue(state.latch.await(10, SECONDS)); + assertTrue(featuresDiscovery.supportsMetrics()); + + // a span is published + tracer.startSpan("test", "test").finish(); + tracer.flush(); + tracer.flushMetrics(); + + // should have sent statistics and informed the agent that we calculate the stats + assertTrue(state.receivedClientComputedHeader); + assertTrue(state.receivedStats); + // 1 trace processed. 1 p0 drop No errors + assertMetrics(healthMetrics, 1, 1, 1, 0, 0); + + // simulate an agent downgrade + state.reset(false, 404); + tracer.startSpan("test", "test").finish(); + tracer.flush(); + tracer.flushMetrics(); + + // a discovery should have done - we do not support anymore stats calculation + assertTrue(state.latch.await(10, SECONDS)); + assertFalse(featuresDiscovery.supportsMetrics()); + // 2 traces processed. 2 p0 dropped. 2 requests and 1 downgrade no errors + assertMetrics(healthMetrics, 2, 2, 2, 0, 1); + + // a span is published (we have a call to stats for the downgrade so reset the counter) + state.reset(false); + tracer.startSpan("test", "test").finish(); + tracer.flush(); + tracer.flushMetrics(); + + // then: should have not sent statistics and informed the agent that we don't calculate the + // stats anymore + assertFalse(state.receivedClientComputedHeader); + assertFalse(state.receivedStats); + // 2 traces processed. 1 p0 dropped. 2 requests and 1 downgrade no errors + assertMetrics(healthMetrics, 2, 2, 2, 0, 1); + + // we detect that the agent can calculate the stats again + state.reset(true); + featuresDiscovery.discover(); + + // we should understand it + assertTrue(state.latch.await(10, SECONDS)); + assertTrue(featuresDiscovery.supportsMetrics()); + + // a span is published + tracer.startSpan("test", "test").finish(); + tracer.flush(); + tracer.flushMetrics(); + + // we should have sent the stats and informed the agent to not calculate the stats on + // the trace payload + assertTrue(state.receivedClientComputedHeader); + assertTrue(state.receivedStats); + // 3 traces processed. 2 p0 dropped. 3 requests and 1 downgrade no errors + assertMetrics(healthMetrics, 3, 3, 3, 0, 1); + + // an error occurred on the agent stats endpoint + state.reset(true, 500); + tracer.startSpan("test", "test").finish(); + tracer.flush(); + tracer.flushMetrics(); + + // the error counter is incremented + assertTrue(state.receivedClientComputedHeader); + assertTrue(state.receivedStats); + // 4 traces processed. 3 p0 dropped. 4 requests and 1 downgrade - 1 error + assertMetrics(healthMetrics, 4, 4, 4, 1, 1); + + // the next call succeed + state.reset(true); + tracer.startSpan("test", "test").setError(true).finish(); + tracer.flush(); + tracer.flushMetrics(); + + // the request counter is incremented + assertTrue(state.receivedClientComputedHeader); + assertTrue(state.receivedStats); + // 5 traces processed. 3 p0 dropped (this one is errored so it's not dropped). + // 5 requests and 1 downgrade - 1 error + assertMetrics(healthMetrics, 5, 4, 5, 1, 1); + + } finally { + tracer.close(); + agent.stop(); + } + } + + private static JavaTestHttpServer newAgent(State state) { + return JavaTestHttpServer.httpServer( + server -> + server.handlers( + h -> { + h.get( + "/info", + api -> { + String res = + "{\"version\":\"7.65.0\",\"endpoints\":[" + + (state.agentMetricsAvailable ? "\"/v0.6/stats\", " : "") + + "\"/v0.4/traces\"], \"client_drop_p0s\" : true}"; + try { + state.hash = Strings.sha256(res); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + api.getResponse().status(200).send(res); + state.latch.countDown(); + }); + h.post( + "/v0.6/stats", + api -> { + state.receivedStats = true; + api.getResponse().status(state.statsResponseCode).send(); + }); + h.put( + "/v0.4/traces", + api -> { + state.receivedClientComputedHeader = + "true" + .equals( + api.getRequest().getHeader("Datadog-Client-Computed-Stats")); + api.getResponse() + .status(200) + .addHeader("Datadog-Agent-State", state.hash) + .send(); + }); + })); + } + + private static void assertMetrics( + HealthMetrics healthMetrics, + int traces, + int drops, + int requests, + int errors, + int downgrades) { + String summary = healthMetrics.summary(); + assertTrue( + summary.contains("clientStatsRequests=" + requests), + "clientStatsRequests mismatch in: " + summary); + assertTrue( + summary.contains("clientStatsErrors=" + errors), + "clientStatsErrors mismatch in: " + summary); + assertTrue( + summary.contains("clientStatsDowngrades=" + downgrades), + "clientStatsDowngrades mismatch in: " + summary); + assertTrue( + summary.contains("clientStatsP0DroppedSpans=" + drops), + "clientStatsP0DroppedSpans mismatch in: " + summary); + assertTrue( + summary.contains("clientStatsP0DroppedTraces=" + drops), + "clientStatsP0DroppedTraces mismatch in: " + summary); + assertTrue( + summary.contains("clientStatsProcessedSpans=" + traces), + "clientStatsProcessedSpans mismatch in: " + summary); + assertTrue( + summary.contains("clientStatsProcessedTraces=" + traces), + "clientStatsProcessedTraces mismatch in: " + summary); + } + + private static class State { + volatile boolean agentMetricsAvailable = true; + volatile int statsResponseCode = 200; + volatile boolean receivedStats; + volatile boolean receivedClientComputedHeader; + volatile CountDownLatch latch; + volatile String hash; + + void reset(boolean agentMetricsAvailable) { + reset(agentMetricsAvailable, 200); + } + + void reset(boolean agentMetricsAvailable, int statsResponseCode) { + this.agentMetricsAvailable = agentMetricsAvailable; + this.statsResponseCode = statsResponseCode; + receivedStats = false; + receivedClientComputedHeader = false; + latch = new CountDownLatch(1); + } + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/common/metrics/OkHttpSinkTest.java b/dd-trace-core/src/test/java/datadog/trace/common/metrics/OkHttpSinkTest.java new file mode 100644 index 00000000000..a548265cea7 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/common/metrics/OkHttpSinkTest.java @@ -0,0 +1,160 @@ +package datadog.trace.common.metrics; + +import static datadog.communication.ddagent.DDAgentFeaturesDiscovery.V06_METRICS_ENDPOINT; +import static datadog.trace.common.metrics.EventListener.EventType.OK; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; +import okhttp3.Call; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.junit.jupiter.api.Test; +import org.tabletest.junit.TableTest; + +class OkHttpSinkTest { + + @TableTest({ + "scenario | eventType | responseCode", + "404 -> DOWNGRADED | DOWNGRADED | 404 ", + "500 -> ERROR | ERROR | 500 ", + "0 throws -> ERROR | ERROR | 0 ", + "400 -> BAD_PAYLOAD | BAD_PAYLOAD | 400 ", + "200 -> OK | OK | 200 ", + "201 -> OK | OK | 201 " + }) + void httpStatusCodeResponseCodeYieldsEventType( + EventListener.EventType eventType, int responseCode) { + String agentUrl = "http://localhost:8126"; + EventListener listener = mock(EventListener.class); + OkHttpClient client = mock(OkHttpClient.class); + OkHttpSink sink = + new OkHttpSink(client, agentUrl, V06_METRICS_ENDPOINT, true, false, Collections.emptyMap()); + sink.register(listener); + + doAnswer(invocation -> respond(invocation.getArgument(0), responseCode)) + .when(client) + .newCall(any()); + sink.accept(0, ByteBuffer.allocate(0)); + + verify(client, times(1)).newCall(any()); + verify(listener).onEvent(eq(eventType), any()); + } + + @Test + void degradeToAsyncModeWhenAgentSlowToRespond() throws Exception { + // metrics payloads are relatively large and we don't want to copy them, + // and we typically expect the agent to respond well within the aggregation + // window, so will send synchronously whenever possible to avoid allocating + // a copy of the payload. When the agent is slow to respond, we degrade to + // an asynchronous mode where up to 100 seconds of requests are copied and + // enqueued for sending in the background, because we don't want to lose + // them if it's possible not to. + + String agentUrl = "http://localhost:8126"; + CountDownLatch latch = new CountDownLatch(2); + BlockingListener listener = new BlockingListener(latch); + OkHttpClient client = mock(OkHttpClient.class); + OkHttpSink sink = + new OkHttpSink(client, agentUrl, V06_METRICS_ENDPOINT, true, false, Collections.emptyMap()); + sink.register(listener); + // Single doAnswer handles all three calls using an atomic counter + AtomicInteger callCount = new AtomicInteger(0); + doAnswer( + invocation -> { + int callNumber = callCount.incrementAndGet(); + Request request = invocation.getArgument(0); + if (callNumber == 1) { + // First call: simulate slow agent + Thread.sleep(1001); + } else if (callNumber == 2) { + // Second call: should be in degraded mode + assertTrue(sink.isInDegradedMode()); + } + return respond(request, 200); + }) + .when(client) + .newCall(any()); + + // one slow response followed by a request + sink.accept(1, ByteBuffer.allocate(0)); + sink.accept(1, ByteBuffer.allocate(0)); + latch.await(); + + // the second request degrades to async mode + verify(client, times(2)).newCall(any()); + assertEquals(2, listener.events.size()); + for (EventListener.EventType eventType : listener.events) { + assertEquals(OK, eventType); + } + long asyncRequests = sink.asyncRequestCount(); + assertEquals(1, asyncRequests); + assertTrue(sink.isInDegradedMode()); + + // the agent has recovered and has responded quickly once + sink.accept(1, ByteBuffer.allocate(0)); + + // the request was sent synchronously + verify(client, times(3)).newCall(any()); + assertEquals(asyncRequests, sink.asyncRequestCount()); + assertFalse(sink.isInDegradedMode()); + } + + private static Call respond(Request request, int code) throws IOException { + if (code == 0) { + return error(); + } + Response response = + new Response.Builder() + .code(code) + .request(request) + .protocol(Protocol.HTTP_1_1) + .message("message") + .body(ResponseBody.create(MediaType.get("text/plain"), "message")) + .build(); + Call call = mock(Call.class); + doReturn(response).when(call).execute(); + return call; + } + + private static Call error() throws IOException { + Call call = mock(Call.class); + doThrow(new IOException("thrown by test")).when(call).execute(); + return call; + } + + private static class BlockingListener implements EventListener { + + private final CountDownLatch latch; + final List events = new CopyOnWriteArrayList<>(); + + BlockingListener(CountDownLatch latch) { + this.latch = latch; + } + + @Override + public void onEvent(EventType eventType, String message) { + events.add(eventType); + latch.countDown(); + } + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/common/metrics/SimpleSpan.java b/dd-trace-core/src/test/java/datadog/trace/common/metrics/SimpleSpan.java new file mode 100644 index 00000000000..41a2a5a0d14 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/common/metrics/SimpleSpan.java @@ -0,0 +1,389 @@ +package datadog.trace.common.metrics; + +import datadog.trace.api.DDSpanId; +import datadog.trace.api.DDTraceId; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.core.CoreSpan; +import datadog.trace.core.MetadataConsumer; +import datadog.trace.core.SpanKindFilter; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +class SimpleSpan implements CoreSpan { + + private final String serviceName; + private final String operationName; + private final CharSequence resourceName; + private final CharSequence serviceNameSource; + private final String type; + private final boolean measured; + private final boolean topLevel; + private final boolean traceRoot; + private final boolean error; + private final short statusCode; + private final long duration; + private final long startTime; + private final long longRunningVersion; + private final Map tags = new HashMap<>(); + + SimpleSpan( + String serviceName, + String operationName, + CharSequence resourceName, + String type, + boolean measured, + boolean topLevel, + boolean error, + long startTime, + long duration, + int statusCode) { + this( + serviceName, + operationName, + resourceName, + type, + measured, + topLevel, + error, + startTime, + duration, + statusCode, + false, + 0, + null); + } + + SimpleSpan( + String serviceName, + String operationName, + CharSequence resourceName, + String type, + boolean measured, + boolean topLevel, + boolean error, + long startTime, + long duration, + int statusCode, + boolean traceRoot) { + this( + serviceName, + operationName, + resourceName, + type, + measured, + topLevel, + error, + startTime, + duration, + statusCode, + traceRoot, + 0, + null); + } + + SimpleSpan( + String serviceName, + String operationName, + CharSequence resourceName, + String type, + boolean measured, + boolean topLevel, + boolean error, + long startTime, + long duration, + int statusCode, + boolean traceRoot, + long longRunningVersion) { + this( + serviceName, + operationName, + resourceName, + type, + measured, + topLevel, + error, + startTime, + duration, + statusCode, + traceRoot, + longRunningVersion, + null); + } + + SimpleSpan( + String serviceName, + String operationName, + CharSequence resourceName, + String type, + boolean measured, + boolean topLevel, + boolean error, + long startTime, + long duration, + int statusCode, + boolean traceRoot, + long longRunningVersion, + CharSequence serviceNameSource) { + this.serviceName = serviceName; + this.operationName = operationName; + this.resourceName = resourceName; + this.serviceNameSource = serviceNameSource; + this.type = type; + this.measured = measured; + this.topLevel = topLevel; + this.traceRoot = traceRoot; + this.error = error; + this.startTime = startTime; + this.duration = duration; + this.statusCode = (short) statusCode; + this.longRunningVersion = longRunningVersion; + } + + @Override + public SimpleSpan getLocalRootSpan() { + return this; + } + + @Override + public String getServiceName() { + return serviceName; + } + + @Override + public CharSequence getServiceNameSource() { + return serviceNameSource; + } + + @Override + public CharSequence getOperationName() { + return operationName; + } + + @Override + public CharSequence getResourceName() { + return resourceName; + } + + @Override + public DDTraceId getTraceId() { + return DDTraceId.ZERO; + } + + @Override + public long getSpanId() { + return DDSpanId.ZERO; + } + + @Override + public long getParentId() { + return traceRoot ? DDSpanId.ZERO : 1L; + } + + @Override + public long getStartTime() { + return startTime; + } + + @Override + public long getDurationNano() { + return duration; + } + + @Override + public int getError() { + return error ? 1 : 0; + } + + @Override + public short getHttpStatusCode() { + return statusCode; + } + + @Override + public CharSequence getOrigin() { + return null; + } + + @Override + public SimpleSpan setMeasured(boolean measured) { + return this; + } + + @Override + public SimpleSpan setErrorMessage(String errorMessage) { + return this; + } + + @Override + public SimpleSpan addThrowable(Throwable error) { + return this; + } + + @Override + public SimpleSpan setTag(String tag, String value) { + return setTag(tag, (Object) value); + } + + @Override + public SimpleSpan setTag(String tag, boolean value) { + return setTag(tag, (Object) value); + } + + @Override + public SimpleSpan setTag(String tag, int value) { + return setTag(tag, (Object) value); + } + + @Override + public SimpleSpan setTag(String tag, long value) { + return setTag(tag, (Object) value); + } + + @Override + public SimpleSpan setTag(String tag, double value) { + return setTag(tag, (Object) value); + } + + @Override + public SimpleSpan setTag(String tag, Number value) { + return setTag(tag, (Object) value); + } + + @Override + public SimpleSpan setTag(String tag, CharSequence value) { + return setTag(tag, (Object) value); + } + + @Override + public SimpleSpan setTag(String tag, Object value) { + tags.put(tag, value); + return this; + } + + @Override + public SimpleSpan removeTag(String tag) { + tags.remove(tag); + return this; + } + + @SuppressWarnings("unchecked") + @Override + public U getTag(CharSequence name, U defaultValue) { + Object value = tags.get(String.valueOf(name)); + return value != null ? (U) value : defaultValue; + } + + @Override + public U getTag(CharSequence name) { + return getTag(name, null); + } + + @Override + public U unsafeGetTag(CharSequence name, U defaultValue) { + return getTag(name, defaultValue); + } + + @Override + public U unsafeGetTag(CharSequence name) { + return getTag(name); + } + + @Override + public boolean hasSamplingPriority() { + return false; + } + + @Override + public boolean isMeasured() { + return measured; + } + + @Override + public boolean isTopLevel() { + return topLevel; + } + + @Override + public boolean isForceKeep() { + return false; + } + + @Override + public boolean isKind(SpanKindFilter filter) { + Object kind = unsafeGetTag(Tags.SPAN_KIND); + return filter.matches(kind == null ? null : kind.toString()); + } + + @Override + public CharSequence getType() { + return type; + } + + @Override + public void processServiceTags() {} + + @Override + public void processTagsAndBaggage(MetadataConsumer consumer) {} + + @Override + public SimpleSpan setSamplingPriority(int samplingPriority, int samplingMechanism) { + return this; + } + + @Override + public SimpleSpan setSamplingPriority( + int samplingPriority, CharSequence rate, double sampleRate, int samplingMechanism) { + return this; + } + + @Override + public SimpleSpan setSpanSamplingPriority(double rate, int limit) { + return this; + } + + @Override + public SimpleSpan setMetric(CharSequence name, int value) { + return this; + } + + @Override + public SimpleSpan setMetric(CharSequence name, long value) { + return this; + } + + @Override + public SimpleSpan setMetric(CharSequence name, float value) { + return this; + } + + @Override + public SimpleSpan setMetric(CharSequence name, double value) { + return this; + } + + @Override + public SimpleSpan setFlag(CharSequence name, boolean value) { + return this; + } + + @Override + public int samplingPriority() { + return 0; + } + + @Override + public Map getMetaStruct() { + return Collections.emptyMap(); + } + + @Override + public SimpleSpan setMetaStruct(String field, Object value) { + return this; + } + + @Override + public int getLongRunningVersion() { + return (int) longRunningVersion; + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/common/sampling/AllSamplerTest.java b/dd-trace-core/src/test/java/datadog/trace/common/sampling/AllSamplerTest.java new file mode 100644 index 00000000000..41bc5921483 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/common/sampling/AllSamplerTest.java @@ -0,0 +1,20 @@ +package datadog.trace.common.sampling; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; + +import datadog.trace.core.DDSpan; +import org.junit.jupiter.api.Test; + +class AllSamplerTest { + + private final DDSpan span = mock(DDSpan.class); + private final AllSampler sampler = new AllSampler(); + + @Test + void testAllSampler() { + for (int i = 0; i < 500; i++) { + assertTrue(sampler.sample(span)); + } + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/common/sampling/AsmStandaloneSamplerTest.java b/dd-trace-core/src/test/java/datadog/trace/common/sampling/AsmStandaloneSamplerTest.java new file mode 100644 index 00000000000..ddd16858036 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/common/sampling/AsmStandaloneSamplerTest.java @@ -0,0 +1,58 @@ +package datadog.trace.common.sampling; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import datadog.trace.api.sampling.PrioritySampling; +import datadog.trace.common.writer.ListWriter; +import datadog.trace.core.CoreTracer; +import datadog.trace.core.DDCoreJavaSpecification; +import datadog.trace.core.DDSpan; +import java.time.Clock; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.jupiter.api.Test; + +class AsmStandaloneSamplerTest extends DDCoreJavaSpecification { + + private final ListWriter writer = new ListWriter(); + + @Test + void testSetSamplingPriority() { + AtomicLong current = new AtomicLong(System.currentTimeMillis()); + Clock clock = mock(Clock.class); + when(clock.millis()).thenAnswer(inv -> current.get()); + AsmStandaloneSampler sampler = new AsmStandaloneSampler(clock); + CoreTracer tracer = tracerBuilder().writer(writer).sampler(sampler).build(); + + try { + doAnswer(inv -> current.updateAndGet(value -> value + 1000)) + .when(clock) + .millis(); // increment in one second + DDSpan span1 = (DDSpan) tracer.buildSpan("datadog", "test").start(); + sampler.setSamplingPriority(span1); + + assertEquals(PrioritySampling.SAMPLER_KEEP, span1.getSamplingPriority()); + + doAnswer(inv -> current.updateAndGet(value -> value + 1000)) + .when(clock) + .millis(); // increment in one second + DDSpan span2 = (DDSpan) tracer.buildSpan("datadog", "test2").start(); + sampler.setSamplingPriority(span2); + + assertEquals(PrioritySampling.SAMPLER_DROP, span2.getSamplingPriority()); + + doAnswer(inv -> current.updateAndGet(value -> value + 60000)) + .when(clock) + .millis(); // increment in one minute + DDSpan span3 = (DDSpan) tracer.buildSpan("datadog", "test3").start(); + sampler.setSamplingPriority(span3); + + // Mock one minute later + assertEquals(PrioritySampling.SAMPLER_KEEP, span3.getSamplingPriority()); + } finally { + tracer.close(); + } + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/common/sampling/DeterministicSpanSamplerTest.java b/dd-trace-core/src/test/java/datadog/trace/common/sampling/DeterministicSpanSamplerTest.java new file mode 100644 index 00000000000..b6cf5d5a817 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/common/sampling/DeterministicSpanSamplerTest.java @@ -0,0 +1,369 @@ +package datadog.trace.common.sampling; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import datadog.trace.core.DDSpan; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.params.converter.ArgumentConversionException; +import org.junit.jupiter.params.converter.ArgumentConverter; +import org.junit.jupiter.params.converter.ConvertWith; +import org.tabletest.junit.TableTest; + +class DeterministicSpanSamplerTest { + + @TableTest({ + "scenario | expected | spanId ", + "10428415896243638596 false | false | 10428415896243638596", + "11199607447739267382 false | false | 11199607447739267382", + "11273630029763932141 false | false | 11273630029763932141", + "11407674492757219439 false | false | 11407674492757219439", + "11792151447964398879 false | false | 11792151447964398879", + "12432680895096110463 false | false | 12432680895096110463", + "13126262220165910460 false | false | 13126262220165910460", + "13174268766980400525 false | false | 13174268766980400525", + "15505210698284655633 false | false | 15505210698284655633", + "15649472107743074779 false | false | 15649472107743074779", + "17204678798284737396 false | false | 17204678798284737396", + "17344948852394588913 false | false | 17344948852394588913", + "17496662575514578077 false | false | 17496662575514578077", + "18252401681137062077 false | false | 18252401681137062077", + "18317291550776694829 false | false | 18317291550776694829", + "1874068156324778273 false | false | 1874068156324778273 ", + "1905388747193831650 false | false | 1905388747193831650 ", + "2202916659517317514 false | false | 2202916659517317514 ", + "2227583514184312746 false | false | 2227583514184312746 ", + "2338498362660772719 false | false | 2338498362660772719 ", + "2781055864473387780 false | false | 2781055864473387780 ", + "3328451335138149956 false | false | 3328451335138149956 ", + "3337066551442961397 false | false | 3337066551442961397 ", + "3409814636252858217 false | false | 3409814636252858217 ", + "3510942875414458836 false | false | 3510942875414458836 ", + "3784560248718450071 false | false | 3784560248718450071 ", + "4751997750760398084 false | false | 4751997750760398084 ", + "4831389563158288344 false | false | 4831389563158288344 ", + "4990765271833742716 false | false | 4990765271833742716 ", + "5089134323978233018 false | false | 5089134323978233018 ", + "5199948958991797301 false | false | 5199948958991797301 ", + "5577006791947779410 false | false | 5577006791947779410 ", + "5600924393587988459 false | false | 5600924393587988459 ", + "5793183108815074904 false | false | 5793183108815074904 ", + "6263450610539110790 false | false | 6263450610539110790 ", + "6382800227808658932 false | false | 6382800227808658932 ", + "6651414131918424343 false | false | 6651414131918424343 ", + "6842348953158377901 false | false | 6842348953158377901 ", + "6941261091797652072 false | false | 6941261091797652072 ", + "7273596521315663110 false | false | 7273596521315663110 ", + "7504504064263669287 false | false | 7504504064263669287 ", + "788787457839692041 false | false | 788787457839692041 ", + "7955079406183515637 false | false | 7955079406183515637 ", + "8549944162621642512 false | false | 8549944162621642512 ", + "8603989663476771718 false | false | 8603989663476771718 ", + "8807817071862113702 false | false | 8807817071862113702 ", + "9010467728050264449 false | false | 9010467728050264449 ", + "10667007354186551956 true | true | 10667007354186551956", + "10683692646452562431 true | true | 10683692646452562431", + "10821471013040158923 true | true | 10821471013040158923", + "10950412492527322440 true | true | 10950412492527322440", + "11239168150708129139 true | true | 11239168150708129139", + "1169089424364679180 true | true | 1169089424364679180 ", + "11818186001859264308 true | true | 11818186001859264308", + "11833901312327420776 true | true | 11833901312327420776", + "11926759511765359899 true | true | 11926759511765359899", + "11926873763676642186 true | true | 11926873763676642186", + "11963748953446345529 true | true | 11963748953446345529", + "11998794077335055257 true | true | 11998794077335055257", + "12096659438561119542 true | true | 12096659438561119542", + "12156940908066221323 true | true | 12156940908066221323", + "12947799971452915849 true | true | 12947799971452915849", + "13260572831089785859 true | true | 13260572831089785859", + "13771804148684671731 true | true | 13771804148684671731", + "14117161486975057715 true | true | 14117161486975057715", + "14242321332569825828 true | true | 14242321332569825828", + "14486903973548550719 true | true | 14486903973548550719", + "14967026985784794439 true | true | 14967026985784794439", + "15213854965919594827 true | true | 15213854965919594827", + "15352856648520921629 true | true | 15352856648520921629", + "15399114114227588261 true | true | 15399114114227588261", + "15595235597337683065 true | true | 15595235597337683065", + "16194613440650274502 true | true | 16194613440650274502", + "1687184559264975024 true | true | 1687184559264975024 ", + "17490665426807838719 true | true | 17490665426807838719", + "18218388313430417611 true | true | 18218388313430417611", + "2601737961087659062 true | true | 2601737961087659062 ", + "261049867304784443 true | true | 261049867304784443 ", + "2740103009342231109 true | true | 2740103009342231109 ", + "2970700287221458280 true | true | 2970700287221458280 ", + "3916589616287113937 true | true | 3916589616287113937 ", + "4324745483838182873 true | true | 4324745483838182873 ", + "4937104021912138218 true | true | 4937104021912138218 ", + "5486140987150761883 true | true | 5486140987150761883 ", + "5944830206637008055 true | true | 5944830206637008055 ", + "6296367092202729479 true | true | 6296367092202729479 ", + "6334824724549167320 true | true | 6334824724549167320 ", + "6556961545928831643 true | true | 6556961545928831643 ", + "6735196588112087610 true | true | 6735196588112087610 ", + "7388428680384065704 true | true | 7388428680384065704 ", + "8249030965139585917 true | true | 8249030965139585917 ", + "837825985403119657 true | true | 837825985403119657 ", + "8505906760983331750 true | true | 8505906760983331750 ", + "8674665223082153551 true | true | 8674665223082153551 ", + "894385949183117216 true | true | 894385949183117216 ", + "898860202204764712 true | true | 898860202204764712 ", + "9768663798983814715 true | true | 9768663798983814715 ", + "9828766684487745566 true | true | 9828766684487745566 ", + "9908585559158765387 true | true | 9908585559158765387 ", + "9956202364908137547 true | true | 9956202364908137547 " + }) + void testKnownValues(boolean expected, @ConvertWith(UnsignedLongConverter.class) long spanId) { + DeterministicSampler sampler = new DeterministicSampler.SpanSampler(0.5); + DDSpan span = mock(DDSpan.class); + when(span.getSpanId()).thenReturn(spanId); + + boolean sampled = sampler.sample(span); + + assertEquals(expected, sampled); + } + + // These values are repeated from the "known values test" + // It is an arbitrary subset of all possible spanIds + @TableTest({ + "scenario | spanId ", + "10428415896243638596 | 10428415896243638596", + "11199607447739267382 | 11199607447739267382", + "11273630029763932141 | 11273630029763932141", + "11407674492757219439 | 11407674492757219439", + "11792151447964398879 | 11792151447964398879", + "12432680895096110463 | 12432680895096110463", + "13126262220165910460 | 13126262220165910460", + "13174268766980400525 | 13174268766980400525", + "15505210698284655633 | 15505210698284655633", + "15649472107743074779 | 15649472107743074779", + "17204678798284737396 | 17204678798284737396", + "17344948852394588913 | 17344948852394588913", + "17496662575514578077 | 17496662575514578077", + "18252401681137062077 | 18252401681137062077", + "18317291550776694829 | 18317291550776694829", + "1874068156324778273 | 1874068156324778273 ", + "1905388747193831650 | 1905388747193831650 ", + "2202916659517317514 | 2202916659517317514 ", + "2227583514184312746 | 2227583514184312746 ", + "2338498362660772719 | 2338498362660772719 ", + "2781055864473387780 | 2781055864473387780 ", + "3328451335138149956 | 3328451335138149956 ", + "3337066551442961397 | 3337066551442961397 ", + "3409814636252858217 | 3409814636252858217 ", + "3510942875414458836 | 3510942875414458836 ", + "3784560248718450071 | 3784560248718450071 ", + "4751997750760398084 | 4751997750760398084 ", + "4831389563158288344 | 4831389563158288344 ", + "4990765271833742716 | 4990765271833742716 ", + "5089134323978233018 | 5089134323978233018 ", + "5199948958991797301 | 5199948958991797301 ", + "5577006791947779410 | 5577006791947779410 ", + "5600924393587988459 | 5600924393587988459 ", + "5793183108815074904 | 5793183108815074904 ", + "6263450610539110790 | 6263450610539110790 ", + "6382800227808658932 | 6382800227808658932 ", + "6651414131918424343 | 6651414131918424343 ", + "6842348953158377901 | 6842348953158377901 ", + "6941261091797652072 | 6941261091797652072 ", + "7273596521315663110 | 7273596521315663110 ", + "7504504064263669287 | 7504504064263669287 ", + "788787457839692041 | 788787457839692041 ", + "7955079406183515637 | 7955079406183515637 ", + "8549944162621642512 | 8549944162621642512 ", + "8603989663476771718 | 8603989663476771718 ", + "8807817071862113702 | 8807817071862113702 ", + "9010467728050264449 | 9010467728050264449 ", + "10667007354186551956 | 10667007354186551956", + "10683692646452562431 | 10683692646452562431", + "10821471013040158923 | 10821471013040158923", + "10950412492527322440 | 10950412492527322440", + "11239168150708129139 | 11239168150708129139", + "1169089424364679180 | 1169089424364679180 ", + "11818186001859264308 | 11818186001859264308", + "11833901312327420776 | 11833901312327420776", + "11926759511765359899 | 11926759511765359899", + "11926873763676642186 | 11926873763676642186", + "11963748953446345529 | 11963748953446345529", + "11998794077335055257 | 11998794077335055257", + "12096659438561119542 | 12096659438561119542", + "12156940908066221323 | 12156940908066221323", + "12947799971452915849 | 12947799971452915849", + "13260572831089785859 | 13260572831089785859", + "13771804148684671731 | 13771804148684671731", + "14117161486975057715 | 14117161486975057715", + "14242321332569825828 | 14242321332569825828", + "14486903973548550719 | 14486903973548550719", + "14967026985784794439 | 14967026985784794439", + "15213854965919594827 | 15213854965919594827", + "15352856648520921629 | 15352856648520921629", + "15399114114227588261 | 15399114114227588261", + "15595235597337683065 | 15595235597337683065", + "16194613440650274502 | 16194613440650274502", + "1687184559264975024 | 1687184559264975024 ", + "17490665426807838719 | 17490665426807838719", + "18218388313430417611 | 18218388313430417611", + "2601737961087659062 | 2601737961087659062 ", + "261049867304784443 | 261049867304784443 ", + "2740103009342231109 | 2740103009342231109 ", + "2970700287221458280 | 2970700287221458280 ", + "3916589616287113937 | 3916589616287113937 ", + "4324745483838182873 | 4324745483838182873 ", + "4937104021912138218 | 4937104021912138218 ", + "5486140987150761883 | 5486140987150761883 ", + "5944830206637008055 | 5944830206637008055 ", + "6296367092202729479 | 6296367092202729479 ", + "6334824724549167320 | 6334824724549167320 ", + "6556961545928831643 | 6556961545928831643 ", + "6735196588112087610 | 6735196588112087610 ", + "7388428680384065704 | 7388428680384065704 ", + "8249030965139585917 | 8249030965139585917 ", + "837825985403119657 | 837825985403119657 ", + "8505906760983331750 | 8505906760983331750 ", + "8674665223082153551 | 8674665223082153551 ", + "894385949183117216 | 894385949183117216 ", + "898860202204764712 | 898860202204764712 ", + "9768663798983814715 | 9768663798983814715 ", + "9828766684487745566 | 9828766684487745566 ", + "9908585559158765387 | 9908585559158765387 ", + "9956202364908137547 | 9956202364908137547 " + }) + void testSamplingNone(@ConvertWith(UnsignedLongConverter.class) long spanId) { + DeterministicSampler sampler = new DeterministicSampler.SpanSampler(0); + DDSpan span = mock(DDSpan.class); + when(span.getSpanId()).thenReturn(spanId); + + assertFalse(sampler.sample(span)); + } + + // These values are repeated from the "known values test" + // It is an arbitrary subset of all possible spanIds + @TableTest({ + "scenario | spanId ", + "10428415896243638596 | 10428415896243638596", + "11199607447739267382 | 11199607447739267382", + "11273630029763932141 | 11273630029763932141", + "11407674492757219439 | 11407674492757219439", + "11792151447964398879 | 11792151447964398879", + "12432680895096110463 | 12432680895096110463", + "13126262220165910460 | 13126262220165910460", + "13174268766980400525 | 13174268766980400525", + "15505210698284655633 | 15505210698284655633", + "15649472107743074779 | 15649472107743074779", + "17204678798284737396 | 17204678798284737396", + "17344948852394588913 | 17344948852394588913", + "17496662575514578077 | 17496662575514578077", + "18252401681137062077 | 18252401681137062077", + "18317291550776694829 | 18317291550776694829", + "1874068156324778273 | 1874068156324778273 ", + "1905388747193831650 | 1905388747193831650 ", + "2202916659517317514 | 2202916659517317514 ", + "2227583514184312746 | 2227583514184312746 ", + "2338498362660772719 | 2338498362660772719 ", + "2781055864473387780 | 2781055864473387780 ", + "3328451335138149956 | 3328451335138149956 ", + "3337066551442961397 | 3337066551442961397 ", + "3409814636252858217 | 3409814636252858217 ", + "3510942875414458836 | 3510942875414458836 ", + "3784560248718450071 | 3784560248718450071 ", + "4751997750760398084 | 4751997750760398084 ", + "4831389563158288344 | 4831389563158288344 ", + "4990765271833742716 | 4990765271833742716 ", + "5089134323978233018 | 5089134323978233018 ", + "5199948958991797301 | 5199948958991797301 ", + "5577006791947779410 | 5577006791947779410 ", + "5600924393587988459 | 5600924393587988459 ", + "5793183108815074904 | 5793183108815074904 ", + "6263450610539110790 | 6263450610539110790 ", + "6382800227808658932 | 6382800227808658932 ", + "6651414131918424343 | 6651414131918424343 ", + "6842348953158377901 | 6842348953158377901 ", + "6941261091797652072 | 6941261091797652072 ", + "7273596521315663110 | 7273596521315663110 ", + "7504504064263669287 | 7504504064263669287 ", + "788787457839692041 | 788787457839692041 ", + "7955079406183515637 | 7955079406183515637 ", + "8549944162621642512 | 8549944162621642512 ", + "8603989663476771718 | 8603989663476771718 ", + "8807817071862113702 | 8807817071862113702 ", + "9010467728050264449 | 9010467728050264449 ", + "10667007354186551956 | 10667007354186551956", + "10683692646452562431 | 10683692646452562431", + "10821471013040158923 | 10821471013040158923", + "10950412492527322440 | 10950412492527322440", + "11239168150708129139 | 11239168150708129139", + "1169089424364679180 | 1169089424364679180 ", + "11818186001859264308 | 11818186001859264308", + "11833901312327420776 | 11833901312327420776", + "11926759511765359899 | 11926759511765359899", + "11926873763676642186 | 11926873763676642186", + "11963748953446345529 | 11963748953446345529", + "11998794077335055257 | 11998794077335055257", + "12096659438561119542 | 12096659438561119542", + "12156940908066221323 | 12156940908066221323", + "12947799971452915849 | 12947799971452915849", + "13260572831089785859 | 13260572831089785859", + "13771804148684671731 | 13771804148684671731", + "14117161486975057715 | 14117161486975057715", + "14242321332569825828 | 14242321332569825828", + "14486903973548550719 | 14486903973548550719", + "14967026985784794439 | 14967026985784794439", + "15213854965919594827 | 15213854965919594827", + "15352856648520921629 | 15352856648520921629", + "15399114114227588261 | 15399114114227588261", + "15595235597337683065 | 15595235597337683065", + "16194613440650274502 | 16194613440650274502", + "1687184559264975024 | 1687184559264975024 ", + "17490665426807838719 | 17490665426807838719", + "18218388313430417611 | 18218388313430417611", + "2601737961087659062 | 2601737961087659062 ", + "261049867304784443 | 261049867304784443 ", + "2740103009342231109 | 2740103009342231109 ", + "2970700287221458280 | 2970700287221458280 ", + "3916589616287113937 | 3916589616287113937 ", + "4324745483838182873 | 4324745483838182873 ", + "4937104021912138218 | 4937104021912138218 ", + "5486140987150761883 | 5486140987150761883 ", + "5944830206637008055 | 5944830206637008055 ", + "6296367092202729479 | 6296367092202729479 ", + "6334824724549167320 | 6334824724549167320 ", + "6556961545928831643 | 6556961545928831643 ", + "6735196588112087610 | 6735196588112087610 ", + "7388428680384065704 | 7388428680384065704 ", + "8249030965139585917 | 8249030965139585917 ", + "837825985403119657 | 837825985403119657 ", + "8505906760983331750 | 8505906760983331750 ", + "8674665223082153551 | 8674665223082153551 ", + "894385949183117216 | 894385949183117216 ", + "898860202204764712 | 898860202204764712 ", + "9768663798983814715 | 9768663798983814715 ", + "9828766684487745566 | 9828766684487745566 ", + "9908585559158765387 | 9908585559158765387 ", + "9956202364908137547 | 9956202364908137547 " + }) + void testSamplingAll(@ConvertWith(UnsignedLongConverter.class) long spanId) { + DeterministicSampler sampler = new DeterministicSampler.SpanSampler(1); + DDSpan span = mock(DDSpan.class); + when(span.getSpanId()).thenReturn(spanId); + + assertTrue(sampler.sample(span)); + } + + /** Converts unsigned decimal string cell values to signed {@code long} bit patterns. */ + private static class UnsignedLongConverter implements ArgumentConverter { + + @Override + public Object convert(Object source, ParameterContext context) + throws ArgumentConversionException { + if (source == null) { + return 0L; + } + return Long.parseUnsignedLong(source.toString().trim()); + } + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/common/sampling/DeterministicTraceSamplerTest.java b/dd-trace-core/src/test/java/datadog/trace/common/sampling/DeterministicTraceSamplerTest.java new file mode 100644 index 00000000000..9e8a90ab9c8 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/common/sampling/DeterministicTraceSamplerTest.java @@ -0,0 +1,378 @@ +package datadog.trace.common.sampling; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import datadog.trace.api.DDTraceId; +import datadog.trace.core.DDSpan; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.tabletest.junit.TableTest; + +class DeterministicTraceSamplerTest { + + @TableTest({ + "scenario | expected | traceId ", + "10428415896243638596 f | false | 10428415896243638596", + "11199607447739267382 f | false | 11199607447739267382", + "11273630029763932141 f | false | 11273630029763932141", + "11407674492757219439 f | false | 11407674492757219439", + "11792151447964398879 f | false | 11792151447964398879", + "12432680895096110463 f | false | 12432680895096110463", + "13126262220165910460 f | false | 13126262220165910460", + "13174268766980400525 f | false | 13174268766980400525", + "15505210698284655633 f | false | 15505210698284655633", + "15649472107743074779 f | false | 15649472107743074779", + "17204678798284737396 f | false | 17204678798284737396", + "17344948852394588913 f | false | 17344948852394588913", + "17496662575514578077 f | false | 17496662575514578077", + "18252401681137062077 f | false | 18252401681137062077", + "18317291550776694829 f | false | 18317291550776694829", + "1874068156324778273 f | false | 1874068156324778273 ", + "1905388747193831650 f | false | 1905388747193831650 ", + "2202916659517317514 f | false | 2202916659517317514 ", + "2227583514184312746 f | false | 2227583514184312746 ", + "2338498362660772719 f | false | 2338498362660772719 ", + "2781055864473387780 f | false | 2781055864473387780 ", + "3328451335138149956 f | false | 3328451335138149956 ", + "3337066551442961397 f | false | 3337066551442961397 ", + "3409814636252858217 f | false | 3409814636252858217 ", + "3510942875414458836 f | false | 3510942875414458836 ", + "3784560248718450071 f | false | 3784560248718450071 ", + "4751997750760398084 f | false | 4751997750760398084 ", + "4831389563158288344 f | false | 4831389563158288344 ", + "4990765271833742716 f | false | 4990765271833742716 ", + "5089134323978233018 f | false | 5089134323978233018 ", + "5199948958991797301 f | false | 5199948958991797301 ", + "5577006791947779410 f | false | 5577006791947779410 ", + "5600924393587988459 f | false | 5600924393587988459 ", + "5793183108815074904 f | false | 5793183108815074904 ", + "6263450610539110790 f | false | 6263450610539110790 ", + "6382800227808658932 f | false | 6382800227808658932 ", + "6651414131918424343 f | false | 6651414131918424343 ", + "6842348953158377901 f | false | 6842348953158377901 ", + "6941261091797652072 f | false | 6941261091797652072 ", + "7273596521315663110 f | false | 7273596521315663110 ", + "7504504064263669287 f | false | 7504504064263669287 ", + "788787457839692041 f | false | 788787457839692041 ", + "7955079406183515637 f | false | 7955079406183515637 ", + "8549944162621642512 f | false | 8549944162621642512 ", + "8603989663476771718 f | false | 8603989663476771718 ", + "8807817071862113702 f | false | 8807817071862113702 ", + "9010467728050264449 f | false | 9010467728050264449 ", + "10667007354186551956 t | true | 10667007354186551956", + "10683692646452562431 t | true | 10683692646452562431", + "10821471013040158923 t | true | 10821471013040158923", + "10950412492527322440 t | true | 10950412492527322440", + "11239168150708129139 t | true | 11239168150708129139", + "1169089424364679180 t | true | 1169089424364679180 ", + "11818186001859264308 t | true | 11818186001859264308", + "11833901312327420776 t | true | 11833901312327420776", + "11926759511765359899 t | true | 11926759511765359899", + "11926873763676642186 t | true | 11926873763676642186", + "11963748953446345529 t | true | 11963748953446345529", + "11998794077335055257 t | true | 11998794077335055257", + "12096659438561119542 t | true | 12096659438561119542", + "12156940908066221323 t | true | 12156940908066221323", + "12947799971452915849 t | true | 12947799971452915849", + "13260572831089785859 t | true | 13260572831089785859", + "13771804148684671731 t | true | 13771804148684671731", + "14117161486975057715 t | true | 14117161486975057715", + "14242321332569825828 t | true | 14242321332569825828", + "14486903973548550719 t | true | 14486903973548550719", + "14967026985784794439 t | true | 14967026985784794439", + "15213854965919594827 t | true | 15213854965919594827", + "15352856648520921629 t | true | 15352856648520921629", + "15399114114227588261 t | true | 15399114114227588261", + "15595235597337683065 t | true | 15595235597337683065", + "16194613440650274502 t | true | 16194613440650274502", + "1687184559264975024 t | true | 1687184559264975024 ", + "17490665426807838719 t | true | 17490665426807838719", + "18218388313430417611 t | true | 18218388313430417611", + "2601737961087659062 t | true | 2601737961087659062 ", + "261049867304784443 t | true | 261049867304784443 ", + "2740103009342231109 t | true | 2740103009342231109 ", + "2970700287221458280 t | true | 2970700287221458280 ", + "3916589616287113937 t | true | 3916589616287113937 ", + "4324745483838182873 t | true | 4324745483838182873 ", + "4937104021912138218 t | true | 4937104021912138218 ", + "5486140987150761883 t | true | 5486140987150761883 ", + "5944830206637008055 t | true | 5944830206637008055 ", + "6296367092202729479 t | true | 6296367092202729479 ", + "6334824724549167320 t | true | 6334824724549167320 ", + "6556961545928831643 t | true | 6556961545928831643 ", + "6735196588112087610 t | true | 6735196588112087610 ", + "7388428680384065704 t | true | 7388428680384065704 ", + "8249030965139585917 t | true | 8249030965139585917 ", + "837825985403119657 t | true | 837825985403119657 ", + "8505906760983331750 t | true | 8505906760983331750 ", + "8674665223082153551 t | true | 8674665223082153551 ", + "894385949183117216 t | true | 894385949183117216 ", + "898860202204764712 t | true | 898860202204764712 ", + "9768663798983814715 t | true | 9768663798983814715 ", + "9828766684487745566 t | true | 9828766684487745566 ", + "9908585559158765387 t | true | 9908585559158765387 ", + "9956202364908137547 t | true | 9956202364908137547 ", + "9223372036854775808 t | true | 9223372036854775808 " + }) + void testKnownValues(boolean expected, String traceId) { + DeterministicSampler sampler = new DeterministicSampler.TraceSampler(0.5); + DDSpan span = mock(DDSpan.class); + when(span.getTraceId()).thenReturn(DDTraceId.from(traceId)); + + assertEquals(expected, sampler.sample(span)); + } + + // These values are repeated from the "known values test" + // It is an arbitrary subset of all possible traceIds + @TableTest({ + "scenario | traceId ", + "10428415896243638596 | 10428415896243638596", + "11199607447739267382 | 11199607447739267382", + "11273630029763932141 | 11273630029763932141", + "11407674492757219439 | 11407674492757219439", + "11792151447964398879 | 11792151447964398879", + "12432680895096110463 | 12432680895096110463", + "13126262220165910460 | 13126262220165910460", + "13174268766980400525 | 13174268766980400525", + "15505210698284655633 | 15505210698284655633", + "15649472107743074779 | 15649472107743074779", + "17204678798284737396 | 17204678798284737396", + "17344948852394588913 | 17344948852394588913", + "17496662575514578077 | 17496662575514578077", + "18252401681137062077 | 18252401681137062077", + "18317291550776694829 | 18317291550776694829", + "1874068156324778273 | 1874068156324778273 ", + "1905388747193831650 | 1905388747193831650 ", + "2202916659517317514 | 2202916659517317514 ", + "2227583514184312746 | 2227583514184312746 ", + "2338498362660772719 | 2338498362660772719 ", + "2781055864473387780 | 2781055864473387780 ", + "3328451335138149956 | 3328451335138149956 ", + "3337066551442961397 | 3337066551442961397 ", + "3409814636252858217 | 3409814636252858217 ", + "3510942875414458836 | 3510942875414458836 ", + "3784560248718450071 | 3784560248718450071 ", + "4751997750760398084 | 4751997750760398084 ", + "4831389563158288344 | 4831389563158288344 ", + "4990765271833742716 | 4990765271833742716 ", + "5089134323978233018 | 5089134323978233018 ", + "5199948958991797301 | 5199948958991797301 ", + "5577006791947779410 | 5577006791947779410 ", + "5600924393587988459 | 5600924393587988459 ", + "5793183108815074904 | 5793183108815074904 ", + "6263450610539110790 | 6263450610539110790 ", + "6382800227808658932 | 6382800227808658932 ", + "6651414131918424343 | 6651414131918424343 ", + "6842348953158377901 | 6842348953158377901 ", + "6941261091797652072 | 6941261091797652072 ", + "7273596521315663110 | 7273596521315663110 ", + "7504504064263669287 | 7504504064263669287 ", + "788787457839692041 | 788787457839692041 ", + "7955079406183515637 | 7955079406183515637 ", + "8549944162621642512 | 8549944162621642512 ", + "8603989663476771718 | 8603989663476771718 ", + "8807817071862113702 | 8807817071862113702 ", + "9010467728050264449 | 9010467728050264449 ", + "10667007354186551956 | 10667007354186551956", + "10683692646452562431 | 10683692646452562431", + "10821471013040158923 | 10821471013040158923", + "10950412492527322440 | 10950412492527322440", + "11239168150708129139 | 11239168150708129139", + "1169089424364679180 | 1169089424364679180 ", + "11818186001859264308 | 11818186001859264308", + "11833901312327420776 | 11833901312327420776", + "11926759511765359899 | 11926759511765359899", + "11926873763676642186 | 11926873763676642186", + "11963748953446345529 | 11963748953446345529", + "11998794077335055257 | 11998794077335055257", + "12096659438561119542 | 12096659438561119542", + "12156940908066221323 | 12156940908066221323", + "12947799971452915849 | 12947799971452915849", + "13260572831089785859 | 13260572831089785859", + "13771804148684671731 | 13771804148684671731", + "14117161486975057715 | 14117161486975057715", + "14242321332569825828 | 14242321332569825828", + "14486903973548550719 | 14486903973548550719", + "14967026985784794439 | 14967026985784794439", + "15213854965919594827 | 15213854965919594827", + "15352856648520921629 | 15352856648520921629", + "15399114114227588261 | 15399114114227588261", + "15595235597337683065 | 15595235597337683065", + "16194613440650274502 | 16194613440650274502", + "1687184559264975024 | 1687184559264975024 ", + "17490665426807838719 | 17490665426807838719", + "18218388313430417611 | 18218388313430417611", + "2601737961087659062 | 2601737961087659062 ", + "261049867304784443 | 261049867304784443 ", + "2740103009342231109 | 2740103009342231109 ", + "2970700287221458280 | 2970700287221458280 ", + "3916589616287113937 | 3916589616287113937 ", + "4324745483838182873 | 4324745483838182873 ", + "4937104021912138218 | 4937104021912138218 ", + "5486140987150761883 | 5486140987150761883 ", + "5944830206637008055 | 5944830206637008055 ", + "6296367092202729479 | 6296367092202729479 ", + "6334824724549167320 | 6334824724549167320 ", + "6556961545928831643 | 6556961545928831643 ", + "6735196588112087610 | 6735196588112087610 ", + "7388428680384065704 | 7388428680384065704 ", + "8249030965139585917 | 8249030965139585917 ", + "837825985403119657 | 837825985403119657 ", + "8505906760983331750 | 8505906760983331750 ", + "8674665223082153551 | 8674665223082153551 ", + "894385949183117216 | 894385949183117216 ", + "898860202204764712 | 898860202204764712 ", + "9768663798983814715 | 9768663798983814715 ", + "9828766684487745566 | 9828766684487745566 ", + "9908585559158765387 | 9908585559158765387 ", + "9956202364908137547 | 9956202364908137547 " + }) + void testSamplingNone(String traceId) { + DeterministicSampler sampler = new DeterministicSampler.TraceSampler(0); + DDSpan span = mock(DDSpan.class); + when(span.getTraceId()).thenReturn(DDTraceId.from(traceId)); + + assertFalse(sampler.sample(span)); + } + + // These values are repeated from the "known values test" + // It is an arbitrary subset of all possible traceIds + @TableTest({ + "scenario | traceId ", + "10428415896243638596 | 10428415896243638596", + "11199607447739267382 | 11199607447739267382", + "11273630029763932141 | 11273630029763932141", + "11407674492757219439 | 11407674492757219439", + "11792151447964398879 | 11792151447964398879", + "12432680895096110463 | 12432680895096110463", + "13126262220165910460 | 13126262220165910460", + "13174268766980400525 | 13174268766980400525", + "15505210698284655633 | 15505210698284655633", + "15649472107743074779 | 15649472107743074779", + "17204678798284737396 | 17204678798284737396", + "17344948852394588913 | 17344948852394588913", + "17496662575514578077 | 17496662575514578077", + "18252401681137062077 | 18252401681137062077", + "18317291550776694829 | 18317291550776694829", + "1874068156324778273 | 1874068156324778273 ", + "1905388747193831650 | 1905388747193831650 ", + "2202916659517317514 | 2202916659517317514 ", + "2227583514184312746 | 2227583514184312746 ", + "2338498362660772719 | 2338498362660772719 ", + "2781055864473387780 | 2781055864473387780 ", + "3328451335138149956 | 3328451335138149956 ", + "3337066551442961397 | 3337066551442961397 ", + "3409814636252858217 | 3409814636252858217 ", + "3510942875414458836 | 3510942875414458836 ", + "3784560248718450071 | 3784560248718450071 ", + "4751997750760398084 | 4751997750760398084 ", + "4831389563158288344 | 4831389563158288344 ", + "4990765271833742716 | 4990765271833742716 ", + "5089134323978233018 | 5089134323978233018 ", + "5199948958991797301 | 5199948958991797301 ", + "5577006791947779410 | 5577006791947779410 ", + "5600924393587988459 | 5600924393587988459 ", + "5793183108815074904 | 5793183108815074904 ", + "6263450610539110790 | 6263450610539110790 ", + "6382800227808658932 | 6382800227808658932 ", + "6651414131918424343 | 6651414131918424343 ", + "6842348953158377901 | 6842348953158377901 ", + "6941261091797652072 | 6941261091797652072 ", + "7273596521315663110 | 7273596521315663110 ", + "7504504064263669287 | 7504504064263669287 ", + "788787457839692041 | 788787457839692041 ", + "7955079406183515637 | 7955079406183515637 ", + "8549944162621642512 | 8549944162621642512 ", + "8603989663476771718 | 8603989663476771718 ", + "8807817071862113702 | 8807817071862113702 ", + "9010467728050264449 | 9010467728050264449 ", + "10667007354186551956 | 10667007354186551956", + "10683692646452562431 | 10683692646452562431", + "10821471013040158923 | 10821471013040158923", + "10950412492527322440 | 10950412492527322440", + "11239168150708129139 | 11239168150708129139", + "1169089424364679180 | 1169089424364679180 ", + "11818186001859264308 | 11818186001859264308", + "11833901312327420776 | 11833901312327420776", + "11926759511765359899 | 11926759511765359899", + "11926873763676642186 | 11926873763676642186", + "11963748953446345529 | 11963748953446345529", + "11998794077335055257 | 11998794077335055257", + "12096659438561119542 | 12096659438561119542", + "12156940908066221323 | 12156940908066221323", + "12947799971452915849 | 12947799971452915849", + "13260572831089785859 | 13260572831089785859", + "13771804148684671731 | 13771804148684671731", + "14117161486975057715 | 14117161486975057715", + "14242321332569825828 | 14242321332569825828", + "14486903973548550719 | 14486903973548550719", + "14967026985784794439 | 14967026985784794439", + "15213854965919594827 | 15213854965919594827", + "15352856648520921629 | 15352856648520921629", + "15399114114227588261 | 15399114114227588261", + "15595235597337683065 | 15595235597337683065", + "16194613440650274502 | 16194613440650274502", + "1687184559264975024 | 1687184559264975024 ", + "17490665426807838719 | 17490665426807838719", + "18218388313430417611 | 18218388313430417611", + "2601737961087659062 | 2601737961087659062 ", + "261049867304784443 | 261049867304784443 ", + "2740103009342231109 | 2740103009342231109 ", + "2970700287221458280 | 2970700287221458280 ", + "3916589616287113937 | 3916589616287113937 ", + "4324745483838182873 | 4324745483838182873 ", + "4937104021912138218 | 4937104021912138218 ", + "5486140987150761883 | 5486140987150761883 ", + "5944830206637008055 | 5944830206637008055 ", + "6296367092202729479 | 6296367092202729479 ", + "6334824724549167320 | 6334824724549167320 ", + "6556961545928831643 | 6556961545928831643 ", + "6735196588112087610 | 6735196588112087610 ", + "7388428680384065704 | 7388428680384065704 ", + "8249030965139585917 | 8249030965139585917 ", + "837825985403119657 | 837825985403119657 ", + "8505906760983331750 | 8505906760983331750 ", + "8674665223082153551 | 8674665223082153551 ", + "894385949183117216 | 894385949183117216 ", + "898860202204764712 | 898860202204764712 ", + "9768663798983814715 | 9768663798983814715 ", + "9828766684487745566 | 9828766684487745566 ", + "9908585559158765387 | 9908585559158765387 ", + "9956202364908137547 | 9956202364908137547 " + }) + void testSamplingAll(String traceId) { + DeterministicSampler sampler = new DeterministicSampler.TraceSampler(1); + DDSpan span = mock(DDSpan.class); + when(span.getTraceId()).thenReturn(DDTraceId.from(traceId)); + + assertTrue(sampler.sample(span)); + } + + @ParameterizedTest(name = "rate={0}") + @MethodSource("testCutoffCalculationArguments") + void testCutoffCalculation(int rate) { + // Groovy's integer division uses BigDecimal arithmetic, equivalent to double precision + // here + long cutoff = DeterministicSampler.cutoff(rate / 100.0); + + BigDecimal cutoffFactor = + new BigDecimal(BigInteger.valueOf(2).pow(64).subtract(BigInteger.ONE)); + long expected = + new BigDecimal(rate / 100D).multiply(cutoffFactor).toBigInteger().longValue() + + Long.MIN_VALUE; + assertTrue(Math.abs(cutoff - expected) <= 1); + } + + static Stream testCutoffCalculationArguments() { + return IntStream.rangeClosed(0, 100).mapToObj(Arguments::arguments); + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/common/sampling/ForcePrioritySamplerTest.java b/dd-trace-core/src/test/java/datadog/trace/common/sampling/ForcePrioritySamplerTest.java new file mode 100644 index 00000000000..6553672e57e --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/common/sampling/ForcePrioritySamplerTest.java @@ -0,0 +1,127 @@ +package datadog.trace.common.sampling; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.api.DDTags; +import datadog.trace.api.sampling.PrioritySampling; +import datadog.trace.api.sampling.SamplingMechanism; +import datadog.trace.common.writer.ListWriter; +import datadog.trace.common.writer.LoggingWriter; +import datadog.trace.core.CoreTracer; +import datadog.trace.core.DDCoreJavaSpecification; +import datadog.trace.core.DDSpan; +import datadog.trace.test.junit.utils.converter.PrioritySamplingConverter; +import datadog.trace.test.junit.utils.converter.SamplingMechanismConverter; +import datadog.trace.test.junit.utils.tabletest.BoxedValueConverter; +import org.junit.jupiter.params.converter.ConvertWith; +import org.tabletest.junit.TableTest; + +class ForcePrioritySamplerTest extends DDCoreJavaSpecification { + + private final ListWriter writer = new ListWriter(); + + @TableTest({ + "scenario | prioritySampling | samplingMechanism | expectedSampling ", + "SAMPLER_KEEP DEFAULT | PrioritySampling.SAMPLER_KEEP | SamplingMechanism.DEFAULT | PrioritySampling.SAMPLER_KEEP", + "SAMPLER_DROP DEFAULT | PrioritySampling.SAMPLER_DROP | SamplingMechanism.DEFAULT | PrioritySampling.SAMPLER_DROP", + "SAMPLER_KEEP AGENT | PrioritySampling.SAMPLER_KEEP | SamplingMechanism.AGENT_RATE | PrioritySampling.SAMPLER_KEEP", + "SAMPLER_DROP AGENT | PrioritySampling.SAMPLER_DROP | SamplingMechanism.AGENT_RATE | PrioritySampling.SAMPLER_DROP", + "SAMPLER_KEEP REMOTE | PrioritySampling.SAMPLER_KEEP | SamplingMechanism.REMOTE_AUTO_RATE | PrioritySampling.SAMPLER_KEEP", + "SAMPLER_DROP REMOTE | PrioritySampling.SAMPLER_DROP | SamplingMechanism.REMOTE_AUTO_RATE | PrioritySampling.SAMPLER_DROP" + }) + void forcePrioritySampling( + @ConvertWith(PrioritySamplingConverter.class) int prioritySampling, + @ConvertWith(SamplingMechanismConverter.class) int samplingMechanism, + @ConvertWith(PrioritySamplingConverter.class) int expectedSampling) { + ForcePrioritySampler sampler = new ForcePrioritySampler(prioritySampling, samplingMechanism); + CoreTracer tracer = tracerBuilder().writer(writer).sampler(sampler).build(); + try { + DDSpan span1 = (DDSpan) tracer.buildSpan("datadog", "test").start(); + sampler.setSamplingPriority(span1); + + assertEquals(expectedSampling, span1.getSamplingPriority()); + assertTrue(sampler.sample(span1)); + } finally { + tracer.close(); + } + } + + @TableTest({ + "scenario | prioritySampling | samplingMechanism | expectedSampling ", + "SAMPLER_KEEP DEFAULT | PrioritySampling.SAMPLER_KEEP | SamplingMechanism.DEFAULT | PrioritySampling.SAMPLER_KEEP", + "SAMPLER_DROP DEFAULT | PrioritySampling.SAMPLER_DROP | SamplingMechanism.DEFAULT | PrioritySampling.SAMPLER_DROP" + }) + void samplingPrioritySet( + @ConvertWith(PrioritySamplingConverter.class) int prioritySampling, + @ConvertWith(SamplingMechanismConverter.class) int samplingMechanism, + @ConvertWith(PrioritySamplingConverter.class) int expectedSampling) + throws Exception { + ForcePrioritySampler sampler = new ForcePrioritySampler(prioritySampling, samplingMechanism); + CoreTracer tracer = tracerBuilder().writer(writer).sampler(sampler).build(); + try { + DDSpan span = (DDSpan) tracer.buildSpan("datadog", "test").start(); + + assertNull(span.getSamplingPriority()); + + span.setTag(DDTags.SERVICE_NAME, "spock"); + + span.finish(); + writer.waitForTraces(1); + assertEquals(expectedSampling, span.getSamplingPriority()); + } finally { + tracer.close(); + } + } + + @TableTest({ + "scenario | tagName | tagValue | expectedPriority ", + "manual.drop true | manual.drop | true | PrioritySampling.USER_DROP", + "manual.keep true | manual.keep | true | PrioritySampling.USER_KEEP" + }) + void settingForcedTracingViaTag( + String tagName, + boolean tagValue, + @ConvertWith(PrioritySamplingConverter.class) int expectedPriority) { + ForcePrioritySampler sampler = + new ForcePrioritySampler(PrioritySampling.SAMPLER_KEEP, SamplingMechanism.DEFAULT); + CoreTracer tracer = tracerBuilder().writer(new LoggingWriter()).sampler(sampler).build(); + try { + DDSpan span = (DDSpan) tracer.buildSpan("datadog", "root").start(); + span.setTag(tagName, tagValue); + span.finish(); + + assertEquals(expectedPriority, span.getSamplingPriority()); + } finally { + tracer.close(); + } + } + + @TableTest({ + "scenario | tagName | tagValue", + "no tag | | ", + "manual.drop false | manual.drop | false ", + "manual.keep false | manual.keep | false ", + "manual.drop 1 | manual.drop | 1 ", + "manual.keep 1 | manual.keep | 1 " + }) + void notSettingForcedTracingViaTagOrSettingItWrongValueNotCausingException( + String tagName, @ConvertWith(BoxedValueConverter.class) Object tagValue) { + ForcePrioritySampler sampler = + new ForcePrioritySampler(PrioritySampling.SAMPLER_KEEP, SamplingMechanism.DEFAULT); + CoreTracer tracer = tracerBuilder().writer(new LoggingWriter()).sampler(sampler).build(); + try { + DDSpan span = (DDSpan) tracer.buildSpan("datadog", "root").start(); + if (tagName != null) { + span.setTag(tagName, tagValue); + } + + assertNull(span.getSamplingPriority()); + + span.finish(); + } finally { + tracer.close(); + } + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/common/sampling/RateByServiceTraceSamplerTest.java b/dd-trace-core/src/test/java/datadog/trace/common/sampling/RateByServiceTraceSamplerTest.java new file mode 100644 index 00000000000..6918cf9c31f --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/common/sampling/RateByServiceTraceSamplerTest.java @@ -0,0 +1,467 @@ +package datadog.trace.common.sampling; + +import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_DROP; +import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_KEEP; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.api.DDTags; +import datadog.trace.api.time.ControllableTimeSource; +import datadog.trace.common.writer.ListWriter; +import datadog.trace.common.writer.LoggingWriter; +import datadog.trace.core.CoreTracer; +import datadog.trace.core.DDCoreJavaSpecification; +import datadog.trace.core.DDSpan; +import datadog.trace.core.DDSpanContext; +import datadog.trace.test.junit.utils.converter.PrioritySamplingConverter; +import datadog.trace.test.junit.utils.tabletest.BoxedValueConverter; +import datadog.trace.test.junit.utils.tabletest.TableTestTypeConverters; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.converter.ConvertWith; +import org.tabletest.junit.TableTest; +import org.tabletest.junit.TypeConverterSources; + +@TypeConverterSources(TableTestTypeConverters.class) +class RateByServiceTraceSamplerTest extends DDCoreJavaSpecification { + + /** Build a rate_by_service response map from a single entry (the fallback key). */ + private static Map> rateResponse(String key, Number rate) { + Map byService = new HashMap<>(); + byService.put(key, rate); + Map> response = new HashMap<>(); + response.put("rate_by_service", byService); + return response; + } + + /** Build a rate_by_service response map from multiple entries preserving insertion order. */ + private static Map> rateResponse(String[][] entries) { + Map byService = new LinkedHashMap<>(); + for (String[] entry : entries) { + byService.put(entry[0], entry[1] == null ? null : Double.parseDouble(entry[1])); + } + Map> response = new HashMap<>(); + response.put("rate_by_service", byService); + return response; + } + + // these values are all precisely represented in floating point + @TableTest({ + "scenario | rate | expectedRate", + "null rate | | 1.0 ", + "rate 1 | 1 | 1.0 ", + "rate 0 | 0 | 0.0 ", + "rate -5 | -5 | 1.0 ", + "rate 5 | 5 | 1.0 ", + "rate 0.5 | 0.5 | 0.5 " + }) + void invalidRateTo1(Number rate, double expectedRate) { + RateByServiceTraceSampler serviceSampler = new RateByServiceTraceSampler(); + Map byService = new HashMap<>(); + byService.put("service:,env:", rate); + Map> response = new HashMap<>(); + response.put("rate_by_service", byService); + serviceSampler.onResponse("traces", response); + + assertEquals(expectedRate, serviceSampler.fallbackSampleRate(), 1e-6); + assertEquals(expectedRate, serviceSampler.sampleRateFor("not", "found"), 1e-6); + } + + @TableTest({ + "scenario | service | env | expectedRate", + "foo/bar | foo | bar | 0.8 ", + "Foo/BAR case insens | Foo | BAR | 0.8 ", + "FOO/BAR case insens | FOO | BAR | 0.8 ", + "not found | not | found | 0.2 ", + "foo/baz fallback | foo | baz | 0.2 ", + "fu/bar no match | fu | bar | 0.2 " + }) + void rateSelection(String service, String env, double expectedRate) { + RateByServiceTraceSampler serviceSampler = new RateByServiceTraceSampler(); + serviceSampler.onResponse( + "traces", + rateResponse( + new String[][] { + {"service:foo,env:bar", "0.8"}, + {"service:,env:", "0.20"} + })); + + double sampleRate = serviceSampler.sampleRateFor(env, service); + assertTrue(sampleRate > expectedRate - 0.01); + assertTrue(sampleRate < expectedRate + 0.01); + } + + // case insensitive equivalence -- undefined behavior, first one wins + @TableTest({ + "scenario | service | env | expectedRate", + "foo/bar | foo | bar | 0.8 ", + "foo/Bar | foo | Bar | 0.8 ", + "Foo/BAR | Foo | BAR | 0.8 ", + "FOO/BAR | FOO | BAR | 0.8 ", + "foo/baz | foo | baz | 0.3 ", + "FOO/BAZ | FOO | BAZ | 0.3 ", + "quux/baz | quux | baz | 0.4 " + }) + void ratePartialAndFullCollisions(String service, String env, double expectedRate) { + RateByServiceTraceSampler serviceSampler = new RateByServiceTraceSampler(); + serviceSampler.onResponse( + "traces", + rateResponse( + new String[][] { + {"service:foo,env:bar", "0.8"}, + {"service:FOO,env:BAR", "0.2"}, + {"service:FOO,env:BAZ", "0.3"}, + {"service:quux,env:BAZ", "0.4"} + })); + + double sampleRate = serviceSampler.sampleRateFor(env, service); + assertTrue(sampleRate > expectedRate - 0.01); + assertTrue(sampleRate < expectedRate + 0.01); + } + + @Test + void rateByServiceName() { + RateByServiceTraceSampler serviceSampler = new RateByServiceTraceSampler(); + ListWriter writer = new ListWriter(); + CoreTracer tracer = tracerBuilder().writer(writer).build(); + try { + serviceSampler.onResponse("traces", rateResponse("service:spock,env:test", 0.0)); + DDSpan span1 = + (DDSpan) + tracer + .buildSpan("datadog", "fakeOperation") + .withServiceName("foo") + .withTag("env", "bar") + .ignoreActiveSpan() + .start(); + serviceSampler.setSamplingPriority(span1); + + assertEquals(SAMPLER_KEEP, span1.getSamplingPriority()); + assertTrue(serviceSampler.sample(span1)); + + // case-insensitive equivalence - undefined in spec, but implemented as first one wins + serviceSampler.onResponse( + "traces", + rateResponse( + new String[][] { + {"service:spock,env:test", "1.0"}, + {"service:SPOCK,env:Test", "0.0"} + })); + DDSpan span2 = + (DDSpan) + tracer + .buildSpan("datadog", "fakeOperation") + .withServiceName("spock") + .withTag("env", "test") + .ignoreActiveSpan() + .start(); + serviceSampler.setSamplingPriority(span2); + + assertEquals(SAMPLER_KEEP, span2.getSamplingPriority()); + assertTrue(serviceSampler.sample(span2)); + } finally { + tracer.close(); + } + } + + @Test + void rateByServiceNameCaseInsensitive() { + RateByServiceTraceSampler serviceSampler = new RateByServiceTraceSampler(); + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + try { + serviceSampler.onResponse("traces", rateResponse("service:spock,env:test", 1.0)); + DDSpan span = + (DDSpan) + tracer + .buildSpan("datadog", "fakeOperation") + .withServiceName("SPOCK") + .withTag("env", "Test") + .ignoreActiveSpan() + .start(); + serviceSampler.setSamplingPriority(span); + + assertEquals(SAMPLER_KEEP, span.getSamplingPriority()); + assertTrue(serviceSampler.sample(span)); + } finally { + tracer.close(); + } + } + + @Test + void samplingPrioritySetOnContext() { + RateByServiceTraceSampler serviceSampler = new RateByServiceTraceSampler(); + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + serviceSampler.onResponse("traces", rateResponse("service:,env:", 1.0)); + try { + DDSpan span = + (DDSpan) + tracer + .buildSpan("datadog", "fakeOperation") + .withServiceName("spock") + .withTag("env", "test") + .ignoreActiveSpan() + .start(); + serviceSampler.setSamplingPriority(span); + + // sets correctly on root span + assertEquals(SAMPLER_KEEP, span.getSamplingPriority()); + // RateByServiceSampler must not set the sample rate + assertNull(span.getTag(DDSpanContext.SAMPLE_RATE_KEY)); + } finally { + tracer.close(); + } + } + + @Test + void samplingPrioritySetWhenServiceLater() throws Exception { + RateByServiceTraceSampler sampler = new RateByServiceTraceSampler(); + ListWriter writer = new ListWriter(); + CoreTracer tracer = tracerBuilder().writer(writer).sampler(sampler).build(); + try { + sampler.onResponse( + "test", + rateResponse( + new String[][] { + {"service:,env:", "1.0"}, + {"service:spock,env:", "0.0"} + })); + + DDSpan span = (DDSpan) tracer.buildSpan("datadog", "test").start(); + + assertNull(span.getSamplingPriority()); + + span.setTag(DDTags.SERVICE_NAME, "spock"); + + span.finish(); + writer.waitForTraces(1); + assertEquals(SAMPLER_DROP, span.getSamplingPriority()); + + span = + (DDSpan) + tracer.buildSpan("datadog", "test").withTag(DDTags.SERVICE_NAME, "spock").start(); + span.finish(); + writer.waitForTraces(2); + + assertEquals(SAMPLER_DROP, span.getSamplingPriority()); + } finally { + tracer.close(); + } + } + + @TableTest({ + "scenario | tagName | tagValue | expectedPriority ", + "manual.drop true | manual.drop | true | PrioritySampling.USER_DROP", + "manual.keep true | manual.keep | true | PrioritySampling.USER_KEEP" + }) + void settingForcedTracingViaTag( + String tagName, + boolean tagValue, + @ConvertWith(PrioritySamplingConverter.class) int expectedPriority) { + RateByServiceTraceSampler sampler = new RateByServiceTraceSampler(); + CoreTracer tracer = tracerBuilder().writer(new LoggingWriter()).sampler(sampler).build(); + try { + DDSpan span = (DDSpan) tracer.buildSpan("datadog", "root").start(); + span.setTag(tagName, tagValue); + span.finish(); + + assertEquals(expectedPriority, span.getSamplingPriority()); + } finally { + tracer.close(); + } + } + + @Test + void shouldCapReturnsFalseWhenRateDecreasesOrStaysSame() { + assertFalse(RateByServiceTraceSampler.shouldCap(0.8, 0.4)); + assertFalse(RateByServiceTraceSampler.shouldCap(0.5, 0.5)); + assertFalse(RateByServiceTraceSampler.shouldCap(0.5, 1.0)); // 1.0 <= 0.5 * 2, no cap needed + } + + @Test + void shouldCapReturnsFalseWhenOldRateIsZero() { + assertFalse(RateByServiceTraceSampler.shouldCap(0.0, 0.5)); + assertFalse(RateByServiceTraceSampler.shouldCap(0.0, 1.0)); + } + + @Test + void shouldCapReturnsTrueWhenNewRateExceeds2xOldRate() { + assertTrue(RateByServiceTraceSampler.shouldCap(0.1, 1.0)); + assertTrue(RateByServiceTraceSampler.shouldCap(0.2, 0.8)); + assertTrue(RateByServiceTraceSampler.shouldCap(0.1, 0.3)); + } + + @Test + void cappedRateReturns2xOldRate() { + assertEquals(0.2, RateByServiceTraceSampler.cappedRate(0.1), 1e-6); + assertEquals(0.4, RateByServiceTraceSampler.cappedRate(0.2), 1e-6); + assertEquals(0.8, RateByServiceTraceSampler.cappedRate(0.4), 1e-6); + } + + @Test + void rampUpCapsRateIncreasesAt2xPerInterval() { + ControllableTimeSource time = new ControllableTimeSource(); + time.set(1_000_000_000L); + RateByServiceTraceSampler serviceSampler = new RateByServiceTraceSampler(time); + double tolerance = 0.01; + + // Set initial rate to 0.1 + serviceSampler.onResponse( + "traces", + rateResponse(new String[][] {{"service:foo,env:bar", "0.1"}, {"service:,env:", "0.1"}})); + + assertTrue(Math.abs(serviceSampler.sampleRateFor("bar", "foo") - 0.1) < tolerance); + + // agent restart sends rate 1.0, first interval + time.advance(RateByServiceTraceSampler.RAMP_UP_INTERVAL_NANOS); + Map> highRateResponse = + rateResponse(new String[][] {{"service:foo,env:bar", "1.0"}, {"service:,env:", "1.0"}}); + serviceSampler.onResponse("traces", highRateResponse); + + // rate is capped at 2x = 0.2 + assertTrue(Math.abs(serviceSampler.sampleRateFor("bar", "foo") - 0.2) < tolerance); + assertTrue(Math.abs(serviceSampler.fallbackSampleRate() - 0.2) < tolerance); + + // second interval + time.advance(RateByServiceTraceSampler.RAMP_UP_INTERVAL_NANOS); + serviceSampler.onResponse("traces", highRateResponse); + + // rate doubles to 0.4 + assertTrue(Math.abs(serviceSampler.sampleRateFor("bar", "foo") - 0.4) < tolerance); + assertTrue(Math.abs(serviceSampler.fallbackSampleRate() - 0.4) < tolerance); + + // third interval + time.advance(RateByServiceTraceSampler.RAMP_UP_INTERVAL_NANOS); + serviceSampler.onResponse("traces", highRateResponse); + + // rate doubles to 0.8 + assertTrue(Math.abs(serviceSampler.sampleRateFor("bar", "foo") - 0.8) < tolerance); + assertTrue(Math.abs(serviceSampler.fallbackSampleRate() - 0.8) < tolerance); + + // fourth interval + time.advance(RateByServiceTraceSampler.RAMP_UP_INTERVAL_NANOS); + serviceSampler.onResponse("traces", highRateResponse); + + // rate reaches target 1.0 (2x=1.6 > 1.0) + assertTrue(Math.abs(serviceSampler.sampleRateFor("bar", "foo") - 1.0) < tolerance); + assertTrue(Math.abs(serviceSampler.fallbackSampleRate() - 1.0) < tolerance); + } + + @Test + void rampDownAppliesImmediately() { + ControllableTimeSource time = new ControllableTimeSource(); + time.set(1_000_000_000L); + RateByServiceTraceSampler serviceSampler = new RateByServiceTraceSampler(time); + double tolerance = 0.01; + + // Set initial rate to 0.8 + serviceSampler.onResponse( + "traces", + rateResponse(new String[][] {{"service:foo,env:bar", "0.8"}, {"service:,env:", "0.8"}})); + + // rate decreases to 0.2 + serviceSampler.onResponse( + "traces", + rateResponse(new String[][] {{"service:foo,env:bar", "0.2"}, {"service:,env:", "0.2"}})); + + // decrease is applied immediately + assertTrue(Math.abs(serviceSampler.sampleRateFor("bar", "foo") - 0.2) < tolerance); + assertTrue(Math.abs(serviceSampler.fallbackSampleRate() - 0.2) < tolerance); + } + + @Test + void rateIncreaseBlockedDuringCooldown() { + ControllableTimeSource time = new ControllableTimeSource(); + time.set(1_000_000_000L); + RateByServiceTraceSampler serviceSampler = new RateByServiceTraceSampler(time); + double tolerance = 0.01; + Map> highRateResponse = rateResponse("service:foo,env:bar", 1.0); + + // Set initial rate to 0.1 + serviceSampler.onResponse("traces", rateResponse("service:foo,env:bar", 0.1)); + + // rate jumps, first capped increase + time.advance(RateByServiceTraceSampler.RAMP_UP_INTERVAL_NANOS); + serviceSampler.onResponse("traces", highRateResponse); + + // capped to 0.2 + assertTrue(Math.abs(serviceSampler.sampleRateFor("bar", "foo") - 0.2) < tolerance); + + // try again immediately (within cooldown) + serviceSampler.onResponse("traces", highRateResponse); + + // rate stays at 0.2 because cooldown hasn't elapsed + assertTrue(Math.abs(serviceSampler.sampleRateFor("bar", "foo") - 0.2) < tolerance); + + // after cooldown elapsed + time.advance(RateByServiceTraceSampler.RAMP_UP_INTERVAL_NANOS); + serviceSampler.onResponse("traces", highRateResponse); + + // rate doubles to 0.4 + assertTrue(Math.abs(serviceSampler.sampleRateFor("bar", "foo") - 0.4) < tolerance); + } + + @Test + void cooldownNotResetByBlockedIncrease() { + ControllableTimeSource time = new ControllableTimeSource(); + time.set(1_000_000_000L); + RateByServiceTraceSampler serviceSampler = new RateByServiceTraceSampler(time); + double tolerance = 0.01; + Map> highRateResponse = rateResponse("service:foo,env:bar", 1.0); + + // Set initial low rate + serviceSampler.onResponse("traces", rateResponse("service:foo,env:bar", 0.01)); + + assertTrue(Math.abs(serviceSampler.sampleRateFor("bar", "foo") - 0.01) < tolerance); + + // wait for cooldown, apply increase: 0.01 -> 0.02 + time.advance(RateByServiceTraceSampler.RAMP_UP_INTERVAL_NANOS); + serviceSampler.onResponse("traces", highRateResponse); + + // rate is capped at 2x = 0.02 + assertTrue(Math.abs(serviceSampler.sampleRateFor("bar", "foo") - 0.02) < tolerance); + + // before cooldown elapses, send another increase - rate should be held and lastCapped NOT reset + time.advance(RateByServiceTraceSampler.RAMP_UP_INTERVAL_NANOS / 2); + serviceSampler.onResponse("traces", highRateResponse); + + // rate stays at 0.02 (cooldown) + assertTrue(Math.abs(serviceSampler.sampleRateFor("bar", "foo") - 0.02) < tolerance); + + // wait remaining half of cooldown from the original cap - should allow next ramp-up + time.advance(RateByServiceTraceSampler.RAMP_UP_INTERVAL_NANOS / 2); + serviceSampler.onResponse("traces", highRateResponse); + + // rate doubles to 0.04 because lastCapped was NOT reset by the blocked increase + assertTrue(Math.abs(serviceSampler.sampleRateFor("bar", "foo") - 0.04) < tolerance); + } + + @TableTest({ + "scenario | tagName | tagValue", + "no tag | | ", + "manual.drop false | manual.drop | false ", + "manual.keep false | manual.keep | false ", + "manual.drop 1 | manual.drop | 1 ", + "manual.keep 1 | manual.keep | 1 " + }) + void notSettingForcedTracingViaTagOrSettingItWrongValueNotCausingException( + String tagName, @ConvertWith(BoxedValueConverter.class) Object tagValue) { + RateByServiceTraceSampler sampler = new RateByServiceTraceSampler(); + CoreTracer tracer = tracerBuilder().writer(new LoggingWriter()).sampler(sampler).build(); + try { + DDSpan span = (DDSpan) tracer.buildSpan("datadog", "root").start(); + if (tagName != null) { + span.setTag(tagName, tagValue); + } + + assertNull(span.getSamplingPriority()); + + span.finish(); + } finally { + tracer.close(); + } + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/common/sampling/RuleBasedSamplingTest.java b/dd-trace-core/src/test/java/datadog/trace/common/sampling/RuleBasedSamplingTest.java new file mode 100644 index 00000000000..cfd4ab373aa --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/common/sampling/RuleBasedSamplingTest.java @@ -0,0 +1,459 @@ +package datadog.trace.common.sampling; + +import static datadog.trace.api.config.TracerConfig.TRACE_RATE_LIMIT; +import static datadog.trace.api.config.TracerConfig.TRACE_SAMPLE_RATE; +import static datadog.trace.api.config.TracerConfig.TRACE_SAMPLING_OPERATION_RULES; +import static datadog.trace.api.config.TracerConfig.TRACE_SAMPLING_RULES; +import static datadog.trace.api.config.TracerConfig.TRACE_SAMPLING_SERVICE_RULES; +import static datadog.trace.api.sampling.PrioritySampling.USER_DROP; +import static datadog.trace.api.sampling.PrioritySampling.USER_KEEP; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import datadog.trace.common.writer.ListWriter; +import datadog.trace.core.CoreTracer; +import datadog.trace.core.DDCoreJavaSpecification; +import datadog.trace.core.DDSpan; +import datadog.trace.test.junit.utils.converter.PrioritySamplingConverter; +import datadog.trace.test.junit.utils.converter.SamplingMechanismConverter; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Map; +import java.util.Properties; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.converter.ConvertWith; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.tabletest.junit.TableTest; + +class RuleBasedSamplingTest extends DDCoreJavaSpecification { + + @Test + void ruleBasedSamplerIsNotCreatedWhenPropertiesNotSet() { + Sampler sampler = Sampler.Builder.forConfig(new Properties()); + + assertFalse(sampler instanceof RuleBasedTraceSampler); + } + + @Test + void ruleBasedSamplerIsNotCreatedWhenJustRateLimitSet() { + Properties properties = new Properties(); + properties.setProperty(TRACE_RATE_LIMIT, "50"); + Sampler sampler = Sampler.Builder.forConfig(properties); + + assertFalse(sampler instanceof RuleBasedTraceSampler); + } + + // NOTE: Using a defaultRate is still considered to be a "rule" + // decisionMaker is LOCAL_USER_RULE and expectedRuleRate is non-null + // When trace is dropped, decisionMaker isn't tracked -- e.g. null + @TableTest({ + "scenario | serviceRules | operationRules | defaultRate | expectedDecisionMaker | expectedPriority | expectedRuleRate | expectedRateLimit | expectedAgentRate", + "svc xx no match | xx:1 | | | SamplingMechanism.AGENT_RATE | PrioritySampling.SAMPLER_KEEP | | | 1.0 ", + "op xx no match | | xx:1 | | SamplingMechanism.AGENT_RATE | PrioritySampling.SAMPLER_KEEP | | | 1.0 ", + "no rules default 1 | | | 1 | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "no rules default 0 | | | 0 | | PrioritySampling.USER_DROP | 0.0 | | ", + "svc xx default 1 | xx:1 | | 1 | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "op xx default 1 | | xx:1 | 1 | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "svc xx default 0 | xx:1 | | 0 | | PrioritySampling.USER_DROP | 0.0 | | ", + "op xx default 0 | | xx:1 | 0 | | PrioritySampling.USER_DROP | 0.0 | | ", + "service:1 | service:1 | | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "s.*:1 | s.*:1 | | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + ".*e:1 | .*e:1 | | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "service:0 | service:0 | | | | PrioritySampling.USER_DROP | 0.0 | | ", + "s.*:0 | s.*:0 | | | | PrioritySampling.USER_DROP | 0.0 | | ", + ".*e:0 | .*e:0 | | | | PrioritySampling.USER_DROP | 0.0 | | ", + "service:1 default 0 | service:1 | | 0 | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "service:0 default 1 | service:0 | | 1 | | PrioritySampling.USER_DROP | 0.0 | | ", + "xxx:0 service:1 | xxx:0,service:1 | | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "xxx:1 service:0 | xxx:1,service:0 | | | | PrioritySampling.USER_DROP | 0.0 | | ", + "operation:1 | | operation:1 | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "o.*:1 | | o.*:1 | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + ".*n:1 | | .*n:1 | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "operation:0 | | operation:0 | | | PrioritySampling.USER_DROP | 0.0 | | ", + "o.*:0 | | o.*:0 | | | PrioritySampling.USER_DROP | 0.0 | | ", + ".*n:0 | | .*n:0 | | | PrioritySampling.USER_DROP | 0.0 | | ", + "op:1 default 0 | | operation:1 | 0 | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "op:0 default 1 | | operation:0 | 1 | | PrioritySampling.USER_DROP | 0.0 | | ", + "xxx:0 op:1 | | xxx:0,operation:1 | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "xxx:1 op:0 | | xxx:1,operation:0 | | | PrioritySampling.USER_DROP | 0.0 | | ", + "svc:1 op:0 | service:1 | operation:0 | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "svc:1 xxx:0 | service:1 | xxx:0 | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "svc:0 op:1 | service:0 | operation:1 | | | PrioritySampling.USER_DROP | 0.0 | | ", + "svc:0 xxx:1 | service:0 | xxx:1 | | | PrioritySampling.USER_DROP | 0.0 | | ", + "xxx:0 op:1 combo | xxx:0 | operation:1 | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "xxx:1 op:0 combo | xxx:1 | operation:0 | | | PrioritySampling.USER_DROP | 0.0 | | " + }) + void samplingConfigCombinations( + String serviceRules, + String operationRules, + String defaultRate, + @ConvertWith(SamplingMechanismConverter.class) Byte expectedDecisionMaker, + @ConvertWith(PrioritySamplingConverter.class) byte expectedPriority, + Double expectedRuleRate, + Integer expectedRateLimit, + Double expectedAgentRate) { + Properties properties = new Properties(); + if (serviceRules != null) { + properties.setProperty(TRACE_SAMPLING_SERVICE_RULES, serviceRules); + } + if (operationRules != null) { + properties.setProperty(TRACE_SAMPLING_OPERATION_RULES, operationRules); + } + if (defaultRate != null) { + properties.setProperty(TRACE_SAMPLE_RATE, defaultRate); + } + properties.setProperty(TRACE_RATE_LIMIT, "50"); + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + try { + Sampler sampler = Sampler.Builder.forConfig(properties); + assertInstanceOf(PrioritySampler.class, sampler); + + DDSpan span = + (DDSpan) + tracer + .buildSpan("datadog", "operation") + .withServiceName("service") + .withTag("env", "bar") + .ignoreActiveSpan() + .start(); + ((PrioritySampler) sampler).setSamplingPriority(span); + + Map propagationMap = span.spanContext().getPropagationTags().createTagMap(); + String decisionMaker = propagationMap.get("_dd.p.dm"); + String expectedDmStr = + expectedDecisionMaker == null ? null : "-" + (int) expectedDecisionMaker; + + assertTagEquals(expectedRuleRate, span.getTag(RuleBasedTraceSampler.SAMPLING_RULE_RATE)); + assertTagEquals(expectedRateLimit, span.getTag(RuleBasedTraceSampler.SAMPLING_LIMIT_RATE)); + assertTagEquals( + expectedAgentRate, span.getTag(RateByServiceTraceSampler.SAMPLING_AGENT_RATE)); + assertEquals(expectedPriority, (int) span.getSamplingPriority()); + assertEquals(expectedDmStr, decisionMaker); + } finally { + tracer.close(); + } + } + + // NOTE: Using a defaultRate is still considered to be a "rule" + // decisionMaker is LOCAL_USER_RULE and expectedRuleRate is non-null + // When trace is dropped, decisionMaker isn't tracked -- e.g. null + @TableTest({ + "scenario | jsonRules | defaultRate | expectedDecisionMaker | expectedPriority | expectedRuleRate | expectedRateLimit | expectedAgentRate", + "svc xx no match | '[{\"service\": \"xx\", \"sample_rate\": 1}]' | | SamplingMechanism.AGENT_RATE | PrioritySampling.SAMPLER_KEEP | | | 1.0 ", + "name xx no match | '[{\"name\": \"xx\", \"sample_rate\": 1}]' | | SamplingMechanism.AGENT_RATE | PrioritySampling.SAMPLER_KEEP | | | 1.0 ", + "sample_rate 1 def 1 | '[{\"sample_rate\": 1}]' | 1 | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "sample_rate 0 def 0 | '[{\"sample_rate\": 0}]' | 0 | | PrioritySampling.USER_DROP | 0.0 | | ", + "empty rules def 0 | '[]' | 0 | | PrioritySampling.USER_DROP | 0.0 | | ", + "svc xx def 1 | '[{\"service\": \"xx\", \"sample_rate\": 1}]' | 1 | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "name xx def 1 | '[{\"name\": \"xx\", \"sample_rate\": 1}]' | 1 | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "svc xx def 0 | '[{\"service\": \"xx\", \"sample_rate\": 1}]' | 0 | | PrioritySampling.USER_DROP | 0.0 | | ", + "name xx def 0 | '[{\"name\": \"xx\", \"sample_rate\": 1}]' | 0 | | PrioritySampling.USER_DROP | 0.0 | | ", + "svc match keep | '[{\"service\": \"service\", \"sample_rate\": 1}]' | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "svc match drop | '[{\"service\": \"service\", \"sample_rate\": 0}]' | | | PrioritySampling.USER_DROP | 0.0 | | ", + "svc keep def 0 | '[{\"service\": \"service\", \"sample_rate\": 1}]' | 0 | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "svc drop def 1 | '[{\"service\": \"service\", \"sample_rate\": 0}]' | 1 | | PrioritySampling.USER_DROP | 0.0 | | ", + "xxx:0 svc:1 | '[{\"service\": \"xxx\", \"sample_rate\": 0}, {\"service\": \"service\", \"sample_rate\": 1}]' | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "xxx:1 svc:0 | '[{\"service\": \"xxx\", \"sample_rate\": 1}, {\"service\": \"service\", \"sample_rate\": 0}]' | | | PrioritySampling.USER_DROP | 0.0 | | ", + "op match keep | '[{\"name\": \"operation\", \"sample_rate\": 1}]' | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "op match drop | '[{\"name\": \"operation\", \"sample_rate\": 0}]' | | | PrioritySampling.USER_DROP | 0.0 | | ", + "op keep def 0 | '[{\"name\": \"operation\", \"sample_rate\": 1}]' | 0 | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "op drop def 1 | '[{\"name\": \"operation\", \"sample_rate\": 0}]' | 1 | | PrioritySampling.USER_DROP | 0.0 | | ", + "xxx:0 op:1 | '[{\"name\": \"xxx\", \"sample_rate\": 0}, {\"name\": \"operation\", \"sample_rate\": 1}]' | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "xxx:1 op:0 | '[{\"name\": \"xxx\", \"sample_rate\": 1}, {\"name\": \"operation\", \"sample_rate\": 0}]' | | | PrioritySampling.USER_DROP | 0.0 | | ", + "res match keep | '[{\"resource\": \"resource\", \"sample_rate\": 1}]' | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "res match drop | '[{\"resource\": \"resource\", \"sample_rate\": 0}]' | | | PrioritySampling.USER_DROP | 0.0 | | ", + "res keep def 0 | '[{\"resource\": \"resource\", \"sample_rate\": 1}]' | 0 | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "res drop def 1 | '[{\"resource\": \"resource\", \"sample_rate\": 0}]' | 1 | | PrioritySampling.USER_DROP | 0.0 | | ", + "xxx res:1 | '[{\"resource\": \"xxx\", \"sample_rate\": 0}, {\"resource\": \"resource\", \"sample_rate\": 1}]' | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "xxx res:0 | '[{\"resource\": \"xxx\", \"sample_rate\": 1}, {\"resource\": \"resource\", \"sample_rate\": 0}]' | | | PrioritySampling.USER_DROP | 0.0 | | ", + "svc:1 op:0 | '[{\"service\": \"service\", \"sample_rate\": 1}, {\"name\": \"operation\", \"sample_rate\": 0}]' | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "svc:1 xxx:0 | '[{\"service\": \"service\", \"sample_rate\": 1}, {\"name\": \"xxx\", \"sample_rate\": 0}]' | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "svc:0 op:1 | '[{\"service\": \"service\", \"sample_rate\": 0}, {\"name\": \"operation\", \"sample_rate\": 1}]' | | | PrioritySampling.USER_DROP | 0.0 | | ", + "svc:0 xxx:1 | '[{\"service\": \"service\", \"sample_rate\": 0}, {\"name\": \"xxx\", \"sample_rate\": 1}]' | | | PrioritySampling.USER_DROP | 0.0 | | ", + "xxx:0 op:1 | '[{\"service\": \"xxx\", \"sample_rate\": 0}, {\"name\": \"operation\", \"sample_rate\": 1}]' | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "xxx:1 op:0 | '[{\"service\": \"xxx\", \"sample_rate\": 1}, {\"name\": \"operation\", \"sample_rate\": 0}]' | | | PrioritySampling.USER_DROP | 0.0 | | ", + "svc+op keep | '[{\"service\": \"service\", \"name\": \"operation\", \"sample_rate\": 1}]' | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "svc+xxx then svc+op | '[{\"service\": \"service\", \"name\": \"xxx\", \"sample_rate\": 0}, {\"service\": \"service\", \"name\": \"operation\", \"sample_rate\": 1}]' | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "svc+xxx then svc | '[{\"service\": \"service\", \"name\": \"xxx\", \"sample_rate\": 0}, {\"service\": \"service\", \"sample_rate\": 1}]' | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "svc+xxx then op | '[{\"service\": \"service\", \"name\": \"xxx\", \"sample_rate\": 0}, {\"name\": \"operation\", \"sample_rate\": 1}]' | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "svc+res:xxx res | '[{\"service\": \"service\", \"resource\": \"xxx\", \"sample_rate\": 0}, {\"resource\": \"resource\", \"sample_rate\": 1}]' | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "svc+op drop first | '[{\"service\": \"service\", \"name\": \"operation\", \"sample_rate\": 0}, {\"service\": \"service\", \"name\": \"operation\", \"sample_rate\": 1}]' | | | PrioritySampling.USER_DROP | 0.0 | | ", + "svc+op drop only | '[{\"service\": \"service\", \"name\": \"operation\", \"sample_rate\": 0}]' | | | PrioritySampling.USER_DROP | 0.0 | | ", + "svc+res match | '[{\"service\": \"service\", \"resource\": \"xxx\", \"sample_rate\": 0}, {\"service\": \"service\", \"resource\": \"resource\", \"sample_rate\": 1}]' | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "svc+res+op match | '[{\"service\": \"service\", \"resource\": \"xxx\", \"sample_rate\": 0}, {\"service\": \"service\", \"resource\": \"resource\", \"name\": \"operation\", \"sample_rate\": 1}]' | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "tag env:bar keep | '[{\"tags\": {\"env\": \"xxx\"}, \"sample_rate\": 0}, {\"tags\": {\"env\": \"bar\"}, \"sample_rate\": 1}]' | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "tag env:*x then * | '[{\"tags\": {\"env\": \"*x\"}, \"sample_rate\": 0}, {\"tags\": {\"env\": \"*\"}, \"sample_rate\": 1}]' | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "tag env:b?r keep | '[{\"tags\": {\"env\": \"x??\"}, \"sample_rate\": 0}, {\"tags\": {\"env\": \"b?r\"}, \"sample_rate\": 1}]' | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "tag env:b?r drop | '[{\"tags\": {\"env\": \"x??\"}, \"sample_rate\": 1}, {\"tags\": {\"env\": \"b?r\"}, \"sample_rate\": 0}]' | | | PrioritySampling.USER_DROP | 0.0 | | ", + "2tags env+tag keep | '[{\"tags\": {\"env\": \"xxx\"}, \"sample_rate\": 0}, {\"tags\": {\"env\": \"bar\", \"tag\": \"foo\"}, \"sample_rate\": 1}]' | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "2tags * keep | '[{\"tags\": {\"env\": \"*x\"}, \"sample_rate\": 0}, {\"tags\": {\"env\": \"*\", \"tag\": \"*\"}, \"sample_rate\": 1}]' | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "2tags b?r+f?? keep | '[{\"tags\": {\"env\": \"x??\"}, \"sample_rate\": 0}, {\"tags\": {\"env\": \"b?r\", \"tag\": \"f??\"}, \"sample_rate\": 1}]' | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | ", + "2tags b?r+f?? drop | '[{\"tags\": {\"env\": \"x??\"}, \"sample_rate\": 1}, {\"tags\": {\"env\": \"b?r\", \"tag\": \"f??\"}, \"sample_rate\": 0}]' | | | PrioritySampling.USER_DROP | 0.0 | | ", + "all combined | '[{\"service\": \"service\", \"resource\": \"xxx\", \"tags\": {\"env\": \"x??\"}, \"sample_rate\": 0}, {\"service\": \"service\", \"resource\": \"resource\", \"name\": \"operation\", \"tags\": {\"env\": \"b?r\", \"tag\": \"f??\"}, \"sample_rate\": 1}]' | | SamplingMechanism.LOCAL_USER_RULE | PrioritySampling.USER_KEEP | 1.0 | 50 | " + }) + void samplingConfigJsonRulesCombinations( + String jsonRules, + String defaultRate, + @ConvertWith(SamplingMechanismConverter.class) Byte expectedDecisionMaker, + @ConvertWith(PrioritySamplingConverter.class) byte expectedPriority, + Double expectedRuleRate, + Integer expectedRateLimit, + Double expectedAgentRate) { + Properties properties = new Properties(); + properties.setProperty(TRACE_SAMPLING_RULES, jsonRules); + if (defaultRate != null) { + properties.setProperty(TRACE_SAMPLE_RATE, defaultRate); + } + properties.setProperty(TRACE_RATE_LIMIT, "50"); + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + try { + Sampler sampler = Sampler.Builder.forConfig(properties); + assertInstanceOf(PrioritySampler.class, sampler); + + DDSpan span = + (DDSpan) + tracer + .buildSpan("datadog", "operation") + .withServiceName("service") + .withTag("env", "bar") + .withTag("tag", "foo") + .withResourceName("resource") + .ignoreActiveSpan() + .start(); + ((PrioritySampler) sampler).setSamplingPriority(span); + + Map propagationMap = span.spanContext().getPropagationTags().createTagMap(); + String decisionMaker = propagationMap.get("_dd.p.dm"); + String expectedDmStr = + expectedDecisionMaker == null ? null : "-" + (int) expectedDecisionMaker; + + assertTagEquals(expectedRuleRate, span.getTag(RuleBasedTraceSampler.SAMPLING_RULE_RATE)); + assertTagEquals(expectedRateLimit, span.getTag(RuleBasedTraceSampler.SAMPLING_LIMIT_RATE)); + assertTagEquals( + expectedAgentRate, span.getTag(RateByServiceTraceSampler.SAMPLING_AGENT_RATE)); + assertEquals(expectedPriority, (int) span.getSamplingPriority()); + assertEquals(expectedDmStr, decisionMaker); + } finally { + tracer.close(); + } + } + + @SuppressWarnings("unused") + @ParameterizedTest(name = "{0}") + @MethodSource("tagTypesTestArguments") + void tagTypesTest(String scenario, String tagPattern, Object tagValue, boolean expectedMatch) { + String json = "[{\"tags\": {\"testTag\": \"" + tagPattern + "\"}, \"sample_rate\": 1}]"; + Properties properties = new Properties(); + properties.setProperty(TRACE_SAMPLING_RULES, json); + properties.setProperty(TRACE_SAMPLE_RATE, "0"); + + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + try { + PrioritySampler sampler = (PrioritySampler) Sampler.Builder.forConfig(properties); + + DDSpan span = + (DDSpan) + tracer + .buildSpan("datadog", "operation") + .withServiceName("service") + .withResourceName("resource") + .withTag("env", "bar") + .ignoreActiveSpan() + .start(); + span.setTag("testTag", tagValue); + sampler.setSamplingPriority(span); + + assertEquals(expectedMatch ? USER_KEEP : (int) USER_DROP, (int) span.getSamplingPriority()); + } finally { + tracer.close(); + } + } + + static Stream tagTypesTestArguments() { + return Stream.of( + arguments("* anything", "*", "anything...", true), + arguments("* null", "*", null, false), + arguments("* StringBuilder", "*", new StringBuilder("foo"), true), + arguments("* object", "*", new Object(), true), + arguments("** object", "**", new Object(), true), + arguments("? object", "?", new Object(), false), + arguments("* foo", "*", "foo", true), + arguments("** foo", "**", "foo", true), + arguments("** true", "**", true, true), + arguments("** false", "**", false, true), + arguments("** 20", "**", 20, true), + arguments("** 20L", "**", 20L, true), + arguments("** 20.1F", "**", 20.1F, true), + arguments("** 20.1D", "**", 20.1D, true), + arguments("** bigInt 20", "**", new BigInteger("20"), true), + arguments("** bigDec 20.1", "**", new BigDecimal("20.1"), true), + arguments("foo match", "foo", "foo", true), + arguments("foo StringBuilder", "foo", new StringBuilder("foo"), true), + arguments("foo not-foo", "foo", "not-foo", false), + arguments("ba? bar", "ba?", "bar", true), + arguments("20 == 20", "20", 20, true), + arguments("20 == Int 20", "20", 20, true), + arguments("20 == 20L", "20", 20L, true), + arguments("20 == Long 20", "20", 20L, true), + arguments("20 == 20F", "20", 20F, true), + arguments("20 != 20.1F", "20", 20.1F, false), + arguments("20.* != 20.1F", "20.*", 20.1F, false), + arguments("20.1 != 20.1D", "20.1", 20.1D, false), + arguments("* == 20.1D", "*", 20.1D, true), + arguments("20 == bigInt 20", "20", new BigInteger("20"), true), + arguments("20 == bigDec 20", "20", new BigDecimal("20"), true), + arguments("* == bigDec 20.1", "*", new BigDecimal("20.1"), true)); + } + + @Test + void preferJsonRulesOverOtherDeprecatedOnes() { + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + try { + Properties properties = new Properties(); + properties.setProperty(TRACE_SAMPLING_SERVICE_RULES, "service:0"); + properties.setProperty(TRACE_SAMPLING_OPERATION_RULES, "operation:0"); + properties.setProperty( + TRACE_SAMPLING_RULES, + "[{\"service\": \"service\", \"name\": \"operation\", \"sample_rate\": 1.0}]"); + properties.setProperty(TRACE_RATE_LIMIT, "1"); + Sampler sampler = Sampler.Builder.forConfig(properties); + + DDSpan span1 = + (DDSpan) + tracer + .buildSpan("datadog", "operation") + .withServiceName("service") + .withTag("env", "bar") + .ignoreActiveSpan() + .start(); + DDSpan span2 = + (DDSpan) + tracer + .buildSpan("datadog", "operation") + .withServiceName("service") + .withTag("env", "bar") + .ignoreActiveSpan() + .start(); + + ((PrioritySampler) sampler).setSamplingPriority(span1); + // Span 2 should be rate limited if there isn't a >1 sec delay between these 2 lines + ((PrioritySampler) sampler).setSamplingPriority(span2); + + assertTagEquals(1.0, span1.getTag(RuleBasedTraceSampler.SAMPLING_RULE_RATE)); + assertTagEquals(1.0, span1.getTag(RuleBasedTraceSampler.SAMPLING_LIMIT_RATE)); + assertNull(span1.getTag(RateByServiceTraceSampler.SAMPLING_AGENT_RATE)); + assertEquals(USER_KEEP, (int) span1.getSamplingPriority()); + + assertTagEquals(1.0, span2.getTag(RuleBasedTraceSampler.SAMPLING_RULE_RATE)); + assertTagEquals(1.0, span2.getTag(RuleBasedTraceSampler.SAMPLING_LIMIT_RATE)); + assertNull(span2.getTag(RateByServiceTraceSampler.SAMPLING_AGENT_RATE)); + assertEquals(USER_DROP, (int) span2.getSamplingPriority()); + } finally { + tracer.close(); + } + } + + @Test + void rateLimitIsSetForRateLimitedSpans() { + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + try { + Properties properties = new Properties(); + properties.setProperty(TRACE_SAMPLING_SERVICE_RULES, "service:1"); + properties.setProperty(TRACE_RATE_LIMIT, "1"); + Sampler sampler = Sampler.Builder.forConfig(properties); + + DDSpan span1 = + (DDSpan) + tracer + .buildSpan("datadog", "operation") + .withServiceName("service") + .withTag("env", "bar") + .ignoreActiveSpan() + .start(); + DDSpan span2 = + (DDSpan) + tracer + .buildSpan("datadog", "operation") + .withServiceName("service") + .withTag("env", "bar") + .ignoreActiveSpan() + .start(); + + ((PrioritySampler) sampler).setSamplingPriority(span1); + // Span 2 should be rate limited if there isn't a >1 sec delay between these 2 lines + ((PrioritySampler) sampler).setSamplingPriority(span2); + + assertTagEquals(1.0, span1.getTag(RuleBasedTraceSampler.SAMPLING_RULE_RATE)); + assertTagEquals(1.0, span1.getTag(RuleBasedTraceSampler.SAMPLING_LIMIT_RATE)); + assertNull(span1.getTag(RateByServiceTraceSampler.SAMPLING_AGENT_RATE)); + assertEquals(USER_KEEP, (int) span1.getSamplingPriority()); + + assertTagEquals(1.0, span2.getTag(RuleBasedTraceSampler.SAMPLING_RULE_RATE)); + assertTagEquals(1.0, span2.getTag(RuleBasedTraceSampler.SAMPLING_LIMIT_RATE)); + assertNull(span2.getTag(RateByServiceTraceSampler.SAMPLING_AGENT_RATE)); + assertEquals(USER_DROP, (int) span2.getSamplingPriority()); + } finally { + tracer.close(); + } + } + + @Test + void rateLimitIsSetForRateLimitedSpansMatchedOnDifferentRules() { + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + try { + Properties properties = new Properties(); + properties.setProperty(TRACE_SAMPLING_SERVICE_RULES, "service:1,foo:1"); + properties.setProperty(TRACE_RATE_LIMIT, "1"); + Sampler sampler = Sampler.Builder.forConfig(properties); + + DDSpan span1 = + (DDSpan) + tracer + .buildSpan("datadog", "operation") + .withServiceName("service") + .withTag("env", "bar") + .ignoreActiveSpan() + .start(); + DDSpan span2 = + (DDSpan) + tracer + .buildSpan("datadog", "operation") + .withServiceName("foo") + .withTag("env", "bar") + .ignoreActiveSpan() + .start(); + + ((PrioritySampler) sampler).setSamplingPriority(span1); + // Span 2 should be rate limited if there isn't a >1 sec delay between these 2 lines + ((PrioritySampler) sampler).setSamplingPriority(span2); + + assertTagEquals(1.0, span1.getTag(RuleBasedTraceSampler.SAMPLING_RULE_RATE)); + assertTagEquals(1.0, span1.getTag(RuleBasedTraceSampler.SAMPLING_LIMIT_RATE)); + assertNull(span1.getTag(RateByServiceTraceSampler.SAMPLING_AGENT_RATE)); + assertEquals(USER_KEEP, (int) span1.getSamplingPriority()); + + assertTagEquals(1.0, span2.getTag(RuleBasedTraceSampler.SAMPLING_RULE_RATE)); + assertTagEquals(1.0, span2.getTag(RuleBasedTraceSampler.SAMPLING_LIMIT_RATE)); + assertNull(span2.getTag(RateByServiceTraceSampler.SAMPLING_AGENT_RATE)); + assertEquals(USER_DROP, (int) span2.getSamplingPriority()); + } finally { + tracer.close(); + } + } + + private static void assertTagEquals(Number expected, Object actual) { + if (expected == null) { + assertNull(actual, "Expected tag to be null"); + } else { + assertNotNull(actual, "Expected tag to be non-null"); + assertEquals(expected.doubleValue(), ((Number) actual).doubleValue(), 1e-9); + } + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/common/sampling/SamplerTest.java b/dd-trace-core/src/test/java/datadog/trace/common/sampling/SamplerTest.java new file mode 100644 index 00000000000..e4658e7046b --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/common/sampling/SamplerTest.java @@ -0,0 +1,168 @@ +package datadog.trace.common.sampling; + +import static datadog.trace.api.config.AppSecConfig.APPSEC_ENABLED; +import static datadog.trace.api.config.AppSecConfig.APPSEC_SCA_ENABLED; +import static datadog.trace.api.config.GeneralConfig.APM_TRACING_ENABLED; +import static datadog.trace.api.config.IastConfig.IAST_ENABLED; +import static datadog.trace.api.config.OtlpConfig.TRACE_OTEL_EXPORTER; +import static datadog.trace.api.config.TracerConfig.PRIORITY_SAMPLING; +import static datadog.trace.api.config.TracerConfig.PRIORITY_SAMPLING_FORCE; +import static datadog.trace.api.config.TracerConfig.TRACE_SAMPLE_RATE; +import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_KEEP; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import datadog.trace.api.Config; +import datadog.trace.common.writer.ListWriter; +import datadog.trace.core.CoreTracer; +import datadog.trace.core.DDSpan; +import datadog.trace.test.junit.utils.config.WithConfig; +import datadog.trace.test.util.DDJavaSpecification; +import org.junit.jupiter.api.Test; + +class SamplerTest extends DDJavaSpecification { + + @WithConfig(key = APM_TRACING_ENABLED, value = "false") + @WithConfig(key = APPSEC_ENABLED, value = "true") + @Test + void asmStandaloneSamplerSelectedWhenApmTracingDisabledAndAppsecEnabled() { + Config config = Config.get(); + + Sampler sampler = Sampler.Builder.forConfig(config, null); + + assertInstanceOf(AsmStandaloneSampler.class, sampler); + } + + @WithConfig(key = APM_TRACING_ENABLED, value = "false") + @WithConfig(key = IAST_ENABLED, value = "true") + @Test + void asmStandaloneSamplerSelectedWhenApmTracingDisabledAndIastEnabled() { + Config config = Config.get(); + + Sampler sampler = Sampler.Builder.forConfig(config, null); + + assertInstanceOf(AsmStandaloneSampler.class, sampler); + } + + @WithConfig(key = APM_TRACING_ENABLED, value = "false") + @WithConfig(key = APPSEC_SCA_ENABLED, value = "true") + @Test + void asmStandaloneSamplerSelectedWhenApmTracingDisabledAndScaEnabled() { + Config config = Config.get(); + + Sampler sampler = Sampler.Builder.forConfig(config, null); + + assertInstanceOf(AsmStandaloneSampler.class, sampler); + } + + @WithConfig(key = APM_TRACING_ENABLED, value = "false") + @Test + void asmStandaloneSamplerNotSelectedWhenApmTracingAndAsmNotEnabled() { + Config config = Config.get(); + + Sampler sampler = Sampler.Builder.forConfig(config, null); + + assertFalse(sampler instanceof AsmStandaloneSampler); + } + + @Test + void asmStandaloneSamplerNotSelectedWhenApmTracingEnabledAndAsmNotEnabled() { + Config config = Config.get(); + + Sampler sampler = Sampler.Builder.forConfig(config, null); + + assertFalse(sampler instanceof AsmStandaloneSampler); + } + + @WithConfig(key = TRACE_OTEL_EXPORTER, value = "otlp") + @WithConfig(key = PRIORITY_SAMPLING, value = "false") + @Test + void parentBasedAlwaysOnSamplerReplacesAllSamplerWhenOtlpEnabledAndPrioritySamplingDisabled() { + Config config = Config.get(); + + Sampler sampler = Sampler.Builder.forConfig(config, null); + + assertInstanceOf(ParentBasedAlwaysOnSampler.class, sampler); + } + + @WithConfig(key = PRIORITY_SAMPLING, value = "false") + @Test + void allSamplerSelectedWhenOtlpDisabledAndPrioritySamplingDisabled() { + Config config = Config.get(); + + Sampler sampler = Sampler.Builder.forConfig(config, null); + + assertInstanceOf(AllSampler.class, sampler); + assertFalse(sampler instanceof ParentBasedAlwaysOnSampler); + } + + @WithConfig(key = TRACE_OTEL_EXPORTER, value = "otlp") + @WithConfig(key = TRACE_SAMPLE_RATE, value = "0.5") + @Test + void traceSamplingRulesRespectedWhenOtlpEnabled() { + Config config = Config.get(); + + Sampler sampler = Sampler.Builder.forConfig(config, null); + + assertInstanceOf(RuleBasedTraceSampler.class, sampler); + assertFalse(sampler instanceof ParentBasedAlwaysOnSampler); + } + + @WithConfig(key = TRACE_OTEL_EXPORTER, value = "otlp") + @Test + void + parentBasedAlwaysOnSamplerReplacesRateByServiceTraceSamplerWhenOtlpEnabledWithDefaultPrioritySampling() { + Config config = Config.get(); + + Sampler sampler = Sampler.Builder.forConfig(config, null); + + assertInstanceOf(ParentBasedAlwaysOnSampler.class, sampler); + assertFalse(sampler instanceof RateByServiceTraceSampler); + } + + @WithConfig(key = TRACE_OTEL_EXPORTER, value = "otlp") + @WithConfig(key = PRIORITY_SAMPLING_FORCE, value = "keep") + @Test + void forcePrioritySamplerRespectedWhenOtlpEnabledAndPrioritySamplingForcedKeep() { + Config config = Config.get(); + + Sampler sampler = Sampler.Builder.forConfig(config, null); + + assertInstanceOf(ForcePrioritySampler.class, sampler); + assertFalse(sampler instanceof ParentBasedAlwaysOnSampler); + } + + @WithConfig(key = TRACE_OTEL_EXPORTER, value = "otlp") + @WithConfig(key = PRIORITY_SAMPLING_FORCE, value = "drop") + @Test + void forcePrioritySamplerRespectedWhenOtlpEnabledAndPrioritySamplingForcedDrop() { + Config config = Config.get(); + + Sampler sampler = Sampler.Builder.forConfig(config, null); + + assertInstanceOf(ForcePrioritySampler.class, sampler); + assertFalse(sampler instanceof ParentBasedAlwaysOnSampler); + } + + @WithConfig(key = TRACE_OTEL_EXPORTER, value = "otlp") + @WithConfig(key = PRIORITY_SAMPLING, value = "false") + @Test + void spansBuiltWithOtlpEnabledAndPrioritySamplingDisabledHaveNonUnsetSamplingPriority() { + Config config = Config.get(); + Sampler sampler = Sampler.Builder.forConfig(config, null); + CoreTracer tracer = CoreTracer.builder().writer(new ListWriter()).sampler(sampler).build(); + try { + DDSpan span = (DDSpan) tracer.buildSpan("datadog", "test").start(); + ((PrioritySampler) sampler).setSamplingPriority(span); + + assertNotNull(span.getSamplingPriority()); + assertEquals(SAMPLER_KEEP, (int) span.getSamplingPriority()); + + span.finish(); + } finally { + tracer.close(); + } + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/common/sampling/SingleSpanSamplerTest.java b/dd-trace-core/src/test/java/datadog/trace/common/sampling/SingleSpanSamplerTest.java new file mode 100644 index 00000000000..3dfb1b19853 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/common/sampling/SingleSpanSamplerTest.java @@ -0,0 +1,256 @@ +package datadog.trace.common.sampling; + +import static datadog.trace.api.config.TracerConfig.SPAN_SAMPLING_RULES; +import static datadog.trace.api.config.TracerConfig.SPAN_SAMPLING_RULES_FILE; +import static datadog.trace.api.config.TracerConfig.TRACE_SAMPLE_RATE; +import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_DROP; +import static datadog.trace.api.sampling.SamplingMechanism.DEFAULT; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.api.Config; +import datadog.trace.common.writer.ListWriter; +import datadog.trace.core.CoreTracer; +import datadog.trace.core.DDCoreJavaSpecification; +import datadog.trace.core.DDSpan; +import datadog.trace.test.junit.utils.converter.SamplingMechanismConverter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Properties; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.converter.ConvertWith; +import org.tabletest.junit.TableTest; + +class SingleSpanSamplerTest extends DDCoreJavaSpecification { + + @TableTest({ + "scenario | rules ", + "null rules | ", + "empty rules | '[]' ", + "invalid sample_rate 10 | '[ { \"service\": \"*\", \"name\": \"*\", \"sample_rate\": 10.0 } ]' ", + "invalid sample_rate all | '[ { \"service\": \"*\", \"name\": \"*\", \"sample_rate\": \"all\" } ]' ", + "invalid max_per_second | '[ { \"service\": \"*\", \"name\": \"*\", \"sample_rate\": 1.0, \"max_per_second\": \"N/A\" } ]'" + }) + void singleSpanSamplerNotCreatedWhenNoRulesProvided(String rules) { + Properties properties = new Properties(); + if (rules != null) { + properties.setProperty(SPAN_SAMPLING_RULES, rules); + } + + SingleSpanSampler sampler = SingleSpanSampler.Builder.forConfig(Config.get(properties)); + + assertNull(sampler); + } + + @TableTest({ + "scenario | rules | isFirstSampled | expectedMechanism | expectedRate | expectedLimit", + "* match rate 1.0 | '[ { \"service\": \"*\", \"name\": \"*\", \"sample_rate\": 1.0 } ]' | true | SamplingMechanism.SPAN_SAMPLING_RATE | 1.0 | ", + "* match rate 1.0 limit 10 | '[ { \"service\": \"*\", \"name\": \"*\", \"sample_rate\": 1.0, \"max_per_second\": 10 } ]' | true | SamplingMechanism.SPAN_SAMPLING_RATE | 1.0 | 10 ", + "ser* oper* limit 15 | '[ { \"service\": \"ser*\", \"name\": \"oper*\", \"sample_rate\": 1.0, \"max_per_second\": 15 } ]' | true | SamplingMechanism.SPAN_SAMPLING_RATE | 1.0 | 15 ", + "?ervice operati?n limit 10 | '[ { \"service\": \"?ervice\", \"name\": \"operati?n\", \"sample_rate\": 1.0, \"max_per_second\": 10 } ]' | true | SamplingMechanism.SPAN_SAMPLING_RATE | 1.0 | 10 ", + "service operation limit 5 | '[ { \"service\": \"service\", \"name\": \"operation\", \"sample_rate\": 1.0, \"max_per_second\": 5 } ]' | true | SamplingMechanism.SPAN_SAMPLING_RATE | 1.0 | 5 ", + "service-b no match | '[ { \"service\": \"service-b\", \"name\": \"*\", \"sample_rate\": 1.0, \"max_per_second\": 10 } ]' | false | | | ", + "rate 0.0 no sample | '[ { \"service\": \"*\", \"name\": \"*\", \"sample_rate\": 0.0 } ]' | false | | | ", + "operation-b no match | '[ { \"service\": \"*\", \"name\": \"operation-b\", \"sample_rate\": 0.5 } ]' | false | | | " + }) + void singleSpanSamplerSetSamplingPriority( + String rules, + boolean isFirstSampled, + @ConvertWith(SamplingMechanismConverter.class) Byte expectedMechanism, + Double expectedRate, + Integer expectedLimit) { + Properties properties = new Properties(); + if (rules != null) { + properties.setProperty(SPAN_SAMPLING_RULES, rules); + } + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + + SingleSpanSampler sampler = SingleSpanSampler.Builder.forConfig(Config.get(properties)); + + DDSpan span = + (DDSpan) + tracer + .buildSpan("datadog", "operation") + .withServiceName("service") + .withTag("env", "bar") + .ignoreActiveSpan() + .start(); + + assertEquals(isFirstSampled, sampler.setSamplingPriority(span)); + + assertTagEquals(expectedMechanism, span.getTag("_dd.span_sampling.mechanism")); + assertTagEquals(expectedRate, span.getTag("_dd.span_sampling.rule_rate")); + assertTagEquals(expectedLimit, span.getTag("_dd.span_sampling.max_per_second")); + } + + @TableTest({ + "scenario | rules | sampleRoot | sampleChild | rootMechanism | childMechanism ", + "web.request match | '[{\"service\": \"webserver\", \"name\": \"web.request\"}]' | true | false | SamplingMechanism.SPAN_SAMPLING_RATE | ", + "web.handler match | '[{\"service\": \"webserver\", \"name\": \"web.handler\"}]' | false | true | | SamplingMechanism.SPAN_SAMPLING_RATE", + "web.* match both | '[{\"service\": \"webserver\", \"name\": \"web.*\"}]' | true | true | SamplingMechanism.SPAN_SAMPLING_RATE | SamplingMechanism.SPAN_SAMPLING_RATE", + "other-server no match | '[{\"service\": \"other-server\"}]' | false | false | | " + }) + void parentChildScenariosWhenTraceDroppedButSpansKeptBySingleSpanSampler( + String rules, + boolean sampleRoot, + boolean sampleChild, + @ConvertWith(SamplingMechanismConverter.class) Byte rootMechanism, + @ConvertWith(SamplingMechanismConverter.class) Byte childMechanism) { + Properties properties = new Properties(); + if (rules != null) { + properties.setProperty(SPAN_SAMPLING_RULES, rules); + properties.setProperty(TRACE_SAMPLE_RATE, "0"); + } + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + + SingleSpanSampler sampler = SingleSpanSampler.Builder.forConfig(Config.get(properties)); + + DDSpan rootSpan = + (DDSpan) + tracer + .buildSpan("datadog", "web.request") + .withServiceName("webserver") + .ignoreActiveSpan() + .start(); + + DDSpan childSpan = + (DDSpan) + tracer + .buildSpan("datadog", "web.handler") + .withServiceName("webserver") + .asChildOf(rootSpan) + .ignoreActiveSpan() + .start(); + + // set trace sampling priority to drop the trace + rootSpan.setSamplingPriority(SAMPLER_DROP, DEFAULT); + + // set spans sampling priority + assertEquals(sampleRoot, sampler.setSamplingPriority(rootSpan)); + assertEquals(sampleChild, sampler.setSamplingPriority(childSpan)); + + assertTagEquals(rootMechanism, rootSpan.getTag("_dd.span_sampling.mechanism")); + assertTagEquals(childMechanism, childSpan.getTag("_dd.span_sampling.mechanism")); + } + + @TableTest({ + "scenario | rules | isFirstSampled | isSecondSampled", + "* limit 1 first only | '[ { \"service\": \"*\", \"name\": \"*\", \"sample_rate\": 1.0, \"max_per_second\": 1 } ]' | true | false ", + "ser* oper* limit 1 | '[ { \"service\": \"ser*\", \"name\": \"oper*\", \"sample_rate\": 1.0, \"max_per_second\": 1 } ]' | true | false ", + "?ervice operati?n limit 1 | '[ { \"service\": \"?ervice\", \"name\": \"operati?n\", \"sample_rate\": 1.0, \"max_per_second\": 1 } ]' | true | false ", + "service operation limit 1 | '[ { \"service\": \"service\", \"name\": \"operation\", \"sample_rate\": 1.0, \"max_per_second\": 1 } ]' | true | false ", + "service only limit 1 | '[ { \"service\": \"service\", \"max_per_second\": 1 } ]' | true | false ", + "name only limit 1 | '[ { \"name\": \"operation\", \"sample_rate\": 1.0, \"max_per_second\": 1 } ]' | true | false ", + "* limit 2 both sampled | '[ { \"service\": \"*\", \"name\": \"*\", \"sample_rate\": 1.0, \"max_per_second\": 2 } ]' | true | true ", + "ser* oper* no rate limit 2 | '[ { \"service\": \"ser*\", \"name\": \"oper*\", \"max_per_second\": 2 } ]' | true | true ", + "?ervice operati?n limit 2 | '[ { \"service\": \"?ervice\", \"name\": \"operati?n\", \"sample_rate\": 1.0, \"max_per_second\": 2 } ]' | true | true " + }) + void singleSpanSamplerSetSamplingPriorityWithMaxPerSecondLimit( + String rules, boolean isFirstSampled, boolean isSecondSampled) { + Properties properties = new Properties(); + if (rules != null) { + properties.setProperty(SPAN_SAMPLING_RULES, rules); + } + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + + SingleSpanSampler sampler = SingleSpanSampler.Builder.forConfig(Config.get(properties)); + + DDSpan span1 = + (DDSpan) + tracer + .buildSpan("datadog", "operation") + .withServiceName("service") + .withTag("env", "bar") + .ignoreActiveSpan() + .start(); + + DDSpan span2 = + (DDSpan) + tracer + .buildSpan("datadog", "operation") + .withServiceName("service") + .withTag("env", "bar") + .ignoreActiveSpan() + .start(); + + assertEquals(isFirstSampled, sampler.setSamplingPriority(span1)); + assertEquals(isSecondSampled, sampler.setSamplingPriority(span2)); + } + + @Test + void loadRulesFromFile() throws Exception { + String rules = + "[ { \"service\": \"*\", \"name\": \"op?ration*\", \"sample_rate\": 1.0, \"max_per_second\": 1 } ]"; + Properties properties = new Properties(); + properties.setProperty(SPAN_SAMPLING_RULES_FILE, createRulesFile(rules)); + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + + SingleSpanSampler sampler = SingleSpanSampler.Builder.forConfig(Config.get(properties)); + + DDSpan span1 = + (DDSpan) + tracer + .buildSpan("datadog", "operation") + .withServiceName("service") + .withTag("env", "bar") + .ignoreActiveSpan() + .start(); + + assertTrue(sampler.setSamplingPriority(span1)); + } + + @TableTest({ + "scenario | envVarRules | fileRules | matched", + "env 0 file 1 -> false | '[ { \"sample_rate\": 0 } ]' | '[ { \"sample_rate\": 1 } ]' | false ", + "env 1 file 0 -> true | '[ { \"sample_rate\": 1 } ]' | '[ { \"sample_rate\": 0 } ]' | true " + }) + void preferRulesInEnvVarOverRulesFromFile(String envVarRules, String fileRules, boolean matched) + throws Exception { + Properties properties = new Properties(); + properties.setProperty(SPAN_SAMPLING_RULES, envVarRules); + properties.setProperty(SPAN_SAMPLING_RULES_FILE, createRulesFile(fileRules)); + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + + SingleSpanSampler sampler = SingleSpanSampler.Builder.forConfig(Config.get(properties)); + + DDSpan span1 = + (DDSpan) + tracer + .buildSpan("datadog", "operation") + .withServiceName("service") + .withTag("env", "bar") + .ignoreActiveSpan() + .start(); + + assertEquals(matched, sampler.setSamplingPriority(span1)); + } + + @Test + void throwNpeWhenPassedListOfRulesIsNull() { + NullPointerException exception = + assertThrows( + NullPointerException.class, + () -> new SingleSpanSampler.RuleBasedSingleSpanSampler(null)); + assertEquals("SpanSamplingRules can't be null.", exception.getMessage()); + } + + private static String createRulesFile(String rules) throws IOException { + Path tempFile = Files.createTempFile("single-span-sampling-rules", ".json"); + Files.write(tempFile, rules.getBytes(StandardCharsets.UTF_8)); + return tempFile.toString(); + } + + private static void assertTagEquals(Number expected, Object actual) { + if (expected == null) { + assertNull(actual, "Expected tag to be null"); + } else { + assertNotNull(actual, "Expected tag to be non-null"); + assertEquals(expected.doubleValue(), ((Number) actual).doubleValue(), 1e-9); + } + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/common/sampling/SpanSamplingRulesFileTest.java b/dd-trace-core/src/test/java/datadog/trace/common/sampling/SpanSamplingRulesFileTest.java new file mode 100644 index 00000000000..7fa7a676673 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/common/sampling/SpanSamplingRulesFileTest.java @@ -0,0 +1,24 @@ +package datadog.trace.common.sampling; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +class SpanSamplingRulesFileTest extends SpanSamplingRulesTest { + + static String createRulesFile(String rules) throws IOException { + Path tempFile = Files.createTempFile("single-span-sampling-rules", ".json"); + Files.write(tempFile, rules.getBytes(StandardCharsets.UTF_8)); + return tempFile.toString(); + } + + @Override + protected SpanSamplingRules deserializeRules(String jsonRules) { + try { + return SpanSamplingRules.deserializeFile(createRulesFile(jsonRules)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/common/sampling/SpanSamplingRulesTest.java b/dd-trace-core/src/test/java/datadog/trace/common/sampling/SpanSamplingRulesTest.java new file mode 100644 index 00000000000..021ab31171b --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/common/sampling/SpanSamplingRulesTest.java @@ -0,0 +1,173 @@ +package datadog.trace.common.sampling; + +import static datadog.trace.api.sampling.SamplingRule.MATCH_ALL; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.Moshi; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.tabletest.junit.TableTest; + +class SpanSamplingRulesTest { + + protected SpanSamplingRules deserializeRules(String jsonRules) { + return SpanSamplingRules.deserialize(jsonRules); + } + + @Test + void deserializeEmptyListOfSpanSamplingRulesFromJson() { + assertTrue(deserializeRules("[]").isEmpty()); + } + + @Test + void deserializeSpanSamplingRulesFromJson() { + SpanSamplingRules result = + deserializeRules( + "[\n" + + " {\"service\": \"service-name\", \"name\": \"operation-name\", \"resource\": \"resource-name\", \"tags\":\n" + + " {\"tag-name1\": \"tag-pattern1\",\n" + + " \"tag-name2\": \"tag-pattern2\"},\n" + + " \"sample_rate\": 0.0, \"max_per_second\": 10.0},\n" + + " {},\n" + + " {\"service\": \"\", \"name\": \"\", \"resource\": \"\", \"tags\": {}},\n" + + " {\"service\": null, \"name\": null, \"resource\": null, \"tags\": null, \"sample_rate\": null, \"max_per_second\": null},\n" + + "\n" + + " {\"sample_rate\": 0.25},\n" + + " {\"sample_rate\": 0.5},\n" + + " {\"sample_rate\": 0.75},\n" + + " {\"sample_rate\": 1},\n" + + "\n" + + " {\"max_per_second\": 0.2},\n" + + " {\"max_per_second\": 1.0},\n" + + " {\"max_per_second\": 10},\n" + + " {\"max_per_second\": 10.123},\n" + + " {\"max_per_second\": 10000}\n" + + "]"); + List rules = result.getRules(); + int ruleIndex = 0; + + assertEquals(13, rules.size()); + + // Test a complete rule + Map expectedTags = new LinkedHashMap<>(); + expectedTags.put("tag-name1", "tag-pattern1"); + expectedTags.put("tag-name2", "tag-pattern2"); + assertEquals("service-name", rules.get(ruleIndex).getService()); + assertEquals("operation-name", rules.get(ruleIndex).getName()); + assertEquals("resource-name", rules.get(ruleIndex).getResource()); + assertEquals(expectedTags, rules.get(ruleIndex).getTags()); + assertEquals(0.0d, rules.get(ruleIndex).getSampleRate(), 1e-9); + assertEquals(10, rules.get(ruleIndex++).getMaxPerSecond()); + + // Test default values with an empty rule + assertEquals(MATCH_ALL, rules.get(ruleIndex).getService()); + assertEquals(MATCH_ALL, rules.get(ruleIndex).getName()); + assertEquals(MATCH_ALL, rules.get(ruleIndex).getResource()); + assertTrue(rules.get(ruleIndex).getTags().isEmpty()); + assertEquals(1d, rules.get(ruleIndex).getSampleRate(), 1e-9); + assertEquals(Integer.MAX_VALUE, rules.get(ruleIndex++).getMaxPerSecond()); + + // Test rule with empty values + assertEquals("", rules.get(ruleIndex).getService()); + assertEquals("", rules.get(ruleIndex).getName()); + assertEquals("", rules.get(ruleIndex).getResource()); + assertTrue(rules.get(ruleIndex).getTags().isEmpty()); + assertEquals(1d, rules.get(ruleIndex).getSampleRate(), 1e-9); + assertEquals(Integer.MAX_VALUE, rules.get(ruleIndex++).getMaxPerSecond()); + + // Test rule with null values + assertEquals(MATCH_ALL, rules.get(ruleIndex).getService()); + assertEquals(MATCH_ALL, rules.get(ruleIndex).getName()); + assertEquals(MATCH_ALL, rules.get(ruleIndex).getResource()); + assertTrue(rules.get(ruleIndex).getTags().isEmpty()); + assertEquals(1d, rules.get(ruleIndex).getSampleRate(), 1e-9); + assertEquals(Integer.MAX_VALUE, rules.get(ruleIndex++).getMaxPerSecond()); + + // Test different sample rate values + assertEquals(0.25d, rules.get(ruleIndex++).getSampleRate(), 1e-9); + assertEquals(0.5d, rules.get(ruleIndex++).getSampleRate(), 1e-9); + assertEquals(0.75d, rules.get(ruleIndex++).getSampleRate(), 1e-9); + assertEquals(1d, rules.get(ruleIndex++).getSampleRate(), 1e-9); + + // Test different max per second values + assertEquals(1, rules.get(ruleIndex++).getMaxPerSecond()); + assertEquals(1, rules.get(ruleIndex++).getMaxPerSecond()); + assertEquals(10, rules.get(ruleIndex++).getMaxPerSecond()); + assertEquals(10, rules.get(ruleIndex++).getMaxPerSecond()); + assertEquals(10000, rules.get(ruleIndex++).getMaxPerSecond()); + } + + @TableTest({ + "scenario | rate ", + "-0.1 | -0.1 ", + "-11 | -11 ", + "1.2 | 1.2 ", + "100 | 100 ", + "\"zero\" | '\"zero\"'", + "\"\" | '\"\"' " + }) + void skipSpanSamplingRulesWithInvalidSampleRateValues(String rate) { + String json = + "[{\"service\": \"usersvc\", \"name\": \"healthcheck\", \"sample_rate\": " + rate + "}]"; + SpanSamplingRules result = deserializeRules(json); + + assertTrue(result.isEmpty()); + } + + @TableTest({ + "scenario | limit ", + "0 | 0 ", + "-11 | -11 ", + "\"zero\" | '\"zero\"'", + "\"\" | '\"\"' " + }) + void skipSpanSamplingRulesWithInvalidMaxPerSecondValues(String limit) { + String json = + "[{\"service\": \"usersvc\", \"name\": \"healthcheck\", \"max_per_second\": " + + limit + + "}]"; + SpanSamplingRules result = deserializeRules(json); + + assertTrue(result.isEmpty()); + } + + @TableTest({ + "scenario | jsonRules ", + "truncated open bracket | '[' ", + "trailing comma | '{\"service\": \"usersvc\",}'", + "empty string | '' " + }) + void skipSpanSamplingRulesWhenIncorrectJsonProvided(String jsonRules) { + assertTrue(deserializeRules(jsonRules).isEmpty()); + } + + @Test + @SuppressWarnings("unchecked") + void renderJsonRuleCorrectlyWhenToStringIsCalled() throws Exception { + String json = + "{\"max_per_second\":\"10\",\"name\":\"name\",\"resource\":\"resource\",\"sample_rate\":\"0.5\",\"service\":\"service\",\"tags\":{\"a\":\"b\",\"foo\":\"bar\"}}"; + Class jsonRuleClass = + Class.forName("datadog.trace.common.sampling.SpanSamplingRules$JsonRule"); + Moshi moshi = new Moshi.Builder().build(); + JsonAdapter adapter = (JsonAdapter) moshi.adapter(jsonRuleClass); + Object jsonRule = adapter.fromJson(json); + + assertEquals(json, jsonRule.toString()); + } + + @Test + void keepOnlyValidRulesWhenInvalidRulesArePresent() { + SpanSamplingRules rules = + SpanSamplingRules.deserialize( + "[\n" + + " {\"service\": \"usersvc\", \"name\": \"healthcheck\", \"sample_rate\": 0.5},\n" + + " {\"service\": \"usersvc\", \"name\": \"healthcheck2\", \"sample_rate\": 200}\n" + + "]"); + + assertEquals(1, rules.getRules().size()); + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/common/sampling/TraceSamplingRulesTest.java b/dd-trace-core/src/test/java/datadog/trace/common/sampling/TraceSamplingRulesTest.java new file mode 100644 index 00000000000..f580b717bcf --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/common/sampling/TraceSamplingRulesTest.java @@ -0,0 +1,137 @@ +package datadog.trace.common.sampling; + +import static datadog.trace.api.sampling.SamplingRule.MATCH_ALL; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.Moshi; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.tabletest.junit.TableTest; + +class TraceSamplingRulesTest { + + @Test + void deserializeEmptyListOfTraceSamplingRulesFromJson() { + assertTrue(TraceSamplingRules.deserialize("[]").isEmpty()); + } + + @Test + void deserializeTraceSamplingRulesFromJson() { + List rules = + TraceSamplingRules.deserialize( + "[\n" + + " {\"service\": \"service-name\", \"name\": \"operation-name\", \"resource\": \"resource-name\", \"tags\":\n" + + " {\"tag-name1\": \"tag-pattern1\",\n" + + " \"tag-name2\": \"tag-pattern2\"},\n" + + " \"sample_rate\": 0.0},\n" + + " {},\n" + + " {\"service\": \"\", \"name\": \"\", \"resource\": \"\", \"tags\": {}},\n" + + " {\"service\": null, \"name\": null, \"resource\": null, \"tags\": null, \"sample_rate\": null},\n" + + "\n" + + " {\"sample_rate\": 0.25},\n" + + " {\"sample_rate\": 0.5},\n" + + " {\"sample_rate\": 0.75},\n" + + " {\"sample_rate\": 1}\n" + + "]") + .getRules(); + int ruleIndex = 0; + + assertEquals(8, rules.size()); + + // Test a complete rule + Map expectedTags = new LinkedHashMap<>(); + expectedTags.put("tag-name1", "tag-pattern1"); + expectedTags.put("tag-name2", "tag-pattern2"); + assertEquals("service-name", rules.get(ruleIndex).getService()); + assertEquals("operation-name", rules.get(ruleIndex).getName()); + assertEquals("resource-name", rules.get(ruleIndex).getResource()); + assertEquals(expectedTags, rules.get(ruleIndex).getTags()); + assertEquals(0d, rules.get(ruleIndex++).getSampleRate(), 1e-9); + + // Test default values with an empty rule + assertEquals(MATCH_ALL, rules.get(ruleIndex).getService()); + assertEquals(MATCH_ALL, rules.get(ruleIndex).getName()); + assertEquals(MATCH_ALL, rules.get(ruleIndex).getResource()); + assertTrue(rules.get(ruleIndex).getTags().isEmpty()); + assertEquals(1d, rules.get(ruleIndex++).getSampleRate(), 1e-9); + + // Test rule with empty values + assertEquals("", rules.get(ruleIndex).getService()); + assertEquals("", rules.get(ruleIndex).getName()); + assertEquals("", rules.get(ruleIndex).getResource()); + assertTrue(rules.get(ruleIndex).getTags().isEmpty()); + assertEquals(1d, rules.get(ruleIndex++).getSampleRate(), 1e-9); + + // Test rule with null values + assertEquals(MATCH_ALL, rules.get(ruleIndex).getService()); + assertEquals(MATCH_ALL, rules.get(ruleIndex).getName()); + assertEquals(MATCH_ALL, rules.get(ruleIndex).getResource()); + assertTrue(rules.get(ruleIndex).getTags().isEmpty()); + assertEquals(1d, rules.get(ruleIndex++).getSampleRate(), 1e-9); + + // Test different sample rate values + assertEquals(0.25d, rules.get(ruleIndex++).getSampleRate(), 1e-9); + assertEquals(0.5d, rules.get(ruleIndex++).getSampleRate(), 1e-9); + assertEquals(0.75d, rules.get(ruleIndex++).getSampleRate(), 1e-9); + assertEquals(1d, rules.get(ruleIndex++).getSampleRate(), 1e-9); + } + + @TableTest({ + "scenario | rate ", + "-0.1 | -0.1 ", + "-11 | -11 ", + "1.2 | 1.2 ", + "100 | 100 ", + "\"zero\" | '\"zero\"'", + "\"\" | '\"\"' ", + "{} | '{}' ", + "[] | '[]' " + }) + void skipTraceSamplingRulesWithInvalidSampleRateValues(String rate) { + String json = + "[{\"service\": \"usersvc\", \"name\": \"healthcheck\", \"sample_rate\": " + rate + "}]"; + TraceSamplingRules result = TraceSamplingRules.deserialize(json); + + assertTrue(result.isEmpty()); + } + + @TableTest({ + "scenario | jsonRules ", + "truncated open bracket | '[' ", + "trailing comma | '{\"service\": \"usersvc\",}'", + "empty string | '' " + }) + void skipTraceSamplingRulesWhenIncorrectJsonProvided(String jsonRules) { + assertTrue(TraceSamplingRules.deserialize(jsonRules).isEmpty()); + } + + @Test + @SuppressWarnings("unchecked") + void renderJsonRuleCorrectlyWhenToStringIsCalled() throws Exception { + String json = + "{\"name\":\"name\",\"resource\":\"resource\",\"sample_rate\":\"0.5\",\"service\":\"service\",\"tags\":{\"a\":\"b\",\"foo\":\"bar\"}}"; + Class jsonRuleClass = + Class.forName("datadog.trace.common.sampling.TraceSamplingRules$JsonRule"); + Moshi moshi = new Moshi.Builder().build(); + JsonAdapter adapter = (JsonAdapter) moshi.adapter(jsonRuleClass); + Object jsonRule = adapter.fromJson(json); + + assertEquals(json, jsonRule.toString()); + } + + @Test + void keepOnlyValidRulesWhenInvalidRulesArePresent() { + TraceSamplingRules rules = + TraceSamplingRules.deserialize( + "[\n" + + " {\"service\": \"usersvc\", \"name\": \"healthcheck\", \"sample_rate\": 0.5},\n" + + " {\"service\": \"usersvc\", \"name\": \"healthcheck\", \"sample_rate\": 200}\n" + + "]"); + + assertEquals(1, rules.getRules().size()); + } +} diff --git a/utils/config-utils/src/test/java/datadog/trace/api/ConfigSettingTest.java b/utils/config-utils/src/test/java/datadog/trace/api/ConfigSettingTest.java index 1d110be1287..0ef2835f0d9 100644 --- a/utils/config-utils/src/test/java/datadog/trace/api/ConfigSettingTest.java +++ b/utils/config-utils/src/test/java/datadog/trace/api/ConfigSettingTest.java @@ -3,10 +3,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import datadog.trace.test.junit.utils.tabletest.BoxedValueConverter; import datadog.trace.test.junit.utils.tabletest.ConfigValueConverter; -import org.junit.jupiter.api.extension.ParameterContext; -import org.junit.jupiter.params.converter.ArgumentConversionException; -import org.junit.jupiter.params.converter.ArgumentConverter; import org.junit.jupiter.params.converter.ConvertWith; import org.tabletest.junit.TableTest; @@ -84,58 +82,4 @@ void convertIterableMapAndBitSetToString( @ConvertWith(ConfigValueConverter.class) Object value, String rendered) { assertEquals(rendered, ConfigSetting.of("key", value, ConfigOrigin.DEFAULT).stringValue()); } - - /** - * Converts a String cell value to the most specific boxed Java primitive type. Use with - * {@code @ConvertWith(BoxedValueConverter.class)} on {@code Object}-typed parameters when the - * test needs actual typed values (e.g. {@code Float} not {@code String "2.33f"}). - * - *

Conversion rules: - * - *

    - *
  • blank/null -> null - *
  • {@code "true"}/{@code "false"} -> {@link Boolean} - *
  • ends with {@code "f"} -> {@link Float} - *
  • contains {@code "."} -> {@link Double} - *
  • parseable as integer -> {@link Integer} - *
  • otherwise -> {@link String} - *
- */ - static class BoxedValueConverter implements ArgumentConverter { - @Override - public Object convert(Object source, ParameterContext context) - throws ArgumentConversionException { - if (source == null) { - return null; - } - - String s = source.toString(); - switch (s) { - case "": - return null; - case "true": - return Boolean.TRUE; - case "false": - return Boolean.FALSE; - } - if (s.endsWith("f")) { - try { - return Float.parseFloat(s.substring(0, s.length() - 1)); - } catch (NumberFormatException ignored) { - } - } - - if (s.contains(".")) { - try { - return Double.parseDouble(s); - } catch (NumberFormatException ignored) { - } - } - try { - return Integer.parseInt(s); - } catch (NumberFormatException ignored) { - } - return s; - } - } } diff --git a/utils/test-junit-converter-utils/src/main/java/datadog/trace/test/junit/utils/tabletest/BoxedValueConverter.java b/utils/test-junit-converter-utils/src/main/java/datadog/trace/test/junit/utils/tabletest/BoxedValueConverter.java new file mode 100644 index 00000000000..c91eeb62daf --- /dev/null +++ b/utils/test-junit-converter-utils/src/main/java/datadog/trace/test/junit/utils/tabletest/BoxedValueConverter.java @@ -0,0 +1,60 @@ +package datadog.trace.test.junit.utils.tabletest; + +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.params.converter.ArgumentConversionException; +import org.junit.jupiter.params.converter.ArgumentConverter; + +/** + * Converts a String cell value to the most specific boxed Java primitive type. Use with + * {@code @ConvertWith(BoxedValueConverter.class)} on {@code Object}-typed parameters when the test + * needs actual typed values (e.g. {@code Float} not {@code String "2.33f"}). + * + *

Conversion rules: + * + *

    + *
  • blank/null -> null + *
  • {@code "true"}/{@code "false"} -> {@link Boolean} + *
  • ends with {@code "f"} -> {@link Float} + *
  • contains {@code "."} -> {@link Double} + *
  • parseable as integer -> {@link Integer} + *
  • otherwise -> {@link String} + *
+ */ +public class BoxedValueConverter implements ArgumentConverter { + + @Override + public Object convert(Object source, ParameterContext context) + throws ArgumentConversionException { + if (source == null) { + return null; + } + + String s = source.toString(); + switch (s) { + case "": + return null; + case "true": + return Boolean.TRUE; + case "false": + return Boolean.FALSE; + } + if (s.endsWith("f")) { + try { + return Float.parseFloat(s.substring(0, s.length() - 1)); + } catch (NumberFormatException ignored) { + } + } + + if (s.contains(".")) { + try { + return Double.parseDouble(s); + } catch (NumberFormatException ignored) { + } + } + try { + return Integer.parseInt(s); + } catch (NumberFormatException ignored) { + } + return s; + } +} diff --git a/utils/test-junit-converter-utils/src/main/java/datadog/trace/test/junit/utils/tabletest/TableTestTypeConverters.java b/utils/test-junit-converter-utils/src/main/java/datadog/trace/test/junit/utils/tabletest/TableTestTypeConverters.java index 6f4157ab9fe..72b8e3a129b 100644 --- a/utils/test-junit-converter-utils/src/main/java/datadog/trace/test/junit/utils/tabletest/TableTestTypeConverters.java +++ b/utils/test-junit-converter-utils/src/main/java/datadog/trace/test/junit/utils/tabletest/TableTestTypeConverters.java @@ -91,7 +91,7 @@ public static Number toNumber(String value) { if (value.endsWith("f")) { return Float.parseFloat(value); } - if (value.endsWith("d")) { + if (value.endsWith("d") || value.contains(".")) { return Double.parseDouble(value); } return Integer.decode(value);