diff --git a/docs/src/main/asciidoc/spring-cloud-sleuth.adoc b/docs/src/main/asciidoc/spring-cloud-sleuth.adoc index 89700aa6c0..ead0233eeb 100644 --- a/docs/src/main/asciidoc/spring-cloud-sleuth.adoc +++ b/docs/src/main/asciidoc/spring-cloud-sleuth.adoc @@ -331,6 +331,8 @@ include::../../../../spring-cloud-sleuth-core/src/test/java/org/springframework/ 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.ratelimit` 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). + == Propagation Propagation is needed to ensure activities originating from the same root are collected together in the same trace. diff --git a/spring-cloud-sleuth-core/pom.xml b/spring-cloud-sleuth-core/pom.xml index 1534a4b2ff..3e746be01b 100644 --- a/spring-cloud-sleuth-core/pom.xml +++ b/spring-cloud-sleuth-core/pom.xml @@ -121,6 +121,11 @@ org.springframework spring-context + + org.springframework.cloud + spring-cloud-context + true + com.netflix.hystrix hystrix-core diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/sampler/RateLimitingSampler.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/sampler/RateLimitingSampler.java new file mode 100644 index 0000000000..2915743935 --- /dev/null +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/sampler/RateLimitingSampler.java @@ -0,0 +1,47 @@ +/* + * Copyright 2013-2018 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 + * + * http://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.sampler.Sampler; + +/** + * The rate-limited sampler allows you 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). + * + * You can read more about it in {@link brave.sampler.RateLimitingSampler} + * + * @author Marcin Grzejszczak + * @since 2.1.0 + */ +public class RateLimitingSampler extends Sampler { + + private final Sampler sampler; + + public RateLimitingSampler(SamplerProperties configuration) { + this.sampler = brave.sampler.RateLimitingSampler.create(rateLimit(configuration)); + } + + private Integer rateLimit(SamplerProperties configuration) { + return configuration.getRatelimit() != null ? configuration.getRatelimit() : 0; + } + + @Override + public boolean isSampled(long traceId) { + return this.sampler.isSampled(traceId); + } + +} 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 new file mode 100644 index 0000000000..91caa6ae9f --- /dev/null +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/sampler/SamplerAutoConfiguration.java @@ -0,0 +1,72 @@ +/* + * Copyright 2013-2018 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 + * + * http://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.sampler.Sampler; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +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.Configuration; + +/** + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration + * Auto-configuration} to setup sampling for Spring Cloud Sleuth. + * + * @author Marcin Grzejszczak + * @since 2.1.0 + */ +@Configuration +@ConditionalOnProperty(value = "spring.sleuth.enabled", matchIfMissing = true) +@EnableConfigurationProperties(SamplerProperties.class) +public class SamplerAutoConfiguration { + + @Configuration + @ConditionalOnBean(type = "org.springframework.cloud.context.scope.refresh.RefreshScope") + protected static class RefreshScopedSamplerConfiguration { + + @Bean + @RefreshScope + @ConditionalOnMissingBean + public Sampler defaultTraceSampler(SamplerProperties config) { + return samplerFromProps(config); + } + + } + + @Configuration + @ConditionalOnMissingBean(type = "org.springframework.cloud.context.scope.refresh.RefreshScope") + protected static class NonRefreshScopeSamplerConfiguration { + + @Bean + @ConditionalOnMissingBean + public Sampler defaultTraceSampler(SamplerProperties config) { + return samplerFromProps(config); + } + + } + + static Sampler samplerFromProps(SamplerProperties config) { + if (config.getRatelimit() != null) { + return new RateLimitingSampler(config); + } + return new ProbabilityBasedSampler(config); + } + +} diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/sampler/SamplerProperties.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/sampler/SamplerProperties.java index 4af7f075c2..467b03d1e2 100644 --- a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/sampler/SamplerProperties.java +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/sampler/SamplerProperties.java @@ -35,6 +35,18 @@ public class SamplerProperties { */ private float probability = 0.1f; + /** + * A rate per second can be a nice choice for low-traffic endpoints as it allows you + * surge protection. For example, you may never expect the endpoint to get more than + * 50 requests per second. If there was a sudden surge of traffic, to 5000 requests + * per second, you would still end up with 50 traces per second. Conversely, if you + * had a percentage, like 10%, the same surge would end up with 500 traces per second, + * possibly overloading your storage. Amazon X-Ray includes a rate-limited sampler + * (named Reservoir) for this purpose. Brave has taken the same approach via the + * {@link brave.sampler.RateLimitingSampler}. + */ + private Integer ratelimit; + public float getProbability() { return this.probability; } @@ -43,4 +55,12 @@ public void setProbability(float probability) { this.probability = probability; } + public Integer getRatelimit() { + return this.ratelimit; + } + + public void setRatelimit(Integer ratelimit) { + this.ratelimit = ratelimit; + } + } diff --git a/spring-cloud-sleuth-core/src/main/resources/META-INF/spring.factories b/spring-cloud-sleuth-core/src/main/resources/META-INF/spring.factories index c7061d6414..28f45d8c58 100644 --- a/spring-cloud-sleuth-core/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-sleuth-core/src/main/resources/META-INF/spring.factories @@ -1,6 +1,7 @@ # Auto Configuration org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.cloud.sleuth.annotation.SleuthAnnotationAutoConfiguration,\ +org.springframework.cloud.sleuth.sampler.SamplerAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.TraceAutoConfiguration,\ org.springframework.cloud.sleuth.log.SleuthLogAutoConfiguration,\ org.springframework.cloud.sleuth.propagation.SleuthTagPropagationAutoConfiguration,\ diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SleuthSpanCreatorAspectMonoTests.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SleuthSpanCreatorAspectMonoTests.java index 21e7b1cd4c..4e61eba5d6 100644 --- a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SleuthSpanCreatorAspectMonoTests.java +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SleuthSpanCreatorAspectMonoTests.java @@ -16,6 +16,10 @@ package org.springframework.cloud.sleuth.annotation; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + import brave.Span; import brave.Tracer; import brave.sampler.Sampler; @@ -24,6 +28,10 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import reactor.core.publisher.Mono; +import zipkin2.Annotation; +import zipkin2.reporter.Reporter; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; @@ -31,21 +39,16 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.util.Pair; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import reactor.core.publisher.Mono; -import zipkin2.Annotation; -import zipkin2.reporter.Reporter; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; import static org.assertj.core.api.BDDAssertions.then; import static org.springframework.cloud.sleuth.annotation.SleuthSpanCreatorAspectMonoTests.TestBean.TEST_STRING; import static reactor.core.publisher.Mono.just; @SpringBootTest(classes = SleuthSpanCreatorAspectMonoTests.TestConfiguration.class) -@RunWith(SpringJUnit4ClassRunner.class) +@RunWith(SpringRunner.class) +@DirtiesContext public class SleuthSpanCreatorAspectMonoTests { @Autowired diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/TraceWebFluxTests.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/TraceWebFluxTests.java index 5dadeaa568..e851b48549 100644 --- a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/TraceWebFluxTests.java +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/TraceWebFluxTests.java @@ -17,6 +17,7 @@ package org.springframework.cloud.sleuth.instrument.web; import java.util.List; +import java.util.stream.Collectors; import brave.Span; import brave.Tracer; @@ -30,6 +31,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.WebApplicationType; @@ -38,7 +41,6 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.cloud.sleuth.DisableWebFluxSecurity; - import org.springframework.cloud.sleuth.annotation.ContinueSpan; import org.springframework.cloud.sleuth.annotation.NewSpan; import org.springframework.cloud.sleuth.instrument.reactor.TraceReactorAutoConfigurationAccessorConfiguration; @@ -48,7 +50,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; -import org.springframework.stereotype.Component; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -60,9 +61,6 @@ import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerResponse; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import zipkin2.reporter.Reporter; import static org.assertj.core.api.BDDAssertions.then; @@ -105,6 +103,7 @@ public void should_instrument_web_filter() throws Exception { ClientResponse nonSampledResponse = whenNonSampledRequestIsSent(port); // then thenNoSpanWasReported(accumulator, nonSampledResponse, controller2); + accumulator.clear(); // when ClientResponse skippedPatternResponse = whenRequestIsSentToSkippedPattern(port); @@ -138,10 +137,13 @@ private void thenSpanWasReportedWithTags(ArrayListSpanReporter accumulator, ClientResponse response) { Awaitility.await().untilAsserted(() -> { then(response.statusCode().value()).isEqualTo(200); - then(accumulator.getSpans()).hasSize(1); }); - then(accumulator.getSpans().get(0).name()).isEqualTo("get /api/c2/{id}"); - then(accumulator.getSpans().get(0).tags()) + List spans = accumulator.getSpans().stream() + .filter(span -> span.name().equals("get /api/c2/{id}")) + .collect(Collectors.toList()); + then(spans).hasSize(1); + then(spans.get(0).name()).isEqualTo("get /api/c2/{id}"); + then(spans.get(0).tags()) .containsEntry("mvc.controller.method", "successful") .containsEntry("mvc.controller.class", "Controller2"); } @@ -150,9 +152,11 @@ private void thenSpanWasReportedForFunction(ArrayListSpanReporter accumulator, ClientResponse response) { Awaitility.await().untilAsserted(() -> { then(response.statusCode().value()).isEqualTo(200); - then(accumulator.getSpans()).hasSize(1); }); - then(accumulator.getSpans().get(0).name()).isEqualTo("get"); + List spans = accumulator.getSpans().stream() + .filter(span -> span.name().equals("get")) + .collect(Collectors.toList()); + then(spans).hasSize(1); } private void thenNoSpanWasReported(ArrayListSpanReporter accumulator, 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 new file mode 100644 index 0000000000..cbf9e38770 --- /dev/null +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/sampler/SamplerAutoConfigurationTests.java @@ -0,0 +1,34 @@ +package org.springframework.cloud.sleuth.sampler; + +import brave.sampler.Sampler; +import org.assertj.core.api.BDDAssertions; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * @author Marcin Grzejszczak + * @since + */ +public class SamplerAutoConfigurationTests { + + @Test + public void should_use_rate_limit_sampler_when_property_set() { + SamplerProperties properties = new SamplerProperties(); + properties.setRatelimit(10); + + Sampler sampler = SamplerAutoConfiguration.samplerFromProps(properties); + + BDDAssertions.then(sampler).isInstanceOf(RateLimitingSampler.class); + } + + @Test + public void should_use_probability_sampler_when_rate_limiting_not_set() { + SamplerProperties properties = new SamplerProperties(); + + Sampler sampler = SamplerAutoConfiguration.samplerFromProps(properties); + + BDDAssertions.then(sampler).isInstanceOf(ProbabilityBasedSampler.class); + } + +} \ No newline at end of file diff --git a/spring-cloud-sleuth-zipkin/pom.xml b/spring-cloud-sleuth-zipkin/pom.xml index 9488ceb1bf..647b311045 100644 --- a/spring-cloud-sleuth-zipkin/pom.xml +++ b/spring-cloud-sleuth-zipkin/pom.xml @@ -45,11 +45,6 @@ org.springframework.cloud spring-cloud-commons - - org.springframework.cloud - spring-cloud-context - true - org.springframework.boot spring-boot-actuator diff --git a/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/ZipkinAutoConfiguration.java b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/ZipkinAutoConfiguration.java index 9a227a2d9f..281a3a6354 100644 --- a/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/ZipkinAutoConfiguration.java +++ b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/ZipkinAutoConfiguration.java @@ -16,11 +16,19 @@ package org.springframework.cloud.sleuth.zipkin2; -import brave.sampler.Sampler; +import java.util.concurrent.TimeUnit; + +import zipkin2.Span; +import zipkin2.codec.BytesEncoder; +import zipkin2.reporter.AsyncReporter; +import zipkin2.reporter.InMemoryReporterMetrics; +import zipkin2.reporter.Reporter; +import zipkin2.reporter.ReporterMetrics; +import zipkin2.reporter.Sender; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -28,30 +36,19 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.client.serviceregistry.Registration; import org.springframework.cloud.commons.util.InetUtils; -import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.cloud.sleuth.autoconfig.TraceAutoConfiguration; -import org.springframework.cloud.sleuth.sampler.ProbabilityBasedSampler; -import org.springframework.cloud.sleuth.sampler.SamplerProperties; +import org.springframework.cloud.sleuth.sampler.SamplerAutoConfiguration; import org.springframework.cloud.sleuth.zipkin2.sender.ZipkinSenderConfigurationImportSelector; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.core.env.Environment; import org.springframework.web.client.RestTemplate; -import zipkin2.Span; -import zipkin2.codec.BytesEncoder; -import zipkin2.reporter.AsyncReporter; -import zipkin2.reporter.InMemoryReporterMetrics; -import zipkin2.reporter.Reporter; -import zipkin2.reporter.ReporterMetrics; -import zipkin2.reporter.Sender; - -import java.util.concurrent.TimeUnit; /** * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration - * Auto-configuration} enables reporting to Zipkin via HTTP. Has a default {@link Sampler} - * set as {@link ProbabilityBasedSampler}. + * Auto-configuration} enables reporting to Zipkin via HTTP. Has a default sampler + * set from the {@link SamplerAutoConfiguration} * * The {@link ZipkinRestTemplateCustomizer} allows you to customize the * {@link RestTemplate} that is used to send Spans to Zipkin. Its default implementation - @@ -59,16 +56,16 @@ * * @author Spencer Gibb * @since 1.0.0 - * @see ProbabilityBasedSampler + * @see SamplerAutoConfiguration * @see ZipkinRestTemplateCustomizer * @see DefaultZipkinRestTemplateCustomizer */ @Configuration -@EnableConfigurationProperties({ ZipkinProperties.class, SamplerProperties.class }) +@EnableConfigurationProperties(ZipkinProperties.class) @ConditionalOnProperty(value = "spring.zipkin.enabled", matchIfMissing = true) @AutoConfigureBefore(TraceAutoConfiguration.class) @AutoConfigureAfter(name = "org.springframework.cloud.autoconfigure.RefreshAutoConfiguration") -@Import(ZipkinSenderConfigurationImportSelector.class) +@Import({ZipkinSenderConfigurationImportSelector.class, SamplerAutoConfiguration.class}) public class ZipkinAutoConfiguration { /** @@ -107,31 +104,6 @@ ReporterMetrics sleuthReporterMetrics() { return new InMemoryReporterMetrics(); } - @Configuration - @ConditionalOnBean(type = "org.springframework.cloud.context.scope.refresh.RefreshScope") - protected static class RefreshScopedProbabilityBasedSamplerConfiguration { - - @Bean - @RefreshScope - @ConditionalOnMissingBean - public Sampler defaultTraceSampler(SamplerProperties config) { - return new ProbabilityBasedSampler(config); - } - - } - - @Configuration - @ConditionalOnMissingBean(type = "org.springframework.cloud.context.scope.refresh.RefreshScope") - protected static class NonRefreshScopeProbabilityBasedSamplerConfiguration { - - @Bean - @ConditionalOnMissingBean - public Sampler defaultTraceSampler(SamplerProperties config) { - return new ProbabilityBasedSampler(config); - } - - } - @Configuration @ConditionalOnMissingBean(EndpointLocator.class) @ConditionalOnProperty(value = "spring.zipkin.locator.discovery.enabled", havingValue = "false", matchIfMissing = true)