Adds Zipkin v2 support (#720)

This adds the ability to use zipkin v2 endpoints natively. It also
allows wiring of standard zipkin reporters and senders.
This commit is contained in:
Adrian Cole
2017-10-03 14:47:33 +08:00
committed by Marcin Grzejszczak
parent 08a526c61b
commit 4ddf929294
28 changed files with 2112 additions and 19 deletions

View File

@@ -29,11 +29,13 @@
<module>spring-cloud-sleuth-dependencies</module>
<module>spring-cloud-sleuth-core</module>
<module>spring-cloud-sleuth-zipkin</module>
<module>spring-cloud-sleuth-zipkin2</module>
<module>spring-cloud-sleuth-stream</module>
<module>spring-cloud-sleuth-reactor</module>
<module>spring-cloud-sleuth-zipkin-stream</module>
<module>spring-cloud-starter-sleuth</module>
<module>spring-cloud-starter-zipkin</module>
<module>spring-cloud-starter-zipkin2</module>
<module>spring-cloud-sleuth-samples</module>
<module>docs</module>
</modules>

View File

@@ -14,8 +14,9 @@
<name>spring-cloud-sleuth-dependencies</name>
<description>Spring Cloud Sleuth Dependencies</description>
<properties>
<zipkin.version>1.28.0</zipkin.version>
<zipkin-reporter.version>1.0.0</zipkin-reporter.version>
<zipkin.version>2.1.0</zipkin.version>
<zipkin-reporter.version>1.1.1</zipkin-reporter.version>
<zipkin-reporter2.version>2.0.2</zipkin-reporter2.version>
</properties>
<dependencyManagement>
<dependencies>
@@ -34,6 +35,11 @@
<artifactId>spring-cloud-sleuth-zipkin</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin2</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-stream</artifactId>
@@ -49,6 +55,11 @@
<artifactId>spring-cloud-starter-zipkin</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin2</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
@@ -59,6 +70,11 @@
<artifactId>zipkin</artifactId>
<version>${zipkin.version}</version>
</dependency>
<dependency>
<groupId>io.zipkin.zipkin2</groupId>
<artifactId>zipkin</artifactId>
<version>${zipkin.version}</version>
</dependency>
<dependency>
<groupId>io.zipkin.java</groupId>
<artifactId>zipkin-server</artifactId>
@@ -84,6 +100,11 @@
<artifactId>zipkin-reporter</artifactId>
<version>${zipkin-reporter.version}</version>
</dependency>
<dependency>
<groupId>io.zipkin.reporter2</groupId>
<artifactId>zipkin-reporter</artifactId>
<version>${zipkin-reporter2.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<profiles>

View File

@@ -23,7 +23,7 @@
<module>spring-cloud-sleuth-sample-websocket</module>
<module>spring-cloud-sleuth-sample-feign</module>
<module>spring-cloud-sleuth-sample-ribbon</module>
<module>spring-cloud-sleuth-sample-zipkin</module>
<module>spring-cloud-sleuth-sample-zipkin2</module>
<module>spring-cloud-sleuth-sample-stream</module>
<module>spring-cloud-sleuth-sample-zipkin-stream</module>
</modules>
@@ -59,12 +59,17 @@
<dependency>
<groupId>io.zipkin.java</groupId>
<artifactId>zipkin</artifactId>
<version>1.28.0</version>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>io.zipkin.zipkin2</groupId>
<artifactId>zipkin</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>io.zipkin.java</groupId>
<artifactId>zipkin-server</artifactId>
<version>1.28.0</version>
<version>2.1.0</version>
</dependency>
</dependencies>
</dependencyManagement>

View File

@@ -19,10 +19,10 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-sleuth-sample-zipkin</artifactId>
<artifactId>spring-cloud-sleuth-sample-zipkin2</artifactId>
<packaging>jar</packaging>
<name>spring-cloud-sleuth-sample-zipkin</name>
<description>Spring Cloud Sleuth Sample</description>
<name>spring-cloud-sleuth-sample-zipkin2</name>
<description>Spring Cloud Sleuth Sample Zipkin v2</description>
<parent>
<groupId>org.springframework.cloud</groupId>
@@ -77,7 +77,7 @@
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin</artifactId>
<artifactId>spring-cloud-sleuth-zipkin2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
@@ -111,7 +111,7 @@
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.6.0</version>
<version>3.9.0</version>
<scope>test</scope>
</dependency>
</dependencies>

View File

@@ -21,11 +21,12 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.sleuth.zipkin.ZipkinSpanReporter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.web.client.RestTemplate;
import zipkin2.Span;
import zipkin2.reporter.Reporter;
/**
* @author Spencer Gibb
@@ -49,13 +50,8 @@ public class SampleZipkinApplication {
// Use this for debugging (or if there is no Zipkin server running on port 9411)
@Bean
@ConditionalOnProperty(value = "sample.zipkin.enabled", havingValue = "false")
public ZipkinSpanReporter spanCollector() {
return new ZipkinSpanReporter() {
@Override
public void report(zipkin.Span span) {
log.info(String.format("Reporting span [%s]", span));
}
};
public Reporter<Span> spanReporter() {
return Reporter.CONSOLE;
}
}

View File

@@ -32,7 +32,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.sleuth.zipkin.ZipkinProperties;
import org.springframework.cloud.sleuth.zipkin2.ZipkinProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

View File

@@ -0,0 +1,133 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2013-2017 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-sleuth-zipkin2</artifactId>
<packaging>jar</packaging>
<name>Spring Cloud Sleuth Zipkin v2</name>
<description>Spring Cloud Sleuth Zipkin v2</description>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth</artifactId>
<version>1.3.0.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.zipkin.zipkin2</groupId>
<artifactId>zipkin</artifactId>
</dependency>
<dependency>
<groupId>io.zipkin.reporter2</groupId>
<artifactId>zipkin-reporter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.zipkin.java</groupId>
<artifactId>zipkin-junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
<!-- otherwise spring boot's version of okhttp kicks zipkin-junit's deps out of alignment -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.9.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<version>3.9.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,136 @@
/*
* Copyright 2015 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.zipkin2;
import java.lang.invoke.MethodHandles;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.embedded.EmbeddedServletContainerInitializedEvent;
import org.springframework.cloud.client.serviceregistry.Registration;
import org.springframework.cloud.commons.util.InetUtils;
import org.springframework.cloud.commons.util.InetUtilsProperties;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.event.EventListener;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;
import zipkin2.Endpoint;
/**
* {@link EndpointLocator} implementation that:
*
* <ul>
* <li><b>serviceName</b> - from {@link ServerProperties} or {@link Registration}</li>
* <li><b>ip</b> - from {@link ServerProperties}</li>
* <li><b>port</b> - from lazily assigned port or {@link ServerProperties}</li>
* </ul>
*
* You can override the name using {@link ZipkinProperties.Service#setName(String)}
*
* @author Dave Syer
* @since 1.0.0
*/
public class DefaultEndpointLocator implements EndpointLocator, EnvironmentAware {
private static final Log log = LogFactory.getLog(MethodHandles.lookup().lookupClass());
private static final String IP_ADDRESS_PROP_NAME = "spring.cloud.client.ipAddress";
private final Registration registration;
private final ServerProperties serverProperties;
private final String appName;
private final InetUtils inetUtils;
private final ZipkinProperties zipkinProperties;
private Integer port;
private Environment environment;
public DefaultEndpointLocator(Registration registration, ServerProperties serverProperties,
String appName, ZipkinProperties zipkinProperties, InetUtils inetUtils) {
this.registration = registration;
this.serverProperties = serverProperties;
this.appName = appName;
this.zipkinProperties = zipkinProperties;
if (inetUtils == null) {
this.inetUtils = new InetUtils(new InetUtilsProperties());
} else {
this.inetUtils = inetUtils;
}
}
@Override
public Endpoint local() {
String serviceName = getLocalServiceName();
if (log.isDebugEnabled()) {
log.debug("Span will contain serviceName [" + serviceName + "]");
}
Endpoint.Builder builder = Endpoint.newBuilder()
.serviceName(serviceName)
.port(getPort());
return addAddress(builder).build();
}
private String getLocalServiceName() {
if (StringUtils.hasText(this.zipkinProperties.getService().getName())) {
return this.zipkinProperties.getService().getName();
} else if (this.registration != null) {
try {
return this.registration.getServiceId();
} catch (RuntimeException e) {
log.warn("error getting service name from registration", e);
}
}
return this.appName;
}
@EventListener(EmbeddedServletContainerInitializedEvent.class)
public void grabPort(EmbeddedServletContainerInitializedEvent event) {
this.port = event.getEmbeddedServletContainer().getPort();
}
private Integer getPort() {
if (this.port!=null) {
return this.port;
}
Integer port;
if (this.serverProperties!=null && this.serverProperties.getPort() != null && this.serverProperties.getPort() > 0) {
port = this.serverProperties.getPort();
}
else {
port = 8080;
}
return port;
}
private Endpoint.Builder addAddress(Endpoint.Builder builder) {
if (this.serverProperties != null && this.serverProperties.getAddress() != null
&& builder.parseIp(this.serverProperties.getAddress())) {
return builder;
}
else if (this.environment != null && this.environment.containsProperty(IP_ADDRESS_PROP_NAME)
&& builder.parseIp(this.environment.getProperty(IP_ADDRESS_PROP_NAME, String.class))) {
return builder;
}
else {
return builder.ip(this.inetUtils.findFirstNonLoopbackAddress());
}
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
}

View File

@@ -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.zipkin2;
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());
}
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2015 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.zipkin2;
import zipkin2.Endpoint;
/**
* Strategy for locating a zipkin {@linkplain Endpoint} for the current process.
*
* @author Dave Syer
* @since 1.0.0
*/
public interface EndpointLocator {
Endpoint local();
}

View File

@@ -0,0 +1,47 @@
package org.springframework.cloud.sleuth.zipkin2;
import org.springframework.cloud.sleuth.metric.SpanMetricReporter;
import zipkin2.reporter.ReporterMetrics;
final class ReporterMetricsAdapter implements ReporterMetrics {
private final SpanMetricReporter spanMetricReporter;
public ReporterMetricsAdapter(SpanMetricReporter spanMetricReporter) {
this.spanMetricReporter = spanMetricReporter;
}
@Override
public void incrementMessages() {
}
@Override
public void incrementMessagesDropped(Throwable throwable) {
}
@Override
public void incrementSpans(int i) {
this.spanMetricReporter.incrementAcceptedSpans(i);
}
@Override
public void incrementSpanBytes(int i) {
}
@Override
public void incrementMessageBytes(int i) {
}
@Override
public void incrementSpansDropped(int i) {
this.spanMetricReporter.incrementDroppedSpans(i);
}
@Override
public void updateQueuedSpans(int i) {
}
@Override
public void updateQueuedBytes(int i) {
}
}

View File

@@ -0,0 +1,123 @@
package org.springframework.cloud.sleuth.zipkin2;
import java.io.IOException;
import java.net.URI;
import java.util.List;
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.RestTemplate;
import zipkin2.Call;
import zipkin2.Callback;
import zipkin2.CheckResult;
import zipkin2.Span;
import zipkin2.codec.BytesEncoder;
import zipkin2.codec.Encoding;
import zipkin2.reporter.BytesMessageEncoder;
import zipkin2.reporter.Sender;
import zipkin2.reporter.internal.BaseCall;
import static zipkin2.codec.SpanBytesEncoder.JSON_V2;
final class RestTemplateSender extends Sender {
final RestTemplate restTemplate;
final String url;
final Encoding encoding;
final MediaType mediaType;
final BytesMessageEncoder messageEncoder;
RestTemplateSender(RestTemplate restTemplate, String baseUrl, BytesEncoder<Span> 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";
} else if (this.encoding == Encoding.JSON) {
this.mediaType = MediaType.APPLICATION_JSON;
this.url = baseUrl + (baseUrl.endsWith("/") ? "" : "/") + "api/v1/spans";
} else {
throw new UnsupportedOperationException("Unsupported encoding: " + this.encoding.name());
}
this.messageEncoder = BytesMessageEncoder.forEncoding(this.encoding);
}
@Override public Encoding encoding() {
return this.encoding;
}
@Override public int messageMaxBytes() {
// This will drop a span larger than 5MiB. Note: values like 512KiB benchmark better.
return 5 * 1024 * 1024;
}
@Override public int messageSizeInBytes(List<byte[]> spans) {
return encoding().listSizeInBytes(spans);
}
/**
* close is typically called from a different thread
*/
transient boolean closeCalled;
@Override public Call<Void> sendSpans(List<byte[]> encodedSpans) {
if (this.closeCalled)
throw new IllegalStateException("close");
return new HttpPostCall(this.messageEncoder.encode(encodedSpans));
}
/**
* Sends an empty json message to the configured endpoint.
*/
@Override public CheckResult check() {
try {
post(new byte[] { '[', ']' });
return CheckResult.OK;
}
catch (Exception e) {
return CheckResult.failed(e);
}
}
@Override public void close() {
this.closeCalled = true;
}
void post(byte[] json) {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(this.mediaType);
RequestEntity<byte[]> requestEntity = new RequestEntity<>(json, httpHeaders,
HttpMethod.POST, URI.create(this.url));
this.restTemplate.exchange(requestEntity, String.class);
}
class HttpPostCall extends BaseCall<Void> {
private final byte[] message;
HttpPostCall(byte[] message) {
this.message = message;
}
@Override protected Void doExecute() throws IOException {
post(this.message);
return null;
}
@Override protected void doEnqueue(Callback<Void> callback) {
try {
post(this.message);
callback.onSuccess(null);
}
catch (RuntimeException | Error e) {
callback.onError(e);
}
}
@Override public Call<Void> clone() {
return new HttpPostCall(this.message);
}
}
}

View File

@@ -0,0 +1,271 @@
/*
* Copyright 2013-2017 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.zipkin2;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.serviceregistry.Registration;
import org.springframework.cloud.commons.util.InetUtils;
import org.springframework.cloud.sleuth.Sampler;
import org.springframework.cloud.sleuth.SpanAdjuster;
import org.springframework.cloud.sleuth.SpanReporter;
import org.springframework.cloud.sleuth.autoconfig.TraceAutoConfiguration;
import org.springframework.cloud.sleuth.metric.SpanMetricReporter;
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.core.env.Environment;
import org.springframework.http.HttpMethod;
import org.springframework.web.client.RequestCallback;
import org.springframework.web.client.ResponseExtractor;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import zipkin2.Span;
import zipkin2.reporter.AsyncReporter;
import zipkin2.reporter.Reporter;
import zipkin2.reporter.Sender;
/**
* {@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})
@ConditionalOnProperty(value = "spring.zipkin.enabled", matchIfMissing = true)
@AutoConfigureBefore(TraceAutoConfiguration.class)
public class ZipkinAutoConfiguration {
@Autowired(required = false) List<SpanAdjuster> spanAdjusters = new ArrayList<>();
@Autowired ZipkinUrlExtractor extractor;
/**
* Accepts a sender so you can plug-in any standard one. Returns a Reporter so you can also
* replace with a standard one.
*/
@Bean
@ConditionalOnMissingBean
public Reporter<Span> reporter(
SpanMetricReporter spanMetricReporter,
ZipkinProperties zipkin,
Sender sender
) {
return AsyncReporter.builder(sender)
.queuedMaxSpans(1000) // historical constraint. Note: AsyncReporter supports memory bounds
.messageTimeout(zipkin.getMessageTimeout(), TimeUnit.SECONDS)
.metrics(new ReporterMetricsAdapter(spanMetricReporter))
.build(zipkin.getEncoder());
}
@Bean
@ConditionalOnMissingBean
public Sender restTemplateSender(ZipkinProperties zipkin,
ZipkinRestTemplateCustomizer zipkinRestTemplateCustomizer) {
RestTemplate restTemplate = new ZipkinRestTemplateWrapper(zipkin, this.extractor);
zipkinRestTemplateCustomizer.customize(restTemplate);
return new RestTemplateSender(restTemplate, zipkin.getBaseUrl(), zipkin.getEncoder());
}
@Configuration
@ConditionalOnClass(DiscoveryClient.class)
static class DiscoveryClientZipkinUrlExtractorConfiguration {
@Autowired(required = false) DiscoveryClient discoveryClient;
@Bean
ZipkinUrlExtractor zipkinUrlExtractor() {
final DiscoveryClient discoveryClient = this.discoveryClient;
return new ZipkinUrlExtractor() {
@Override
public URI zipkinUrl(ZipkinProperties zipkinProperties) {
if (discoveryClient != null) {
URI uri = URI.create(zipkinProperties.getBaseUrl());
String host = uri.getHost();
List<ServiceInstance> instances = discoveryClient.getInstances(host);
if (!instances.isEmpty()) {
return instances.get(0).getUri();
}
}
return URI.create(zipkinProperties.getBaseUrl());
}
};
}
}
@Configuration
@ConditionalOnMissingClass("org.springframework.cloud.client.discovery.DiscoveryClient")
static class DefaultZipkinUrlExtractorConfiguration {
@Bean
ZipkinUrlExtractor zipkinUrlExtractor() {
return new ZipkinUrlExtractor() {
@Override
public URI zipkinUrl(ZipkinProperties zipkinProperties) {
return URI.create(zipkinProperties.getBaseUrl());
}
};
}
}
@Bean
@ConditionalOnMissingBean
public ZipkinRestTemplateCustomizer zipkinRestTemplateCustomizer(ZipkinProperties zipkinProperties) {
return new DefaultZipkinRestTemplateCustomizer(zipkinProperties);
}
@Bean
@ConditionalOnMissingBean
public Sampler defaultTraceSampler(SamplerProperties config) {
return new PercentageBasedSampler(config);
}
@Bean
public SpanReporter zipkinSpanListener(Reporter<Span> reporter, EndpointLocator endpointLocator,
Environment environment) {
return new ZipkinSpanListener(reporter, endpointLocator, environment, this.spanAdjusters);
}
@Configuration
@ConditionalOnMissingBean(EndpointLocator.class)
@ConditionalOnProperty(value = "spring.zipkin.locator.discovery.enabled", havingValue = "false", matchIfMissing = true)
protected static class DefaultEndpointLocatorConfiguration {
@Autowired(required=false)
private ServerProperties serverProperties;
@Autowired
private ZipkinProperties zipkinProperties;
@Autowired(required=false)
private InetUtils inetUtils;
@Value("${spring.application.name:unknown}")
private String appName;
@Bean
public EndpointLocator zipkinEndpointLocator() {
return new DefaultEndpointLocator(null, this.serverProperties, this.appName,
this.zipkinProperties, this.inetUtils);
}
}
@Configuration
@ConditionalOnClass(Registration.class)
@ConditionalOnMissingBean(EndpointLocator.class)
@ConditionalOnProperty(value = "spring.zipkin.locator.discovery.enabled", havingValue = "true")
protected static class RegistrationEndpointLocatorConfiguration {
@Autowired(required=false)
private ServerProperties serverProperties;
@Autowired
private ZipkinProperties zipkinProperties;
@Autowired(required=false)
private InetUtils inetUtils;
@Value("${spring.application.name:unknown}")
private String appName;
@Autowired(required=false)
private Registration registration;
@Bean
public EndpointLocator zipkinEndpointLocator() {
return new DefaultEndpointLocator(this.registration, this.serverProperties, this.appName,
this.zipkinProperties, this.inetUtils);
}
}
}
/**
* Internal interface to provide a way to retrieve Zipkin URI. If there's no discovery client
* then this value will be taken from the properties. Otherwise host will be assumed to
* be a service id.
*/
interface ZipkinUrlExtractor {
URI zipkinUrl(ZipkinProperties zipkinProperties);
}
/**
* Resolves at runtime where the Zipkin server is. If there's no discovery client then
* {@link URI} from the properties is taken. Otherwise service discovery is pinged
* for current Zipkin address.
*/
class ZipkinRestTemplateWrapper extends RestTemplate {
private static final Log log = LogFactory.getLog(ZipkinRestTemplateWrapper.class);
private final ZipkinProperties zipkinProperties;
private final ZipkinUrlExtractor extractor;
ZipkinRestTemplateWrapper(ZipkinProperties zipkinProperties,
ZipkinUrlExtractor extractor) {
this.zipkinProperties = zipkinProperties;
this.extractor = extractor;
}
@Override protected <T> T doExecute(URI originalUrl, HttpMethod method,
RequestCallback requestCallback,
ResponseExtractor<T> responseExtractor) throws RestClientException {
URI uri = this.extractor.zipkinUrl(this.zipkinProperties);
URI newUri = resolvedZipkinUri(originalUrl, uri);
return super.doExecute(newUri, method, requestCallback, responseExtractor);
}
private URI resolvedZipkinUri(URI originalUrl, URI resolvedZipkinUri) {
try {
return new URI(resolvedZipkinUri.getScheme(), resolvedZipkinUri.getUserInfo(),
resolvedZipkinUri.getHost(), resolvedZipkinUri.getPort(), originalUrl.getPath(),
originalUrl.getQuery(), originalUrl.getFragment());
} catch (URISyntaxException e) {
if (log.isDebugEnabled()) {
log.debug("Failed to create the new URI from original [" + originalUrl + "] and new one [" + resolvedZipkinUri + "]");
}
return originalUrl;
}
}
}

View File

@@ -0,0 +1,175 @@
/*
* Copyright 2013-2015 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.zipkin2;
import org.springframework.boot.context.properties.ConfigurationProperties;
import zipkin2.codec.SpanBytesEncoder;
/**
* Zipkin settings
*
* @author Spencer Gibb
* @since 1.0.0
*/
@ConfigurationProperties("spring.zipkin")
public class ZipkinProperties {
/** URL of the zipkin query server instance. You can also provide
* the service id of the Zipkin server if Zipkin's registered in
* service discovery (e.g. http://zipkinserver/)
*/
private String baseUrl = "http://localhost:9411/";
/**
* Enables sending spans to Zipkin
*/
private boolean enabled = true;
/**
* Timeout in seconds before pending spans will be sent in batches to Zipkin
*/
private int messageTimeout = 1;
/**
* Encoding type of spans sent to Zipkin. Set to {@link SpanBytesEncoder#JSON_V1} if your server
* is not recent.
*/
private SpanBytesEncoder encoder = SpanBytesEncoder.JSON_V2;
/**
* Configuration related to compressions of spans sent to Zipkin
*/
private Compression compression = new Compression();
private Service service = new Service();
private Locator locator = new Locator();
public Locator getLocator() {
return this.locator;
}
public String getBaseUrl() {
return this.baseUrl;
}
public boolean isEnabled() {
return this.enabled;
}
public int getMessageTimeout() {
return this.messageTimeout;
}
public Compression getCompression() {
return this.compression;
}
public Service getService() {
return this.service;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public void setMessageTimeout(int messageTimeout) {
this.messageTimeout = messageTimeout;
}
public void setCompression(Compression compression) {
this.compression = compression;
}
public void setService(Service service) {
this.service = service;
}
public void setLocator(Locator locator) {
this.locator = locator;
}
public SpanBytesEncoder getEncoder() {
return this.encoder;
}
public void setEncoder(SpanBytesEncoder encoder) {
this.encoder = encoder;
}
/** When enabled, spans are gzipped before sent to the zipkin server */
public static class Compression {
private boolean enabled = false;
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
/** When set will override the default {@code spring.application.name} value of the service id */
public static class Service {
/** The name of the service, from which the Span was sent via HTTP, that should appear in Zipkin */
private String name;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
/** Configuration related to locating of the host name from service discovery.
* This property is NOT related to finding Zipkin via Service Disovery.
* To do so use the {@link ZipkinProperties#baseUrl} property with the
* service name set inside the URL.
*/
public static class Locator {
private Discovery discovery;
public Discovery getDiscovery() {
return this.discovery;
}
public void setDiscovery(Discovery discovery) {
this.discovery = discovery;
}
public static class Discovery {
/** Enabling of locating the host name via service discovery */
private boolean enabled;
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
}
}

View File

@@ -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.zipkin2;
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.
*
* <p>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);
}

View File

@@ -0,0 +1,205 @@
/*
* Copyright 2013-2015 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.zipkin2;
import java.util.List;
import java.util.Map;
import org.springframework.cloud.commons.util.IdUtils;
import org.springframework.cloud.sleuth.Log;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.SpanAdjuster;
import org.springframework.cloud.sleuth.SpanReporter;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;
import zipkin2.Endpoint;
import zipkin2.reporter.Reporter;
/**
* Listener of Sleuth events. Reports to Zipkin via {@link Reporter}.
*/
public class ZipkinSpanListener implements SpanReporter {
private static final org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory
.getLog(ZipkinSpanListener.class);
private final Reporter<zipkin2.Span> reporter;
private final Environment environment;
private final List<SpanAdjuster> spanAdjusters;
/**
* Endpoint is the visible IP address of this service, the port it is listening on and
* the service name from discovery.
*/
// Visible for testing
final EndpointLocator endpointLocator;
public ZipkinSpanListener(Reporter<zipkin2.Span> reporter, EndpointLocator endpointLocator,
Environment environment, List<SpanAdjuster> spanAdjusters) {
this.reporter = reporter;
this.endpointLocator = endpointLocator;
this.environment = environment;
this.spanAdjusters = spanAdjusters;
}
/**
* Converts a given Sleuth span to a Zipkin Span.
* <ul>
* <li>Set ids, etc
* <li>Create timeline annotations based on data from Span object.
* <li>Create tags based on data from Span object.
* </ul>
*/
// Visible for testing
zipkin2.Span convert(Span span) {
//TODO: Consider adding support for the debug flag (related to #496)
Span convertedSpan = span;
for (SpanAdjuster adjuster : this.spanAdjusters) {
convertedSpan = adjuster.adjust(span);
}
zipkin2.Span.Builder zipkinSpan = zipkin2.Span.newBuilder();
zipkinSpan.localEndpoint(this.endpointLocator.local());
processLogs(convertedSpan, zipkinSpan);
addZipkinTags(zipkinSpan, convertedSpan);
if (zipkinSpan.kind() != null && this.environment != null) {
setInstanceIdIfPresent(zipkinSpan, Span.INSTANCEID);
}
zipkinSpan.shared(convertedSpan.isShared());
zipkinSpan.timestamp(convertedSpan.getBegin() * 1000L);
if (!convertedSpan.isRunning()) { // duration is authoritative, only write when the span stopped
zipkinSpan.duration(calculateDurationInMicros(convertedSpan));
}
zipkinSpan.traceId(convertedSpan.traceIdString());
if (convertedSpan.getParents().size() > 0) {
if (convertedSpan.getParents().size() > 1) {
log.error("Zipkin doesn't support spans with multiple parents. Omitting "
+ "other parents for " + convertedSpan);
}
zipkinSpan.parentId(Span.idToHex(convertedSpan.getParents().get(0)));
}
zipkinSpan.id(Span.idToHex(convertedSpan.getSpanId()));
if (StringUtils.hasText(convertedSpan.getName())) {
zipkinSpan.name(convertedSpan.getName());
}
return zipkinSpan.build();
}
// Instead of going through the list of logs multiple times we're doing it only once
void processLogs(Span span, zipkin2.Span.Builder zipkinSpan) {
for (Log log : span.logs()) {
String event = log.getEvent();
long micros = log.getTimestamp() * 1000L;
// don't add redundant annotations to the output
if (event.length() == 2) {
if (event.equals("cs")) {
zipkinSpan.kind(zipkin2.Span.Kind.CLIENT);
} else if (event.equals("sr")) {
zipkinSpan.kind(zipkin2.Span.Kind.SERVER);
} else if (event.equals("ss")) {
zipkinSpan.kind(zipkin2.Span.Kind.SERVER);
} else if (event.equals("cr")) {
zipkinSpan.kind(zipkin2.Span.Kind.CLIENT);
} else if (event.equals("ms")) {
zipkinSpan.kind(zipkin2.Span.Kind.PRODUCER);
} else if (event.equals("mr")) {
zipkinSpan.kind(zipkin2.Span.Kind.CONSUMER);
} else {
zipkinSpan.addAnnotation(micros, event);
}
} else {
zipkinSpan.addAnnotation(micros, event);
}
}
}
private void setInstanceIdIfPresent(zipkin2.Span.Builder zipkinSpan, String key) {
String property = defaultInstanceId();
if (StringUtils.hasText(property)) {
zipkinSpan.putTag(key, property);
}
}
String defaultInstanceId() {
return IdUtils.getDefaultInstanceId(this.environment);
}
/**
* Adds tags from the sleuth Span
*/
private void addZipkinTags(zipkin2.Span.Builder zipkinSpan, Span span) {
Endpoint.Builder remoteEndpoint = Endpoint.newBuilder();
boolean shouldAddRemote = false;
// don't add redundant tags to the output
for (Map.Entry<String, String> e : span.tags().entrySet()) {
String key = e.getKey();
if (key.equals("peer.service")) {
shouldAddRemote = true;
remoteEndpoint.serviceName(e.getValue());
} else if (key.equals("peer.ipv4") || key.equals("peer.ipv6")) {
shouldAddRemote = true;
remoteEndpoint.ip(e.getValue());
} else if (key.equals("peer.port")) {
shouldAddRemote = true;
try {
remoteEndpoint.port(Integer.parseInt(e.getValue()));
} catch (NumberFormatException ignored) {
}
} else {
zipkinSpan.putTag(e.getKey(), e.getValue());
}
}
if (shouldAddRemote) {
zipkinSpan.remoteEndpoint(remoteEndpoint.build());
}
}
/**
* There could be instrumentation delay between span creation and the
* semantic start of the span (client send). When there's a difference,
* spans look confusing. Ex users expect duration to be client
* receive - send, but it is a little more than that. Rather than have
* to teach each user about the possibility of instrumentation overhead,
* we truncate absolute duration (span finish - create) to semantic
* duration (client receive - send)
*/
private long calculateDurationInMicros(Span span) {
Log clientSend = hasLog(Span.CLIENT_SEND, span);
Log clientReceived = hasLog(Span.CLIENT_RECV, span);
if (clientSend != null && clientReceived != null) {
return (clientReceived.getTimestamp() - clientSend.getTimestamp()) * 1000;
}
return span.getAccumulatedMicros();
}
private Log hasLog(String logName, Span span) {
for (Log log : span.logs()) {
if (logName.equals(log.getEvent())) {
return log;
}
}
return null;
}
@Override
public void report(Span span) {
if (span.isExportable()) {
this.reporter.report(convert(span));
} else {
if (log.isDebugEnabled()) {
log.debug("The span " + span + " will not be sent to Zipkin due to sampling");
}
}
}
}

View File

@@ -0,0 +1,3 @@
# Auto Configuration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.sleuth.zipkin2.ZipkinAutoConfiguration

View File

@@ -0,0 +1,176 @@
package org.springframework.cloud.sleuth.zipkin2;
import java.net.InetAddress;
import java.net.UnknownHostException;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.cloud.client.serviceregistry.Registration;
import org.springframework.cloud.commons.util.InetUtils;
import org.springframework.cloud.commons.util.InetUtilsProperties;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Matcin Wielgus
*/
public class DefaultEndpointLocatorConfigurationTest {
@Test
public void endpointLocatorShouldDefaultToServerPropertiesEndpointLocator() {
ConfigurableApplicationContext ctxt = new SpringApplication(
EmptyConfiguration.class).run("--spring.jmx.enabled=false");
assertThat(ctxt.getBean(EndpointLocator.class))
.isInstanceOf(DefaultEndpointLocator.class);
ctxt.close();
}
@Test
public void endpointLocatorShouldDefaultToServerPropertiesEndpointLocatorEvenWhenDiscoveryClientPresent() {
ConfigurableApplicationContext ctxt = new SpringApplication(
ConfigurationWithRegistration.class).run("--spring.jmx.enabled=false");
assertThat(ctxt.getBean(EndpointLocator.class))
.isInstanceOf(DefaultEndpointLocator.class);
ctxt.close();
}
@Test
public void endpointLocatorShouldRespectExistingEndpointLocator() {
ConfigurableApplicationContext ctxt = new SpringApplication(
ConfigurationWithCustomLocator.class).run("--spring.jmx.enabled=false");
assertThat(ctxt.getBean(EndpointLocator.class))
.isSameAs(ConfigurationWithCustomLocator.locator);
ctxt.close();
}
@Test
public void endpointLocatorShouldSetServiceNameToServiceId() {
ConfigurableApplicationContext ctxt = new SpringApplication(
ConfigurationWithRegistration.class).run("--spring.jmx.enabled=false",
"--spring.zipkin.locator.discovery.enabled=true");
assertThat(ctxt.getBean(EndpointLocator.class).local().serviceName())
.isEqualTo("from-registration");
ctxt.close();
}
@Test
public void endpointLocatorShouldAcceptServiceNameOverride() {
ConfigurableApplicationContext ctxt = new SpringApplication(
ConfigurationWithRegistration.class).run("--spring.jmx.enabled=false",
"--spring.zipkin.locator.discovery.enabled=true",
"--spring.zipkin.service.name=foo");
assertThat(ctxt.getBean(EndpointLocator.class).local().serviceName())
.isEqualTo("foo");
ctxt.close();
}
@Test
public void endpointLocatorShouldRespectExistingEndpointLocatorEvenWhenAskedToBeDiscovery() {
ConfigurableApplicationContext ctxt = new SpringApplication(
ConfigurationWithRegistration.class,
ConfigurationWithCustomLocator.class).run("--spring.jmx.enabled=false",
"--spring.zipkin.locator.discovery.enabled=true");
assertThat(ctxt.getBean(EndpointLocator.class))
.isSameAs(ConfigurationWithCustomLocator.locator);
ctxt.close();
}
@Configuration
@EnableAutoConfiguration
public static class EmptyConfiguration {
}
@Configuration
@EnableAutoConfiguration
public static class ConfigurationWithRegistration {
@Bean public Registration getRegistration() {
return () -> "from-registration";
}
}
@Configuration
@EnableAutoConfiguration
public static class ConfigurationWithCustomLocator {
static EndpointLocator locator = Mockito.mock(EndpointLocator.class);
@Bean public EndpointLocator getEndpointLocator() {
return locator;
}
}
public static final byte[] ADDRESS1234 = { 1, 2, 3, 4 };
@Test
public void portDefaultsTo8080() throws UnknownHostException {
DefaultEndpointLocator locator = new DefaultEndpointLocator(null,
new ServerProperties(), "unknown", new ZipkinProperties(),
localAddress(ADDRESS1234));
assertThat(locator.local().port()).isEqualTo(8080);
}
@Test
public void portFromServerProperties() throws UnknownHostException {
ServerProperties properties = new ServerProperties();
properties.setPort(1234);
DefaultEndpointLocator locator = new DefaultEndpointLocator(null,
properties, "unknown", new ZipkinProperties(),localAddress(ADDRESS1234));
assertThat(locator.local().port()).isEqualTo(1234);
}
@Test
public void portDefaultsToLocalhost() throws UnknownHostException {
DefaultEndpointLocator locator = new DefaultEndpointLocator(null,
new ServerProperties(), "unknown", new ZipkinProperties(), localAddress(ADDRESS1234));
assertThat(locator.local().ipv4()).isEqualTo("1.2.3.4");
}
@Test
public void hostFromServerPropertiesIp() throws UnknownHostException {
ServerProperties properties = new ServerProperties();
properties.setAddress(InetAddress.getByAddress(ADDRESS1234));
DefaultEndpointLocator locator = new DefaultEndpointLocator(null,
properties, "unknown", new ZipkinProperties(),
localAddress(new byte[] { 4, 4, 4, 4 }));
assertThat(locator.local().ipv4()).isEqualTo("1.2.3.4");
}
@Test
public void appNameFromProperties() throws UnknownHostException {
ServerProperties properties = new ServerProperties();
ZipkinProperties zipkinProperties = new ZipkinProperties();
zipkinProperties.getService().setName("foo");
DefaultEndpointLocator locator = new DefaultEndpointLocator(null,
properties, "unknown", zipkinProperties,localAddress(ADDRESS1234));
assertThat(locator.local().serviceName()).isEqualTo("foo");
}
@Test
public void negativePortFromServerProperties() throws UnknownHostException {
ServerProperties properties = new ServerProperties();
properties.setPort(-1);
DefaultEndpointLocator locator = new DefaultEndpointLocator(null,
properties, "unknown", new ZipkinProperties(),localAddress(ADDRESS1234));
assertThat(locator.local().port()).isEqualTo(8080);
}
private InetUtils localAddress(byte[] address) throws UnknownHostException {
InetUtils mocked = Mockito.spy(new InetUtils(new InetUtilsProperties()));
Mockito.when(mocked.findFirstNonLoopbackAddress())
.thenReturn(InetAddress.getByAddress(address));
return mocked;
}
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2013-2017 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.zipkin2;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.awaitility.Awaitility;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.SpanReporter;
import org.springframework.cloud.sleuth.metric.TraceMetricsAutoConfiguration;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.BDDAssertions.then;
import static org.springframework.boot.test.util.EnvironmentTestUtils.addEnvironment;
/**
* Not using {@linkplain SpringBootTest} as we need to change properties per test.
*/
public class ZipkinAutoConfigurationTests {
@Rule public ExpectedException thrown = ExpectedException.none();
@Rule public MockWebServer server = new MockWebServer();
Span span =
Span.builder().traceIdHigh(1L).traceId(2L).spanId(3L).name("foo").tag("foo", "bar").build();
AnnotationConfigApplicationContext context;
@After
public void close() {
if (context != null) {
context.close();
}
}
@Test
public void defaultsToV2Endpoint() throws Exception {
context = new AnnotationConfigApplicationContext();
addEnvironment(context, "spring.zipkin.base-url:" + server.url("/"));
context.register(
PropertyPlaceholderAutoConfiguration.class,
TraceMetricsAutoConfiguration.class,
ZipkinAutoConfiguration.class);
context.refresh();
SpanReporter spanReporter = context.getBean(SpanReporter.class);
spanReporter.report(span);
Awaitility.await().untilAsserted(() -> then(server.getRequestCount()).isGreaterThan(0));
RecordedRequest request = server.takeRequest();
then(request.getPath()).isEqualTo("/api/v2/spans");
then(request.getBody().readUtf8()).contains("localEndpoint");
}
@Test
public void encoderDirectsEndpoint() throws Exception {
context = new AnnotationConfigApplicationContext();
addEnvironment(
context, "spring.zipkin.base-url:" + server.url("/"), "spring.zipkin.encoder:JSON_V1");
context.register(
PropertyPlaceholderAutoConfiguration.class,
TraceMetricsAutoConfiguration.class,
ZipkinAutoConfiguration.class);
context.refresh();
SpanReporter spanReporter = context.getBean(SpanReporter.class);
spanReporter.report(span);
Awaitility.await().untilAsserted(() -> then(server.getRequestCount()).isGreaterThan(0));
RecordedRequest request = server.takeRequest();
then(request.getPath()).isEqualTo("/api/v1/spans");
then(request.getBody().readUtf8()).contains("binaryAnnotations");
}
}

View File

@@ -0,0 +1,113 @@
package org.springframework.cloud.sleuth.zipkin2;
import static org.assertj.core.api.BDDAssertions.then;
import static org.springframework.cloud.sleuth.zipkin2.ZipkinDiscoveryClientTests.ZIPKIN_RULE;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.awaitility.Awaitility;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.SpanReporter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit4.SpringRunner;
import zipkin.junit.ZipkinRule;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ZipkinDiscoveryClientTests.Config.class,
properties = "spring.zipkin.baseUrl=http://zipkin/")
public class ZipkinDiscoveryClientTests {
@ClassRule public static ZipkinRule ZIPKIN_RULE = new ZipkinRule();
@Autowired SpanReporter spanReporter;
@Test
public void shouldUseDiscoveryClientToFindZipkinUrlIfPresent() throws Exception {
Span span = Span.builder().traceIdHigh(1L).traceId(2L).spanId(3L).name("foo")
.build();
this.spanReporter.report(span);
Awaitility.await().untilAsserted(() -> then(ZIPKIN_RULE.httpRequestCount()).isGreaterThan(0));
}
@Configuration
@EnableAutoConfiguration
static class Config {
@Bean
DiscoveryClient client() {
return new ZipkinDiscoveryClient();
}
}
}
class ZipkinDiscoveryClient implements DiscoveryClient {
@Override
public String description() {
return "";
}
@Override
public ServiceInstance getLocalServiceInstance() {
return null;
}
@Override
public List<ServiceInstance> getInstances(String s) {
if ("zipkin".equals(s)) {
return Collections.singletonList(new ServiceInstance() {
@Override
public String getServiceId() {
return "zipkin";
}
@Override
public String getHost() {
return "localhost";
}
@Override
public int getPort() {
return URI.create(ZIPKIN_RULE.httpUrl()).getPort();
}
@Override
public boolean isSecure() {
return false;
}
@Override
public URI getUri() {
return URI.create(ZIPKIN_RULE.httpUrl());
}
@Override
public Map<String, String> getMetadata() {
return null;
}
});
}
return Collections.emptyList();
}
@Override
public List<String> getServices() {
return Collections.singletonList("zipkin");
}
}

View File

@@ -0,0 +1,397 @@
/*
* Copyright 2013-2017 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.zipkin2;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.annotation.PostConstruct;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.sleuth.Sampler;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.SpanAdjuster;
import org.springframework.cloud.sleuth.SpanReporter;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.cloud.sleuth.sampler.AlwaysSampler;
import org.springframework.cloud.sleuth.zipkin2.ZipkinSpanListenerTests.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.test.context.junit4.SpringRunner;
import zipkin2.Endpoint;
import zipkin2.reporter.Reporter;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
import static org.assertj.core.api.BDDAssertions.then;
import static org.junit.Assert.assertEquals;
/**
* @author Dave Syer
*
*/
@SpringBootTest(classes = TestConfiguration.class)
@RunWith(SpringRunner.class)
public class ZipkinSpanListenerTests {
@Autowired Tracer tracer;
@Autowired TestConfiguration test;
@Autowired ZipkinSpanListener spanListener;
@Autowired Reporter<zipkin2.Span> spanReporter;
@Autowired MockEnvironment mockEnvironment;
@Autowired EndpointLocator endpointLocator;
@PostConstruct
public void init() {
this.test.zipkinSpans.clear();
}
Span parent = Span.builder().traceId(1L).name("http:parent").remote(true).build();
/** Sleuth timestamps are millisecond granularity while zipkin is microsecond. */
@Test
public void convertsTimestampToMicrosecondsAndSetsDurationToAccumulatedMicros() {
Span span = Span.builder().traceId(1L).name("http:api").build();
long start = System.currentTimeMillis();
span.logEvent("hystrix/retry"); // System.currentTimeMillis
span.stop();
zipkin2.Span result = this.spanListener.convert(span);
assertThat(result.timestamp())
.isEqualTo(span.getBegin() * 1000);
assertThat(result.duration())
.isEqualTo(span.getAccumulatedMicros());
assertThat(result.annotations().get(0).timestamp())
.isGreaterThanOrEqualTo(start * 1000)
.isLessThanOrEqualTo(System.currentTimeMillis() * 1000);
}
@Test
public void setsTheDurationToTheDifferenceBetweenCRandCS()
throws InterruptedException {
Span span = Span.builder().traceId(1L).name("http:api").build();
span.logEvent(Span.CLIENT_SEND);
Thread.sleep(10);
span.logEvent(Span.CLIENT_RECV);
Thread.sleep(20);
span.stop();
zipkin2.Span result = this.spanListener.convert(span);
assertThat(result.timestamp()).isEqualTo(span.getBegin() * 1000);
long clientSendTimestamp = span.logs().stream()
.filter(log -> Span.CLIENT_SEND.equals(log.getEvent())).findFirst().get()
.getTimestamp();
long clientRecvTimestamp = span.logs().stream()
.filter(log -> Span.CLIENT_RECV.equals(log.getEvent())).findFirst().get()
.getTimestamp();
assertThat(result.duration()).isNotEqualTo(span.getAccumulatedMicros())
.isEqualTo((clientRecvTimestamp - clientSendTimestamp) * 1000);
}
/** Zipkin's duration should only be set when the span is finished. */
@Test
public void doesntSetDurationWhenStillRunning() {
Span span = Span.builder().traceId(1L).name("http:api").build();
zipkin2.Span result = this.spanListener.convert(span);
assertThat(result.timestamp())
.isGreaterThan(0); // sanity check it did start
assertThat(result.duration())
.isNull();
}
/** Sleuth host corresponds to localEndpoint in zipkin. */
@Test
public void spanIncludesLocalEndpoint() {
this.parent.logEvent("hystrix/retry");
this.parent.tag("spring-boot/version", "1.3.1.RELEASE");
zipkin2.Span result = this.spanListener.convert(this.parent);
assertThat(result.localEndpoint())
.isEqualTo(this.spanListener.endpointLocator.local());
}
/** zipkin's Endpoint.serviceName should never be null. */
@Test
public void localEndpointIncludesServiceName() {
assertThat(this.spanListener.endpointLocator.local().serviceName())
.isNotEmpty();
}
@Test
public void spanWithoutAnnotationsStillHasEndpoint() {
Span context = this.tracer.createSpan("http:foo");
this.tracer.close(context);
assertEquals(1, this.test.zipkinSpans.size());
assertThat(this.test.zipkinSpans.get(0).localEndpoint())
.isNotNull();
}
@Test
public void rpcAnnotations() {
Span context = this.tracer.createSpan("http:child", this.parent);
context.logEvent(Span.CLIENT_SEND);
logServerReceived(this.parent);
logServerSent(this.spanListener, this.parent);
this.tracer.close(context);
assertEquals(2, this.test.zipkinSpans.size());
}
void logServerReceived(Span parent) {
if (parent != null && parent.isRemote()) {
parent.logEvent(Span.SERVER_RECV);
}
}
void logServerSent(SpanReporter spanReporter, Span parent) {
if (parent != null && parent.isRemote()) {
parent.logEvent(Span.SERVER_SEND);
spanReporter.report(parent);
}
}
@Test
public void localComponentNotNeeded() {
this.parent.logEvent("hystrix/retry");
this.parent.stop();
zipkin2.Span result = this.spanListener.convert(this.parent);
assertThat(result.tags())
.isEmpty();
}
@Test
public void addsRemoteEndpointWhenClientLogIsPresentAndPeerServiceIsPresent() {
this.parent.logEvent("cs");
this.parent.tag(Span.SPAN_PEER_SERVICE_TAG_NAME, "fooservice");
this.parent.stop();
zipkin2.Span result = this.spanListener.convert(this.parent);
assertThat(result.remoteEndpoint())
.isEqualTo(Endpoint.newBuilder().serviceName("fooservice").build());
}
@Test
public void doesNotAddRemoteEndpointTagIfClientLogIsPresent() {
this.parent.logEvent("cs");
this.parent.stop();
zipkin2.Span result = this.spanListener.convert(this.parent);
assertThat(result.remoteEndpoint())
.isNull();
}
@Test
public void converts128BitTraceId() {
Span span = Span.builder().traceIdHigh(1L).traceId(2L).spanId(3L).name("foo").build();
zipkin2.Span result = this.spanListener.convert(span);
assertThat(result.traceId())
.isEqualTo("00000000000000010000000000000002");
}
@Test
public void shouldReuseServerAddressTag() {
this.parent.logEvent("cs");
this.parent.tag(Span.SPAN_PEER_SERVICE_TAG_NAME, "fooservice");
this.parent.stop();
zipkin2.Span result = this.spanListener.convert(this.parent);
assertThat(result.remoteEndpoint())
.isEqualTo(Endpoint.newBuilder().serviceName("fooservice").build());
}
@Test
public void shouldNotReportToZipkinWhenSpanIsNotExportable() {
Span span = Span.builder().exportable(false).build();
this.spanListener.report(span);
assertThat(this.test.zipkinSpans).isEmpty();
}
@Test
public void shouldAddClientServiceIdTagWhenSpanContainsRpcEvent() {
this.parent.logEvent(Span.CLIENT_SEND);
this.mockEnvironment.setProperty("vcap.application.instance_id", "foo");
zipkin2.Span result = this.spanListener.convert(this.parent);
assertThat(result.tags())
.containsExactly(entry(Span.INSTANCEID, "foo"));
}
@Test
public void shouldNotAddAnyServiceIdTagWhenSpanContainsRpcEventAndThereIsNoEnvironment() {
this.parent.logEvent(Span.CLIENT_RECV);
ZipkinSpanListener spanListener = new ZipkinSpanListener(this.spanReporter,
this.endpointLocator, null, new ArrayList<>());
zipkin2.Span result = spanListener.convert(this.parent);
assertThat(result.tags())
.isEmpty();
}
@Test
public void should_adjust_span_before_reporting_it() {
this.parent.logEvent(Span.CLIENT_RECV);
ZipkinSpanListener spanListener = new ZipkinSpanListener(this.spanReporter,
this.endpointLocator, null, Collections.<SpanAdjuster>singletonList(
span -> Span.builder().from(span).name("foo").build())) {
@Override String defaultInstanceId() {
return "foo";
}
};
zipkin2.Span result = spanListener.convert(this.parent);
assertThat(result.name()).isEqualTo("foo");
}
/** Zipkin will take care of processing the shared flag wrt timestamp authority */
@Test
public void setsSharedFlag() {
Span span = Span.builder()
.name("foo")
.exportable(false)
.remote(false)
.shared(true)
.build();
span.stop();
zipkin2.Span result = this.spanListener.convert(span);
assertThat(result.duration()).isNotNull();
assertThat(result.timestamp()).isNotNull();
assertThat(result.shared()).isTrue();
}
@Test
public void should_create_server_span() {
Span span = tracer.createSpan("get");
span.logEvent("sr");
span.logEvent("ss");
span.stop();
zipkin2.Span result = this.spanListener.convert(span);
then(result.kind()).isEqualTo(zipkin2.Span.Kind.SERVER);
then(result.annotations()).isEmpty();
}
@Test
public void should_create_client_span() {
Span span = tracer.createSpan("redis");
span.logEvent("cs");
span.logEvent("cr");
span.stop();
zipkin2.Span result = this.spanListener.convert(span);
then(result.kind()).isEqualTo(zipkin2.Span.Kind.CLIENT);
then(result.annotations()).isEmpty();
}
@Test
public void should_create_produceer_span() {
Span span = tracer.createSpan("produce");
span.logEvent("ms");
span.stop();
zipkin2.Span result = this.spanListener.convert(span);
then(result.kind()).isEqualTo(zipkin2.Span.Kind.PRODUCER);
then(result.annotations()).isEmpty();
}
@Test
public void should_create_consumer_span() {
Span span = tracer.createSpan("consume");
span.logEvent("mr");
span.stop();
zipkin2.Span result = this.spanListener.convert(span);
then(result.kind()).isEqualTo(zipkin2.Span.Kind.CONSUMER);
then(result.annotations()).isEmpty();
}
@Test
public void should_change_the_service_name_in_zipkin_to_the_manually_provided_one() {
// tag::service_name[]
Span span = tracer.createSpan("redis");
try {
span.tag("redis.op", "get");
span.tag("lc", "redis");
span.logEvent("cs");
// call redis service e.g
// return (SomeObj) redisTemplate.opsForHash().get("MYHASH", someObjKey);
} finally {
span.tag("peer.service", "redis");
span.tag("peer.ipv4", "1.2.3.4");
span.tag("peer.port", "1234");
span.logEvent("cr");
span.stop();
}
// end::service_name[]
zipkin2.Span result = this.spanListener.convert(span);
then(result.remoteEndpoint())
.isEqualTo(Endpoint.newBuilder().serviceName("redis").ip("1.2.3.4").port(1234).build());
then(result.tags())
.doesNotContainKeys("peer.service", "peer.ipv4", "peer.port");
}
@Configuration
@EnableAutoConfiguration
protected static class TestConfiguration {
private List<zipkin2.Span> zipkinSpans = new ArrayList<>();
@Bean
public Sampler sampler() {
return new AlwaysSampler();
}
@Bean
public Reporter<zipkin2.Span> reporter() {
return this.zipkinSpans::add;
}
@Bean @Primary MockEnvironment mockEnvironment() {
return new MockEnvironment();
}
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2013-2017 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.zipkin2;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ZipkinWithDisabledSleuthTests.Config.class)
@TestPropertySource(properties = "spring.sleuth.enabled=false")
public class ZipkinWithDisabledSleuthTests {
@Test public void shouldStartContext() {
}
@EnableAutoConfiguration
static class Config {
}
}

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth</artifactId>
<version>1.3.0.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<artifactId>spring-cloud-starter-zipkin2</artifactId>
<name>spring-cloud-starter-zipkin2</name>
<description>Spring Cloud Starter Zipkin v2</description>
<properties>
<main.basedir>${basedir}/../..</main.basedir>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin2</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1 @@
provides: spring-platform-netflix-core, eureka-client