From 013e148526fd3b9fb5e44efa37c5dc9cc1d2715b Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 20 Feb 2024 14:21:44 +0100 Subject: [PATCH] Implement JDK HttpClient based Zipkin sender Closes gh-39545 --- .../tracing/zipkin/HttpSender.java | 4 +- .../tracing/zipkin/ZipkinConfigurations.java | 33 ++- .../ZipkinHttpClientBuilderCustomizer.java | 37 +++ .../zipkin/ZipkinHttpClientSender.java | 71 ++++++ .../ZipkinRestTemplateBuilderCustomizer.java | 5 +- .../zipkin/ZipkinRestTemplateSender.java | 3 +- .../ZipkinWebClientBuilderCustomizer.java | 5 +- .../tracing/zipkin/ZipkinWebClientSender.java | 3 +- ...onfigurationsSenderConfigurationTests.java | 25 +++ .../zipkin/ZipkinHttpClientSenderTests.java | 210 ++++++++++++++++++ .../zipkin/ZipkinRestTemplateSenderTests.java | 3 +- .../zipkin/ZipkinWebClientSenderTests.java | 3 +- 12 files changed, 392 insertions(+), 10 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinHttpClientBuilderCustomizer.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinHttpClientSender.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinHttpClientSenderTests.java 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 b3446a64393..91ddc66b277 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 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. @@ -74,7 +74,7 @@ abstract class HttpSender extends BaseHttpSender { * @param headers headers for the POST request * @param body list of possibly gzipped, encoded spans. */ - abstract void postSpans(URI endpoint, HttpHeaders headers, byte[] body); + abstract void postSpans(URI endpoint, HttpHeaders headers, byte[] body) throws IOException; HttpHeaders getDefaultHeaders() { HttpHeaders headers = new HttpHeaders(); 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 f19e9d96418..57db8042b2f 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 @@ -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,6 +16,9 @@ package org.springframework.boot.actuate.autoconfigure.tracing.zipkin; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Builder; + import brave.Tag; import brave.Tags; import brave.handler.MutableSpan; @@ -53,7 +56,7 @@ class ZipkinConfigurations { @Configuration(proxyBeanMethods = false) @Import({ UrlConnectionSenderConfiguration.class, WebClientSenderConfiguration.class, - RestTemplateSenderConfiguration.class }) + RestTemplateSenderConfiguration.class, HttpClientSenderConfiguration.class }) static class SenderConfiguration { } @@ -90,6 +93,7 @@ class ZipkinConfigurations { @Bean @ConditionalOnMissingBean(BytesMessageSender.class) + @SuppressWarnings("removal") ZipkinRestTemplateSender restTemplateSender(ZipkinProperties properties, Encoding encoding, ObjectProvider customizers, ObjectProvider connectionDetailsProvider, @@ -106,6 +110,7 @@ class ZipkinConfigurations { restTemplateBuilder.build()); } + @SuppressWarnings("removal") private RestTemplateBuilder applyCustomizers(RestTemplateBuilder restTemplateBuilder, ObjectProvider customizers) { Iterable orderedCustomizers = () -> customizers.orderedStream() @@ -126,6 +131,7 @@ class ZipkinConfigurations { @Bean @ConditionalOnMissingBean(BytesMessageSender.class) + @SuppressWarnings("removal") ZipkinWebClientSender webClientSender(ZipkinProperties properties, Encoding encoding, ObjectProvider customizers, ObjectProvider connectionDetailsProvider, @@ -142,6 +148,29 @@ class ZipkinConfigurations { } + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(HttpClient.class) + @EnableConfigurationProperties(ZipkinProperties.class) + static class HttpClientSenderConfiguration { + + @Bean + @ConditionalOnMissingBean(BytesMessageSender.class) + ZipkinHttpClientSender httpClientSender(ZipkinProperties properties, Encoding encoding, + ObjectProvider customizers, + ObjectProvider connectionDetailsProvider, + ObjectProvider endpointSupplierFactoryProvider) { + ZipkinConnectionDetails connectionDetails = connectionDetailsProvider + .getIfAvailable(() -> new PropertiesZipkinConnectionDetails(properties)); + HttpEndpointSupplier.Factory endpointSupplierFactory = endpointSupplierFactoryProvider + .getIfAvailable(HttpEndpointSuppliers::constantFactory); + Builder httpClientBuilder = HttpClient.newBuilder().connectTimeout(properties.getConnectTimeout()); + customizers.orderedStream().forEach((customizer) -> customizer.customize(httpClientBuilder)); + return new ZipkinHttpClientSender(encoding, endpointSupplierFactory, connectionDetails.getSpanEndpoint(), + httpClientBuilder.build(), properties.getReadTimeout()); + } + + } + @Configuration(proxyBeanMethods = false) @ConditionalOnClass(AsyncZipkinSpanHandler.class) static class BraveConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinHttpClientBuilderCustomizer.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinHttpClientBuilderCustomizer.java new file mode 100644 index 00000000000..73ed2e46f17 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinHttpClientBuilderCustomizer.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 java.net.http.HttpClient; + +/** + * Callback interface that can be implemented by beans wishing to customize the + * {@link HttpClient.Builder} used to send spans to Zipkin. + * + * @author Moritz Halbritter + * @since 3.3.0 + */ +@FunctionalInterface +public interface ZipkinHttpClientBuilderCustomizer { + + /** + * Customize the http client builder. + * @param httpClient the http client builder to customize + */ + void customize(HttpClient.Builder httpClient); + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinHttpClientSender.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinHttpClientSender.java new file mode 100644 index 00000000000..c1cc60631d6 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinHttpClientSender.java @@ -0,0 +1,71 @@ +/* + * 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 java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpRequest.Builder; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.time.Duration; + +import zipkin2.reporter.Encoding; +import zipkin2.reporter.HttpEndpointSupplier.Factory; + +import org.springframework.http.HttpHeaders; + +/** + * A {@link HttpSender} which uses the JDK {@link HttpClient} for HTTP communication. + * + * @author Moritz Halbritter + */ +class ZipkinHttpClientSender extends HttpSender { + + private final HttpClient httpClient; + + private final Duration readTimeout; + + ZipkinHttpClientSender(Encoding encoding, Factory endpointSupplierFactory, String endpoint, HttpClient httpClient, + Duration readTimeout) { + super(encoding, endpointSupplierFactory, endpoint); + this.httpClient = httpClient; + this.readTimeout = readTimeout; + } + + @Override + void postSpans(URI endpoint, HttpHeaders headers, byte[] body) throws IOException { + Builder request = HttpRequest.newBuilder() + .POST(BodyPublishers.ofByteArray(body)) + .uri(endpoint) + .timeout(this.readTimeout); + headers.forEach((key, values) -> values.forEach((value) -> request.header(key, value))); + try { + HttpResponse response = this.httpClient.send(request.build(), BodyHandlers.discarding()); + if (response.statusCode() / 100 != 2) { + throw new IOException("Expected HTTP status 2xx, got %d".formatted(response.statusCode())); + } + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new IOException("Got interrupted while sending spans", ex); + } + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinRestTemplateBuilderCustomizer.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinRestTemplateBuilderCustomizer.java index eef1ec385b9..d0760023827 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinRestTemplateBuilderCustomizer.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinRestTemplateBuilderCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 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. @@ -24,8 +24,11 @@ import org.springframework.boot.web.client.RestTemplateBuilder; * * @author Marcin Grzejszczak * @since 3.0.0 + * @deprecated since 3.3.0 for removal in 3.5.0 in favor of + * {@link ZipkinHttpClientBuilderCustomizer} */ @FunctionalInterface +@Deprecated(since = "3.3.0", forRemoval = true) public interface ZipkinRestTemplateBuilderCustomizer { /** 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 1e9c6eaf905..69246a7be19 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 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. @@ -32,6 +32,7 @@ import org.springframework.web.client.RestTemplate; * @author Moritz Halbritter * @author Stefan Bratanov */ +@Deprecated(since = "3.3.0", forRemoval = true) class ZipkinRestTemplateSender extends HttpSender { private final RestTemplate restTemplate; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinWebClientBuilderCustomizer.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinWebClientBuilderCustomizer.java index bd6be6e9332..20631577214 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinWebClientBuilderCustomizer.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinWebClientBuilderCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 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. @@ -25,8 +25,11 @@ import org.springframework.web.reactive.function.client.WebClient.Builder; * * @author Marcin Grzejszczak * @since 3.0.0 + * @deprecated since 3.3.0 for removal in 3.5.0 in favor of + * {@link ZipkinHttpClientBuilderCustomizer} */ @FunctionalInterface +@Deprecated(since = "3.3.0", forRemoval = true) public interface ZipkinWebClientBuilderCustomizer { /** 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 dea64c28557..9c3ffe38879 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 @@ -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. @@ -31,6 +31,7 @@ import org.springframework.web.reactive.function.client.WebClient; * @author Stefan Bratanov * @author Moritz Halbritter */ +@Deprecated(since = "3.3.0", forRemoval = true) class ZipkinWebClientSender extends HttpSender { private final WebClient webClient; 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 3c8ab9537d8..5e7d7340b5b 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 @@ -50,6 +50,7 @@ import static org.mockito.Mockito.mock; * * @author Moritz Halbritter */ +@SuppressWarnings("removal") class ZipkinConfigurationsSenderConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() @@ -70,6 +71,20 @@ class ZipkinConfigurationsSenderConfigurationTests { }); } + @Test + void shouldUseHttpClientIfUrlSenderIsNotAvailable() { + this.contextRunner.withUserConfiguration(HttpClientConfiguration.class) + .withClassLoader(new FilteredClassLoader("zipkin2.reporter.urlconnection", "org.springframework.web.client", + "org.springframework.web.reactive.function.client")) + .run((context) -> { + assertThat(context).doesNotHaveBean(URLConnectionSender.class); + assertThat(context).hasSingleBean(BytesMessageSender.class); + assertThat(context).hasSingleBean(ZipkinHttpClientSender.class); + then(context.getBean(ZipkinHttpClientBuilderCustomizer.class)).should() + .customize(ArgumentMatchers.any()); + }); + } + @Test void shouldPreferWebClientSenderIfWebApplicationIsReactiveAndUrlSenderIsNotAvailable() { this.reactiveContextRunner.withUserConfiguration(RestTemplateConfiguration.class, WebClientConfiguration.class) @@ -220,6 +235,16 @@ class ZipkinConfigurationsSenderConfigurationTests { } + @Configuration(proxyBeanMethods = false) + private static final class HttpClientConfiguration { + + @Bean + ZipkinHttpClientBuilderCustomizer httpClientBuilderCustomizer() { + return mock(ZipkinHttpClientBuilderCustomizer.class); + } + + } + @Configuration(proxyBeanMethods = false) private static final class CustomConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinHttpClientSenderTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinHttpClientSenderTests.java new file mode 100644 index 00000000000..8a1f52b0c5c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinHttpClientSenderTests.java @@ -0,0 +1,210 @@ +/* + * 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 java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +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.atomic.AtomicInteger; +import java.util.function.Consumer; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.QueueDispatcher; +import okhttp3.mockwebserver.RecordedRequest; +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 zipkin2.reporter.BytesMessageSender; +import zipkin2.reporter.Encoding; +import zipkin2.reporter.HttpEndpointSupplier; +import zipkin2.reporter.HttpEndpointSuppliers; + +import org.springframework.http.HttpHeaders; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatException; +import static org.assertj.core.api.Assertions.assertThatIOException; + +/** + * Tests for {@link ZipkinHttpClientSender}. + * + * @author Moritz Halbritter + */ +class ZipkinHttpClientSenderTests extends ZipkinHttpSenderTests { + + private static ClearableDispatcher dispatcher; + + private static MockWebServer mockBackEnd; + + private static String zipkinUrl; + + @BeforeAll + static void beforeAll() throws IOException { + dispatcher = new ClearableDispatcher(); + mockBackEnd = new MockWebServer(); + mockBackEnd.setDispatcher(dispatcher); + mockBackEnd.start(); + zipkinUrl = mockBackEnd.url("/api/v2/spans").toString(); + } + + @AfterAll + static void afterAll() throws IOException { + mockBackEnd.shutdown(); + } + + @Override + @BeforeEach + void beforeEach() throws Exception { + super.beforeEach(); + clearResponses(); + clearRequests(); + } + + @Override + BytesMessageSender createSender() { + return createSender(Encoding.JSON, Duration.ofSeconds(10)); + } + + ZipkinHttpClientSender createSender(Encoding encoding, Duration timeout) { + return createSender(HttpEndpointSuppliers.constantFactory(), encoding, timeout); + } + + ZipkinHttpClientSender createSender(HttpEndpointSupplier.Factory endpointSupplierFactory, Encoding encoding, + Duration timeout) { + HttpClient httpClient = HttpClient.newBuilder().connectTimeout(timeout).build(); + return new ZipkinHttpClientSender(encoding, endpointSupplierFactory, zipkinUrl, httpClient, timeout); + } + + @Test + void sendShouldSendSpansToZipkin() throws IOException, InterruptedException { + mockBackEnd.enqueue(new MockResponse()); + List encodedSpans = List.of(toByteArray("span1"), toByteArray("span2")); + this.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]"); + }); + } + + @Test + void sendShouldSendSpansToZipkinInProto3() throws IOException, InterruptedException { + mockBackEnd.enqueue(new MockResponse()); + List 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 zipkinUrl + "/" + 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("Expected HTTP status 2xx, got 500"); + requestAssertions((request) -> assertThat(request.getMethod()).isEqualTo("POST")); + } + + @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()); + this.sender.send(List.of(toByteArray(uncompressed))); + requestAssertions((request) -> { + assertThat(request.getMethod()).isEqualTo("POST"); + assertThat(request.getHeader("Content-Type")).isEqualTo("application/json"); + assertThat(request.getHeader("Content-Encoding")).isEqualTo("gzip"); + assertThat(request.getBody().readByteArray()).isEqualTo(compressed); + }); + } + + @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); + assertThatIOException().isThrownBy(() -> sender.send(Collections.emptyList())) + .withMessageContaining("timed out"); + } + } + + private void requestAssertions(Consumer assertions) throws InterruptedException { + RecordedRequest request = mockBackEnd.takeRequest(); + assertThat(request).satisfies(assertions); + } + + private static void clearRequests() throws InterruptedException { + RecordedRequest request; + do { + request = mockBackEnd.takeRequest(0, TimeUnit.SECONDS); + } + while (request != null); + } + + private static void clearResponses() { + dispatcher.clear(); + } + + private static final class ClearableDispatcher extends QueueDispatcher { + + void clear() { + getResponseQueue().clear(); + } + + } + +} 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 a825b530c6c..dff4fab42eb 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 @@ -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. @@ -49,6 +49,7 @@ import static org.springframework.test.web.client.response.MockRestResponseCreat * @author Moritz Halbritter * @author Stefan Bratanov */ +@SuppressWarnings("removal") class ZipkinRestTemplateSenderTests extends ZipkinHttpSenderTests { private static final String ZIPKIN_URL = "http://localhost:9411/api/v2/spans"; 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 f42eeddda19..07028484b04 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 @@ -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. @@ -51,6 +51,7 @@ import static org.assertj.core.api.Assertions.assertThatException; * * @author Stefan Bratanov */ +@SuppressWarnings("removal") class ZipkinWebClientSenderTests extends ZipkinHttpSenderTests { private static ClearableDispatcher dispatcher;