diff --git a/docs/src/main/asciidoc/spring-cloud-sleuth.adoc b/docs/src/main/asciidoc/spring-cloud-sleuth.adoc index de4957584..11d2ccda5 100644 --- a/docs/src/main/asciidoc/spring-cloud-sleuth.adoc +++ b/docs/src/main/asciidoc/spring-cloud-sleuth.adoc @@ -305,15 +305,15 @@ Span nextSpan(final Request input) { === Sampling in Spring Cloud Sleuth -By default Spring Cloud Sleuth sets all spans to non-exportable. -That means that traces appear in logs but not in any remote store. -For testing the default is often enough, and it probably is all you need if you use only the logs (for example, with an ELK aggregator). -If you export span data to Zipkin, there is also an `Sampler.ALWAYS_SAMPLE` setting that exports everything, `RateLimitingSampler` setting that samples X transactions per second (defaults to `1000`) or `ProbabilityBasedSampler` setting that samples a fixed fraction of spans. +Sampling only applies to tracing backends, such as Zipkin. Trace IDs appear in logs regardless of +sample rate. Sampling is a way to prevent overloading the system, by consistently tracing some, but +not all requests. -NOTE: The `RateLimitingSampler` is the default if you use `spring-cloud-sleuth-zipkin`. -You can configure the rate limit by setting `spring.sleuth.sampler.rate`. +The default rate of 10 traces per second is controlled by the `spring.sleuth.sampler.rate` +property and applies when we know Sleuth is used for reasons besides logging. Use a rate above 100 +traces per second with extreme caution as it can overload your tracing system. -A sampler can be installed by creating a bean definition, as shown in the following example: +The sampler can be set by Java Config also, as shown in the following example: [source,java] ---- @@ -321,9 +321,7 @@ include::{project-root}/spring-cloud-sleuth-core/src/test/java/org/springframewo ---- TIP: You can set the HTTP header `X-B3-Flags` to `1`, or, when doing messaging, you can set the `spanFlags` header to `1`. -Doing so forces the current span to be exportable regardless of the sampling decision. - -In order to use the rate-limited sampler set the `spring.sleuth.sampler.rate` property to choose an amount of traces to accept on a per-second interval. The minimum number is 0 and the max is 2,147,483,647 (max int). +Doing so forces the corresponding trace to be sampled regardless of the sampling configuration. == Propagation diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/sampler/SamplerAutoConfiguration.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/sampler/SamplerAutoConfiguration.java index 25270732d..473fbdd5e 100644 --- a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/sampler/SamplerAutoConfiguration.java +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/sampler/SamplerAutoConfiguration.java @@ -16,6 +16,7 @@ package org.springframework.cloud.sleuth.sampler; +import brave.sampler.CountingSampler; import brave.sampler.Sampler; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -23,12 +24,14 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; /** * {@linkplain Configuration configuration} for {@link Sampler}. * * @author Marcin Grzejszczak + * @see SamplerCondition * @since 2.1.0 */ @Configuration(proxyBeanMethods = false) @@ -44,14 +47,17 @@ public class SamplerAutoConfiguration { return Sampler.NEVER_SAMPLE; } + // NOTE: Brave's default samplers return Sampler.NEVER_SAMPLE if the config implies + // that static Sampler samplerFromProps(SamplerProperties config) { if (config.getProbability() != null) { - return new ProbabilityBasedSampler(config); + return CountingSampler.create(config.getProbability()); } - return new RateLimitingSampler(config); + return brave.sampler.RateLimitingSampler.create(config.getRate()); } @Configuration(proxyBeanMethods = false) + @Conditional(SamplerCondition.class) @ConditionalOnBean( type = "org.springframework.cloud.context.scope.refresh.RefreshScope") protected static class RefreshScopedSamplerConfiguration { @@ -60,12 +66,18 @@ public class SamplerAutoConfiguration { @RefreshScope @ConditionalOnMissingBean public Sampler defaultTraceSampler(SamplerProperties config) { - return samplerFromProps(config); + // TODO: Rewrite: refresh should replace the sampler, not change its state + // internally + if (config.getProbability() != null) { + return new ProbabilityBasedSampler(config); + } + return new RateLimitingSampler(config); } } @Configuration(proxyBeanMethods = false) + @Conditional(SamplerCondition.class) @ConditionalOnMissingBean( type = "org.springframework.cloud.context.scope.refresh.RefreshScope") protected static class NonRefreshScopeSamplerConfiguration { diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/sampler/SamplerCondition.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/sampler/SamplerCondition.java new file mode 100644 index 000000000..77c6164d9 --- /dev/null +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/sampler/SamplerCondition.java @@ -0,0 +1,82 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.sampler; + +import brave.TracingCustomizer; +import brave.handler.FinishedSpanHandler; +import brave.sampler.Sampler; + +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; + +/** + * Sleuth 1.x optimized for log-correlation only. Unless "spring-cloud-sleuth-zipkin" was + * present, the sampler defaulted to {@link Sampler#NEVER_SAMPLE}. This was to ensure the + * log only nodes never set the sampled flag. + * + *

+ * "spring-cloud-sleuth-zipkin" obviated the {@link Sampler#NEVER_SAMPLE} default by + * importing {@link SamplerAutoConfiguration}. Nothing else did, so sampling properties + * were effectively ignored unless "spring-cloud-sleuth-zipkin" was in use, or something + * else similarly imported {@link SamplerAutoConfiguration}. + * + *

+ * During a review of Wavefront integration, it was considered not correct to have other + * code import {@link SamplerAutoConfiguration}. To avoid that, retain the old behaviour + * about log only nodes, and also not pin configuration to Zipkin involves a more complex + * condition. + * + *

+ * This condition looks for signs of non-default setup which likely requires sampling + * configuration. It passes on one of the following beans exist: + * + *

+ *

+ * + *

+ * An integrated test that shows {@link Sampler#NEVER_SAMPLE} is default on fail exists in + * {@code TraceAutoConfigurationTests} intentionally, as users now needn't import + * {@link SamplerAutoConfiguration} directly. + */ +final class SamplerCondition extends AnyNestedCondition { + + SamplerCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + // zipkin2.reporter.Reporter is in the classpath here, but technically it is optional + @ConditionalOnBean(type = "zipkin2.reporter.Reporter") + static final class ReporterAvailable { + + } + + @ConditionalOnBean(FinishedSpanHandler.class) + static final class FinishedSpanHandlerAvailable { + + } + + @ConditionalOnBean(TracingCustomizer.class) + static final class TracingCustomizerAvailable { + + } + +} diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/autoconfig/TraceAutoConfigurationTests.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/autoconfig/TraceAutoConfigurationTests.java index ebfd2e26a..f61bf5902 100644 --- a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/autoconfig/TraceAutoConfigurationTests.java +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/autoconfig/TraceAutoConfigurationTests.java @@ -19,11 +19,14 @@ package org.springframework.cloud.sleuth.autoconfig; import brave.propagation.B3Propagation; import brave.propagation.ExtraFieldPropagation; import brave.propagation.Propagation; +import brave.sampler.RateLimitingSampler; +import brave.sampler.Sampler; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.assertj.core.api.BDDAssertions; import org.junit.Test; import zipkin2.reporter.InMemoryReporterMetrics; +import zipkin2.reporter.Reporter; import zipkin2.reporter.ReporterMetrics; import zipkin2.reporter.metrics.micrometer.MicrometerReporterMetrics; @@ -68,8 +71,48 @@ public class TraceAutoConfigurationTests { }); } + /** + * Duplicates + * {@link org.springframework.cloud.sleuth.sampler.SamplerAutoConfigurationTests} + * intentionally, to ensure configuration condition bugs do not exist. + */ @Test - public void should_use_B3Propagation_factory_if_no_have_any_config() { + public void should_use_NEVER_SAMPLER_when_only_logging() { + this.contextRunner.run((context -> { + final Sampler bean = context.getBean(Sampler.class); + BDDAssertions.then(bean).isSameAs(Sampler.NEVER_SAMPLE); + })); + } + + /** + * Duplicates + * {@link org.springframework.cloud.sleuth.sampler.SamplerAutoConfigurationTests} + * intentionally, to ensure configuration condition bugs do not exist. + */ + @Test + public void should_use_RateLimitedSampler_when_reporting() { + this.contextRunner.withUserConfiguration(WithReporter.class).run((context -> { + final Sampler bean = context.getBean(Sampler.class); + BDDAssertions.then(bean).isInstanceOf(RateLimitingSampler.class); + })); + } + + /** + * Duplicates + * {@link org.springframework.cloud.sleuth.sampler.SamplerAutoConfigurationTests} + * intentionally, to ensure configuration condition bugs do not exist. + */ + @Test + public void should_override_sampler() { + this.contextRunner.withUserConfiguration(WithReporter.class, WithSampler.class) + .run((context -> { + final Sampler bean = context.getBean(Sampler.class); + BDDAssertions.then(bean).isSameAs(Sampler.ALWAYS_SAMPLE); + })); + } + + @Test + public void should_use_B3Propagation_factory_by_default() { this.contextRunner.run((context -> { final Propagation.Factory bean = context.getBean(Propagation.Factory.class); BDDAssertions.then(bean).isInstanceOf(Propagation.Factory.class); @@ -106,6 +149,26 @@ public class TraceAutoConfigurationTests { } + @Configuration + static class WithReporter { + + @Bean + Reporter spanReporter() { + return zipkin2.Span::toString; + } + + } + + @Configuration + static class WithSampler { + + @Bean + Sampler alwaysSampler() { + return Sampler.ALWAYS_SAMPLE; + } + + } + @Configuration static class WithLocalKeys { diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/sampler/SamplerAutoConfigurationTests.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/sampler/SamplerAutoConfigurationTests.java index 7f3c204b5..f369d4145 100644 --- a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/sampler/SamplerAutoConfigurationTests.java +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/sampler/SamplerAutoConfigurationTests.java @@ -16,44 +16,170 @@ package org.springframework.cloud.sleuth.sampler; +import brave.Tracing; +import brave.TracingCustomizer; +import brave.handler.FinishedSpanHandler; +import brave.handler.MutableSpan; +import brave.propagation.TraceContext; +import brave.sampler.RateLimitingSampler; import brave.sampler.Sampler; import org.assertj.core.api.BDDAssertions; import org.junit.Test; +import zipkin2.Span; +import zipkin2.reporter.Reporter; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; /** * @author Marcin Grzejszczak * @since */ +// TODO: missing spring cloud context tests public class SamplerAutoConfigurationTests { + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(SamplerAutoConfiguration.class)); + @Test - public void should_use_probability_sampler_when_property_set() { - SamplerProperties properties = new SamplerProperties(); - properties.setProbability(10f); - - Sampler sampler = SamplerAutoConfiguration.samplerFromProps(properties); - - BDDAssertions.then(sampler).isInstanceOf(ProbabilityBasedSampler.class); + public void should_use_NEVER_SAMPLER_when_only_logging() { + this.contextRunner.run((context -> { + final Sampler bean = context.getBean(Sampler.class); + BDDAssertions.then(bean).isSameAs(Sampler.NEVER_SAMPLE); + })); } @Test - public void should_use_rate_limiting_sampler_when_probability_not_set() { + public void should_use_RateLimitedSampler_withFinishedSpanHandler() { + this.contextRunner.withUserConfiguration(WithFinishedSpanHandler.class) + .run((context -> { + final Sampler bean = context.getBean(Sampler.class); + BDDAssertions.then(bean).isInstanceOf(RateLimitingSampler.class); + })); + } + + @Test + public void should_use_RateLimitedSampler_withReporter() { + this.contextRunner.withUserConfiguration(WithReporter.class).run((context -> { + final Sampler bean = context.getBean(Sampler.class); + BDDAssertions.then(bean).isInstanceOf(RateLimitingSampler.class); + })); + } + + @Test + public void should_use_RateLimitedSampler_withTracingCustomizer() { + this.contextRunner.withUserConfiguration(WithTracingCustomizer.class) + .run((context -> { + final Sampler bean = context.getBean(Sampler.class); + BDDAssertions.then(bean).isInstanceOf(RateLimitingSampler.class); + })); + } + + @Test + public void should_override_sampler() { + this.contextRunner.withUserConfiguration(WithReporter.class, WithSampler.class) + .run((context -> { + final Sampler bean = context.getBean(Sampler.class); + BDDAssertions.then(bean).isSameAs(Sampler.ALWAYS_SAMPLE); + })); + } + + @Test + public void samplerFromProps_probability() { + SamplerProperties properties = new SamplerProperties(); + properties.setProbability(0.01f); + + Sampler sampler = SamplerAutoConfiguration.samplerFromProps(properties); + + BDDAssertions.then(sampler).isInstanceOf(brave.sampler.CountingSampler.class); + } + + @Test + public void samplerFromProps_rateLimit() { SamplerProperties properties = new SamplerProperties(); Sampler sampler = SamplerAutoConfiguration.samplerFromProps(properties); - BDDAssertions.then(sampler).isInstanceOf(RateLimitingSampler.class); + BDDAssertions.then(sampler).isInstanceOf(brave.sampler.RateLimitingSampler.class); } @Test - public void should_use_probability_sampler_when_both_rate_and_probability_is_set() { + public void samplerFromProps_rateLimitZero() { SamplerProperties properties = new SamplerProperties(); - properties.setProbability(10f); + properties.setRate(0); + + Sampler sampler = SamplerAutoConfiguration.samplerFromProps(properties); + + BDDAssertions.then(sampler).isSameAs(Sampler.NEVER_SAMPLE); + } + + @Test + public void samplerFromProps_prefersProbability() { + SamplerProperties properties = new SamplerProperties(); + properties.setProbability(0.01f); properties.setRate(20); Sampler sampler = SamplerAutoConfiguration.samplerFromProps(properties); - BDDAssertions.then(sampler).isInstanceOf(ProbabilityBasedSampler.class); + BDDAssertions.then(sampler).isInstanceOf(brave.sampler.CountingSampler.class); + } + + @Test + public void samplerFromProps_prefersZeroProbability() { + SamplerProperties properties = new SamplerProperties(); + properties.setProbability(0.0f); + properties.setRate(20); + + Sampler sampler = SamplerAutoConfiguration.samplerFromProps(properties); + + BDDAssertions.then(sampler).isSameAs(Sampler.NEVER_SAMPLE); + } + + @Configuration + static class WithFinishedSpanHandler { + + @Bean + FinishedSpanHandler finishedSpanHandler() { + return new FinishedSpanHandler() { + @Override + public boolean handle(TraceContext context, MutableSpan span) { + return true; + } + }; + } + + } + + @Configuration + static class WithReporter { + + @Bean + Reporter spanReporter() { + return zipkin2.Span::toString; + } + + } + + @Configuration + static class WithSampler { + + @Bean + Sampler alwaysSampler() { + return Sampler.ALWAYS_SAMPLE; + } + + } + + @Configuration + static class WithTracingCustomizer { + + @Bean + TracingCustomizer tracingCustomizer() { + return Tracing.Builder::toString; + } + } }