Migrate to Brave 6 and Zipkin Reporter 3

Signed-off-by: Adrian Cole <adrian@tetrate.io>

See gh-39049
This commit is contained in:
Adrian Cole 2024-02-10 17:43:23 +07:00 committed by Moritz Halbritter
parent 3a565e4e4b
commit 4b0bed23b0
23 changed files with 647 additions and 605 deletions

View File

@ -30,7 +30,6 @@ import brave.context.slf4j.MDCScopeDecorator;
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 @@ class BravePropagationConfigurations {
return builder;
}
@SuppressWarnings("deprecation")
private Factory createThrowAwayFactory() {
return new Factory() {
@Override
public <K> Propagation<K> create(KeyFactory<K> keyFactory) {
public Propagation<String> get() {
return null;
}

View File

@ -22,7 +22,6 @@ import java.util.List;
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 @@ class CompositePropagationFactory extends Propagation.Factory {
}
@Override
@SuppressWarnings("deprecation")
public <K> Propagation<K> create(Propagation.KeyFactory<K> keyFactory) {
return StringPropagationAdapter.create(this.propagation, keyFactory);
public Propagation<String> get() {
return this.propagation;
}
@Override

View File

@ -18,135 +18,81 @@ package org.springframework.boot.actuate.autoconfigure.tracing.zipkin;
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<URI, byte[]> {
private static final DataSize MESSAGE_MAX_SIZE = DataSize.ofKilobytes(512);
/**
* Only use gzip compression on data which is bigger than this in bytes.
*/
private static final DataSize COMPRESSION_THRESHOLD = DataSize.ofKilobytes(1);
private volatile boolean closed;
@Override
public Encoding encoding() {
return Encoding.JSON;
HttpSender(Encoding encoding, Factory endpointSupplierFactory, String endpoint) {
super(encoding, endpointSupplierFactory, endpoint);
}
@Override
public int messageMaxBytes() {
return (int) MESSAGE_MAX_SIZE.toBytes();
protected URI newEndpoint(String endpoint) {
return URI.create(endpoint);
}
@Override
public int messageSizeInBytes(List<byte[]> encodedSpans) {
return encoding().listSizeInBytes(encodedSpans);
protected byte[] newBody(List<byte[]> list) {
return this.encoding.encode(list);
}
@Override
public int messageSizeInBytes(int encodedSizeInBytes) {
return encoding().listSizeInBytes(encodedSizeInBytes);
}
@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<Void> sendSpans(List<byte[]> 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<Void> {
private boolean needsCompression(byte[] body) {
return body.length > COMPRESSION_THRESHOLD.toBytes();
}
/**
* 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;
private byte[] compress(byte[] input) throws IOException {
ByteArrayOutputStream result = new ByteArrayOutputStream();
try (GZIPOutputStream gzip = new GZIPOutputStream(result)) {
gzip.write(input);
}
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 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();
}
return result.toByteArray();
}
}

View File

@ -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 @@ import org.springframework.context.annotation.Import;
* @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 @@ public class ZipkinAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public BytesEncoder<Span> spanBytesEncoder() {
return SpanBytesEncoder.JSON_V2;
public Encoding encoding(ZipkinProperties properties) {
return properties.getEncoding();
}
}

View File

@ -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 @@ class ZipkinConfigurations {
static class UrlConnectionSenderConfiguration {
@Bean
@ConditionalOnMissingBean(Sender.class)
URLConnectionSender urlConnectionSender(ZipkinProperties properties,
ObjectProvider<ZipkinConnectionDetails> connectionDetailsProvider) {
@ConditionalOnMissingBean(BytesMessageSender.class)
URLConnectionSender urlConnectionSender(ZipkinProperties properties, Encoding encoding,
ObjectProvider<ZipkinConnectionDetails> connectionDetailsProvider,
ObjectProvider<HttpEndpointSupplier.Factory> 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 @@ class ZipkinConfigurations {
static class RestTemplateSenderConfiguration {
@Bean
@ConditionalOnMissingBean(Sender.class)
ZipkinRestTemplateSender restTemplateSender(ZipkinProperties properties,
@ConditionalOnMissingBean(BytesMessageSender.class)
ZipkinRestTemplateSender restTemplateSender(ZipkinProperties properties, Encoding encoding,
ObjectProvider<ZipkinRestTemplateBuilderCustomizer> customizers,
ObjectProvider<ZipkinConnectionDetails> connectionDetailsProvider) {
ObjectProvider<ZipkinConnectionDetails> connectionDetailsProvider,
ObjectProvider<HttpEndpointSupplier.Factory> 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 @@ class ZipkinConfigurations {
static class WebClientSenderConfiguration {
@Bean
@ConditionalOnMissingBean(Sender.class)
ZipkinWebClientSender webClientSender(ZipkinProperties properties,
@ConditionalOnMissingBean(BytesMessageSender.class)
ZipkinWebClientSender webClientSender(ZipkinProperties properties, Encoding encoding,
ObjectProvider<ZipkinWebClientBuilderCustomizer> customizers,
ObjectProvider<ZipkinConnectionDetails> connectionDetailsProvider) {
ObjectProvider<ZipkinConnectionDetails> connectionDetailsProvider,
ObjectProvider<HttpEndpointSupplier.Factory> 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 {
@Bean
@ConditionalOnMissingBean(Reporter.class)
@ConditionalOnBean(Sender.class)
AsyncReporter<Span> spanReporter(Sender sender, BytesEncoder<Span> encoder) {
return AsyncReporter.builder(sender).build(encoder);
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ZipkinSpanHandler.class)
@ConditionalOnClass(AsyncZipkinSpanHandler.class)
static class BraveConfiguration {
@Bean
@ConditionalOnMissingBean(value = MutableSpan.class, parameterizedContainer = BytesEncoder.class)
BytesEncoder<MutableSpan> braveSpanEncoder(Encoding encoding,
ObjectProvider<Tag<Throwable>> throwableTagProvider) {
Tag<Throwable> throwableTag = throwableTagProvider.getIfAvailable(() -> Tags.ERROR);
return MutableSpanBytesEncoder.create(encoding, throwableTag);
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean(Reporter.class)
@ConditionalOnBean(BytesMessageSender.class)
@ConditionalOnEnabledTracing
ZipkinSpanHandler zipkinSpanHandler(Reporter<Span> spanReporter) {
return (ZipkinSpanHandler) ZipkinSpanHandler.newBuilder(spanReporter).build();
AsyncZipkinSpanHandler asyncZipkinSpanHandler(BytesMessageSender sender,
BytesEncoder<MutableSpan> braveSpanEncoder) {
return AsyncZipkinSpanHandler.newBuilder(sender).build(braveSpanEncoder);
}
}
@ -155,12 +170,18 @@ class ZipkinConfigurations {
@ConditionalOnClass(ZipkinSpanExporter.class)
static class OpenTelemetryConfiguration {
@Bean
@ConditionalOnMissingBean(value = Span.class, parameterizedContainer = BytesEncoder.class)
BytesEncoder<Span> zipkinSpanEncoder(Encoding encoding) {
return SpanBytesEncoder.forEncoding(encoding);
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean(Sender.class)
@ConditionalOnBean(BytesMessageSender.class)
@ConditionalOnEnabledTracing
ZipkinSpanExporter zipkinSpanExporter(BytesEncoder<Span> encoder, Sender sender) {
return ZipkinSpanExporter.builder().setEncoder(encoder).setSender(sender).build();
ZipkinSpanExporter zipkinSpanExporter(BytesMessageSender sender, BytesEncoder<Span> zipkinSpanEncoder) {
return ZipkinSpanExporter.builder().setSender(sender).setEncoder(zipkinSpanEncoder).build();
}
}

View File

@ -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.
*
* <p>
* 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
*/

View File

@ -18,6 +18,8 @@ package org.springframework.boot.actuate.autoconfigure.tracing.zipkin;
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 class ZipkinProperties {
this.endpoint = endpoint;
}
public Encoding getEncoding() {
return this.encoding;
}
public void setEncoding(Encoding encoding) {
this.encoding = encoding;
}
public Duration getConnectTimeout() {
return this.connectTimeout;
}

View File

@ -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 @@ import org.springframework.web.client.RestTemplate;
*/
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<Void> clone() {
return new RestTemplateHttpPostCall(this.endpoint, getUncompressedBody(), this.restTemplate);
}
@Override
protected Void doExecute() {
HttpEntity<byte[]> request = new HttpEntity<>(getBody(), getDefaultHeaders());
this.restTemplate.exchange(this.endpoint, HttpMethod.POST, request, Void.class);
return null;
}
@Override
protected void doEnqueue(Callback<Void> callback) {
try {
doExecute();
callback.onSuccess(null);
}
catch (Exception ex) {
callback.onError(ex);
}
}
void postSpans(URI endpoint, HttpHeaders headers, byte[] body) {
HttpEntity<byte[]> request = new HttpEntity<>(body, headers);
this.restTemplate.exchange(endpoint, HttpMethod.POST, request, Void.class);
}
}

View File

@ -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 @@ import org.springframework.web.reactive.function.client.WebClient;
*/
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<Void> clone() {
return new WebClientHttpPostCall(this.endpoint, getUncompressedBody(), this.webClient, this.timeout);
}
@Override
protected Void doExecute() {
sendRequest().block();
return null;
}
@Override
protected void doEnqueue(Callback<Void> callback) {
sendRequest().subscribe((entity) -> callback.onSuccess(null), callback::onError);
}
private Mono<ResponseEntity<Void>> 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();
}
}

View File

@ -21,7 +21,6 @@ import java.util.HashMap;
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 @@ class CompositePropagationFactoryTests {
}
@Override
@SuppressWarnings("deprecation")
public <K> Propagation<K> create(Propagation.KeyFactory<K> keyFactory) {
return StringPropagationAdapter.create(this, keyFactory);
public Propagation<String> get() {
return this;
}
@Override

View File

@ -22,7 +22,6 @@ import brave.baggage.BaggagePropagation.FactoryBuilder;
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 @@ class LocalBaggageFieldsTests {
assertThat(LocalBaggageFields.empty().asList()).isEmpty();
}
@SuppressWarnings("deprecation")
private static FactoryBuilder createBuilder() {
return BaggagePropagation.newFactoryBuilder(new Factory() {
@Override
public <K> Propagation<K> create(KeyFactory<K> keyFactory) {
public Propagation<String> get() {
return null;
}
});

View File

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

View File

@ -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 @@ class NoopSender extends Sender {
}
@Override
public int messageSizeInBytes(List<byte[]> encodedSpans) {
return encoding().listSizeInBytes(encodedSpans);
public void send(List<byte[]> encodedSpans) {
}
@Override
public Call<Void> sendSpans(List<byte[]> encodedSpans) {
return new Call.Base<>() {
@Override
public Call<Void> clone() {
return this;
}
@Override
protected Void doExecute() {
return null;
}
@Override
protected void doEnqueue(Callback<Void> callback) {
}
};
public void close() throws IOException {
}
}

View File

@ -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 @@ class ZipkinAutoConfigurationTests {
private static final class CustomConfiguration {
@Bean
BytesEncoder<Span> customBytesEncoder() {
return SpanBytesEncoder.JSON_V2;
Encoding customEncoding() {
return Encoding.PROTO3;
}
}

View File

@ -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 @@ import static org.mockito.Mockito.mock;
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 shouldNotSupplyZipkinSpanHandlerIfTracingIsDisabled() {
this.tracingDisabledContextRunner.withUserConfiguration(ReporterConfiguration.class)
.run((context) -> assertThat(context).doesNotHaveBean(ZipkinSpanHandler.class));
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 shouldUseDefaultThrowableTagBean() {
this.contextRunner.withUserConfiguration(SenderConfiguration.class).run((context) -> {
@SuppressWarnings("unchecked")
BytesEncoder<MutableSpan> 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<MutableSpan> 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<Span> reporter() {
return mock(Reporter.class);
BytesMessageSender sender(Encoding encoding) {
return new NoopSender(encoding);
}
}
@ -103,9 +174,23 @@ class ZipkinConfigurationsBraveConfigurationTests {
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<Throwable> throwableTag() {
return new Tag<Throwable>("exception") {
@Override
protected String parseValue(Throwable throwable, TraceContext traceContext) {
return throwable.getMessage();
}
};
}
}
@ -120,4 +205,38 @@ class ZipkinConfigurationsBraveConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
private static final class CustomEncodingConfiguration {
@Bean
Encoding encoding() {
return Encoding.PROTO3;
}
}
@Configuration(proxyBeanMethods = false)
private static final class CustomEncoderConfiguration {
@Bean
BytesEncoder<MutableSpan> encoder(Encoding encoding) {
return new CustomMutableSpanEncoder(encoding);
}
}
private record CustomMutableSpanEncoder(Encoding encoding) implements BytesEncoder<MutableSpan> {
@Override
public int sizeInBytes(MutableSpan span) {
throw new UnsupportedOperationException();
}
@Override
public byte[] encode(MutableSpan span) {
throw new UnsupportedOperationException();
}
}
}

View File

@ -19,13 +19,12 @@ package org.springframework.boot.actuate.autoconfigure.tracing.zipkin;
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 @@ import static org.assertj.core.api.Assertions.assertThat;
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 @@ class ZipkinConfigurationsOpenTelemetryConfigurationTests {
.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 @@ class ZipkinConfigurationsOpenTelemetryConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
private static final class BaseConfiguration {
private static final class CustomEncoderConfiguration {
@Bean
@ConditionalOnMissingBean
BytesEncoder<Span> spanBytesEncoder() {
return SpanBytesEncoder.JSON_V2;
BytesEncoder<Span> encoder(Encoding encoding) {
return new CustomSpanEncoder(encoding);
}
}
record CustomSpanEncoder(Encoding encoding) implements BytesEncoder<Span> {
@Override
public int sizeInBytes(Span span) {
throw new UnsupportedOperationException();
}
@Override
public byte[] encode(Span span) {
throw new UnsupportedOperationException();
}
}

View File

@ -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<Span> customReporter() {
return mock(Reporter.class);
}
}
@Configuration(proxyBeanMethods = false)
private static final class BaseConfiguration {
@Bean
@ConditionalOnMissingBean
BytesEncoder<Span> spanBytesEncoder() {
return SpanBytesEncoder.JSON_V2;
}
}
}

View File

@ -18,6 +18,7 @@ package org.springframework.boot.actuate.autoconfigure.tracing.zipkin;
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.MockWebServer;
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 @@ import static org.mockito.Mockito.mock;
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 @@ class ZipkinConfigurationsSenderConfigurationTests {
.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 @@ class ZipkinConfigurationsSenderConfigurationTests {
.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 @@ class ZipkinConfigurationsSenderConfigurationTests {
.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 @@ class ZipkinConfigurationsSenderConfigurationTests {
.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 @@ class ZipkinConfigurationsSenderConfigurationTests {
.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 @@ class ZipkinConfigurationsSenderConfigurationTests {
.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 @@ class ZipkinConfigurationsSenderConfigurationTests {
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 @@ class ZipkinConfigurationsSenderConfigurationTests {
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 @@ class ZipkinConfigurationsSenderConfigurationTests {
.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 @@ class ZipkinConfigurationsSenderConfigurationTests {
}
}
@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 @@ class ZipkinConfigurationsSenderConfigurationTests {
private static final class CustomConfiguration {
@Bean
Sender customSender() {
return mock(Sender.class);
BytesMessageSender customSender() {
return mock(BytesMessageSender.class);
}
}
@ -211,4 +239,35 @@ class ZipkinConfigurationsSenderConfigurationTests {
}
@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() {
}
}
}

View File

@ -18,21 +18,14 @@ package org.springframework.boot.actuate.autoconfigure.tracing.zipkin;
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 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
*/
abstract class ZipkinHttpSenderTests {
protected Sender sender;
protected BytesMessageSender sender;
abstract Sender createSender();
abstract BytesMessageSender createSender();
@BeforeEach
void beforeEach() throws Exception {
@ -58,55 +51,14 @@ abstract class ZipkinHttpSenderTests {
}
@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<byte[]> encodedSpans, boolean async) throws IOException {
if (async) {
CallbackResult callbackResult = makeAsyncRequest(encodedSpans);
assertThat(callbackResult.success()).isTrue();
}
else {
makeSyncRequest(encodedSpans);
}
}
protected CallbackResult makeAsyncRequest(List<byte[]> encodedSpans) {
return makeAsyncRequest(this.sender, encodedSpans);
}
protected CallbackResult makeAsyncRequest(Sender sender, List<byte[]> encodedSpans) {
AtomicReference<CallbackResult> 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<byte[]> encodedSpans) throws IOException {
makeSyncRequest(this.sender, encodedSpans);
}
protected void makeSyncRequest(Sender sender, List<byte[]> 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) {
}
}

View File

@ -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 @@ class ZipkinRestTemplateSenderTests extends ZipkinHttpSenderTests {
}
@Test
void checkShouldSendEmptySpanList() {
this.mockServer.expect(requestTo(ZIPKIN_URL))
.andExpect(method(HttpMethod.POST))
.andExpect(content().string("[]"))
.andRespond(withStatus(HttpStatus.ACCEPTED));
assertThat(this.sender.check()).isEqualTo(CheckResult.OK);
}
@Test
void checkShouldNotRaiseException() {
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");
}
@ParameterizedTest
@ValueSource(booleans = { true, false })
void sendSpansShouldSendSpansToZipkin(boolean async) throws IOException {
void sendShouldSendSpansToZipkin() 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.sender.send(List.of(toByteArray("span1"), toByteArray("span2")));
}
@ParameterizedTest
@ValueSource(booleans = { true, false })
void sendSpansShouldHandleHttpFailures(boolean async) {
@Test
void sendShouldSendSpansToZipkinInProto3() throws IOException {
this.mockServer.expect(requestTo(ZIPKIN_URL))
.andExpect(method(HttpMethod.POST))
.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")));
}
}
/**
* 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());
}
}
@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 @@ class ZipkinRestTemplateSenderTests extends ZipkinHttpSenderTests {
.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)));
}
}

View File

@ -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.AfterAll;
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,40 +82,25 @@ class ZipkinWebClientSenderTests extends ZipkinHttpSenderTests {
}
@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 {
mockBackEnd.enqueue(new MockResponse());
assertThat(this.sender.check()).isEqualTo(CheckResult.OK);
requestAssertions((request) -> {
assertThat(request.getMethod()).isEqualTo("POST");
assertThat(request.getBody().readUtf8()).isEqualTo("[]");
});
}
@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 sendShouldSendSpansToZipkin() throws IOException, InterruptedException {
mockBackEnd.enqueue(new MockResponse());
List<byte[]> encodedSpans = List.of(toByteArray("span1"), toByteArray("span2"));
makeRequest(encodedSpans, async);
this.sender.send(encodedSpans);
requestAssertions((request) -> {
assertThat(request.getMethod()).isEqualTo("POST");
assertThat(request.getHeader("Content-Type")).isEqualTo("application/json");
@ -120,31 +108,66 @@ class ZipkinWebClientSenderTests extends ZipkinHttpSenderTests {
});
}
@ParameterizedTest
@ValueSource(booleans = { true, false })
void sendSpansShouldHandleHttpFailures(boolean async) throws InterruptedException {
@Test
void sendShouldSendSpansToZipkinInProto3() throws IOException, InterruptedException {
mockBackEnd.enqueue(new MockResponse());
List<byte[]> encodedSpans = List.of(toByteArray("span1"), toByteArray("span2"));
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/x-protobuf");
assertThat(request.getBody().readUtf8()).isEqualTo("span1span2");
});
}
/**
* 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));
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");
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 @@ class ZipkinWebClientSenderTests extends ZipkinHttpSenderTests {
});
}
@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);
}
}

View File

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

View File

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