diff --git a/pom.xml b/pom.xml index 408b2fe15..d7db7f6a4 100644 --- a/pom.xml +++ b/pom.xml @@ -29,11 +29,13 @@ spring-cloud-sleuth-dependencies spring-cloud-sleuth-core spring-cloud-sleuth-zipkin + spring-cloud-sleuth-zipkin2 spring-cloud-sleuth-stream spring-cloud-sleuth-reactor spring-cloud-sleuth-zipkin-stream spring-cloud-starter-sleuth spring-cloud-starter-zipkin + spring-cloud-starter-zipkin2 spring-cloud-sleuth-samples docs diff --git a/spring-cloud-sleuth-dependencies/pom.xml b/spring-cloud-sleuth-dependencies/pom.xml index 57f966d32..c2fef981f 100644 --- a/spring-cloud-sleuth-dependencies/pom.xml +++ b/spring-cloud-sleuth-dependencies/pom.xml @@ -14,8 +14,9 @@ spring-cloud-sleuth-dependencies Spring Cloud Sleuth Dependencies - 1.28.0 - 1.0.0 + 2.1.0 + 1.1.1 + 2.0.2 @@ -34,6 +35,11 @@ spring-cloud-sleuth-zipkin ${project.version} + + org.springframework.cloud + spring-cloud-sleuth-zipkin2 + ${project.version} + org.springframework.cloud spring-cloud-sleuth-stream @@ -49,6 +55,11 @@ spring-cloud-starter-zipkin ${project.version} + + org.springframework.cloud + spring-cloud-starter-zipkin2 + ${project.version} + org.springframework.cloud spring-cloud-starter-sleuth @@ -59,6 +70,11 @@ zipkin ${zipkin.version} + + io.zipkin.zipkin2 + zipkin + ${zipkin.version} + io.zipkin.java zipkin-server @@ -84,6 +100,11 @@ zipkin-reporter ${zipkin-reporter.version} + + io.zipkin.reporter2 + zipkin-reporter + ${zipkin-reporter2.version} + diff --git a/spring-cloud-sleuth-samples/pom.xml b/spring-cloud-sleuth-samples/pom.xml index 60477744f..2d73e2731 100644 --- a/spring-cloud-sleuth-samples/pom.xml +++ b/spring-cloud-sleuth-samples/pom.xml @@ -23,7 +23,7 @@ spring-cloud-sleuth-sample-websocket spring-cloud-sleuth-sample-feign spring-cloud-sleuth-sample-ribbon - spring-cloud-sleuth-sample-zipkin + spring-cloud-sleuth-sample-zipkin2 spring-cloud-sleuth-sample-stream spring-cloud-sleuth-sample-zipkin-stream @@ -59,12 +59,17 @@ io.zipkin.java zipkin - 1.28.0 + 2.1.0 + + + io.zipkin.zipkin2 + zipkin + 2.1.0 io.zipkin.java zipkin-server - 1.28.0 + 2.1.0 diff --git a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/pom.xml b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin2/pom.xml similarity index 92% rename from spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/pom.xml rename to spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin2/pom.xml index 8e47c1773..776b0fd75 100644 --- a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/pom.xml +++ b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin2/pom.xml @@ -19,10 +19,10 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - spring-cloud-sleuth-sample-zipkin + spring-cloud-sleuth-sample-zipkin2 jar - spring-cloud-sleuth-sample-zipkin - Spring Cloud Sleuth Sample + spring-cloud-sleuth-sample-zipkin2 + Spring Cloud Sleuth Sample Zipkin v2 org.springframework.cloud @@ -77,7 +77,7 @@ org.springframework.cloud - spring-cloud-sleuth-zipkin + spring-cloud-sleuth-zipkin2 org.springframework.boot @@ -111,7 +111,7 @@ com.squareup.okhttp3 okhttp - 3.6.0 + 3.9.0 test diff --git a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/src/main/java/sample/SampleBackground.java b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin2/src/main/java/sample/SampleBackground.java similarity index 100% rename from spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/src/main/java/sample/SampleBackground.java rename to spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin2/src/main/java/sample/SampleBackground.java diff --git a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/src/main/java/sample/SampleController.java b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin2/src/main/java/sample/SampleController.java similarity index 100% rename from spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/src/main/java/sample/SampleController.java rename to spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin2/src/main/java/sample/SampleController.java diff --git a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/src/main/java/sample/SampleZipkinApplication.java b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin2/src/main/java/sample/SampleZipkinApplication.java similarity index 86% rename from spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/src/main/java/sample/SampleZipkinApplication.java rename to spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin2/src/main/java/sample/SampleZipkinApplication.java index 89cb5a070..df5a11112 100644 --- a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/src/main/java/sample/SampleZipkinApplication.java +++ b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin2/src/main/java/sample/SampleZipkinApplication.java @@ -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 spanReporter() { + return Reporter.CONSOLE; } } diff --git a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/src/main/resources/application.yml b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin2/src/main/resources/application.yml similarity index 100% rename from spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/src/main/resources/application.yml rename to spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin2/src/main/resources/application.yml diff --git a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/src/test/java/integration/ZipkinTests.java b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin2/src/test/java/integration/ZipkinTests.java similarity index 98% rename from spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/src/test/java/integration/ZipkinTests.java rename to spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin2/src/test/java/integration/ZipkinTests.java index 246e891df..fbbf8e959 100644 --- a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/src/test/java/integration/ZipkinTests.java +++ b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin2/src/test/java/integration/ZipkinTests.java @@ -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; diff --git a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/src/test/java/sample/SampleSleuthApplicationTests.java b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin2/src/test/java/sample/SampleSleuthApplicationTests.java similarity index 100% rename from spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/src/test/java/sample/SampleSleuthApplicationTests.java rename to spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin2/src/test/java/sample/SampleSleuthApplicationTests.java diff --git a/spring-cloud-sleuth-zipkin2/pom.xml b/spring-cloud-sleuth-zipkin2/pom.xml new file mode 100644 index 000000000..bacb0bf46 --- /dev/null +++ b/spring-cloud-sleuth-zipkin2/pom.xml @@ -0,0 +1,133 @@ + + + + + 4.0.0 + + spring-cloud-sleuth-zipkin2 + jar + Spring Cloud Sleuth Zipkin v2 + Spring Cloud Sleuth Zipkin v2 + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + + + + org.springframework.cloud + spring-cloud-sleuth + 1.3.0.BUILD-SNAPSHOT + .. + + + + + org.springframework.cloud + spring-cloud-sleuth-core + + + org.springframework + spring-web + + + org.springframework.cloud + spring-cloud-commons + + + org.springframework.boot + spring-boot-actuator + true + + + org.springframework.boot + spring-boot-starter-logging + true + + + org.springframework.boot + spring-boot-configuration-processor + true + + + io.zipkin.zipkin2 + zipkin + + + io.zipkin.reporter2 + zipkin-reporter + + + org.springframework + spring-messaging + test + + + org.springframework.boot + spring-boot-starter-test + test + + + org.assertj + assertj-core + test + + + io.zipkin.java + zipkin-junit + test + + + org.awaitility + awaitility + test + + + + com.squareup.okhttp3 + okhttp + 3.9.0 + test + + + com.squareup.okhttp3 + mockwebserver + 3.9.0 + test + + + org.aspectj + aspectjrt + test + + + org.aspectj + aspectjweaver + test + + + + diff --git a/spring-cloud-sleuth-zipkin2/src/main/java/org/springframework/cloud/sleuth/zipkin2/DefaultEndpointLocator.java b/spring-cloud-sleuth-zipkin2/src/main/java/org/springframework/cloud/sleuth/zipkin2/DefaultEndpointLocator.java new file mode 100644 index 000000000..03d79adc1 --- /dev/null +++ b/spring-cloud-sleuth-zipkin2/src/main/java/org/springframework/cloud/sleuth/zipkin2/DefaultEndpointLocator.java @@ -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: + * + *
    + *
  • serviceName - from {@link ServerProperties} or {@link Registration}
  • + *
  • ip - from {@link ServerProperties}
  • + *
  • port - from lazily assigned port or {@link ServerProperties}
  • + *
+ * + * 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; + } +} diff --git a/spring-cloud-sleuth-zipkin2/src/main/java/org/springframework/cloud/sleuth/zipkin2/DefaultZipkinRestTemplateCustomizer.java b/spring-cloud-sleuth-zipkin2/src/main/java/org/springframework/cloud/sleuth/zipkin2/DefaultZipkinRestTemplateCustomizer.java new file mode 100644 index 000000000..4e4a457d6 --- /dev/null +++ b/spring-cloud-sleuth-zipkin2/src/main/java/org/springframework/cloud/sleuth/zipkin2/DefaultZipkinRestTemplateCustomizer.java @@ -0,0 +1,64 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.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()); + } + } +} diff --git a/spring-cloud-sleuth-zipkin2/src/main/java/org/springframework/cloud/sleuth/zipkin2/EndpointLocator.java b/spring-cloud-sleuth-zipkin2/src/main/java/org/springframework/cloud/sleuth/zipkin2/EndpointLocator.java new file mode 100644 index 000000000..8b6a78172 --- /dev/null +++ b/spring-cloud-sleuth-zipkin2/src/main/java/org/springframework/cloud/sleuth/zipkin2/EndpointLocator.java @@ -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(); + +} diff --git a/spring-cloud-sleuth-zipkin2/src/main/java/org/springframework/cloud/sleuth/zipkin2/ReporterMetricsAdapter.java b/spring-cloud-sleuth-zipkin2/src/main/java/org/springframework/cloud/sleuth/zipkin2/ReporterMetricsAdapter.java new file mode 100644 index 000000000..b8f8b14b7 --- /dev/null +++ b/spring-cloud-sleuth-zipkin2/src/main/java/org/springframework/cloud/sleuth/zipkin2/ReporterMetricsAdapter.java @@ -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) { + } +} diff --git a/spring-cloud-sleuth-zipkin2/src/main/java/org/springframework/cloud/sleuth/zipkin2/RestTemplateSender.java b/spring-cloud-sleuth-zipkin2/src/main/java/org/springframework/cloud/sleuth/zipkin2/RestTemplateSender.java new file mode 100644 index 000000000..e83bde0bb --- /dev/null +++ b/spring-cloud-sleuth-zipkin2/src/main/java/org/springframework/cloud/sleuth/zipkin2/RestTemplateSender.java @@ -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 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 spans) { + return encoding().listSizeInBytes(spans); + } + + /** + * close is typically called from a different thread + */ + transient boolean closeCalled; + + @Override public Call sendSpans(List 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 requestEntity = new RequestEntity<>(json, httpHeaders, + HttpMethod.POST, URI.create(this.url)); + this.restTemplate.exchange(requestEntity, String.class); + } + + class HttpPostCall extends BaseCall { + 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 callback) { + try { + post(this.message); + callback.onSuccess(null); + } + catch (RuntimeException | Error e) { + callback.onError(e); + } + } + + @Override public Call clone() { + return new HttpPostCall(this.message); + } + } +} diff --git a/spring-cloud-sleuth-zipkin2/src/main/java/org/springframework/cloud/sleuth/zipkin2/ZipkinAutoConfiguration.java b/spring-cloud-sleuth-zipkin2/src/main/java/org/springframework/cloud/sleuth/zipkin2/ZipkinAutoConfiguration.java new file mode 100644 index 000000000..1363481a2 --- /dev/null +++ b/spring-cloud-sleuth-zipkin2/src/main/java/org/springframework/cloud/sleuth/zipkin2/ZipkinAutoConfiguration.java @@ -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 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 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 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 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 doExecute(URI originalUrl, HttpMethod method, + RequestCallback requestCallback, + ResponseExtractor 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