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