From 4b0bed23b054e400c33589d27812c0ad686421e7 Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Sat, 10 Feb 2024 17:43:23 +0700 Subject: [PATCH 1/2] Migrate to Brave 6 and Zipkin Reporter 3 Signed-off-by: Adrian Cole See gh-39049 --- .../BravePropagationConfigurations.java | 4 +- .../tracing/CompositePropagationFactory.java | 6 +- .../tracing/zipkin/HttpSender.java | 138 +++++--------- .../zipkin/ZipkinAutoConfiguration.java | 16 +- .../tracing/zipkin/ZipkinConfigurations.java | 89 +++++---- .../zipkin/ZipkinConnectionDetails.java | 6 + .../tracing/zipkin/ZipkinProperties.java | 15 ++ .../zipkin/ZipkinRestTemplateSender.java | 56 ++---- .../tracing/zipkin/ZipkinWebClientSender.java | 72 ++------ .../CompositePropagationFactoryTests.java | 6 +- .../tracing/LocalBaggageFieldsTests.java | 4 +- .../zipkin/DefaultEncodingConfiguration.java | 37 ++++ .../tracing/zipkin/NoopSender.java | 34 +--- .../zipkin/ZipkinAutoConfigurationTests.java | 16 +- ...ConfigurationsBraveConfigurationTests.java | 169 +++++++++++++++--- ...ationsOpenTelemetryConfigurationTests.java | 70 ++++++-- ...figurationsReporterConfigurationTests.java | 98 ---------- ...onfigurationsSenderConfigurationTests.java | 91 ++++++++-- .../tracing/zipkin/ZipkinHttpSenderTests.java | 58 +----- .../zipkin/ZipkinRestTemplateSenderTests.java | 106 ++++++----- .../zipkin/ZipkinWebClientSenderTests.java | 124 +++++++------ .../spring-boot-dependencies/build.gradle | 11 +- .../testcontainers/DockerImageNames.java | 2 +- 23 files changed, 635 insertions(+), 593 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/DefaultEncodingConfiguration.java delete mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurationsReporterConfigurationTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BravePropagationConfigurations.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BravePropagationConfigurations.java index 31de139c7df3..974c93a86f37 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BravePropagationConfigurations.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BravePropagationConfigurations.java @@ -30,7 +30,6 @@ import brave.propagation.CurrentTraceContext.ScopeDecorator; import brave.propagation.Propagation; import brave.propagation.Propagation.Factory; -import brave.propagation.Propagation.KeyFactory; import io.micrometer.tracing.brave.bridge.BraveBaggageManager; import org.springframework.beans.factory.ObjectProvider; @@ -99,12 +98,11 @@ BaggagePropagation.FactoryBuilder propagationFactoryBuilder( return builder; } - @SuppressWarnings("deprecation") private Factory createThrowAwayFactory() { return new Factory() { @Override - public Propagation create(KeyFactory keyFactory) { + public Propagation get() { return null; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/CompositePropagationFactory.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/CompositePropagationFactory.java index 14ec2aea1e66..baaf9bc0a901 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/CompositePropagationFactory.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/CompositePropagationFactory.java @@ -22,7 +22,6 @@ import java.util.function.Predicate; import java.util.stream.Stream; -import brave.internal.propagation.StringPropagationAdapter; import brave.propagation.B3Propagation; import brave.propagation.Propagation; import brave.propagation.Propagation.Factory; @@ -71,9 +70,8 @@ public boolean requires128BitTraceId() { } @Override - @SuppressWarnings("deprecation") - public Propagation create(Propagation.KeyFactory keyFactory) { - return StringPropagationAdapter.create(this.propagation, keyFactory); + public Propagation get() { + return this.propagation; } @Override diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/HttpSender.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/HttpSender.java index ac7ba066671e..b3446a643931 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/HttpSender.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/HttpSender.java @@ -18,135 +18,81 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.UncheckedIOException; -import java.util.Collections; +import java.net.URI; import java.util.List; import java.util.zip.GZIPOutputStream; -import zipkin2.Call; -import zipkin2.CheckResult; -import zipkin2.codec.Encoding; -import zipkin2.reporter.BytesMessageEncoder; -import zipkin2.reporter.ClosedSenderException; -import zipkin2.reporter.Sender; +import zipkin2.reporter.BaseHttpSender; +import zipkin2.reporter.BytesMessageSender; +import zipkin2.reporter.Encoding; +import zipkin2.reporter.HttpEndpointSupplier.Factory; import org.springframework.http.HttpHeaders; import org.springframework.util.unit.DataSize; /** - * A Zipkin {@link Sender} that uses an HTTP client to send JSON spans. Supports automatic - * compression with gzip. + * A Zipkin {@link BytesMessageSender} that uses an HTTP client to send JSON spans. + * Supports automatic compression with gzip. * * @author Moritz Halbritter * @author Stefan Bratanov */ -abstract class HttpSender extends Sender { +abstract class HttpSender extends BaseHttpSender { - private static final DataSize MESSAGE_MAX_SIZE = DataSize.ofKilobytes(512); - - private volatile boolean closed; - - @Override - public Encoding encoding() { - return Encoding.JSON; - } + /** + * Only use gzip compression on data which is bigger than this in bytes. + */ + private static final DataSize COMPRESSION_THRESHOLD = DataSize.ofKilobytes(1); - @Override - public int messageMaxBytes() { - return (int) MESSAGE_MAX_SIZE.toBytes(); + HttpSender(Encoding encoding, Factory endpointSupplierFactory, String endpoint) { + super(encoding, endpointSupplierFactory, endpoint); } @Override - public int messageSizeInBytes(List encodedSpans) { - return encoding().listSizeInBytes(encodedSpans); + protected URI newEndpoint(String endpoint) { + return URI.create(endpoint); } @Override - public int messageSizeInBytes(int encodedSizeInBytes) { - return encoding().listSizeInBytes(encodedSizeInBytes); + protected byte[] newBody(List list) { + return this.encoding.encode(list); } @Override - public CheckResult check() { - try { - sendSpans(Collections.emptyList()).execute(); - return CheckResult.OK; + protected void postSpans(URI endpoint, byte[] body) throws IOException { + HttpHeaders headers = getDefaultHeaders(); + if (needsCompression(body)) { + body = compress(body); + headers.set("Content-Encoding", "gzip"); } - catch (IOException | RuntimeException ex) { - return CheckResult.failed(ex); - } - } - - @Override - public void close() throws IOException { - this.closed = true; + postSpans(endpoint, headers, body); } /** - * The returned {@link HttpPostCall} will send span(s) as a POST to a zipkin endpoint - * when executed. - * @param batchedEncodedSpans list of encoded spans as a byte array - * @return an instance of a Zipkin {@link Call} which can be executed + * This will send span(s) as a POST to a zipkin endpoint. + * @param endpoint the POST endpoint. For example, http://localhost:9411/api/v2/spans. + * @param headers headers for the POST request + * @param body list of possibly gzipped, encoded spans. */ - protected abstract HttpPostCall sendSpans(byte[] batchedEncodedSpans); + abstract void postSpans(URI endpoint, HttpHeaders headers, byte[] body); - @Override - public Call sendSpans(List encodedSpans) { - if (this.closed) { - throw new ClosedSenderException(); - } - return sendSpans(BytesMessageEncoder.JSON.encode(encodedSpans)); + HttpHeaders getDefaultHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.set("b3", "0"); + headers.set("Content-Type", this.encoding.mediaType()); + return headers; } - abstract static class HttpPostCall extends Call.Base { - - /** - * Only use gzip compression on data which is bigger than this in bytes. - */ - private static final DataSize COMPRESSION_THRESHOLD = DataSize.ofKilobytes(1); - - private final byte[] body; - - HttpPostCall(byte[] body) { - this.body = body; - } - - protected byte[] getBody() { - if (needsCompression()) { - return compress(this.body); - } - return this.body; - } - - protected byte[] getUncompressedBody() { - return this.body; - } - - protected HttpHeaders getDefaultHeaders() { - HttpHeaders headers = new HttpHeaders(); - headers.set("b3", "0"); - headers.set("Content-Type", "application/json"); - if (needsCompression()) { - headers.set("Content-Encoding", "gzip"); - } - return headers; - } - - private boolean needsCompression() { - return this.body.length > COMPRESSION_THRESHOLD.toBytes(); - } + private boolean needsCompression(byte[] body) { + return body.length > COMPRESSION_THRESHOLD.toBytes(); + } - private byte[] compress(byte[] input) { - ByteArrayOutputStream result = new ByteArrayOutputStream(); - try (GZIPOutputStream gzip = new GZIPOutputStream(result)) { - gzip.write(input); - } - catch (IOException ex) { - throw new UncheckedIOException(ex); - } - return result.toByteArray(); + private byte[] compress(byte[] input) throws IOException { + ByteArrayOutputStream result = new ByteArrayOutputStream(); + try (GZIPOutputStream gzip = new GZIPOutputStream(result)) { + gzip.write(input); } - + return result.toByteArray(); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinAutoConfiguration.java index daff635f8631..6c70fc0ebada 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinAutoConfiguration.java @@ -16,14 +16,11 @@ package org.springframework.boot.actuate.autoconfigure.tracing.zipkin; -import zipkin2.Span; -import zipkin2.codec.BytesEncoder; -import zipkin2.codec.SpanBytesEncoder; -import zipkin2.reporter.Sender; +import zipkin2.reporter.BytesMessageSender; +import zipkin2.reporter.Encoding; import org.springframework.boot.actuate.autoconfigure.tracing.zipkin.ZipkinConfigurations.BraveConfiguration; import org.springframework.boot.actuate.autoconfigure.tracing.zipkin.ZipkinConfigurations.OpenTelemetryConfiguration; -import org.springframework.boot.actuate.autoconfigure.tracing.zipkin.ZipkinConfigurations.ReporterConfiguration; import org.springframework.boot.actuate.autoconfigure.tracing.zipkin.ZipkinConfigurations.SenderConfiguration; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -44,9 +41,8 @@ * @since 3.0.0 */ @AutoConfiguration(after = RestTemplateAutoConfiguration.class) -@ConditionalOnClass(Sender.class) -@Import({ SenderConfiguration.class, ReporterConfiguration.class, BraveConfiguration.class, - OpenTelemetryConfiguration.class }) +@ConditionalOnClass(BytesMessageSender.class) +@Import({ SenderConfiguration.class, BraveConfiguration.class, OpenTelemetryConfiguration.class }) @EnableConfigurationProperties(ZipkinProperties.class) public class ZipkinAutoConfiguration { @@ -58,8 +54,8 @@ PropertiesZipkinConnectionDetails zipkinConnectionDetails(ZipkinProperties prope @Bean @ConditionalOnMissingBean - public BytesEncoder spanBytesEncoder() { - return SpanBytesEncoder.JSON_V2; + public Encoding encoding(ZipkinProperties properties) { + return properties.getEncoding(); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurations.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurations.java index f4ecc9503125..dc7d93cd2644 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurations.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurations.java @@ -16,13 +16,19 @@ package org.springframework.boot.actuate.autoconfigure.tracing.zipkin; +import brave.Tag; +import brave.Tags; +import brave.handler.MutableSpan; import io.opentelemetry.exporter.zipkin.ZipkinSpanExporter; import zipkin2.Span; -import zipkin2.codec.BytesEncoder; -import zipkin2.reporter.AsyncReporter; -import zipkin2.reporter.Reporter; -import zipkin2.reporter.Sender; -import zipkin2.reporter.brave.ZipkinSpanHandler; +import zipkin2.reporter.BytesEncoder; +import zipkin2.reporter.BytesMessageSender; +import zipkin2.reporter.Encoding; +import zipkin2.reporter.HttpEndpointSupplier; +import zipkin2.reporter.HttpEndpointSuppliers; +import zipkin2.reporter.SpanBytesEncoder; +import zipkin2.reporter.brave.AsyncZipkinSpanHandler; +import zipkin2.reporter.brave.MutableSpanBytesEncoder; import zipkin2.reporter.urlconnection.URLConnectionSender; import org.springframework.beans.factory.ObjectProvider; @@ -59,15 +65,20 @@ static class SenderConfiguration { static class UrlConnectionSenderConfiguration { @Bean - @ConditionalOnMissingBean(Sender.class) - URLConnectionSender urlConnectionSender(ZipkinProperties properties, - ObjectProvider connectionDetailsProvider) { + @ConditionalOnMissingBean(BytesMessageSender.class) + URLConnectionSender urlConnectionSender(ZipkinProperties properties, Encoding encoding, + ObjectProvider connectionDetailsProvider, + ObjectProvider endpointSupplierFactoryProvider) { ZipkinConnectionDetails connectionDetails = connectionDetailsProvider .getIfAvailable(() -> new PropertiesZipkinConnectionDetails(properties)); + HttpEndpointSupplier.Factory endpointSupplierFactory = endpointSupplierFactoryProvider + .getIfAvailable(HttpEndpointSuppliers::constantFactory); URLConnectionSender.Builder builder = URLConnectionSender.newBuilder(); builder.connectTimeout((int) properties.getConnectTimeout().toMillis()); builder.readTimeout((int) properties.getReadTimeout().toMillis()); + builder.endpointSupplierFactory(endpointSupplierFactory); builder.endpoint(connectionDetails.getSpanEndpoint()); + builder.encoding(encoding); return builder.build(); } @@ -79,17 +90,21 @@ URLConnectionSender urlConnectionSender(ZipkinProperties properties, static class RestTemplateSenderConfiguration { @Bean - @ConditionalOnMissingBean(Sender.class) - ZipkinRestTemplateSender restTemplateSender(ZipkinProperties properties, + @ConditionalOnMissingBean(BytesMessageSender.class) + ZipkinRestTemplateSender restTemplateSender(ZipkinProperties properties, Encoding encoding, ObjectProvider customizers, - ObjectProvider connectionDetailsProvider) { + ObjectProvider connectionDetailsProvider, + ObjectProvider endpointSupplierFactoryProvider) { ZipkinConnectionDetails connectionDetails = connectionDetailsProvider .getIfAvailable(() -> new PropertiesZipkinConnectionDetails(properties)); + HttpEndpointSupplier.Factory endpointSupplierFactory = endpointSupplierFactoryProvider + .getIfAvailable(HttpEndpointSuppliers::constantFactory); RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder() .setConnectTimeout(properties.getConnectTimeout()) .setReadTimeout(properties.getReadTimeout()); restTemplateBuilder = applyCustomizers(restTemplateBuilder, customizers); - return new ZipkinRestTemplateSender(connectionDetails.getSpanEndpoint(), restTemplateBuilder.build()); + return new ZipkinRestTemplateSender(encoding, endpointSupplierFactory, connectionDetails.getSpanEndpoint(), + restTemplateBuilder.build()); } private RestTemplateBuilder applyCustomizers(RestTemplateBuilder restTemplateBuilder, @@ -111,42 +126,42 @@ private RestTemplateBuilder applyCustomizers(RestTemplateBuilder restTemplateBui static class WebClientSenderConfiguration { @Bean - @ConditionalOnMissingBean(Sender.class) - ZipkinWebClientSender webClientSender(ZipkinProperties properties, + @ConditionalOnMissingBean(BytesMessageSender.class) + ZipkinWebClientSender webClientSender(ZipkinProperties properties, Encoding encoding, ObjectProvider customizers, - ObjectProvider connectionDetailsProvider) { + ObjectProvider connectionDetailsProvider, + ObjectProvider endpointSupplierFactoryProvider) { ZipkinConnectionDetails connectionDetails = connectionDetailsProvider .getIfAvailable(() -> new PropertiesZipkinConnectionDetails(properties)); + HttpEndpointSupplier.Factory endpointSupplierFactory = endpointSupplierFactoryProvider + .getIfAvailable(HttpEndpointSuppliers::constantFactory); WebClient.Builder builder = WebClient.builder(); customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); - return new ZipkinWebClientSender(connectionDetails.getSpanEndpoint(), builder.build(), - properties.getConnectTimeout().plus(properties.getReadTimeout())); + return new ZipkinWebClientSender(encoding, endpointSupplierFactory, connectionDetails.getSpanEndpoint(), + builder.build(), properties.getConnectTimeout().plus(properties.getReadTimeout())); } } @Configuration(proxyBeanMethods = false) - static class ReporterConfiguration { + @ConditionalOnClass(AsyncZipkinSpanHandler.class) + static class BraveConfiguration { @Bean - @ConditionalOnMissingBean(Reporter.class) - @ConditionalOnBean(Sender.class) - AsyncReporter spanReporter(Sender sender, BytesEncoder encoder) { - return AsyncReporter.builder(sender).build(encoder); + @ConditionalOnMissingBean(value = MutableSpan.class, parameterizedContainer = BytesEncoder.class) + BytesEncoder braveSpanEncoder(Encoding encoding, + ObjectProvider> throwableTagProvider) { + Tag throwableTag = throwableTagProvider.getIfAvailable(() -> Tags.ERROR); + return MutableSpanBytesEncoder.create(encoding, throwableTag); } - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(ZipkinSpanHandler.class) - static class BraveConfiguration { - @Bean @ConditionalOnMissingBean - @ConditionalOnBean(Reporter.class) + @ConditionalOnBean(BytesMessageSender.class) @ConditionalOnEnabledTracing - ZipkinSpanHandler zipkinSpanHandler(Reporter spanReporter) { - return (ZipkinSpanHandler) ZipkinSpanHandler.newBuilder(spanReporter).build(); + AsyncZipkinSpanHandler asyncZipkinSpanHandler(BytesMessageSender sender, + BytesEncoder braveSpanEncoder) { + return AsyncZipkinSpanHandler.newBuilder(sender).build(braveSpanEncoder); } } @@ -155,12 +170,18 @@ ZipkinSpanHandler zipkinSpanHandler(Reporter spanReporter) { @ConditionalOnClass(ZipkinSpanExporter.class) static class OpenTelemetryConfiguration { + @Bean + @ConditionalOnMissingBean(value = Span.class, parameterizedContainer = BytesEncoder.class) + BytesEncoder zipkinSpanEncoder(Encoding encoding) { + return SpanBytesEncoder.forEncoding(encoding); + } + @Bean @ConditionalOnMissingBean - @ConditionalOnBean(Sender.class) + @ConditionalOnBean(BytesMessageSender.class) @ConditionalOnEnabledTracing - ZipkinSpanExporter zipkinSpanExporter(BytesEncoder encoder, Sender sender) { - return ZipkinSpanExporter.builder().setEncoder(encoder).setSender(sender).build(); + ZipkinSpanExporter zipkinSpanExporter(BytesMessageSender sender, BytesEncoder zipkinSpanEncoder) { + return ZipkinSpanExporter.builder().setSender(sender).setEncoder(zipkinSpanEncoder).build(); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConnectionDetails.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConnectionDetails.java index f5cc8ce43c00..1dba047a70f5 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConnectionDetails.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConnectionDetails.java @@ -16,11 +16,17 @@ package org.springframework.boot.actuate.autoconfigure.tracing.zipkin; +import zipkin2.reporter.HttpEndpointSupplier; + import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; /** * Details required to establish a connection to a Zipkin server. * + *

+ * Note: {@linkplain #getSpanEndpoint()} is only read once and passed to a bean of type + * {@link HttpEndpointSupplier.Factory} which defaults to no-op (constant). + * * @author Moritz Halbritter * @since 3.1.0 */ diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinProperties.java index 088ae8ee42b1..4642e88c8402 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinProperties.java @@ -18,6 +18,8 @@ import java.time.Duration; +import zipkin2.reporter.Encoding; + import org.springframework.boot.context.properties.ConfigurationProperties; /** @@ -34,6 +36,11 @@ public class ZipkinProperties { */ private String endpoint = "http://localhost:9411/api/v2/spans"; + /** + * How to encode the POST body to the Zipkin API. + */ + private Encoding encoding = Encoding.JSON; + /** * Connection timeout for requests to Zipkin. */ @@ -52,6 +59,14 @@ public void setEndpoint(String endpoint) { this.endpoint = endpoint; } + public Encoding getEncoding() { + return this.encoding; + } + + public void setEncoding(Encoding encoding) { + this.encoding = encoding; + } + public Duration getConnectTimeout() { return this.connectTimeout; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinRestTemplateSender.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinRestTemplateSender.java index d29e9eee60ff..1e9c6eaf9053 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinRestTemplateSender.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinRestTemplateSender.java @@ -16,10 +16,13 @@ package org.springframework.boot.actuate.autoconfigure.tracing.zipkin; -import zipkin2.Call; -import zipkin2.Callback; +import java.net.URI; + +import zipkin2.reporter.Encoding; +import zipkin2.reporter.HttpEndpointSupplier.Factory; import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.web.client.RestTemplate; @@ -31,55 +34,18 @@ */ class ZipkinRestTemplateSender extends HttpSender { - private final String endpoint; - private final RestTemplate restTemplate; - ZipkinRestTemplateSender(String endpoint, RestTemplate restTemplate) { - this.endpoint = endpoint; + ZipkinRestTemplateSender(Encoding encoding, Factory endpointSupplierFactory, String endpoint, + RestTemplate restTemplate) { + super(encoding, endpointSupplierFactory, endpoint); this.restTemplate = restTemplate; } @Override - public HttpPostCall sendSpans(byte[] batchedEncodedSpans) { - return new RestTemplateHttpPostCall(this.endpoint, batchedEncodedSpans, this.restTemplate); - } - - private static class RestTemplateHttpPostCall extends HttpPostCall { - - private final String endpoint; - - private final RestTemplate restTemplate; - - RestTemplateHttpPostCall(String endpoint, byte[] body, RestTemplate restTemplate) { - super(body); - this.endpoint = endpoint; - this.restTemplate = restTemplate; - } - - @Override - public Call clone() { - return new RestTemplateHttpPostCall(this.endpoint, getUncompressedBody(), this.restTemplate); - } - - @Override - protected Void doExecute() { - HttpEntity request = new HttpEntity<>(getBody(), getDefaultHeaders()); - this.restTemplate.exchange(this.endpoint, HttpMethod.POST, request, Void.class); - return null; - } - - @Override - protected void doEnqueue(Callback callback) { - try { - doExecute(); - callback.onSuccess(null); - } - catch (Exception ex) { - callback.onError(ex); - } - } - + void postSpans(URI endpoint, HttpHeaders headers, byte[] body) { + HttpEntity request = new HttpEntity<>(body, headers); + this.restTemplate.exchange(endpoint, HttpMethod.POST, request, Void.class); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinWebClientSender.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinWebClientSender.java index 2ef8cb74c09a..dea64c285571 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinWebClientSender.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinWebClientSender.java @@ -16,14 +16,13 @@ package org.springframework.boot.actuate.autoconfigure.tracing.zipkin; +import java.net.URI; import java.time.Duration; -import reactor.core.publisher.Mono; -import zipkin2.Call; -import zipkin2.Callback; +import zipkin2.reporter.Encoding; +import zipkin2.reporter.HttpEndpointSupplier.Factory; import org.springframework.http.HttpHeaders; -import org.springframework.http.ResponseEntity; import org.springframework.web.reactive.function.client.WebClient; /** @@ -34,68 +33,27 @@ */ class ZipkinWebClientSender extends HttpSender { - private final String endpoint; - private final WebClient webClient; private final Duration timeout; - ZipkinWebClientSender(String endpoint, WebClient webClient, Duration timeout) { - this.endpoint = endpoint; + ZipkinWebClientSender(Encoding encoding, Factory endpointSupplierFactory, String endpoint, WebClient webClient, + Duration timeout) { + super(encoding, endpointSupplierFactory, endpoint); this.webClient = webClient; this.timeout = timeout; } @Override - public HttpPostCall sendSpans(byte[] batchedEncodedSpans) { - return new WebClientHttpPostCall(this.endpoint, batchedEncodedSpans, this.webClient, this.timeout); - } - - private static class WebClientHttpPostCall extends HttpPostCall { - - private final String endpoint; - - private final WebClient webClient; - - private final Duration timeout; - - WebClientHttpPostCall(String endpoint, byte[] body, WebClient webClient, Duration timeout) { - super(body); - this.endpoint = endpoint; - this.webClient = webClient; - this.timeout = timeout; - } - - @Override - public Call clone() { - return new WebClientHttpPostCall(this.endpoint, getUncompressedBody(), this.webClient, this.timeout); - } - - @Override - protected Void doExecute() { - sendRequest().block(); - return null; - } - - @Override - protected void doEnqueue(Callback callback) { - sendRequest().subscribe((entity) -> callback.onSuccess(null), callback::onError); - } - - private Mono> sendRequest() { - return this.webClient.post() - .uri(this.endpoint) - .headers(this::addDefaultHeaders) - .bodyValue(getBody()) - .retrieve() - .toBodilessEntity() - .timeout(this.timeout); - } - - private void addDefaultHeaders(HttpHeaders headers) { - headers.addAll(getDefaultHeaders()); - } - + void postSpans(URI endpoint, HttpHeaders headers, byte[] body) { + this.webClient.post() + .uri(endpoint) + .headers((h) -> h.addAll(headers)) + .bodyValue(body) + .retrieve() + .toBodilessEntity() + .timeout(this.timeout) + .block(); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/CompositePropagationFactoryTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/CompositePropagationFactoryTests.java index 5c756514684c..954faa0d2fe2 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/CompositePropagationFactoryTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/CompositePropagationFactoryTests.java @@ -21,7 +21,6 @@ import java.util.List; import java.util.Map; -import brave.internal.propagation.StringPropagationAdapter; import brave.propagation.Propagation; import brave.propagation.TraceContext; import brave.propagation.TraceContextOrSamplingFlags; @@ -143,9 +142,8 @@ private DummyPropagation(String field) { } @Override - @SuppressWarnings("deprecation") - public Propagation create(Propagation.KeyFactory keyFactory) { - return StringPropagationAdapter.create(this, keyFactory); + public Propagation get() { + return this; } @Override diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/LocalBaggageFieldsTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/LocalBaggageFieldsTests.java index 6f6e99d87d35..15d4508066d5 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/LocalBaggageFieldsTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/LocalBaggageFieldsTests.java @@ -22,7 +22,6 @@ import brave.baggage.BaggagePropagationConfig; import brave.propagation.Propagation; import brave.propagation.Propagation.Factory; -import brave.propagation.Propagation.KeyFactory; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -50,11 +49,10 @@ void empty() { assertThat(LocalBaggageFields.empty().asList()).isEmpty(); } - @SuppressWarnings("deprecation") private static FactoryBuilder createBuilder() { return BaggagePropagation.newFactoryBuilder(new Factory() { @Override - public Propagation create(KeyFactory keyFactory) { + public Propagation get() { return null; } }); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/DefaultEncodingConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/DefaultEncodingConfiguration.java new file mode 100644 index 000000000000..7cf98ccd8c6e --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/DefaultEncodingConfiguration.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2024 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.boot.actuate.autoconfigure.tracing.zipkin; + +import zipkin2.reporter.Encoding; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +/** + * Configures the bean {@linkplain ZipkinAutoConfiguration} would from properties. + */ +@TestConfiguration(proxyBeanMethods = false) +class DefaultEncodingConfiguration { + + @Bean + @ConditionalOnMissingBean + Encoding encoding() { + return Encoding.JSON; + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/NoopSender.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/NoopSender.java index 48a7926e7818..4c6c95425896 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/NoopSender.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/NoopSender.java @@ -16,18 +16,16 @@ package org.springframework.boot.actuate.autoconfigure.tracing.zipkin; +import java.io.IOException; import java.util.List; -import zipkin2.Call; -import zipkin2.Callback; -import zipkin2.codec.Encoding; -import zipkin2.reporter.Sender; +import zipkin2.reporter.BytesMessageSender; +import zipkin2.reporter.Encoding; -class NoopSender extends Sender { +class NoopSender extends BytesMessageSender.Base { - @Override - public Encoding encoding() { - return Encoding.JSON; + NoopSender(Encoding encoding) { + super(encoding); } @Override @@ -36,27 +34,11 @@ public int messageMaxBytes() { } @Override - public int messageSizeInBytes(List encodedSpans) { - return encoding().listSizeInBytes(encodedSpans); + public void send(List encodedSpans) { } @Override - public Call sendSpans(List encodedSpans) { - return new Call.Base<>() { - @Override - public Call clone() { - return this; - } - - @Override - protected Void doExecute() { - return null; - } - - @Override - protected void doEnqueue(Callback callback) { - } - }; + public void close() throws IOException { } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinAutoConfigurationTests.java index 1d744175f26b..e70d9f308cd2 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinAutoConfigurationTests.java @@ -17,9 +17,7 @@ package org.springframework.boot.actuate.autoconfigure.tracing.zipkin; import org.junit.jupiter.api.Test; -import zipkin2.Span; -import zipkin2.codec.BytesEncoder; -import zipkin2.codec.SpanBytesEncoder; +import zipkin2.reporter.Encoding; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.FilteredClassLoader; @@ -41,21 +39,21 @@ class ZipkinAutoConfigurationTests { @Test void shouldSupplyBeans() { - this.contextRunner.run((context) -> assertThat(context).hasSingleBean(BytesEncoder.class) + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(Encoding.class) .hasSingleBean(PropertiesZipkinConnectionDetails.class)); } @Test void shouldNotSupplyBeansIfZipkinReporterIsMissing() { this.contextRunner.withClassLoader(new FilteredClassLoader("zipkin2.reporter")) - .run((context) -> assertThat(context).doesNotHaveBean(BytesEncoder.class)); + .run((context) -> assertThat(context).doesNotHaveBean(Encoding.class)); } @Test void shouldBackOffOnCustomBeans() { this.contextRunner.withUserConfiguration(CustomConfiguration.class).run((context) -> { - assertThat(context).hasBean("customBytesEncoder"); - assertThat(context).hasSingleBean(BytesEncoder.class); + assertThat(context).hasBean("customEncoding"); + assertThat(context).hasSingleBean(Encoding.class); }); } @@ -90,8 +88,8 @@ void shouldWorkWithoutSenders() { private static final class CustomConfiguration { @Bean - BytesEncoder customBytesEncoder() { - return SpanBytesEncoder.JSON_V2; + Encoding customEncoding() { + return Encoding.PROTO3; } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurationsBraveConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurationsBraveConfigurationTests.java index 7874d5ec7730..d1a6dd20ffe6 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurationsBraveConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurationsBraveConfigurationTests.java @@ -16,11 +16,17 @@ package org.springframework.boot.actuate.autoconfigure.tracing.zipkin; +import java.nio.charset.StandardCharsets; + +import brave.Tag; +import brave.handler.MutableSpan; import brave.handler.SpanHandler; +import brave.propagation.TraceContext; import org.junit.jupiter.api.Test; -import zipkin2.Span; -import zipkin2.reporter.Reporter; -import zipkin2.reporter.brave.ZipkinSpanHandler; +import zipkin2.reporter.BytesEncoder; +import zipkin2.reporter.BytesMessageSender; +import zipkin2.reporter.Encoding; +import zipkin2.reporter.brave.AsyncZipkinSpanHandler; import org.springframework.boot.actuate.autoconfigure.tracing.zipkin.ZipkinConfigurations.BraveConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -40,61 +46,126 @@ class ZipkinConfigurationsBraveConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(BraveConfiguration.class)); + .withConfiguration(AutoConfigurations.of(DefaultEncodingConfiguration.class, BraveConfiguration.class)); private final ApplicationContextRunner tracingDisabledContextRunner = this.contextRunner .withPropertyValues("management.tracing.enabled=false"); @Test void shouldSupplyBeans() { - this.contextRunner.withUserConfiguration(ReporterConfiguration.class) - .run((context) -> assertThat(context).hasSingleBean(ZipkinSpanHandler.class)); + this.contextRunner.withUserConfiguration(SenderConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(AsyncZipkinSpanHandler.class)); } @Test void shouldNotSupplySpanHandlerIfReporterIsMissing() { - this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(ZipkinSpanHandler.class)); + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(AsyncZipkinSpanHandler.class)); } @Test void shouldNotSupplyIfZipkinReporterBraveIsNotOnClasspath() { + // Note: Technically, Brave can work without zipkin-reporter. For example, + // WavefrontSpanHandler doesn't require this to operate. If we remove this + // dependency enforcement when WavefrontSpanHandler is in use, we can resolve + // micrometer-metrics/tracing#509. We also need this for any configuration that + // uses senders defined in the Spring Boot source tree, such as HttpSender. this.contextRunner.withClassLoader(new FilteredClassLoader("zipkin2.reporter.brave")) - .withUserConfiguration(ReporterConfiguration.class) - .run((context) -> assertThat(context).doesNotHaveBean(ZipkinSpanHandler.class)); + .withUserConfiguration(SenderConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(AsyncZipkinSpanHandler.class)); } @Test void shouldBackOffOnCustomBeans() { - this.contextRunner.withUserConfiguration(ReporterConfiguration.class, CustomConfiguration.class) + this.contextRunner.withUserConfiguration(SenderConfiguration.class, CustomConfiguration.class) .run((context) -> { - assertThat(context).hasBean("customZipkinSpanHandler"); - assertThat(context).hasSingleBean(ZipkinSpanHandler.class); + assertThat(context).hasBean("customAsyncZipkinSpanHandler"); + assertThat(context).hasSingleBean(AsyncZipkinSpanHandler.class); }); } @Test - void shouldSupplyZipkinSpanHandlerWithCustomSpanHandler() { - this.contextRunner.withUserConfiguration(ReporterConfiguration.class, CustomSpanHandlerConfiguration.class) + void shouldSupplyAsyncZipkinSpanHandlerWithCustomSpanHandler() { + this.contextRunner.withUserConfiguration(SenderConfiguration.class, CustomSpanHandlerConfiguration.class) .run((context) -> { assertThat(context).hasBean("customSpanHandler"); - assertThat(context).hasSingleBean(ZipkinSpanHandler.class); + assertThat(context).hasSingleBean(AsyncZipkinSpanHandler.class); + }); + } + + @Test + void shouldNotSupplyAsyncZipkinSpanHandlerIfTracingIsDisabled() { + this.tracingDisabledContextRunner.withUserConfiguration(SenderConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(AsyncZipkinSpanHandler.class)); + } + + @Test + void shouldUseCustomEncoderBean() { + this.contextRunner.withUserConfiguration(SenderConfiguration.class, CustomEncoderConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(AsyncZipkinSpanHandler.class); + assertThat(context.getBean(AsyncZipkinSpanHandler.class)).extracting("spanReporter.encoder") + .isInstanceOf(CustomMutableSpanEncoder.class) + .extracting("encoding") + .isEqualTo(Encoding.JSON); + }); + } + + @Test + void shouldUseCustomEncodingBean() { + this.contextRunner + .withUserConfiguration(SenderConfiguration.class, CustomEncodingConfiguration.class, + CustomEncoderConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(AsyncZipkinSpanHandler.class); + assertThat(context.getBean(AsyncZipkinSpanHandler.class)).extracting("encoding") + .isEqualTo(Encoding.PROTO3); }); } @Test - void shouldNotSupplyZipkinSpanHandlerIfTracingIsDisabled() { - this.tracingDisabledContextRunner.withUserConfiguration(ReporterConfiguration.class) - .run((context) -> assertThat(context).doesNotHaveBean(ZipkinSpanHandler.class)); + void shouldUseDefaultThrowableTagBean() { + this.contextRunner.withUserConfiguration(SenderConfiguration.class).run((context) -> { + @SuppressWarnings("unchecked") + BytesEncoder encoder = context.getBean(BytesEncoder.class); + + MutableSpan span = new MutableSpan(); + span.traceId("1"); + span.id("1"); + span.tag("error", "true"); + span.error(new RuntimeException("ice cream")); + + // default tag key name is "error", and doesn't overwrite + assertThat(new String(encoder.encode(span), StandardCharsets.UTF_8)).isEqualTo( + "{\"traceId\":\"0000000000000001\",\"id\":\"0000000000000001\",\"tags\":{\"error\":\"true\"}}"); + }); + } + + @Test + void shouldUseCustomThrowableTagBean() { + this.contextRunner.withUserConfiguration(SenderConfiguration.class, CustomThrowableTagConfiguration.class) + .run((context) -> { + @SuppressWarnings("unchecked") + BytesEncoder encoder = context.getBean(BytesEncoder.class); + + MutableSpan span = new MutableSpan(); + span.traceId("1"); + span.id("1"); + span.tag("error", "true"); + span.error(new RuntimeException("ice cream")); + + // The custom throwable parser doesn't use the key "error" we can see both + assertThat(new String(encoder.encode(span), StandardCharsets.UTF_8)).isEqualTo( + "{\"traceId\":\"0000000000000001\",\"id\":\"0000000000000001\",\"tags\":{\"error\":\"true\",\"exception\":\"ice cream\"}}"); + }); } @Configuration(proxyBeanMethods = false) - private static final class ReporterConfiguration { + private static final class SenderConfiguration { @Bean - @SuppressWarnings("unchecked") - Reporter reporter() { - return mock(Reporter.class); + BytesMessageSender sender(Encoding encoding) { + return new NoopSender(encoding); } } @@ -103,9 +174,23 @@ Reporter reporter() { private static final class CustomConfiguration { @Bean - @SuppressWarnings("unchecked") - ZipkinSpanHandler customZipkinSpanHandler() { - return (ZipkinSpanHandler) ZipkinSpanHandler.create(mock(Reporter.class)); + AsyncZipkinSpanHandler customAsyncZipkinSpanHandler() { + return AsyncZipkinSpanHandler.create(new NoopSender(Encoding.JSON)); + } + + } + + @Configuration(proxyBeanMethods = false) + private static final class CustomThrowableTagConfiguration { + + @Bean + Tag throwableTag() { + return new Tag("exception") { + @Override + protected String parseValue(Throwable throwable, TraceContext traceContext) { + return throwable.getMessage(); + } + }; } } @@ -120,4 +205,38 @@ SpanHandler customSpanHandler() { } + @Configuration(proxyBeanMethods = false) + private static final class CustomEncodingConfiguration { + + @Bean + Encoding encoding() { + return Encoding.PROTO3; + } + + } + + @Configuration(proxyBeanMethods = false) + private static final class CustomEncoderConfiguration { + + @Bean + BytesEncoder encoder(Encoding encoding) { + return new CustomMutableSpanEncoder(encoding); + } + + } + + private record CustomMutableSpanEncoder(Encoding encoding) implements BytesEncoder { + + @Override + public int sizeInBytes(MutableSpan span) { + throw new UnsupportedOperationException(); + } + + @Override + public byte[] encode(MutableSpan span) { + throw new UnsupportedOperationException(); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurationsOpenTelemetryConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurationsOpenTelemetryConfigurationTests.java index 76d1e3b5d06d..a32a49ffb6fb 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurationsOpenTelemetryConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurationsOpenTelemetryConfigurationTests.java @@ -19,13 +19,12 @@ import io.opentelemetry.exporter.zipkin.ZipkinSpanExporter; import org.junit.jupiter.api.Test; import zipkin2.Span; -import zipkin2.codec.BytesEncoder; -import zipkin2.codec.SpanBytesEncoder; -import zipkin2.reporter.Sender; +import zipkin2.reporter.BytesEncoder; +import zipkin2.reporter.BytesMessageSender; +import zipkin2.reporter.Encoding; import org.springframework.boot.actuate.autoconfigure.tracing.zipkin.ZipkinConfigurations.OpenTelemetryConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; @@ -41,7 +40,7 @@ class ZipkinConfigurationsOpenTelemetryConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(BaseConfiguration.class, OpenTelemetryConfiguration.class)); + .withConfiguration(AutoConfigurations.of(DefaultEncodingConfiguration.class, OpenTelemetryConfiguration.class)); private final ApplicationContextRunner tracingDisabledContextRunner = this.contextRunner .withPropertyValues("management.tracing.enabled=false"); @@ -79,12 +78,48 @@ void shouldNotSupplyZipkinSpanExporterIfTracingIsDisabled() { .run((context) -> assertThat(context).doesNotHaveBean(ZipkinSpanExporter.class)); } + @Test + void shouldUseCustomEncoderBean() { + this.contextRunner.withUserConfiguration(SenderConfiguration.class, CustomEncoderConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(ZipkinSpanExporter.class); + assertThat(context.getBean(ZipkinSpanExporter.class)).extracting("encoder") + .isInstanceOf(CustomSpanEncoder.class) + .extracting("encoding") + .isEqualTo(Encoding.JSON); + }); + } + + @Test + void shouldUseCustomEncodingBean() { + this.contextRunner + .withUserConfiguration(SenderConfiguration.class, CustomEncodingConfiguration.class, + CustomEncoderConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(ZipkinSpanExporter.class); + assertThat(context.getBean(ZipkinSpanExporter.class)).extracting("encoder") + .isInstanceOf(CustomSpanEncoder.class) + .extracting("encoding") + .isEqualTo(Encoding.PROTO3); + }); + } + + @Configuration(proxyBeanMethods = false) + private static final class CustomEncodingConfiguration { + + @Bean + Encoding encoding() { + return Encoding.PROTO3; + } + + } + @Configuration(proxyBeanMethods = false) private static final class SenderConfiguration { @Bean - Sender sender() { - return new NoopSender(); + BytesMessageSender sender(Encoding encoding) { + return new NoopSender(encoding); } } @@ -100,12 +135,25 @@ ZipkinSpanExporter customZipkinSpanExporter() { } @Configuration(proxyBeanMethods = false) - private static final class BaseConfiguration { + private static final class CustomEncoderConfiguration { @Bean - @ConditionalOnMissingBean - BytesEncoder spanBytesEncoder() { - return SpanBytesEncoder.JSON_V2; + BytesEncoder encoder(Encoding encoding) { + return new CustomSpanEncoder(encoding); + } + + } + + record CustomSpanEncoder(Encoding encoding) implements BytesEncoder { + + @Override + public int sizeInBytes(Span span) { + throw new UnsupportedOperationException(); + } + + @Override + public byte[] encode(Span span) { + throw new UnsupportedOperationException(); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurationsReporterConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurationsReporterConfigurationTests.java deleted file mode 100644 index 45a9f3f1d22d..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurationsReporterConfigurationTests.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2012-2024 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.boot.actuate.autoconfigure.tracing.zipkin; - -import org.junit.jupiter.api.Test; -import zipkin2.Span; -import zipkin2.codec.BytesEncoder; -import zipkin2.codec.SpanBytesEncoder; -import zipkin2.reporter.Reporter; -import zipkin2.reporter.Sender; - -import org.springframework.boot.actuate.autoconfigure.tracing.zipkin.ZipkinConfigurations.ReporterConfiguration; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link ReporterConfiguration}. - * - * @author Moritz Halbritter - */ -class ZipkinConfigurationsReporterConfigurationTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(BaseConfiguration.class, ReporterConfiguration.class)); - - @Test - void shouldSupplyBeans() { - this.contextRunner.withUserConfiguration(SenderConfiguration.class) - .run((context) -> assertThat(context).hasSingleBean(Reporter.class)); - } - - @Test - void shouldNotSupplyReporterIfSenderIsMissing() { - this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(Reporter.class)); - } - - @Test - void shouldBackOffOnCustomBeans() { - this.contextRunner.withUserConfiguration(SenderConfiguration.class, CustomConfiguration.class) - .run((context) -> { - assertThat(context).hasBean("customReporter"); - assertThat(context).hasSingleBean(Reporter.class); - }); - } - - @Configuration(proxyBeanMethods = false) - private static final class SenderConfiguration { - - @Bean - Sender sender() { - return new NoopSender(); - } - - } - - @Configuration(proxyBeanMethods = false) - private static final class CustomConfiguration { - - @Bean - @SuppressWarnings("unchecked") - Reporter customReporter() { - return mock(Reporter.class); - } - - } - - @Configuration(proxyBeanMethods = false) - private static final class BaseConfiguration { - - @Bean - @ConditionalOnMissingBean - BytesEncoder spanBytesEncoder() { - return SpanBytesEncoder.JSON_V2; - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurationsSenderConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurationsSenderConfigurationTests.java index 9b98d24710a4..3c8ab9537d88 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurationsSenderConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurationsSenderConfigurationTests.java @@ -18,6 +18,7 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.List; import java.util.concurrent.TimeUnit; import okhttp3.mockwebserver.MockResponse; @@ -25,7 +26,8 @@ import okhttp3.mockwebserver.RecordedRequest; import org.junit.jupiter.api.Test; import org.mockito.ArgumentMatchers; -import zipkin2.reporter.Sender; +import zipkin2.reporter.BytesMessageSender; +import zipkin2.reporter.HttpEndpointSupplier; import zipkin2.reporter.urlconnection.URLConnectionSender; import org.springframework.boot.actuate.autoconfigure.tracing.zipkin.ZipkinConfigurations.SenderConfiguration; @@ -51,18 +53,18 @@ class ZipkinConfigurationsSenderConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(SenderConfiguration.class)); + .withConfiguration(AutoConfigurations.of(DefaultEncodingConfiguration.class, SenderConfiguration.class)); private final ReactiveWebApplicationContextRunner reactiveContextRunner = new ReactiveWebApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(SenderConfiguration.class)); + .withConfiguration(AutoConfigurations.of(DefaultEncodingConfiguration.class, SenderConfiguration.class)); private final WebApplicationContextRunner servletContextRunner = new WebApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(SenderConfiguration.class)); + .withConfiguration(AutoConfigurations.of(DefaultEncodingConfiguration.class, SenderConfiguration.class)); @Test void shouldSupplyBeans() { this.contextRunner.run((context) -> { - assertThat(context).hasSingleBean(Sender.class); + assertThat(context).hasSingleBean(BytesMessageSender.class); assertThat(context).hasSingleBean(URLConnectionSender.class); assertThat(context).doesNotHaveBean(ZipkinRestTemplateSender.class); }); @@ -74,7 +76,7 @@ void shouldPreferWebClientSenderIfWebApplicationIsReactiveAndUrlSenderIsNotAvail .withClassLoader(new FilteredClassLoader("zipkin2.reporter.urlconnection")) .run((context) -> { assertThat(context).doesNotHaveBean(URLConnectionSender.class); - assertThat(context).hasSingleBean(Sender.class); + assertThat(context).hasSingleBean(BytesMessageSender.class); assertThat(context).hasSingleBean(ZipkinWebClientSender.class); then(context.getBean(ZipkinWebClientBuilderCustomizer.class)).should() .customize(ArgumentMatchers.any()); @@ -87,7 +89,7 @@ void shouldPreferWebClientSenderIfWebApplicationIsServletAndUrlSenderIsNotAvaila .withClassLoader(new FilteredClassLoader("zipkin2.reporter.urlconnection")) .run((context) -> { assertThat(context).doesNotHaveBean(URLConnectionSender.class); - assertThat(context).hasSingleBean(Sender.class); + assertThat(context).hasSingleBean(BytesMessageSender.class); assertThat(context).hasSingleBean(ZipkinWebClientSender.class); }); } @@ -98,7 +100,7 @@ void shouldPreferWebClientInNonWebApplicationAndUrlConnectionSenderIsNotAvailabl .withClassLoader(new FilteredClassLoader("zipkin2.reporter.urlconnection")) .run((context) -> { assertThat(context).doesNotHaveBean(URLConnectionSender.class); - assertThat(context).hasSingleBean(Sender.class); + assertThat(context).hasSingleBean(BytesMessageSender.class); assertThat(context).hasSingleBean(ZipkinWebClientSender.class); }); } @@ -109,7 +111,7 @@ void willUseRestTemplateInNonWebApplicationIfUrlConnectionSenderAndWebClientAreN .withClassLoader(new FilteredClassLoader(URLConnectionSender.class, WebClient.class)) .run((context) -> { assertThat(context).doesNotHaveBean(URLConnectionSender.class); - assertThat(context).hasSingleBean(Sender.class); + assertThat(context).hasSingleBean(BytesMessageSender.class); assertThat(context).hasSingleBean(ZipkinRestTemplateSender.class); }); } @@ -120,7 +122,7 @@ void willUseRestTemplateInServletWebApplicationIfUrlConnectionSenderAndWebClient .withClassLoader(new FilteredClassLoader(URLConnectionSender.class, WebClient.class)) .run((context) -> { assertThat(context).doesNotHaveBean(URLConnectionSender.class); - assertThat(context).hasSingleBean(Sender.class); + assertThat(context).hasSingleBean(BytesMessageSender.class); assertThat(context).hasSingleBean(ZipkinRestTemplateSender.class); }); } @@ -131,7 +133,7 @@ void willUseRestTemplateInReactiveWebApplicationIfUrlConnectionSenderAndWebClien .withClassLoader(new FilteredClassLoader(URLConnectionSender.class, WebClient.class)) .run((context) -> { assertThat(context).doesNotHaveBean(URLConnectionSender.class); - assertThat(context).hasSingleBean(Sender.class); + assertThat(context).hasSingleBean(BytesMessageSender.class); assertThat(context).hasSingleBean(ZipkinRestTemplateSender.class); }); } @@ -140,7 +142,7 @@ void willUseRestTemplateInReactiveWebApplicationIfUrlConnectionSenderAndWebClien void shouldNotUseWebClientSenderIfNoBuilderIsAvailable() { this.reactiveContextRunner.run((context) -> { assertThat(context).doesNotHaveBean(ZipkinWebClientSender.class); - assertThat(context).hasSingleBean(Sender.class); + assertThat(context).hasSingleBean(BytesMessageSender.class); assertThat(context).hasSingleBean(URLConnectionSender.class); }); } @@ -149,7 +151,7 @@ void shouldNotUseWebClientSenderIfNoBuilderIsAvailable() { void shouldBackOffOnCustomBeans() { this.contextRunner.withUserConfiguration(CustomConfiguration.class).run((context) -> { assertThat(context).hasBean("customSender"); - assertThat(context).hasSingleBean(Sender.class); + assertThat(context).hasSingleBean(BytesMessageSender.class); }); } @@ -164,7 +166,7 @@ void shouldApplyZipkinRestTemplateBuilderCustomizers() throws IOException { .run((context) -> { assertThat(context).hasSingleBean(ZipkinRestTemplateSender.class); ZipkinRestTemplateSender sender = context.getBean(ZipkinRestTemplateSender.class); - sender.sendSpans("spans".getBytes(StandardCharsets.UTF_8)).execute(); + sender.send(List.of("spans".getBytes(StandardCharsets.UTF_8))); RecordedRequest recordedRequest = mockWebServer.takeRequest(1, TimeUnit.SECONDS); assertThat(recordedRequest).isNotNull(); assertThat(recordedRequest.getHeaders().get("x-dummy")).isEqualTo("dummy"); @@ -172,6 +174,32 @@ void shouldApplyZipkinRestTemplateBuilderCustomizers() throws IOException { } } + @Test + void shouldUseCustomHttpEndpointSupplierFactory() { + this.contextRunner.withUserConfiguration(CustomHttpEndpointSupplierFactoryConfiguration.class) + .run((context) -> assertThat(context.getBean(URLConnectionSender.class)) + .extracting("delegate.endpointSupplier") + .isInstanceOf(CustomHttpEndpointSupplier.class)); + } + + @Test + void shouldUseCustomHttpEndpointSupplierFactoryWhenReactive() { + this.reactiveContextRunner.withUserConfiguration(WebClientConfiguration.class) + .withClassLoader(new FilteredClassLoader(URLConnectionSender.class)) + .withUserConfiguration(CustomHttpEndpointSupplierFactoryConfiguration.class) + .run((context) -> assertThat(context.getBean(ZipkinWebClientSender.class)).extracting("endpointSupplier") + .isInstanceOf(CustomHttpEndpointSupplier.class)); + } + + @Test + void shouldUseCustomHttpEndpointSupplierFactoryWhenRestTemplate() { + this.contextRunner.withUserConfiguration(RestTemplateConfiguration.class) + .withClassLoader(new FilteredClassLoader(URLConnectionSender.class, WebClient.class)) + .withUserConfiguration(CustomHttpEndpointSupplierFactoryConfiguration.class) + .run((context) -> assertThat(context.getBean(ZipkinRestTemplateSender.class)).extracting("endpointSupplier") + .isInstanceOf(CustomHttpEndpointSupplier.class)); + } + @Configuration(proxyBeanMethods = false) private static final class RestTemplateConfiguration { @@ -196,8 +224,8 @@ ZipkinWebClientBuilderCustomizer webClientBuilder() { private static final class CustomConfiguration { @Bean - Sender customSender() { - return mock(Sender.class); + BytesMessageSender customSender() { + return mock(BytesMessageSender.class); } } @@ -211,4 +239,35 @@ public RestTemplateBuilder customize(RestTemplateBuilder restTemplateBuilder) { } + @Configuration(proxyBeanMethods = false) + private static final class CustomHttpEndpointSupplierFactoryConfiguration { + + @Bean + HttpEndpointSupplier.Factory httpEndpointSupplier() { + return new CustomHttpEndpointSupplierFactory(); + } + + } + + private static final class CustomHttpEndpointSupplierFactory implements HttpEndpointSupplier.Factory { + + @Override + public HttpEndpointSupplier create(String endpoint) { + return new CustomHttpEndpointSupplier(endpoint); + } + + } + + private record CustomHttpEndpointSupplier(String endpoint) implements HttpEndpointSupplier { + + @Override + public String get() { + return this.endpoint; + } + + @Override + public void close() { + } + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinHttpSenderTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinHttpSenderTests.java index 15fd231e4a1c..9b1d56f6f555 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinHttpSenderTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinHttpSenderTests.java @@ -18,21 +18,14 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.time.Duration; import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.atomic.AtomicReference; -import org.awaitility.Awaitility; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import zipkin2.Callback; +import zipkin2.reporter.BytesMessageSender; import zipkin2.reporter.ClosedSenderException; -import zipkin2.reporter.Sender; -import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** @@ -43,9 +36,9 @@ */ abstract class ZipkinHttpSenderTests { - protected Sender sender; + protected BytesMessageSender sender; - abstract Sender createSender(); + abstract BytesMessageSender createSender(); @BeforeEach void beforeEach() throws Exception { @@ -58,55 +51,14 @@ void afterEach() throws IOException { } @Test - void sendSpansShouldThrowIfCloseWasCalled() throws IOException { + void sendShouldThrowIfCloseWasCalled() throws IOException { this.sender.close(); assertThatExceptionOfType(ClosedSenderException.class) - .isThrownBy(() -> this.sender.sendSpans(Collections.emptyList())); - } - - protected void makeRequest(List encodedSpans, boolean async) throws IOException { - if (async) { - CallbackResult callbackResult = makeAsyncRequest(encodedSpans); - assertThat(callbackResult.success()).isTrue(); - } - else { - makeSyncRequest(encodedSpans); - } - } - - protected CallbackResult makeAsyncRequest(List encodedSpans) { - return makeAsyncRequest(this.sender, encodedSpans); - } - - protected CallbackResult makeAsyncRequest(Sender sender, List encodedSpans) { - AtomicReference callbackResult = new AtomicReference<>(); - sender.sendSpans(encodedSpans).enqueue(new Callback<>() { - @Override - public void onSuccess(Void value) { - callbackResult.set(new CallbackResult(true, null)); - } - - @Override - public void onError(Throwable t) { - callbackResult.set(new CallbackResult(false, t)); - } - }); - return Awaitility.await().atMost(Duration.ofSeconds(5)).until(callbackResult::get, Objects::nonNull); - } - - protected void makeSyncRequest(List encodedSpans) throws IOException { - makeSyncRequest(this.sender, encodedSpans); - } - - protected void makeSyncRequest(Sender sender, List encodedSpans) throws IOException { - sender.sendSpans(encodedSpans).execute(); + .isThrownBy(() -> this.sender.send(Collections.emptyList())); } protected byte[] toByteArray(String input) { return input.getBytes(StandardCharsets.UTF_8); } - record CallbackResult(boolean success, Throwable error) { - } - } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinRestTemplateSenderTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinRestTemplateSenderTests.java index ad30c2e5eb52..a825b530c6c9 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinRestTemplateSenderTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinRestTemplateSenderTests.java @@ -17,23 +17,25 @@ package org.springframework.boot.actuate.autoconfigure.tracing.zipkin; import java.io.IOException; +import java.net.URI; import java.util.Base64; import java.util.Collections; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import zipkin2.CheckResult; -import zipkin2.reporter.Sender; +import zipkin2.reporter.BytesMessageSender; +import zipkin2.reporter.Encoding; +import zipkin2.reporter.HttpEndpointSupplier; +import zipkin2.reporter.HttpEndpointSuppliers; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.web.client.RestTemplate; -import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatException; import static org.springframework.test.web.client.match.MockRestRequestMatchers.content; import static org.springframework.test.web.client.match.MockRestRequestMatchers.header; @@ -51,13 +53,23 @@ class ZipkinRestTemplateSenderTests extends ZipkinHttpSenderTests { private static final String ZIPKIN_URL = "http://localhost:9411/api/v2/spans"; + private RestTemplate restTemplate; + private MockRestServiceServer mockServer; @Override - Sender createSender() { - RestTemplate restTemplate = new RestTemplate(); - this.mockServer = MockRestServiceServer.createServer(restTemplate); - return new ZipkinRestTemplateSender(ZIPKIN_URL, restTemplate); + BytesMessageSender createSender() { + this.restTemplate = new RestTemplate(); + this.mockServer = MockRestServiceServer.createServer(this.restTemplate); + return createSender(Encoding.JSON); + } + + BytesMessageSender createSender(Encoding encoding) { + return createSender(HttpEndpointSuppliers.constantFactory(), encoding); + } + + BytesMessageSender createSender(HttpEndpointSupplier.Factory endpointSupplierFactory, Encoding encoding) { + return new ZipkinRestTemplateSender(encoding, endpointSupplierFactory, ZIPKIN_URL, this.restTemplate); } @AfterEach @@ -68,55 +80,65 @@ void afterEach() throws IOException { } @Test - void checkShouldSendEmptySpanList() { + void sendShouldSendSpansToZipkin() throws IOException { this.mockServer.expect(requestTo(ZIPKIN_URL)) .andExpect(method(HttpMethod.POST)) - .andExpect(content().string("[]")) + .andExpect(content().contentType("application/json")) + .andExpect(content().string("[span1,span2]")) .andRespond(withStatus(HttpStatus.ACCEPTED)); - assertThat(this.sender.check()).isEqualTo(CheckResult.OK); + this.sender.send(List.of(toByteArray("span1"), toByteArray("span2"))); } @Test - void checkShouldNotRaiseException() { + void sendShouldSendSpansToZipkinInProto3() throws IOException { this.mockServer.expect(requestTo(ZIPKIN_URL)) .andExpect(method(HttpMethod.POST)) - .andRespond(withStatus(HttpStatus.INTERNAL_SERVER_ERROR)); - CheckResult result = this.sender.check(); - assertThat(result.ok()).isFalse(); - assertThat(result.error()).hasMessageContaining("500 Internal Server Error"); + .andExpect(content().contentType("application/x-protobuf")) + .andExpect(content().string("span1span2")) + .andRespond(withStatus(HttpStatus.ACCEPTED)); + + try (BytesMessageSender sender = createSender(Encoding.PROTO3)) { + sender.send(List.of(toByteArray("span1"), toByteArray("span2"))); + } } - @ParameterizedTest - @ValueSource(booleans = { true, false }) - void sendSpansShouldSendSpansToZipkin(boolean async) throws IOException { - this.mockServer.expect(requestTo(ZIPKIN_URL)) - .andExpect(method(HttpMethod.POST)) - .andExpect(content().contentType("application/json")) - .andExpect(content().string("[span1,span2]")) - .andRespond(withStatus(HttpStatus.ACCEPTED)); - makeRequest(List.of(toByteArray("span1"), toByteArray("span2")), async); + /** + * This tests that a dynamic {@linkplain HttpEndpointSupplier} updates are visible to + * {@link HttpSender#postSpans(URI, HttpHeaders, byte[])}. + */ + @Test + void sendUsesDynamicEndpoint() throws Exception { + this.mockServer.expect(requestTo(ZIPKIN_URL + "/1")).andRespond(withStatus(HttpStatus.ACCEPTED)); + this.mockServer.expect(requestTo(ZIPKIN_URL + "/2")).andRespond(withStatus(HttpStatus.ACCEPTED)); + + AtomicInteger suffix = new AtomicInteger(); + try (BytesMessageSender sender = createSender((e) -> new HttpEndpointSupplier() { + @Override + public String get() { + return ZIPKIN_URL + "/" + suffix.incrementAndGet(); + } + + @Override + public void close() { + } + }, Encoding.JSON)) { + sender.send(Collections.emptyList()); + sender.send(Collections.emptyList()); + } } - @ParameterizedTest - @ValueSource(booleans = { true, false }) - void sendSpansShouldHandleHttpFailures(boolean async) { + @Test + void sendShouldHandleHttpFailures() { this.mockServer.expect(requestTo(ZIPKIN_URL)) .andExpect(method(HttpMethod.POST)) .andRespond(withStatus(HttpStatus.INTERNAL_SERVER_ERROR)); - if (async) { - CallbackResult callbackResult = makeAsyncRequest(Collections.emptyList()); - assertThat(callbackResult.success()).isFalse(); - assertThat(callbackResult.error()).isNotNull().hasMessageContaining("500 Internal Server Error"); - } - else { - assertThatException().isThrownBy(() -> makeSyncRequest(Collections.emptyList())) - .withMessageContaining("500 Internal Server Error"); - } + + assertThatException().isThrownBy(() -> this.sender.send(Collections.emptyList())) + .withMessageContaining("500 Internal Server Error"); } - @ParameterizedTest - @ValueSource(booleans = { true, false }) - void sendSpansShouldCompressData(boolean async) throws IOException { + @Test + void sendShouldCompressData() throws IOException { String uncompressed = "a".repeat(10000); // This is gzip compressed 10000 times 'a' byte[] compressed = Base64.getDecoder() @@ -127,7 +149,7 @@ void sendSpansShouldCompressData(boolean async) throws IOException { .andExpect(content().contentType("application/json")) .andExpect(content().bytes(compressed)) .andRespond(withStatus(HttpStatus.ACCEPTED)); - makeRequest(List.of(toByteArray(uncompressed)), async); + this.sender.send(List.of(toByteArray(uncompressed))); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinWebClientSenderTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinWebClientSenderTests.java index 2a2f38198c03..f42eeddda199 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinWebClientSenderTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinWebClientSenderTests.java @@ -17,12 +17,14 @@ package org.springframework.boot.actuate.autoconfigure.tracing.zipkin; import java.io.IOException; +import java.net.URI; import java.time.Duration; import java.util.Base64; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import okhttp3.mockwebserver.MockResponse; @@ -33,11 +35,12 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import zipkin2.CheckResult; -import zipkin2.reporter.Sender; +import zipkin2.reporter.BytesMessageSender; +import zipkin2.reporter.Encoding; +import zipkin2.reporter.HttpEndpointSupplier; +import zipkin2.reporter.HttpEndpointSuppliers; +import org.springframework.http.HttpHeaders; import org.springframework.web.reactive.function.client.WebClient; import static org.assertj.core.api.Assertions.assertThat; @@ -79,72 +82,92 @@ void beforeEach() throws Exception { } @Override - Sender createSender() { - return createSender(Duration.ofSeconds(10)); + BytesMessageSender createSender() { + return createSender(Encoding.JSON, Duration.ofSeconds(10)); } - Sender createSender(Duration timeout) { + ZipkinWebClientSender createSender(Encoding encoding, Duration timeout) { + return createSender(HttpEndpointSuppliers.constantFactory(), encoding, timeout); + } + + ZipkinWebClientSender createSender(HttpEndpointSupplier.Factory endpointSupplierFactory, Encoding encoding, + Duration timeout) { WebClient webClient = WebClient.builder().build(); - return new ZipkinWebClientSender(ZIPKIN_URL, webClient, timeout); + return new ZipkinWebClientSender(encoding, endpointSupplierFactory, ZIPKIN_URL, webClient, timeout); } @Test - void checkShouldSendEmptySpanList() throws InterruptedException { + void sendShouldSendSpansToZipkin() throws IOException, InterruptedException { mockBackEnd.enqueue(new MockResponse()); - assertThat(this.sender.check()).isEqualTo(CheckResult.OK); + List encodedSpans = List.of(toByteArray("span1"), toByteArray("span2")); + this.sender.send(encodedSpans); requestAssertions((request) -> { assertThat(request.getMethod()).isEqualTo("POST"); - assertThat(request.getBody().readUtf8()).isEqualTo("[]"); + assertThat(request.getHeader("Content-Type")).isEqualTo("application/json"); + assertThat(request.getBody().readUtf8()).isEqualTo("[span1,span2]"); }); } @Test - void checkShouldNotRaiseException() throws InterruptedException { - mockBackEnd.enqueue(new MockResponse().setResponseCode(500)); - CheckResult result = this.sender.check(); - assertThat(result.ok()).isFalse(); - assertThat(result.error()).hasMessageContaining("500 Internal Server Error"); - requestAssertions((request) -> assertThat(request.getMethod()).isEqualTo("POST")); - } - - @ParameterizedTest - @ValueSource(booleans = { true, false }) - void sendSpansShouldSendSpansToZipkin(boolean async) throws IOException, InterruptedException { + void sendShouldSendSpansToZipkinInProto3() throws IOException, InterruptedException { mockBackEnd.enqueue(new MockResponse()); List encodedSpans = List.of(toByteArray("span1"), toByteArray("span2")); - makeRequest(encodedSpans, async); + + try (BytesMessageSender sender = createSender(Encoding.PROTO3, Duration.ofSeconds(10))) { + sender.send(encodedSpans); + } + requestAssertions((request) -> { assertThat(request.getMethod()).isEqualTo("POST"); - assertThat(request.getHeader("Content-Type")).isEqualTo("application/json"); - assertThat(request.getBody().readUtf8()).isEqualTo("[span1,span2]"); + assertThat(request.getHeader("Content-Type")).isEqualTo("application/x-protobuf"); + assertThat(request.getBody().readUtf8()).isEqualTo("span1span2"); }); } - @ParameterizedTest - @ValueSource(booleans = { true, false }) - void sendSpansShouldHandleHttpFailures(boolean async) throws InterruptedException { - mockBackEnd.enqueue(new MockResponse().setResponseCode(500)); - if (async) { - CallbackResult callbackResult = makeAsyncRequest(Collections.emptyList()); - assertThat(callbackResult.success()).isFalse(); - assertThat(callbackResult.error()).isNotNull().hasMessageContaining("500 Internal Server Error"); - } - else { - assertThatException().isThrownBy(() -> makeSyncRequest(Collections.emptyList())) - .withMessageContaining("500 Internal Server Error"); + /** + * This tests that a dynamic {@linkplain HttpEndpointSupplier} updates are visible to + * {@link HttpSender#postSpans(URI, HttpHeaders, byte[])}. + */ + @Test + void sendUsesDynamicEndpoint() throws Exception { + mockBackEnd.enqueue(new MockResponse()); + mockBackEnd.enqueue(new MockResponse()); + + AtomicInteger suffix = new AtomicInteger(); + try (BytesMessageSender sender = createSender((e) -> new HttpEndpointSupplier() { + @Override + public String get() { + return ZIPKIN_URL + "/" + suffix.incrementAndGet(); + } + + @Override + public void close() { + } + }, Encoding.JSON, Duration.ofSeconds(10))) { + sender.send(Collections.emptyList()); + sender.send(Collections.emptyList()); } + + assertThat(mockBackEnd.takeRequest().getPath()).endsWith("/1"); + assertThat(mockBackEnd.takeRequest().getPath()).endsWith("/2"); + } + + @Test + void sendShouldHandleHttpFailures() throws InterruptedException { + mockBackEnd.enqueue(new MockResponse().setResponseCode(500)); + assertThatException().isThrownBy(() -> this.sender.send(Collections.emptyList())) + .withMessageContaining("500 Internal Server Error"); requestAssertions((request) -> assertThat(request.getMethod()).isEqualTo("POST")); } - @ParameterizedTest - @ValueSource(booleans = { true, false }) - void sendSpansShouldCompressData(boolean async) throws IOException, InterruptedException { + @Test + void sendShouldCompressData() throws IOException, InterruptedException { String uncompressed = "a".repeat(10000); // This is gzip compressed 10000 times 'a' byte[] compressed = Base64.getDecoder() .decode("H4sIAAAAAAAA/+3BMQ0AAAwDIKFLj/k3UR8NcA8AAAAAAAAAAAADUsAZfeASJwAA"); mockBackEnd.enqueue(new MockResponse()); - makeRequest(List.of(toByteArray(uncompressed)), async); + this.sender.send(List.of(toByteArray(uncompressed))); requestAssertions((request) -> { assertThat(request.getMethod()).isEqualTo("POST"); assertThat(request.getHeader("Content-Type")).isEqualTo("application/json"); @@ -153,19 +176,12 @@ void sendSpansShouldCompressData(boolean async) throws IOException, InterruptedE }); } - @ParameterizedTest - @ValueSource(booleans = { true, false }) - void shouldTimeout(boolean async) { - Sender sender = createSender(Duration.ofMillis(1)); - MockResponse response = new MockResponse().setResponseCode(200).setHeadersDelay(100, TimeUnit.MILLISECONDS); - mockBackEnd.enqueue(response); - if (async) { - CallbackResult callbackResult = makeAsyncRequest(sender, Collections.emptyList()); - assertThat(callbackResult.success()).isFalse(); - assertThat(callbackResult.error()).isInstanceOf(TimeoutException.class); - } - else { - assertThatException().isThrownBy(() -> makeSyncRequest(sender, Collections.emptyList())) + @Test + void shouldTimeout() throws IOException { + try (BytesMessageSender sender = createSender(Encoding.JSON, Duration.ofMillis(1))) { + MockResponse response = new MockResponse().setResponseCode(200).setHeadersDelay(100, TimeUnit.MILLISECONDS); + mockBackEnd.enqueue(response); + assertThatException().isThrownBy(() -> sender.send(Collections.emptyList())) .withCauseInstanceOf(TimeoutException.class); } } diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index b3244bf633bd..34734b532283 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -109,7 +109,14 @@ bom { ] } } - library("Brave", "5.17.1") { + library("Zipkin Reporter", "3.3.0") { + group("io.zipkin.reporter2") { + imports = [ + "zipkin-reporter-bom" + ] + } + } + library("Brave", "6.0.1") { group("io.zipkin.brave") { imports = [ "brave-bom" @@ -1096,7 +1103,7 @@ bom { ] } } - library("OpenTelemetry", "1.33.0") { + library("OpenTelemetry", "1.35.0") { group("io.opentelemetry") { imports = [ "opentelemetry-bom" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java index da27927f0704..a0e9f0f95cc0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java @@ -70,7 +70,7 @@ public final class DockerImageNames { private static final String REGISTRY_VERSION = "2.7.1"; - private static final String ZIPKIN_VERSION = "2.24.1"; + private static final String ZIPKIN_VERSION = "3.0.6"; private DockerImageNames() { } From 7555f6c71ec84984d31f62a001d24afb4152f3c8 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 20 Feb 2024 12:12:39 +0100 Subject: [PATCH 2/2] Polish "Migrate to Brave 6 and Zipkin Reporter 3" See gh-39049 --- .../zipkin/ZipkinAutoConfiguration.java | 21 ++++++++++++++----- .../tracing/zipkin/ZipkinConfigurations.java | 7 ------- .../tracing/zipkin/ZipkinProperties.java | 18 ++++++++++++++-- .../zipkin/ZipkinAutoConfigurationTests.java | 3 ++- ...ationsOpenTelemetryConfigurationTests.java | 2 ++ .../spring-boot-dependencies/build.gradle | 2 +- 6 files changed, 37 insertions(+), 16 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinAutoConfiguration.java index 6c70fc0ebada..ebb1e4c7818a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -16,8 +16,10 @@ package org.springframework.boot.actuate.autoconfigure.tracing.zipkin; -import zipkin2.reporter.BytesMessageSender; +import zipkin2.Span; +import zipkin2.reporter.BytesEncoder; import zipkin2.reporter.Encoding; +import zipkin2.reporter.SpanBytesEncoder; import org.springframework.boot.actuate.autoconfigure.tracing.zipkin.ZipkinConfigurations.BraveConfiguration; import org.springframework.boot.actuate.autoconfigure.tracing.zipkin.ZipkinConfigurations.OpenTelemetryConfiguration; @@ -41,7 +43,7 @@ * @since 3.0.0 */ @AutoConfiguration(after = RestTemplateAutoConfiguration.class) -@ConditionalOnClass(BytesMessageSender.class) +@ConditionalOnClass(Encoding.class) @Import({ SenderConfiguration.class, BraveConfiguration.class, OpenTelemetryConfiguration.class }) @EnableConfigurationProperties(ZipkinProperties.class) public class ZipkinAutoConfiguration { @@ -54,8 +56,17 @@ PropertiesZipkinConnectionDetails zipkinConnectionDetails(ZipkinProperties prope @Bean @ConditionalOnMissingBean - public Encoding encoding(ZipkinProperties properties) { - return properties.getEncoding(); + Encoding encoding(ZipkinProperties properties) { + return switch (properties.getEncoding()) { + case JSON -> Encoding.JSON; + case PROTO3 -> Encoding.PROTO3; + }; + } + + @Bean + @ConditionalOnMissingBean(value = Span.class, parameterizedContainer = BytesEncoder.class) + BytesEncoder zipkinSpanEncoder(Encoding encoding) { + return SpanBytesEncoder.forEncoding(encoding); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurations.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurations.java index dc7d93cd2644..f19e9d96418b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurations.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurations.java @@ -26,7 +26,6 @@ import zipkin2.reporter.Encoding; import zipkin2.reporter.HttpEndpointSupplier; import zipkin2.reporter.HttpEndpointSuppliers; -import zipkin2.reporter.SpanBytesEncoder; import zipkin2.reporter.brave.AsyncZipkinSpanHandler; import zipkin2.reporter.brave.MutableSpanBytesEncoder; import zipkin2.reporter.urlconnection.URLConnectionSender; @@ -170,12 +169,6 @@ AsyncZipkinSpanHandler asyncZipkinSpanHandler(BytesMessageSender sender, @ConditionalOnClass(ZipkinSpanExporter.class) static class OpenTelemetryConfiguration { - @Bean - @ConditionalOnMissingBean(value = Span.class, parameterizedContainer = BytesEncoder.class) - BytesEncoder zipkinSpanEncoder(Encoding encoding) { - return SpanBytesEncoder.forEncoding(encoding); - } - @Bean @ConditionalOnMissingBean @ConditionalOnBean(BytesMessageSender.class) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinProperties.java index 4642e88c8402..4b8f2698afc9 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinProperties.java @@ -18,8 +18,6 @@ import java.time.Duration; -import zipkin2.reporter.Encoding; - import org.springframework.boot.context.properties.ConfigurationProperties; /** @@ -83,4 +81,20 @@ public void setReadTimeout(Duration readTimeout) { this.readTimeout = readTimeout; } + /** + * Zipkin message encoding. + */ + public enum Encoding { + + /** + * JSON. + */ + JSON, + /** + * Protocol Buffers v3. + */ + PROTO3 + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinAutoConfigurationTests.java index e70d9f308cd2..e0fac436583c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinAutoConfigurationTests.java @@ -40,7 +40,8 @@ class ZipkinAutoConfigurationTests { @Test void shouldSupplyBeans() { this.contextRunner.run((context) -> assertThat(context).hasSingleBean(Encoding.class) - .hasSingleBean(PropertiesZipkinConnectionDetails.class)); + .hasSingleBean(PropertiesZipkinConnectionDetails.class) + .hasBean("zipkinSpanEncoder")); } @Test diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurationsOpenTelemetryConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurationsOpenTelemetryConfigurationTests.java index a32a49ffb6fb..92f241c966f4 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurationsOpenTelemetryConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurationsOpenTelemetryConfigurationTests.java @@ -31,6 +31,7 @@ import org.springframework.context.annotation.Configuration; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * Tests for {@link OpenTelemetryConfiguration}. @@ -48,6 +49,7 @@ class ZipkinConfigurationsOpenTelemetryConfigurationTests { @Test void shouldSupplyBeans() { this.contextRunner.withUserConfiguration(SenderConfiguration.class) + .withBean(BytesEncoder.class, () -> mock(BytesEncoder.class)) .run((context) -> assertThat(context).hasSingleBean(ZipkinSpanExporter.class)); } diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 34734b532283..b1f3bc7f1295 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -112,7 +112,7 @@ bom { library("Zipkin Reporter", "3.3.0") { group("io.zipkin.reporter2") { imports = [ - "zipkin-reporter-bom" + "zipkin-reporter-bom" ] } }