From 10ff66372ea2bc9ed1be16058480c6ce7b525ac7 Mon Sep 17 00:00:00 2001 From: Maziz Date: Sat, 27 Feb 2021 17:16:46 +0800 Subject: [PATCH] Implement mechanism to apply custom api path (#1859) * Implement mechanism to apply custom api path to the sleuth server baseUrl. This comes in handy when another monitoring tool is being used, for instance new relic, which doesn't not require path in its tracing api. * Fix up documentation on the ZipkinProperties * Update the main documentation for api path * Add missing comma --- .../main/asciidoc/spring-cloud-sleuth.adoc | 7 +++ .../sleuth/zipkin2/ZipkinProperties.java | 15 ++++++ .../zipkin2/sender/RestTemplateSender.java | 22 ++++++-- ...ZipkinRestTemplateSenderConfiguration.java | 2 +- .../zipkin2/ZipkinAutoConfigurationTests.java | 54 +++++++++++++++++++ .../sender/RestTemplateSenderTest.java | 25 ++++++++- 6 files changed, 118 insertions(+), 7 deletions(-) diff --git a/docs/src/main/asciidoc/spring-cloud-sleuth.adoc b/docs/src/main/asciidoc/spring-cloud-sleuth.adoc index b6698b3c6..006d39466 100644 --- a/docs/src/main/asciidoc/spring-cloud-sleuth.adoc +++ b/docs/src/main/asciidoc/spring-cloud-sleuth.adoc @@ -1050,6 +1050,13 @@ If you want to find Zipkin through service discovery, you can pass the Zipkin's spring.zipkin.baseUrl: https://zipkinserver/ ---- +By default, api path will be set to `api/v2/spans` or `api/v1/spans` depending on the encoder version. If you want to use a custom api path, you can configure it using the following property (empty case, set ""): + +[source,yaml] +---- +spring.zipkin.apiPath: v2/path2 +---- + To disable this feature just set `spring.zipkin.discoveryClientEnabled` to `false. When the Discovery Client feature is enabled, Sleuth uses diff --git a/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/ZipkinProperties.java b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/ZipkinProperties.java index de1faccbe..14d150819 100644 --- a/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/ZipkinProperties.java +++ b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/ZipkinProperties.java @@ -37,6 +37,13 @@ public class ZipkinProperties { */ private String baseUrl = "http://localhost:9411/"; + /** + * The API path to append to baseUrl (above) as suffix. This applies if you use other + * monitoring tools, such as New Relic. The trace API doesn't need the API path, so + * you can set it to blank ("") in the configuration. + */ + private String apiPath = null; + /** * If set to {@code false}, will treat the {@link ZipkinProperties#baseUrl} as a URL * always. @@ -84,6 +91,14 @@ public class ZipkinProperties { this.baseUrl = baseUrl; } + public String getApiPath() { + return this.apiPath; + } + + public void setApiPath(String apiPath) { + this.apiPath = apiPath; + } + public boolean isEnabled() { return this.enabled; } diff --git a/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/sender/RestTemplateSender.java b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/sender/RestTemplateSender.java index 73f7fd1ea..9495af4b7 100644 --- a/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/sender/RestTemplateSender.java +++ b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/sender/RestTemplateSender.java @@ -19,6 +19,7 @@ package org.springframework.cloud.sleuth.zipkin2.sender; import java.io.IOException; import java.net.URI; import java.util.List; +import java.util.Objects; import zipkin2.Call; import zipkin2.Callback; @@ -54,21 +55,24 @@ final class RestTemplateSender extends Sender { */ transient boolean closeCalled; - RestTemplateSender(RestTemplate restTemplate, String baseUrl, + RestTemplateSender(RestTemplate restTemplate, String baseUrl, String apiPath, BytesEncoder encoder) { this.restTemplate = restTemplate; this.encoding = encoder.encoding(); if (encoder.equals(JSON_V2)) { this.mediaType = MediaType.APPLICATION_JSON; - this.url = baseUrl + (baseUrl.endsWith("/") ? "" : "/") + "api/v2/spans"; + this.url = buildUrlWithCustomPathIfNecessary(baseUrl, apiPath, + baseUrl + (baseUrl.endsWith("/") ? "" : "/") + "api/v2/spans"); } else if (this.encoding == Encoding.PROTO3) { this.mediaType = MediaType.parseMediaType("application/x-protobuf"); - this.url = baseUrl + (baseUrl.endsWith("/") ? "" : "/") + "api/v2/spans"; + this.url = buildUrlWithCustomPathIfNecessary(baseUrl, apiPath, + baseUrl + (baseUrl.endsWith("/") ? "" : "/") + "api/v2/spans"); } else if (this.encoding == Encoding.JSON) { this.mediaType = MediaType.APPLICATION_JSON; - this.url = baseUrl + (baseUrl.endsWith("/") ? "" : "/") + "api/v1/spans"; + this.url = buildUrlWithCustomPathIfNecessary(baseUrl, apiPath, + baseUrl + (baseUrl.endsWith("/") ? "" : "/") + "api/v1/spans"); } else { throw new UnsupportedOperationException( @@ -77,6 +81,16 @@ final class RestTemplateSender extends Sender { this.messageEncoder = BytesMessageEncoder.forEncoding(this.encoding); } + private String buildUrlWithCustomPathIfNecessary(final String baseUrl, + final String customApiPath, final String defaultUrl) { + if (Objects.nonNull(customApiPath)) { + return baseUrl + (baseUrl.endsWith("/") || customApiPath.startsWith("/") + || customApiPath.isEmpty() ? "" : "/") + customApiPath; + } + + return defaultUrl; + } + @Override public Encoding encoding() { return this.encoding; diff --git a/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/sender/ZipkinRestTemplateSenderConfiguration.java b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/sender/ZipkinRestTemplateSenderConfiguration.java index 13bf4afc9..bb1d3b207 100644 --- a/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/sender/ZipkinRestTemplateSenderConfiguration.java +++ b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/sender/ZipkinRestTemplateSenderConfiguration.java @@ -64,7 +64,7 @@ class ZipkinRestTemplateSenderConfiguration { RestTemplate restTemplate = new ZipkinRestTemplateWrapper(zipkin, this.extractor); restTemplate = zipkinRestTemplateCustomizer.customizeTemplate(restTemplate); return new RestTemplateSender(restTemplate, zipkin.getBaseUrl(), - zipkin.getEncoder()); + zipkin.getApiPath(), zipkin.getEncoder()); } @Bean diff --git a/spring-cloud-sleuth-zipkin/src/test/java/org/springframework/cloud/sleuth/zipkin2/ZipkinAutoConfigurationTests.java b/spring-cloud-sleuth-zipkin/src/test/java/org/springframework/cloud/sleuth/zipkin2/ZipkinAutoConfigurationTests.java index 5c9220223..f0b7965d6 100644 --- a/spring-cloud-sleuth-zipkin/src/test/java/org/springframework/cloud/sleuth/zipkin2/ZipkinAutoConfigurationTests.java +++ b/spring-cloud-sleuth-zipkin/src/test/java/org/springframework/cloud/sleuth/zipkin2/ZipkinAutoConfigurationTests.java @@ -106,6 +106,60 @@ public class ZipkinAutoConfigurationTests { then(request.getBody().readUtf8()).contains("localEndpoint"); } + @Test + public void useCustomApiPathIfSetEmpty() throws InterruptedException { + this.context = new AnnotationConfigApplicationContext(); + + environment().setProperty("spring.zipkin.base-url", + this.server.url("/").toString()); + environment().setProperty("spring.zipkin.api-path", ""); + + this.context.register(ZipkinAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class, TraceAutoConfiguration.class, + Config.class, ZipkinBackwardsCompatibilityAutoConfiguration.class); + this.context.refresh(); + Span span = this.context.getBean(Tracing.class).tracer().nextSpan().name("foo") + .tag("foo", "bar").start(); + + span.finish(); + + Awaitility.await().untilAsserted( + () -> then(this.server.getRequestCount()).isGreaterThan(1)); + // first request is for health check + this.server.takeRequest(); + // second request is the span one + RecordedRequest request = this.server.takeRequest(); + then(request.getPath()).isEqualTo("/"); + } + + @Test + public void useCustomApiPathIfSetNonEmpty() throws InterruptedException { + final String testPath = "test/v2"; + + this.context = new AnnotationConfigApplicationContext(); + + environment().setProperty("spring.zipkin.base-url", + this.server.url("").toString()); + environment().setProperty("spring.zipkin.api-path", testPath); + + this.context.register(ZipkinAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class, TraceAutoConfiguration.class, + Config.class, ZipkinBackwardsCompatibilityAutoConfiguration.class); + this.context.refresh(); + Span span = this.context.getBean(Tracing.class).tracer().nextSpan().name("foo") + .tag("foo", "bar").start(); + + span.finish(); + + Awaitility.await().untilAsserted( + () -> then(this.server.getRequestCount()).isGreaterThan(1)); + // first request is for health check + this.server.takeRequest(); + // second request is the span one + RecordedRequest request = this.server.takeRequest(); + then(request.getPath()).isEqualTo("/" + testPath); + } + private MockEnvironment environment() { this.context.setEnvironment(this.environment); return this.environment; diff --git a/spring-cloud-sleuth-zipkin/src/test/java/org/springframework/cloud/sleuth/zipkin2/sender/RestTemplateSenderTest.java b/spring-cloud-sleuth-zipkin/src/test/java/org/springframework/cloud/sleuth/zipkin2/sender/RestTemplateSenderTest.java index 7a0ae3cbe..d627a8449 100644 --- a/spring-cloud-sleuth-zipkin/src/test/java/org/springframework/cloud/sleuth/zipkin2/sender/RestTemplateSenderTest.java +++ b/spring-cloud-sleuth-zipkin/src/test/java/org/springframework/cloud/sleuth/zipkin2/sender/RestTemplateSenderTest.java @@ -54,7 +54,7 @@ public class RestTemplateSenderTest { String baseUrl = "http://localhost:" + this.server.getPort(); RestTemplateSender sender = new RestTemplateSender(new RestTemplate(), this.baseUrl, - JSON_V2); + null, JSON_V2); /** * Tests that json is not manipulated as a side-effect of using rest template. @@ -73,7 +73,8 @@ public class RestTemplateSenderTest { @Test public void proto3() throws Exception { this.server.enqueue(new MockResponse()); - this.sender = new RestTemplateSender(new RestTemplate(), this.baseUrl, PROTO3); + this.sender = new RestTemplateSender(new RestTemplate(), this.baseUrl, "", + PROTO3); send(SPAN).execute(); @@ -85,6 +86,26 @@ public class RestTemplateSenderTest { .containsExactly(SpanBytesEncoder.PROTO3.encode(SPAN)); } + @Test + public void testWhereApiIsSetNonEmpty() { + final String mockedApiPath = "/test/v2"; + final RestTemplateSender senderWithMockedApiPath = new RestTemplateSender( + new RestTemplate(), this.baseUrl, mockedApiPath, JSON_V2); + + assertThat(senderWithMockedApiPath.toString()) + .isEqualTo("RestTemplateSender{" + baseUrl + mockedApiPath + "}"); + } + + @Test + public void testWhereApiIsSetToEmpty() { + final String mockedApiPath = ""; + final RestTemplateSender senderWithMockedApiPath = new RestTemplateSender( + new RestTemplate(), this.baseUrl, mockedApiPath, JSON_V2); + + assertThat(senderWithMockedApiPath.toString()) + .isEqualTo("RestTemplateSender{" + baseUrl + "}"); + } + /** * The output of toString() on {@link Sender} implementations appears in thread names * created by {@link AsyncZipkinSpanHandler}. Since thread names are likely to be