diff --git a/docs/src/main/asciidoc/project-features.adoc b/docs/src/main/asciidoc/project-features.adoc index e5e8da943..ba7bcc6f7 100644 --- a/docs/src/main/asciidoc/project-features.adoc +++ b/docs/src/main/asciidoc/project-features.adoc @@ -286,6 +286,8 @@ The following example shows setting the sender type for `web`: spring.zipkin.sender.type: web ---- +If you're running a non-reactive application we will use a `RestTemplate` based span sender. Otherwise a `WebClient` based span sender will be chosen. + To customize the `RestTemplate` that sends spans to Zipkin via HTTP, you can register the `ZipkinRestTemplateCustomizer` bean. [source,java,indent=0] @@ -321,6 +323,17 @@ By default, api path will be set to `api/v2/spans` or `api/v1/spans` depending o spring.zipkin.api-path: v2/path2 ---- +In case of a reactive application, we're creating a simple `WebClient.Builder` instance. If you want to provide your own or reuse an existing one you need to create an instance of a `ZipkinWebClientBuilderProvider` bean. + +[source,java,indent=0] +---- + @Bean + ZipkinWebClientBuilderProvider myZipkinWebClientBuilderProvider() { + // create your own instance or inject one from the Spring Context + return () -> WebClient.builder(); + } +---- + [[features-zipkin-custom-service-name]] === Custom service name diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/redis/TraceRedisAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/redis/TraceRedisAutoConfiguration.java index 3e798db1d..9fbe6be24 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/redis/TraceRedisAutoConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/redis/TraceRedisAutoConfiguration.java @@ -65,7 +65,8 @@ public class TraceRedisAutoConfiguration { @Bean @ConditionalOnMissingClass("brave.sampler.Sampler") - TraceLettuceClientResourcesBuilderCustomizer otherTracersTraceLettuceClientResourcesBuilderCustomizer(Tracing tracing) { + TraceLettuceClientResourcesBuilderCustomizer otherTracersTraceLettuceClientResourcesBuilderCustomizer( + Tracing tracing) { return new TraceLettuceClientResourcesBuilderCustomizer(tracing); } diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinRestTemplateSenderConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinHttpSenderConfiguration.java similarity index 60% rename from spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinRestTemplateSenderConfiguration.java rename to spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinHttpSenderConfiguration.java index 57c2795e6..c49706cd7 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinRestTemplateSenderConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinHttpSenderConfiguration.java @@ -19,50 +19,81 @@ package org.springframework.cloud.sleuth.autoconfig.zipkin2; import zipkin2.reporter.Sender; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnNotWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; import org.springframework.cloud.sleuth.zipkin2.CachingZipkinUrlExtractor; import org.springframework.cloud.sleuth.zipkin2.LoadBalancerClientZipkinLoadBalancer; import org.springframework.cloud.sleuth.zipkin2.RestTemplateSender; import org.springframework.cloud.sleuth.zipkin2.StaticInstanceZipkinLoadBalancer; +import org.springframework.cloud.sleuth.zipkin2.WebClientSender; import org.springframework.cloud.sleuth.zipkin2.ZipkinLoadBalancer; import org.springframework.cloud.sleuth.zipkin2.ZipkinProperties; import org.springframework.cloud.sleuth.zipkin2.ZipkinRestTemplateCustomizer; import org.springframework.cloud.sleuth.zipkin2.ZipkinRestTemplateProvider; import org.springframework.cloud.sleuth.zipkin2.ZipkinRestTemplateWrapper; import org.springframework.cloud.sleuth.zipkin2.ZipkinUrlExtractor; +import org.springframework.cloud.sleuth.zipkin2.ZipkinWebClientBuilderProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; +import org.springframework.web.reactive.function.client.WebClient; @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(name = ZipkinAutoConfiguration.SENDER_BEAN_NAME) @Conditional(ZipkinSenderCondition.class) @EnableConfigurationProperties(ZipkinSenderProperties.class) -class ZipkinRestTemplateSenderConfiguration { +class ZipkinHttpSenderConfiguration { + + @Configuration(proxyBeanMethods = false) + @Conditional(NonWebApplicationOrServletCondition.class) + static class ZipkinServletConfiguration { + + @Bean(ZipkinAutoConfiguration.SENDER_BEAN_NAME) + Sender restTemplateSender(ZipkinProperties zipkin, ZipkinRestTemplateCustomizer zipkinRestTemplateCustomizer, + ZipkinRestTemplateProvider zipkinRestTemplateProvider) { + RestTemplate restTemplate = zipkinRestTemplateProvider.zipkinRestTemplate(); + restTemplate = zipkinRestTemplateCustomizer.customizeTemplate(restTemplate); + return new RestTemplateSender(restTemplate, zipkin.getBaseUrl(), zipkin.getApiPath(), zipkin.getEncoder()); + } + + @Bean + @ConditionalOnMissingBean + ZipkinRestTemplateProvider zipkinRestTemplateProvider(ZipkinProperties zipkin, ZipkinUrlExtractor extractor) { + return () -> new ZipkinRestTemplateWrapper(zipkin, extractor); + } + + @Bean + ZipkinUrlExtractor defaultZipkinUrlExtractor(final ZipkinLoadBalancer zipkinLoadBalancer) { + return new CachingZipkinUrlExtractor(zipkinLoadBalancer); + } - @Bean(ZipkinAutoConfiguration.SENDER_BEAN_NAME) - Sender restTemplateSender(ZipkinProperties zipkin, ZipkinRestTemplateCustomizer zipkinRestTemplateCustomizer, - ZipkinRestTemplateProvider zipkinRestTemplateProvider) { - RestTemplate restTemplate = zipkinRestTemplateProvider.zipkinRestTemplate(); - restTemplate = zipkinRestTemplateCustomizer.customizeTemplate(restTemplate); - return new RestTemplateSender(restTemplate, zipkin.getBaseUrl(), zipkin.getApiPath(), zipkin.getEncoder()); } - @Bean - @ConditionalOnMissingBean - ZipkinRestTemplateProvider zipkinRestTemplateProvider(ZipkinProperties zipkin, ZipkinUrlExtractor extractor) { - return () -> new ZipkinRestTemplateWrapper(zipkin, extractor); - } + @Configuration(proxyBeanMethods = false) + @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) + static class ZipkinReactiveConfiguration { + + @Bean(ZipkinAutoConfiguration.SENDER_BEAN_NAME) + Sender webClientSender(ZipkinProperties zipkin, ZipkinWebClientBuilderProvider zipkinWebClientBuilderProvider) { + WebClient.Builder webClientBuilder = zipkinWebClientBuilderProvider.zipkinWebClientBuilder(); + return new WebClientSender(webClientBuilder.build(), zipkin.getBaseUrl(), zipkin.getApiPath(), + zipkin.getEncoder()); + } + + @Bean + @ConditionalOnMissingBean + ZipkinWebClientBuilderProvider defaultZipkinWebClientProvider() { + return WebClient::builder; + } - @Bean - ZipkinUrlExtractor defaultZipkinUrlExtractor(final ZipkinLoadBalancer zipkinLoadBalancer) { - return new CachingZipkinUrlExtractor(zipkinLoadBalancer); } @Configuration(proxyBeanMethods = false) @@ -114,4 +145,21 @@ class ZipkinRestTemplateSenderConfiguration { } + static class NonWebApplicationOrServletCondition extends AnyNestedCondition { + + private NonWebApplicationOrServletCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnNotWebApplication + static class OnNonWeb { + + } + + @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) + static class OnServlet { + + } + + } } diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinSenderConfigurationImportSelector.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinSenderConfigurationImportSelector.java index 31cca6154..274ff5fff 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinSenderConfigurationImportSelector.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinSenderConfigurationImportSelector.java @@ -39,7 +39,7 @@ class ZipkinSenderConfigurationImportSelector implements ImportSelector { mappings.put("activemq", ZipkinActiveMqSenderConfiguration.class.getName()); mappings.put("rabbit", ZipkinRabbitSenderConfiguration.class.getName()); mappings.put("kafka", ZipkinKafkaSenderConfiguration.class.getName()); - mappings.put("web", ZipkinRestTemplateSenderConfiguration.class.getName()); + mappings.put("web", ZipkinHttpSenderConfiguration.class.getName()); MAPPINGS = Collections.unmodifiableMap(mappings); } diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinHttpSenderConfigurationReactiveTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinHttpSenderConfigurationReactiveTests.java new file mode 100644 index 000000000..306de3dca --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinHttpSenderConfigurationReactiveTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2013-2021 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.autoconfig.zipkin2; + +import org.junit.jupiter.api.Test; +import zipkin2.reporter.Sender; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; +import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; +import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; +import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; +import org.springframework.cloud.gateway.config.GatewayAutoConfiguration; +import org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration; +import org.springframework.cloud.gateway.config.GatewayMetricsAutoConfiguration; +import org.springframework.cloud.sleuth.autoconfig.TraceNoOpAutoConfiguration; +import org.springframework.cloud.sleuth.zipkin2.WebClientSender; +import org.springframework.cloud.sleuth.zipkin2.ZipkinProperties; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +import static org.assertj.core.api.BDDAssertions.then; + +/** + * @author Marcin Grzejszczak + */ +class ZipkinHttpSenderConfigurationReactiveTests { + + @Test + void should_work_when_using_web_client_without_the_web_environment() { + SpringApplication springApplication = new SpringApplication(Config.class); + springApplication.setWebApplicationType(WebApplicationType.REACTIVE); + + try (ConfigurableApplicationContext context = springApplication.run("--spring.sleuth.noop.enabled=true")) { + then(context.getBean(Sender.class)).isInstanceOf(WebClientSender.class); + } + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, + GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, + MongoAutoConfiguration.class, QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, + R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class, CassandraAutoConfiguration.class }) + public static class Config { + + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinRestTemplateSenderConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinHttpSenderConfigurationTests.java similarity index 77% rename from spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinRestTemplateSenderConfigurationTests.java rename to spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinHttpSenderConfigurationTests.java index 336fd2ca9..bb06c66e9 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinRestTemplateSenderConfigurationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinHttpSenderConfigurationTests.java @@ -18,8 +18,10 @@ package org.springframework.cloud.sleuth.autoconfig.zipkin2; import org.assertj.core.api.BDDAssertions; import org.junit.jupiter.api.Test; +import zipkin2.reporter.Sender; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.zipkin2.WebClientSender; import org.springframework.cloud.sleuth.zipkin2.ZipkinProperties; import org.springframework.cloud.sleuth.zipkin2.ZipkinRestTemplateCustomizer; import org.springframework.cloud.sleuth.zipkin2.ZipkinRestTemplateProvider; @@ -29,12 +31,12 @@ import org.springframework.web.client.RestTemplate; import static org.assertj.core.api.Assertions.assertThat; -class ZipkinRestTemplateSenderConfigurationTests { +class ZipkinHttpSenderConfigurationTests { @Test void should_override_the_default_rest_template() { ApplicationContextRunner runner = new ApplicationContextRunner().withUserConfiguration(Config.class, - ZipkinRestTemplateSenderConfiguration.class, ZipkinProperties.class); + ZipkinHttpSenderConfiguration.class, ZipkinProperties.class); runner.run(context -> { Config config = context.getBean(Config.class); @@ -42,6 +44,18 @@ class ZipkinRestTemplateSenderConfigurationTests { }); } + @Test + void should_use_web_client_when_reactive_type() { + ApplicationContextRunner runner = new ApplicationContextRunner().withUserConfiguration( + ZipkinHttpSenderConfiguration.class, ZipkinProperties.class) + .withPropertyValues("spring.main.web-application-type=REACTIVE"); + + runner.run(context -> { + Sender sender = context.getBean(Sender.class); + assertThat(sender).isInstanceOf(WebClientSender.class); + }); + } + @Configuration(proxyBeanMethods = false) static class Config { diff --git a/spring-cloud-sleuth-zipkin/pom.xml b/spring-cloud-sleuth-zipkin/pom.xml index 59e26e344..a8efeb2b9 100644 --- a/spring-cloud-sleuth-zipkin/pom.xml +++ b/spring-cloud-sleuth-zipkin/pom.xml @@ -46,6 +46,16 @@ org.springframework spring-web + + org.springframework + spring-webflux + true + + + io.projectreactor + reactor-core + true + org.springframework.cloud spring-cloud-commons @@ -181,11 +191,6 @@ aspectjweaver test - - io.projectreactor - reactor-core - test - diff --git a/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/HttpSender.java b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/HttpSender.java new file mode 100644 index 000000000..4b9c8ce41 --- /dev/null +++ b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/HttpSender.java @@ -0,0 +1,172 @@ +/* + * Copyright 2013-2021 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.zipkin2; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; + +import zipkin2.Call; +import zipkin2.Callback; +import zipkin2.CheckResult; +import zipkin2.Span; +import zipkin2.codec.BytesEncoder; +import zipkin2.codec.Encoding; +import zipkin2.reporter.BytesMessageEncoder; +import zipkin2.reporter.Sender; + +import org.springframework.http.MediaType; + +import static zipkin2.codec.SpanBytesEncoder.JSON_V2; + +/** + * {@link Sender} that uses an HTTP client to send spans to Zipkin. + * + * @since 3.1.0 + */ +abstract class HttpSender extends Sender { + + final ZipkinHttpClientSender sender; + + final String url; + + final Encoding encoding; + + final MediaType mediaType; + + final BytesMessageEncoder messageEncoder; + + /** + * close is typically called from a different thread. + */ + transient boolean closeCalled; + + HttpSender(ZipkinHttpClientSender sender, String baseUrl, String apiPath, BytesEncoder encoder) { + this.sender = sender; + this.encoding = encoder.encoding(); + if (encoder.equals(JSON_V2)) { + this.mediaType = MediaType.APPLICATION_JSON; + this.url = buildUrlWithCustomPathIfNecessary(baseUrl, apiPath, + baseUrl + (baseUrl.endsWith("/") ? "" : "/") + "api/v2/spans"); + } + else if (this.encoding == Encoding.PROTO3) { + this.mediaType = MediaType.parseMediaType("application/x-protobuf"); + this.url = buildUrlWithCustomPathIfNecessary(baseUrl, apiPath, + baseUrl + (baseUrl.endsWith("/") ? "" : "/") + "api/v2/spans"); + } + else if (this.encoding == Encoding.JSON) { + this.mediaType = MediaType.APPLICATION_JSON; + this.url = buildUrlWithCustomPathIfNecessary(baseUrl, apiPath, + baseUrl + (baseUrl.endsWith("/") ? "" : "/") + "api/v1/spans"); + } + else { + throw new UnsupportedOperationException("Unsupported encoding: " + this.encoding.name()); + } + this.messageEncoder = BytesMessageEncoder.forEncoding(this.encoding); + } + + private String buildUrlWithCustomPathIfNecessary(final String baseUrl, final String customApiPath, + final String defaultUrl) { + if (Objects.nonNull(customApiPath)) { + return baseUrl + + (baseUrl.endsWith("/") || customApiPath.startsWith("/") || customApiPath.isEmpty() ? "" : "/") + + customApiPath; + } + return defaultUrl; + } + + @Override + public Encoding encoding() { + return this.encoding; + } + + @Override + public int messageMaxBytes() { + // This will drop a span larger than 5MiB. Note: values like 512KiB benchmark + // better. + return 5 * 1024 * 1024; + } + + @Override + public int messageSizeInBytes(List spans) { + return encoding().listSizeInBytes(spans); + } + + @Override + public Call sendSpans(List encodedSpans) { + if (this.closeCalled) { + throw new IllegalStateException("close"); + } + return new HttpPostCall(this.messageEncoder.encode(encodedSpans)); + } + + /** + * Sends an empty json message to the configured endpoint. + */ + @Override + public CheckResult check() { + try { + post(new byte[] { '[', ']' }); + return CheckResult.OK; + } + catch (Exception e) { + return CheckResult.failed(e); + } + } + + @Override + public void close() { + this.closeCalled = true; + } + + void post(byte[] json) { + this.sender.call(this.url, this.mediaType, json); + } + + class HttpPostCall extends Call.Base { + + private final byte[] message; + + HttpPostCall(byte[] message) { + this.message = message; + } + + @Override + protected Void doExecute() throws IOException { + post(this.message); + return null; + } + + @Override + protected void doEnqueue(Callback callback) { + try { + post(this.message); + callback.onSuccess(null); + } + catch (RuntimeException | Error e) { + callback.onError(e); + } + } + + @Override + public Call clone() { + return new HttpPostCall(this.message); + } + + } + +} diff --git a/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/RestTemplateSender.java b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/RestTemplateSender.java index 72db2db33..67e70a042 100644 --- a/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/RestTemplateSender.java +++ b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/RestTemplateSender.java @@ -16,18 +16,10 @@ package org.springframework.cloud.sleuth.zipkin2; -import java.io.IOException; import java.net.URI; -import java.util.List; -import java.util.Objects; -import zipkin2.Call; -import zipkin2.Callback; -import zipkin2.CheckResult; import zipkin2.Span; import zipkin2.codec.BytesEncoder; -import zipkin2.codec.Encoding; -import zipkin2.reporter.BytesMessageEncoder; import zipkin2.reporter.Sender; import org.springframework.http.HttpHeaders; @@ -36,29 +28,12 @@ import org.springframework.http.MediaType; import org.springframework.http.RequestEntity; import org.springframework.web.client.RestTemplate; -import static zipkin2.codec.SpanBytesEncoder.JSON_V2; - /** * {@link Sender} that uses {@link RestTemplate} to send spans to Zipkin. * * @since 3.0.0 */ -public class RestTemplateSender extends Sender { - - final RestTemplate restTemplate; - - final String url; - - final Encoding encoding; - - final MediaType mediaType; - - final BytesMessageEncoder messageEncoder; - - /** - * close is typically called from a different thread. - */ - transient boolean closeCalled; +public class RestTemplateSender extends HttpSender { @Deprecated public RestTemplateSender(RestTemplate restTemplate, String baseUrl, BytesEncoder encoder) { @@ -66,89 +41,14 @@ public class RestTemplateSender extends Sender { } public RestTemplateSender(RestTemplate restTemplate, String baseUrl, String apiPath, BytesEncoder encoder) { - this.restTemplate = restTemplate; - this.encoding = encoder.encoding(); - if (encoder.equals(JSON_V2)) { - this.mediaType = MediaType.APPLICATION_JSON; - this.url = buildUrlWithCustomPathIfNecessary(baseUrl, apiPath, - baseUrl + (baseUrl.endsWith("/") ? "" : "/") + "api/v2/spans"); - } - else if (this.encoding == Encoding.PROTO3) { - this.mediaType = MediaType.parseMediaType("application/x-protobuf"); - this.url = buildUrlWithCustomPathIfNecessary(baseUrl, apiPath, - baseUrl + (baseUrl.endsWith("/") ? "" : "/") + "api/v2/spans"); - } - else if (this.encoding == Encoding.JSON) { - this.mediaType = MediaType.APPLICATION_JSON; - this.url = buildUrlWithCustomPathIfNecessary(baseUrl, apiPath, - baseUrl + (baseUrl.endsWith("/") ? "" : "/") + "api/v1/spans"); - } - else { - throw new UnsupportedOperationException("Unsupported encoding: " + this.encoding.name()); - } - this.messageEncoder = BytesMessageEncoder.forEncoding(this.encoding); + super((url, mediaType, bytes) -> post(url, mediaType, bytes, restTemplate), baseUrl, apiPath, encoder); } - private String buildUrlWithCustomPathIfNecessary(final String baseUrl, final String customApiPath, - final String defaultUrl) { - if (Objects.nonNull(customApiPath)) { - return baseUrl - + (baseUrl.endsWith("/") || customApiPath.startsWith("/") || customApiPath.isEmpty() ? "" : "/") - + customApiPath; - } - return defaultUrl; - } - - @Override - public Encoding encoding() { - return this.encoding; - } - - @Override - public int messageMaxBytes() { - // This will drop a span larger than 5MiB. Note: values like 512KiB benchmark - // better. - return 5 * 1024 * 1024; - } - - @Override - public int messageSizeInBytes(List spans) { - return encoding().listSizeInBytes(spans); - } - - @Override - public Call sendSpans(List encodedSpans) { - if (this.closeCalled) { - throw new IllegalStateException("close"); - } - return new HttpPostCall(this.messageEncoder.encode(encodedSpans)); - } - - /** - * Sends an empty json message to the configured endpoint. - */ - @Override - public CheckResult check() { - try { - post(new byte[] { '[', ']' }); - return CheckResult.OK; - } - catch (Exception e) { - return CheckResult.failed(e); - } - } - - @Override - public void close() { - this.closeCalled = true; - } - - void post(byte[] json) { + private static void post(String url, MediaType mediaType, byte[] json, RestTemplate restTemplate) { HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.setContentType(this.mediaType); - RequestEntity requestEntity = new RequestEntity<>(json, httpHeaders, HttpMethod.POST, - URI.create(this.url)); - this.restTemplate.exchange(requestEntity, String.class); + httpHeaders.setContentType(mediaType); + RequestEntity requestEntity = new RequestEntity<>(json, httpHeaders, HttpMethod.POST, URI.create(url)); + restTemplate.exchange(requestEntity, String.class); } @Override @@ -156,36 +56,4 @@ public class RestTemplateSender extends Sender { return "RestTemplateSender{" + url + "}"; } - class HttpPostCall extends Call.Base { - - private final byte[] message; - - HttpPostCall(byte[] message) { - this.message = message; - } - - @Override - protected Void doExecute() throws IOException { - post(this.message); - return null; - } - - @Override - protected void doEnqueue(Callback callback) { - try { - post(this.message); - callback.onSuccess(null); - } - catch (RuntimeException | Error e) { - callback.onError(e); - } - } - - @Override - public Call clone() { - return new HttpPostCall(this.message); - } - - } - } diff --git a/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/WebClientSender.java b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/WebClientSender.java new file mode 100644 index 000000000..bad08821c --- /dev/null +++ b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/WebClientSender.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013-2021 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.zipkin2; + +import java.net.URI; + +import zipkin2.Span; +import zipkin2.codec.BytesEncoder; +import zipkin2.reporter.Sender; + +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * {@link Sender} that uses {@link WebClient} to send spans to Zipkin. + * + * @since 3.1.0 + */ +public class WebClientSender extends HttpSender { + + public WebClientSender(WebClient webClient, String baseUrl, String apiPath, BytesEncoder encoder) { + super((url, mediaType, bytes) -> post(url, mediaType, bytes, webClient), baseUrl, apiPath, encoder); + } + + private static void post(String url, MediaType mediaType, byte[] json, WebClient webClient) { + webClient.post().uri(URI.create(url)).accept(mediaType).bodyValue(json).retrieve().toBodilessEntity() + .subscribe(); + } + + @Override + public String toString() { + return "WebClientSender{" + url + "}"; + } + +} diff --git a/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/ZipkinHttpClientSender.java b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/ZipkinHttpClientSender.java new file mode 100644 index 000000000..b4549d1b3 --- /dev/null +++ b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/ZipkinHttpClientSender.java @@ -0,0 +1,37 @@ +/* + * Copyright 2013-2021 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.zipkin2; + +import org.springframework.http.MediaType; + +/** + * Sends spans to Zipkin via an HTTP Client. + * + * @author Marcin Grzejszczak + * @since 3.1.1 + */ +interface ZipkinHttpClientSender { + + /** + * Sends spans to Zipkin via an HTTP Client. + * @param url Zipkin URL + * @param mediaType HTTP message media type + * @param payload payload to send + */ + void call(String url, MediaType mediaType, byte[] payload); + +} diff --git a/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/ZipkinWebClientBuilderProvider.java b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/ZipkinWebClientBuilderProvider.java new file mode 100644 index 000000000..98717710e --- /dev/null +++ b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/ZipkinWebClientBuilderProvider.java @@ -0,0 +1,31 @@ +/* + * Copyright 2013-2021 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.zipkin2; + +import org.springframework.web.reactive.function.client.WebClient; + +/** + * A provider for a {@link WebClient.Builder} used to send spans to Zipkin. + * + * @author Marcin Grzejszczak + * @since 3.1.1 + */ +public interface ZipkinWebClientBuilderProvider { + + WebClient.Builder zipkinWebClientBuilder(); + +}