Added a sender that works in a reactive environment; fixes gh-2106

This commit is contained in:
Marcin Grzejszczak
2022-02-02 12:41:48 +01:00
parent bdfc32e5bf
commit 074b55dadd
12 changed files with 469 additions and 162 deletions

View File

@@ -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

View File

@@ -65,7 +65,8 @@ public class TraceRedisAutoConfiguration {
@Bean
@ConditionalOnMissingClass("brave.sampler.Sampler")
TraceLettuceClientResourcesBuilderCustomizer otherTracersTraceLettuceClientResourcesBuilderCustomizer(Tracing tracing) {
TraceLettuceClientResourcesBuilderCustomizer otherTracersTraceLettuceClientResourcesBuilderCustomizer(
Tracing tracing) {
return new TraceLettuceClientResourcesBuilderCustomizer(tracing);
}

View File

@@ -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 {
}
}
}

View File

@@ -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);
}

View File

@@ -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 {
}
}

View File

@@ -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 {

View File

@@ -46,6 +46,16 @@
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
@@ -181,11 +191,6 @@
<artifactId>aspectjweaver</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -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<Span> 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<byte[]> spans) {
return encoding().listSizeInBytes(spans);
}
@Override
public Call<Void> sendSpans(List<byte[]> 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<Void> {
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<Void> callback) {
try {
post(this.message);
callback.onSuccess(null);
}
catch (RuntimeException | Error e) {
callback.onError(e);
}
}
@Override
public Call<Void> clone() {
return new HttpPostCall(this.message);
}
}
}

View File

@@ -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<Span> encoder) {
@@ -66,89 +41,14 @@ public class RestTemplateSender extends Sender {
}
public RestTemplateSender(RestTemplate restTemplate, String baseUrl, String apiPath, BytesEncoder<Span> 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<byte[]> spans) {
return encoding().listSizeInBytes(spans);
}
@Override
public Call<Void> sendSpans(List<byte[]> 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<byte[]> requestEntity = new RequestEntity<>(json, httpHeaders, HttpMethod.POST,
URI.create(this.url));
this.restTemplate.exchange(requestEntity, String.class);
httpHeaders.setContentType(mediaType);
RequestEntity<byte[]> 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<Void> {
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<Void> callback) {
try {
post(this.message);
callback.onSuccess(null);
}
catch (RuntimeException | Error e) {
callback.onError(e);
}
}
@Override
public Call<Void> clone() {
return new HttpPostCall(this.message);
}
}
}

View File

@@ -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<Span> 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 + "}";
}
}

View File

@@ -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);
}

View File

@@ -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();
}