diff --git a/apm-agent-benchmarks/src/main/java/co/elastic/apm/impl/AbstractReporterBenchmark.java b/apm-agent-benchmarks/src/main/java/co/elastic/apm/impl/AbstractReporterBenchmark.java
index 7eea5cad1b..d830032093 100644
--- a/apm-agent-benchmarks/src/main/java/co/elastic/apm/impl/AbstractReporterBenchmark.java
+++ b/apm-agent-benchmarks/src/main/java/co/elastic/apm/impl/AbstractReporterBenchmark.java
@@ -10,6 +10,7 @@
import co.elastic.apm.impl.payload.Service;
import co.elastic.apm.impl.payload.SystemInfo;
import co.elastic.apm.impl.payload.TransactionPayload;
+import co.elastic.apm.impl.sampling.ConstantSampler;
import co.elastic.apm.impl.stacktrace.StacktraceFactory;
import co.elastic.apm.impl.transaction.Span;
import co.elastic.apm.impl.transaction.Transaction;
@@ -69,7 +70,7 @@ public void setUp() throws Exception {
payload = new TransactionPayload(process, service, system);
for (int i = 0; i < reporterConfiguration.getMaxQueueSize(); i++) {
Transaction t = new Transaction();
- t.start(tracer, 0, true);
+ t.start(tracer, 0, ConstantSampler.of(true));
fillTransaction(t);
payload.getTransactions().add(t);
}
diff --git a/apm-agent-core/src/main/java/co/elastic/apm/configuration/CoreConfiguration.java b/apm-agent-core/src/main/java/co/elastic/apm/configuration/CoreConfiguration.java
index 16936844fb..1bbb9e71ec 100644
--- a/apm-agent-core/src/main/java/co/elastic/apm/configuration/CoreConfiguration.java
+++ b/apm-agent-core/src/main/java/co/elastic/apm/configuration/CoreConfiguration.java
@@ -57,6 +57,16 @@ public class CoreConfiguration extends ConfigurationOptionProvider {
"To reduce overhead and storage requirements, you can set the sample rate to a value between 0.0 and 1.0. " +
"We still record overall time and the result for unsampled transactions, but no context information, tags, or spans.")
.dynamic(true)
+ .addValidator(new ConfigurationOption.Validator
+ * A sampling rate of 0.5 means that 50% of all transactions should be {@linkplain Sampler sampled}.
+ *
+ * Implementation notes:
+ *
+ * We are taking advantage of the fact, that the {@link TransactionId} is randomly generated.
+ * So instead of generating another random number,
+ * we just see if the long value returned by {@link TransactionId#getMostSignificantBits()}
+ * falls into the range between the lowerBound and the higherBound.
+ * This is a visual representation of the mechanism with a sampling rate of 0.5 (=50%):
+ *
+ * Long.MIN_VALUE 0 Long.MAX_VALUE
+ * v v v
+ * [----------[----------|----------]----------]
+ * ^ ^
+ * lowerBound higherBound = Long.MAX_VALUE * samplingRate
+ * = Long.MAX_VALUE * samplingRate * -1
+ *
+ *
+ * In contrast other tracing systems, + * in Elastic APM, + * non-sampled {@link Transaction}s do get reported to the APM server. + * However, + * to keep the size at a minimum, + * the reported {@link Transaction} only contains the transaction name, + * the duration and the id. + * Also, + * {@link co.elastic.apm.api.Span}s of non sampled {@link Transaction}s are not reported. + *
+ */ +public interface Sampler { + + /** + * Determines whether the given transaction should be sampled. + * + * @param transactionId The id of the transaction. + * @return The sampling decision. + */ + boolean isSampled(TransactionId transactionId); +} diff --git a/apm-agent-core/src/main/java/co/elastic/apm/impl/transaction/Span.java b/apm-agent-core/src/main/java/co/elastic/apm/impl/transaction/Span.java index d15ffd963f..1252d8f2eb 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/impl/transaction/Span.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/impl/transaction/Span.java @@ -79,6 +79,7 @@ public Span start(ElasticApmTracer tracer, Transaction transaction, @Nullable Sp if (sampled) { start = (nanoTime - transaction.getDuration()) / MS_IN_NANOS; duration = nanoTime; + transaction.addSpan(this); } return this; } diff --git a/apm-agent-core/src/main/java/co/elastic/apm/impl/transaction/Transaction.java b/apm-agent-core/src/main/java/co/elastic/apm/impl/transaction/Transaction.java index 7d48ec6390..cfc0357fd1 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/impl/transaction/Transaction.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/impl/transaction/Transaction.java @@ -2,6 +2,7 @@ import co.elastic.apm.impl.ElasticApmTracer; import co.elastic.apm.impl.context.Context; +import co.elastic.apm.impl.sampling.Sampler; import co.elastic.apm.objectpool.Recyclable; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonInclude; @@ -86,12 +87,12 @@ public class Transaction implements Recyclable, co.elastic.apm.api.Transaction { @JsonProperty("sampled") private boolean sampled; - public Transaction start(ElasticApmTracer tracer, long startTimestampNanos, boolean sampled) { + public Transaction start(ElasticApmTracer tracer, long startTimestampNanos, Sampler sampler) { this.tracer = tracer; this.duration = startTimestampNanos; - this.sampled = sampled; this.timestamp.setTime(System.currentTimeMillis()); this.id.setToRandomValue(); + this.sampled = sampler.isSampled(id); return this; } diff --git a/apm-agent-core/src/main/java/co/elastic/apm/impl/transaction/TransactionId.java b/apm-agent-core/src/main/java/co/elastic/apm/impl/transaction/TransactionId.java index a96e44db29..eab0851bbd 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/impl/transaction/TransactionId.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/impl/transaction/TransactionId.java @@ -6,6 +6,7 @@ import java.io.IOException; import java.io.OutputStream; +import java.nio.ByteBuffer; import java.util.Arrays; import java.util.Random; import java.util.UUID; @@ -29,6 +30,10 @@ public void setToRandomValue(Random random) { random.nextBytes(data); } + public void setValue(long mostSignificantBits, long leastSignificantBits) { + ByteBuffer.wrap(data).putLong(mostSignificantBits).putLong(leastSignificantBits); + } + @Override public void resetState() { for (int i = 0; i < data.length; i++) { diff --git a/apm-agent-core/src/test/java/co/elastic/apm/configuration/SpyConfiguration.java b/apm-agent-core/src/test/java/co/elastic/apm/configuration/SpyConfiguration.java index 1ca6e9c04c..f54a56bc3e 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/configuration/SpyConfiguration.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/configuration/SpyConfiguration.java @@ -10,6 +10,8 @@ public class SpyConfiguration { + public static final String CONFIG_SOURCE_NAME = "test config source"; + /** * Creates a configuration registry where all {@link ConfigurationOptionProvider}s are wrapped with * {@link org.mockito.Mockito#spy(Object)} @@ -24,7 +26,7 @@ public static ConfigurationRegistry createSpyConfig() { builder.addOptionProvider(spy(options)); } return builder - .addConfigSource(SimpleSource.forTest("service_name", "elastic-apm-test")) + .addConfigSource(new SimpleSource(CONFIG_SOURCE_NAME).add("service_name", "elastic-apm-test")) .build(); } } diff --git a/apm-agent-core/src/test/java/co/elastic/apm/impl/ElasticApmTracerTest.java b/apm-agent-core/src/test/java/co/elastic/apm/impl/ElasticApmTracerTest.java index 832b8805e1..3393a95e37 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/impl/ElasticApmTracerTest.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/impl/ElasticApmTracerTest.java @@ -14,6 +14,8 @@ import org.junit.jupiter.api.Test; import org.stagemonitor.configuration.ConfigurationRegistry; +import java.io.IOException; + import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; @@ -195,4 +197,17 @@ void testDisableMidTransaction() { assertThat(reporter.getFirstTransaction()).isSameAs(transaction); } + + @Test + void testSamplingNone() throws IOException { + config.getConfig(CoreConfiguration.class).getSampleRate().update(0.0, SpyConfiguration.CONFIG_SOURCE_NAME); + try (Transaction transaction = tracerImpl.startTransaction()) { + transaction.setUser("1", "jon.doe@example.com", "jondoe"); + try (Span span = tracerImpl.startSpan()) { + } + } + assertThat(reporter.getTransactions()).hasSize(1); + assertThat(reporter.getFirstTransaction().getSpans()).hasSize(0); + assertThat(reporter.getFirstTransaction().getContext().getUser().getEmail()).isNull(); + } } diff --git a/apm-agent-core/src/test/java/co/elastic/apm/impl/payload/TransactionPayloadJsonSchemaTest.java b/apm-agent-core/src/test/java/co/elastic/apm/impl/payload/TransactionPayloadJsonSchemaTest.java index 505448393b..28a59e6ecb 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/impl/payload/TransactionPayloadJsonSchemaTest.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/impl/payload/TransactionPayloadJsonSchemaTest.java @@ -1,6 +1,7 @@ package co.elastic.apm.impl.payload; import co.elastic.apm.impl.ElasticApmTracer; +import co.elastic.apm.impl.sampling.ConstantSampler; import co.elastic.apm.impl.transaction.Span; import co.elastic.apm.impl.transaction.Transaction; import com.fasterxml.jackson.databind.ObjectMapper; @@ -35,7 +36,7 @@ private TransactionPayload createPayloadWithRequiredValues() { private Transaction createTransactionWithRequiredValues() { Transaction t = new Transaction(); - t.start(mock(ElasticApmTracer.class), 0, true); + t.start(mock(ElasticApmTracer.class), 0, ConstantSampler.of(true)); t.setType("type"); t.getContext().getRequest().withMethod("GET"); Span s = new Span(); diff --git a/apm-agent-core/src/test/java/co/elastic/apm/impl/sampling/ProbabilitySamplerTest.java b/apm-agent-core/src/test/java/co/elastic/apm/impl/sampling/ProbabilitySamplerTest.java new file mode 100644 index 0000000000..ee7b1e4b9b --- /dev/null +++ b/apm-agent-core/src/test/java/co/elastic/apm/impl/sampling/ProbabilitySamplerTest.java @@ -0,0 +1,64 @@ +package co.elastic.apm.impl.sampling; + +import co.elastic.apm.impl.transaction.TransactionId; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class ProbabilitySamplerTest { + + public static final int ITERATIONS = 1_000_000; + public static final int DELTA = (int) (ITERATIONS * 0.01); + public static final double SAMPLING_RATE = 0.5; + private Sampler sampler; + + @BeforeEach + void setUp() { + sampler = ProbabilitySampler.of(SAMPLING_RATE); + } + + @Test + void isSampledEmpiricalTest() { + int sampledTransactions = 0; + TransactionId id = new TransactionId(); + for (int i = 0; i < ITERATIONS; i++) { + id.setToRandomValue(); + if (sampler.isSampled(id)) { + sampledTransactions++; + } + } + assertThat(sampledTransactions).isBetween((int) (SAMPLING_RATE * ITERATIONS - DELTA), (int) (SAMPLING_RATE * ITERATIONS + DELTA)); + } + + @Test + void testSamplingUpperBoundary() { + long upperBound = Long.MAX_VALUE / 2; + final TransactionId transactionId = new TransactionId(); + + transactionId.setValue(upperBound - 1, 0); + assertThat(ProbabilitySampler.of(0.5).isSampled(transactionId)).isTrue(); + + transactionId.setValue(upperBound, 0); + assertThat(ProbabilitySampler.of(0.5).isSampled(transactionId)).isTrue(); + + transactionId.setValue(upperBound + 1, 0); + assertThat(ProbabilitySampler.of(0.5).isSampled(transactionId)).isFalse(); + } + + @Test + void testSamplingLowerBoundary() { + long lowerBound = -Long.MAX_VALUE / 2; + final TransactionId transactionId = new TransactionId(); + + transactionId.setValue(lowerBound + 1, 0); + assertThat(ProbabilitySampler.of(0.5).isSampled(transactionId)).isTrue(); + + transactionId.setValue(lowerBound, 0); + assertThat(ProbabilitySampler.of(0.5).isSampled(transactionId)).isTrue(); + + transactionId.setValue(lowerBound - 1, 0); + assertThat(ProbabilitySampler.of(0.5).isSampled(transactionId)).isFalse(); + } + +}