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;
+ }
+ }
+}
diff --git a/spring-cloud-sleuth-zipkin2/src/main/java/org/springframework/cloud/sleuth/zipkin2/ZipkinProperties.java b/spring-cloud-sleuth-zipkin2/src/main/java/org/springframework/cloud/sleuth/zipkin2/ZipkinProperties.java
new file mode 100644
index 000000000..82a88791f
--- /dev/null
+++ b/spring-cloud-sleuth-zipkin2/src/main/java/org/springframework/cloud/sleuth/zipkin2/ZipkinProperties.java
@@ -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;
+ }
+ }
+ }
+}
diff --git a/spring-cloud-sleuth-zipkin2/src/main/java/org/springframework/cloud/sleuth/zipkin2/ZipkinRestTemplateCustomizer.java b/spring-cloud-sleuth-zipkin2/src/main/java/org/springframework/cloud/sleuth/zipkin2/ZipkinRestTemplateCustomizer.java
new file mode 100644
index 000000000..e8b7423f1
--- /dev/null
+++ b/spring-cloud-sleuth-zipkin2/src/main/java/org/springframework/cloud/sleuth/zipkin2/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.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.
+ *
+ * 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-zipkin2/src/main/java/org/springframework/cloud/sleuth/zipkin2/ZipkinSpanListener.java b/spring-cloud-sleuth-zipkin2/src/main/java/org/springframework/cloud/sleuth/zipkin2/ZipkinSpanListener.java
new file mode 100644
index 000000000..333dc7f53
--- /dev/null
+++ b/spring-cloud-sleuth-zipkin2/src/main/java/org/springframework/cloud/sleuth/zipkin2/ZipkinSpanListener.java
@@ -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 reporter;
+ private final Environment environment;
+ private final List 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 reporter, EndpointLocator endpointLocator,
+ Environment environment, List spanAdjusters) {
+ this.reporter = reporter;
+ this.endpointLocator = endpointLocator;
+ this.environment = environment;
+ this.spanAdjusters = spanAdjusters;
+ }
+
+ /**
+ * Converts a given Sleuth span to a Zipkin Span.
+ *
+ * - Set ids, etc
+ *
- Create timeline annotations based on data from Span object.
+ *
- Create tags based on data from Span object.
+ *
+ */
+ // 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 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");
+ }
+ }
+ }
+}
diff --git a/spring-cloud-sleuth-zipkin2/src/main/resources/META-INF/spring.factories b/spring-cloud-sleuth-zipkin2/src/main/resources/META-INF/spring.factories
new file mode 100644
index 000000000..7a0e23d33
--- /dev/null
+++ b/spring-cloud-sleuth-zipkin2/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,3 @@
+# Auto Configuration
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+org.springframework.cloud.sleuth.zipkin2.ZipkinAutoConfiguration
\ No newline at end of file
diff --git a/spring-cloud-sleuth-zipkin2/src/test/java/org/springframework/cloud/sleuth/zipkin2/DefaultEndpointLocatorConfigurationTest.java b/spring-cloud-sleuth-zipkin2/src/test/java/org/springframework/cloud/sleuth/zipkin2/DefaultEndpointLocatorConfigurationTest.java
new file mode 100644
index 000000000..e567c2758
--- /dev/null
+++ b/spring-cloud-sleuth-zipkin2/src/test/java/org/springframework/cloud/sleuth/zipkin2/DefaultEndpointLocatorConfigurationTest.java
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/spring-cloud-sleuth-zipkin2/src/test/java/org/springframework/cloud/sleuth/zipkin2/ZipkinAutoConfigurationTests.java b/spring-cloud-sleuth-zipkin2/src/test/java/org/springframework/cloud/sleuth/zipkin2/ZipkinAutoConfigurationTests.java
new file mode 100644
index 000000000..45ebea279
--- /dev/null
+++ b/spring-cloud-sleuth-zipkin2/src/test/java/org/springframework/cloud/sleuth/zipkin2/ZipkinAutoConfigurationTests.java
@@ -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");
+ }
+}
diff --git a/spring-cloud-sleuth-zipkin2/src/test/java/org/springframework/cloud/sleuth/zipkin2/ZipkinDiscoveryClientTests.java b/spring-cloud-sleuth-zipkin2/src/test/java/org/springframework/cloud/sleuth/zipkin2/ZipkinDiscoveryClientTests.java
new file mode 100644
index 000000000..5162d22e8
--- /dev/null
+++ b/spring-cloud-sleuth-zipkin2/src/test/java/org/springframework/cloud/sleuth/zipkin2/ZipkinDiscoveryClientTests.java
@@ -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 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 getMetadata() {
+ return null;
+ }
+ });
+ }
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List getServices() {
+ return Collections.singletonList("zipkin");
+ }
+
+}
\ No newline at end of file
diff --git a/spring-cloud-sleuth-zipkin2/src/test/java/org/springframework/cloud/sleuth/zipkin2/ZipkinSpanListenerTests.java b/spring-cloud-sleuth-zipkin2/src/test/java/org/springframework/cloud/sleuth/zipkin2/ZipkinSpanListenerTests.java
new file mode 100644
index 000000000..ec409647f
--- /dev/null
+++ b/spring-cloud-sleuth-zipkin2/src/test/java/org/springframework/cloud/sleuth/zipkin2/ZipkinSpanListenerTests.java
@@ -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 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.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 zipkinSpans = new ArrayList<>();
+
+ @Bean
+ public Sampler sampler() {
+ return new AlwaysSampler();
+ }
+
+ @Bean
+ public Reporter reporter() {
+ return this.zipkinSpans::add;
+ }
+
+ @Bean @Primary MockEnvironment mockEnvironment() {
+ return new MockEnvironment();
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/spring-cloud-sleuth-zipkin2/src/test/java/org/springframework/cloud/sleuth/zipkin2/ZipkinWithDisabledSleuthTests.java b/spring-cloud-sleuth-zipkin2/src/test/java/org/springframework/cloud/sleuth/zipkin2/ZipkinWithDisabledSleuthTests.java
new file mode 100644
index 000000000..f4066684b
--- /dev/null
+++ b/spring-cloud-sleuth-zipkin2/src/test/java/org/springframework/cloud/sleuth/zipkin2/ZipkinWithDisabledSleuthTests.java
@@ -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 {
+ }
+}
diff --git a/spring-cloud-starter-zipkin2/pom.xml b/spring-cloud-starter-zipkin2/pom.xml
new file mode 100644
index 000000000..d7cd0f8f3
--- /dev/null
+++ b/spring-cloud-starter-zipkin2/pom.xml
@@ -0,0 +1,27 @@
+
+
+ 4.0.0
+
+ org.springframework.cloud
+ spring-cloud-sleuth
+ 1.3.0.BUILD-SNAPSHOT
+ ..
+
+ spring-cloud-starter-zipkin2
+ spring-cloud-starter-zipkin2
+ Spring Cloud Starter Zipkin v2
+
+ ${basedir}/../..
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-sleuth
+
+
+ org.springframework.cloud
+ spring-cloud-sleuth-zipkin2
+
+
+
diff --git a/spring-cloud-starter-zipkin2/src/main/resources/META-INF/spring.provides b/spring-cloud-starter-zipkin2/src/main/resources/META-INF/spring.provides
new file mode 100644
index 000000000..3dc45a036
--- /dev/null
+++ b/spring-cloud-starter-zipkin2/src/main/resources/META-INF/spring.provides
@@ -0,0 +1 @@
+provides: spring-platform-netflix-core, eureka-client
\ No newline at end of file