From 9641d4ef98bde030b40690a615c6f9368b14772f Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Tue, 28 Jun 2016 12:11:40 +0200 Subject: [PATCH] Possibility to customize HTTP Client for Zipkin calls * Added RestTemplate for Zipkin * Added ZipkinRestTemplateCustomizer to allow possibility to customize RestTemplate * Addded DefaultZipkinRestTemplateCustomizer with GZip compression interceptor --- spring-cloud-sleuth-zipkin/pom.xml | 4 ++ .../DefaultZipkinRestTemplateCustomizer.java | 64 +++++++++++++++++++ .../sleuth/zipkin/HttpZipkinSpanReporter.java | 55 +++++----------- .../zipkin/ZipkinAutoConfiguration.java | 25 ++++++-- .../zipkin/ZipkinRestTemplateCustomizer.java | 35 ++++++++++ .../zipkin/HttpZipkinSpanReporterTest.java | 28 ++++++-- 6 files changed, 162 insertions(+), 49 deletions(-) create mode 100644 spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/DefaultZipkinRestTemplateCustomizer.java create mode 100644 spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/ZipkinRestTemplateCustomizer.java diff --git a/spring-cloud-sleuth-zipkin/pom.xml b/spring-cloud-sleuth-zipkin/pom.xml index 6f301ae3f..b6a82e16b 100644 --- a/spring-cloud-sleuth-zipkin/pom.xml +++ b/spring-cloud-sleuth-zipkin/pom.xml @@ -37,6 +37,10 @@ org.springframework.cloud spring-cloud-sleuth-core + + org.springframework + spring-web + org.springframework.cloud spring-cloud-commons diff --git a/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/DefaultZipkinRestTemplateCustomizer.java b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/DefaultZipkinRestTemplateCustomizer.java new file mode 100644 index 000000000..5121e8d5c --- /dev/null +++ b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/DefaultZipkinRestTemplateCustomizer.java @@ -0,0 +1,64 @@ +/* + * Copyright 2013-2016 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 + * + * http://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.cloud.sleuth.zipkin; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.zip.GZIPOutputStream; + +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.web.client.RestTemplate; + +/** + * Default {@link ZipkinRestTemplateCustomizer} that provides the GZip compression if + * {@link ZipkinProperties#compression} is enabled. + * + * @author Marcin Grzejszczak + * + * @since 1.1.0 + */ +public class DefaultZipkinRestTemplateCustomizer implements ZipkinRestTemplateCustomizer { + private final ZipkinProperties zipkinProperties; + + public DefaultZipkinRestTemplateCustomizer( + ZipkinProperties zipkinProperties) { + this.zipkinProperties = zipkinProperties; + } + + @Override + public void customize(RestTemplate restTemplate) { + if (this.zipkinProperties.getCompression().isEnabled()) { + restTemplate.getInterceptors().add(0, new GZipInterceptor()); + } + } + + private class GZipInterceptor implements ClientHttpRequestInterceptor { + + public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws + IOException { + request.getHeaders().add("Content-Encoding", "gzip"); + ByteArrayOutputStream gzipped = new ByteArrayOutputStream(); + try (GZIPOutputStream compressor = new GZIPOutputStream(gzipped)) { + compressor.write(body); + } + return execution.execute(request, gzipped.toByteArray()); + } + } +} diff --git a/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/HttpZipkinSpanReporter.java b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/HttpZipkinSpanReporter.java index 17fa56b06..70d1db654 100644 --- a/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/HttpZipkinSpanReporter.java +++ b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/HttpZipkinSpanReporter.java @@ -1,12 +1,9 @@ package org.springframework.cloud.sleuth.zipkin; -import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.Flushable; import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URL; +import java.net.URI; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.LinkedList; @@ -16,9 +13,14 @@ import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ScheduledExecutorService; -import java.util.zip.GZIPOutputStream; import org.apache.commons.logging.Log; import org.springframework.cloud.sleuth.metric.SpanMetricReporter; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.RequestEntity; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; import zipkin.Codec; import zipkin.Span; @@ -37,23 +39,23 @@ public final class HttpZipkinSpanReporter .getLog(HttpZipkinSpanReporter.class); private static final Charset UTF_8 = Charset.forName("UTF-8"); + private final RestTemplate restTemplate; private final String url; private final BlockingQueue pending = new LinkedBlockingQueue<>(1000); private final Flusher flusher; // Nullable for testing - private final boolean compressionEnabled; private final SpanMetricReporter spanMetricReporter; /** + * @param restTemplate {@link RestTemplate} used for sending requests to Zipkin * @param baseUrl URL of the zipkin query server instance. Like: http://localhost:9411/ * @param flushInterval in seconds. 0 implies spans are {@link #flush() flushed} externally. - * @param compressionEnabled compress spans using gzip before posting to the zipkin server. * @param spanMetricReporter service to count number of accepted / dropped spans */ - public HttpZipkinSpanReporter(String baseUrl, int flushInterval, boolean compressionEnabled, + public HttpZipkinSpanReporter(RestTemplate restTemplate, String baseUrl, int flushInterval, SpanMetricReporter spanMetricReporter) { + this.restTemplate = restTemplate; this.url = baseUrl + (baseUrl.endsWith("/") ? "" : "/") + "api/v1/spans"; this.flusher = flushInterval > 0 ? new Flusher(this, flushInterval) : null; - this.compressionEnabled = compressionEnabled; this.spanMetricReporter = spanMetricReporter; } @@ -95,7 +97,7 @@ public final class HttpZipkinSpanReporter try { postSpans(json); } - catch (IOException e) { + catch (RestClientException e) { if (log.isDebugEnabled()) { // don't pollute logs unless debug is on. // TODO: logger test log.debug( @@ -128,34 +130,11 @@ public final class HttpZipkinSpanReporter } } - void postSpans(byte[] json) throws IOException { - // intentionally not closing the connection, so as to use keep-alives - HttpURLConnection connection = (HttpURLConnection) new URL(this.url).openConnection(); - connection.setRequestMethod("POST"); - connection.addRequestProperty("Content-Type", "application/json"); - if (this.compressionEnabled) { - connection.addRequestProperty("Content-Encoding", "gzip"); - ByteArrayOutputStream gzipped = new ByteArrayOutputStream(); - try (GZIPOutputStream compressor = new GZIPOutputStream(gzipped)) { - compressor.write(json); - } - json = gzipped.toByteArray(); - } - connection.setDoOutput(true); - connection.setFixedLengthStreamingMode(json.length); - connection.getOutputStream().write(json); - - try (InputStream in = connection.getInputStream()) { - while (in.read() != -1); // skip - } - catch (IOException e) { - try (InputStream err = connection.getErrorStream()) { - if (err != null) { // possible, if the connection was dropped - while (err.read() != -1); // skip - } - } - throw e; - } + void postSpans(byte[] json) { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.APPLICATION_JSON); + RequestEntity requestEntity = new RequestEntity<>(json, httpHeaders, HttpMethod.POST, URI.create(this.url)); + this.restTemplate.exchange(requestEntity, String.class); } /** diff --git a/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/ZipkinAutoConfiguration.java b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/ZipkinAutoConfiguration.java index f6c95285a..90530d4d0 100644 --- a/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/ZipkinAutoConfiguration.java +++ b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/ZipkinAutoConfiguration.java @@ -34,15 +34,23 @@ import org.springframework.cloud.sleuth.sampler.PercentageBasedSampler; import org.springframework.cloud.sleuth.sampler.SamplerProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; - +import org.springframework.web.client.RestTemplate; /** * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration Auto-configuration} * enables reporting to Zipkin via HTTP. Has a default {@link Sampler} set as * {@link PercentageBasedSampler}. * + * The {@link ZipkinRestTemplateCustomizer} allows you to customize the {@link RestTemplate} + * that is used to send Spans to Zipkin. Its default implementation - {@link DefaultZipkinRestTemplateCustomizer} + * adds the GZip compression. + * * @author Spencer Gibb * @since 1.0.0 + * + * @see PercentageBasedSampler + * @see ZipkinRestTemplateCustomizer + * @see DefaultZipkinRestTemplateCustomizer */ @Configuration @EnableConfigurationProperties({ZipkinProperties.class, SamplerProperties.class}) @@ -52,9 +60,18 @@ public class ZipkinAutoConfiguration { @Bean @ConditionalOnMissingBean - public ZipkinSpanReporter reporter(SpanMetricReporter spanMetricReporter, ZipkinProperties zipkin) { - return new HttpZipkinSpanReporter(zipkin.getBaseUrl(), zipkin.getFlushInterval(), - zipkin.getCompression().isEnabled(), spanMetricReporter); + public ZipkinSpanReporter reporter(SpanMetricReporter spanMetricReporter, ZipkinProperties zipkin, + ZipkinRestTemplateCustomizer zipkinRestTemplateCustomizer) { + RestTemplate restTemplate = new RestTemplate(); + zipkinRestTemplateCustomizer.customize(restTemplate); + return new HttpZipkinSpanReporter(restTemplate, zipkin.getBaseUrl(), zipkin.getFlushInterval(), + spanMetricReporter); + } + + @Bean + @ConditionalOnMissingBean + public ZipkinRestTemplateCustomizer zipkinRestTemplateCustomizer(ZipkinProperties zipkinProperties) { + return new DefaultZipkinRestTemplateCustomizer(zipkinProperties); } @Bean diff --git a/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/ZipkinRestTemplateCustomizer.java b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/ZipkinRestTemplateCustomizer.java new file mode 100644 index 000000000..1edd4ab68 --- /dev/null +++ b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/ZipkinRestTemplateCustomizer.java @@ -0,0 +1,35 @@ +/* + * Copyright 2013-2016 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 + * + * http://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.cloud.sleuth.zipkin; + +import org.springframework.web.client.RestTemplate; + +/** + * Implementations customize the {@link RestTemplate} used to report spans to Zipkin. + * For example, they can add an additional header needed by their environment. + * + *

Implementors must gzip according to {@link ZipkinProperties.Compression}, + * for example by using the {@link DefaultZipkinRestTemplateCustomizer}. + * + * @author Marcin Grzejszczak + * + * @since 1.1.0 + */ +public interface ZipkinRestTemplateCustomizer { + + void customize(RestTemplate restTemplate); +} diff --git a/spring-cloud-sleuth-zipkin/src/test/java/org/springframework/cloud/sleuth/zipkin/HttpZipkinSpanReporterTest.java b/spring-cloud-sleuth-zipkin/src/test/java/org/springframework/cloud/sleuth/zipkin/HttpZipkinSpanReporterTest.java index 02ace6de6..8daa6c77b 100644 --- a/spring-cloud-sleuth-zipkin/src/test/java/org/springframework/cloud/sleuth/zipkin/HttpZipkinSpanReporterTest.java +++ b/spring-cloud-sleuth-zipkin/src/test/java/org/springframework/cloud/sleuth/zipkin/HttpZipkinSpanReporterTest.java @@ -4,8 +4,9 @@ import org.junit.Rule; import org.junit.Test; import org.springframework.cloud.sleuth.metric.CounterServiceBasedSpanMetricReporter; import org.springframework.cloud.sleuth.metric.SpanMetricReporter; -import zipkin.Span; +import org.springframework.web.client.RestTemplate; +import zipkin.Span; import zipkin.junit.HttpFailure; import zipkin.junit.ZipkinRule; @@ -18,11 +19,10 @@ public class HttpZipkinSpanReporterTest { InMemorySpanCounter inMemorySpanCounter = new InMemorySpanCounter(); SpanMetricReporter spanMetricReporter = new CounterServiceBasedSpanMetricReporter("accepted", "dropped", this.inMemorySpanCounter); + RestTemplate restTemplate = defaultRestTemplate(); - HttpZipkinSpanReporter reporter = new HttpZipkinSpanReporter( - this.zipkin.httpUrl(), + HttpZipkinSpanReporter reporter = new HttpZipkinSpanReporter(restTemplate, this.zipkin.httpUrl(), 0, // so that tests can drive flushing explicitly - false, // disable compression this.spanMetricReporter ); @@ -68,10 +68,8 @@ public class HttpZipkinSpanReporterTest { @Test public void postsCompressedSpans() throws Exception { - this.reporter = new HttpZipkinSpanReporter( - this.zipkin.httpUrl(), + this.reporter = new HttpZipkinSpanReporter(restTemplateWithCompression(), this.zipkin.httpUrl(), 0, // so that tests can drive flushing explicitly - false, // enable compression this.spanMetricReporter ); @@ -116,4 +114,20 @@ public class HttpZipkinSpanReporterTest { static Span span(long traceId, String spanName) { return Span.builder().traceId(traceId).id(traceId).name(spanName).build(); } + + private RestTemplate restTemplate(ZipkinProperties zipkinProperties) { + RestTemplate restTemplate = new RestTemplate(); + new DefaultZipkinRestTemplateCustomizer(zipkinProperties).customize(restTemplate); + return restTemplate; + } + + private RestTemplate defaultRestTemplate() { + return restTemplate(new ZipkinProperties()); + } + + private RestTemplate restTemplateWithCompression() { + ZipkinProperties zipkinProperties = new ZipkinProperties(); + zipkinProperties.getCompression().setEnabled(true); + return restTemplate(zipkinProperties); + } }