diff --git a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-messaging/src/test/java/integration/MessagingApplicationDockerTests.java b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-messaging/src/test/java/integration/MessagingApplicationDockerTests.java index 0a76e75af..2e571eb9c 100644 --- a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-messaging/src/test/java/integration/MessagingApplicationDockerTests.java +++ b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-messaging/src/test/java/integration/MessagingApplicationDockerTests.java @@ -15,7 +15,6 @@ */ package integration; -import com.github.kristofa.brave.SpanCollector; import com.twitter.zipkin.gen.BinaryAnnotation; import com.twitter.zipkin.gen.Span; import lombok.extern.slf4j.Slf4j; @@ -26,16 +25,13 @@ import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.boot.test.WebIntegrationTest; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.util.JdkIdGenerator; import org.springframework.util.StringUtils; -import org.springframework.web.client.RestTemplate; import org.testcontainers.containers.DockerComposeContainer; import sample.SampleMessagingApplication; import tools.AbstractIntegrationTest; -import tools.AssertingRestTemplate; import tools.IntegrationTestSpanCollector; import tools.RequestSendingRunnable; @@ -45,14 +41,14 @@ import java.util.Collection; import static org.assertj.core.api.BDDAssertions.then; @RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(classes = { MessagingApplicationDockerTests.Config.class, SampleMessagingApplication.class }) +@SpringApplicationConfiguration(classes = { AbstractIntegrationTest.Config.class, SampleMessagingApplication.class }) @WebIntegrationTest +@TestPropertySource(properties="sample.zipkin.enabled=true") @Slf4j public class MessagingApplicationDockerTests extends AbstractIntegrationTest { private static int port = 3381; private static String sampleAppUrl = "http://localhost:" + port; - RestTemplate restTemplate = new AssertingRestTemplate(); @Autowired IntegrationTestSpanCollector integrationTestSpanCollector; @ClassRule @@ -106,10 +102,4 @@ public class MessagingApplicationDockerTests extends AbstractIntegrationTest { then(integrationTestSpanCollector.hashedSpans.stream().allMatch(span -> span.getTrace_id() == zipkinHashedTraceId(traceId))).isTrue(); } - @Configuration - static class Config { - @Bean SpanCollector integrationTestSpanCollector() { - return new IntegrationTestSpanCollector(); - } - } } diff --git a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-test-core/pom.xml b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-test-core/pom.xml index 3d6490461..5899d8fbe 100644 --- a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-test-core/pom.xml +++ b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-test-core/pom.xml @@ -63,6 +63,12 @@ junit junit + + org.hamcrest + hamcrest-core + 1.3 + compile + org.springframework.boot spring-boot-starter-test @@ -102,6 +108,12 @@ ${testcontainers.jackson.version} compile + + io.zipkin + zipkin-java-core + 0.1.1 + compile + \ No newline at end of file diff --git a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-test-core/src/main/java/tools/AbstractIntegrationTest.java b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-test-core/src/main/java/tools/AbstractIntegrationTest.java index 80a94c073..23f9e2390 100644 --- a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-test-core/src/main/java/tools/AbstractIntegrationTest.java +++ b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-test-core/src/main/java/tools/AbstractIntegrationTest.java @@ -15,23 +15,35 @@ */ package tools; +import com.github.kristofa.brave.SpanCollector; +import com.github.kristofa.brave.scribe.ScribeSpanCollector; import com.jayway.awaitility.Awaitility; import com.jayway.awaitility.core.ConditionFactory; -import org.junit.experimental.categories.Category; -import org.springframework.beans.factory.annotation.Value; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.sleuth.zipkin.ZipkinProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.*; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestTemplate; + +import java.net.URI; import static java.util.concurrent.TimeUnit.SECONDS; +import static org.assertj.core.api.BDDAssertions.then; /** * @author Marcin Grzejszczak */ -@Category(DockerTests.class) -abstract public class AbstractIntegrationTest { +@Slf4j +public abstract class AbstractIntegrationTest { - @Value("${test.pollinterval:1}") protected int pollInterval; - @Value("${test.timeout:10}") protected int timeout; + protected static int pollInterval = 1; + protected static int timeout = 120; + protected RestTemplate restTemplate = new AssertingRestTemplate(); - protected ConditionFactory await() { + protected static ConditionFactory await() { return Awaitility.await().pollInterval(pollInterval, SECONDS).atMost(timeout, SECONDS); } @@ -47,4 +59,120 @@ abstract public class AbstractIntegrationTest { } return h; } + + String zipkinHashedHexStringTraceId(String traceId) { + long hashedTraceId = zipkinHashedTraceId(traceId); + return Long.toHexString(hashedTraceId); + } + + protected static String getDockerUrl() { + URI dockerUri = getDockerURI(); + if (StringUtils.isEmpty(dockerUri.getScheme())) { + return "http://localhost"; + } + return "http://" + dockerUri.getHost(); + } + + protected static URI getDockerURI() { + String dockerHost = System.getenv("DOCKER_HOST"); + if (StringUtils.isEmpty(dockerHost)) { + return URI.create("http://localhost"); + } + return URI.create(dockerHost); + } + + protected Runnable zipkinQueryServerIsUp() { + return new Runnable() { + @Override + public void run() { + ResponseEntity response = endpointToCheckZipkinQueryHealth(); + log.info("Response from the Zipkin query with current traces [{}]", response); + then(response.getStatusCode()).isEqualTo(HttpStatus.OK); + log.info("Zipkin query server is up!"); + } + }; + } + + protected Runnable zipkinCollectorServerIsUp() { + return new Runnable() { + @Override + public void run() { + ResponseEntity response = endpointToCheckZipkinCollectorHealth(); + log.info("Response from the Zipkin collector's health endpoint is [{}]", response); + then(response.getStatusCode()).isEqualTo(HttpStatus.OK); + log.info("Zipkin collector server is up!"); + } + }; + } + + protected ResponseEntity endpointToCheckZipkinQueryHealth() { + URI uri = URI.create(getZipkinServicesQueryUrl()); + log.info("Sending request to the Zipkin query service [{}]", uri); + return exchangeRequest(uri); + } + + protected ResponseEntity endpointToCheckZipkinCollectorHealth() { + URI uri = URI.create(getZipkinCollectorHealthUrl()); + log.info("Sending request to the Zipkin collector service [{}]", uri); + return exchangeRequest(uri); + } + + protected ResponseEntity checkStateOfTheTraceId(String traceId) { + String hexTraceId = zipkinHashedHexStringTraceId(traceId); + URI uri = URI.create(getZipkinTraceQueryUrl() + hexTraceId); + log.info("Sending request to the Zipkin query service [{}]. Checking presence of trace id [{}] and its hex version [{}]", uri, traceId, hexTraceId); + return exchangeRequest(uri); + } + + protected ResponseEntity exchangeRequest(URI uri) { + return restTemplate.exchange( + new RequestEntity<>(new HttpHeaders(), HttpMethod.GET, uri), String.class + ); + } + + protected String getZipkinTraceQueryUrl() { + return getDockerUrl() + ":9411/api/v1/trace/"; + } + + protected String getZipkinServicesQueryUrl() { + return getDockerUrl() + ":9411/api/v1/services"; + } + + protected String getZipkinCollectorHealthUrl() { + return getDockerUrl() + ":9900/health"; + } + + @Configuration + public static class Config { + @Bean + SpanCollector integrationTestSpanCollector() { + return new IntegrationTestSpanCollector(); + } + } + + @Configuration + @Slf4j + public static class ZipkinConfig { + @Bean + @SneakyThrows + public ScribeSpanCollector spanCollector(final ZipkinProperties zipkin) { + await().until(new Runnable() { + @Override + public void run() { + try { + ZipkinConfig.this.getSpanCollector(zipkin); + } catch (Exception e) { + log.error("Exception occurred while trying to connect to zipkin [" + e.getCause() + "]"); + throw new AssertionError(e); + } + } + }); + return getSpanCollector(zipkin); + } + + private ScribeSpanCollector getSpanCollector(ZipkinProperties zipkin) { + return new ScribeSpanCollector(getDockerURI().getHost(), + zipkin.getPort(), zipkin.getCollector()); + } + } } diff --git a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-test-core/src/main/java/tools/AssertingRestTemplate.java b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-test-core/src/main/java/tools/AssertingRestTemplate.java index 1d868b08a..b7142c9b3 100644 --- a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-test-core/src/main/java/tools/AssertingRestTemplate.java +++ b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-test-core/src/main/java/tools/AssertingRestTemplate.java @@ -48,7 +48,7 @@ public class AssertingRestTemplate extends RestTemplate { try { return super.doExecute(url, method, requestCallback, responseExtractor); } catch (Exception e) { - log.error("Exception occurred while sending the message", e); + log.error("Exception occurred while sending the message to uri [" + url +"]. Exception [" + e.getCause() + "]"); throw new AssertionError(e); } } diff --git a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-test-core/src/main/java/tools/DockerTests.java b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-test-core/src/main/java/tools/DockerTests.java deleted file mode 100644 index c53a9fb99..000000000 --- a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-test-core/src/main/java/tools/DockerTests.java +++ /dev/null @@ -1,7 +0,0 @@ -package tools; - -/** - * @author Marcin Grzejszczak - */ -interface DockerTests { -} diff --git a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/pom.xml b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/pom.xml index 8e4a9d787..580055ee0 100644 --- a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/pom.xml +++ b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/pom.xml @@ -69,6 +69,29 @@ spring-boot-starter-test test + + org.springframework.cloud + spring-cloud-sleuth-sample-test-core + test + + + com.fasterxml.jackson.core + jackson-databind + ${testcontainers.jackson.version} + compile + + + com.fasterxml.jackson.core + jackson-core + ${testcontainers.jackson.version} + compile + + + com.fasterxml.jackson.core + jackson-annotations + ${testcontainers.jackson.version} + compile + diff --git a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/src/main/resources/logback.xml b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/src/main/resources/logback.xml new file mode 100644 index 000000000..3cee99001 --- /dev/null +++ b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/src/main/resources/logback.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/src/test/java/integration/ZipkinDockerTests.java b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/src/test/java/integration/ZipkinDockerTests.java new file mode 100644 index 000000000..867fa19d7 --- /dev/null +++ b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/src/test/java/integration/ZipkinDockerTests.java @@ -0,0 +1,139 @@ +/* + * 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 integration; + +import io.zipkin.Codec; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.test.WebIntegrationTest; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.util.JdkIdGenerator; +import org.testcontainers.containers.DockerComposeContainer; +import sample.SampleZipkinApplication; +import tools.AbstractIntegrationTest; +import tools.RequestSendingRunnable; + +import java.io.File; +import java.util.*; +import java.util.stream.Collectors; + +import static org.assertj.core.api.BDDAssertions.then; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = { AbstractIntegrationTest.ZipkinConfig.class, SampleZipkinApplication.class }) +@WebIntegrationTest +@TestPropertySource(properties="sample.zipkin.enabled=true") +@Slf4j +public class ZipkinDockerTests extends AbstractIntegrationTest { + + private static final String APP_NAME = "testsleuthzipkin"; + private static int port = 3380; + private static String sampleAppUrl = "http://localhost:" + port; + + @ClassRule + public static DockerComposeContainer environment = + new DockerComposeContainer(new File("src/test/resources/docker-compose.yml")) + .withExposedService("rabbitmq_1", 5672) + .withExposedService("collector_1", 9410) + .withExposedService("collector_1", 9900) + .withExposedService("mysql_1", 3306) + .withExposedService("query_1", 9411) + .withExposedService("query_1", 9901); + + @Before + public void setup() { + await().until(zipkinQueryServerIsUp()); + await().until(zipkinCollectorServerIsUp()); + } + + @Test + @SneakyThrows + public void should_propagate_spans_to_zipkin() { + String traceId = new JdkIdGenerator().generateId().toString(); + + httpMessageWithTraceIdInHeadersIsSuccessfullySent(sampleAppUrl + "/hi2", traceId); + + await().until(() -> { + allSpansWereRegisteredInZipkinWithTraceIdEqualTo(traceId); + }); + } + + private void allSpansWereRegisteredInZipkinWithTraceIdEqualTo(String traceId) { + ResponseEntity response = checkStateOfTheTraceId(traceId); + log.info("Response from the Zipkin query service about the trace id [{}] for trace with id [{}]", response, traceId); + then(response.getStatusCode()).isEqualTo(HttpStatus.OK); + then(response.hasBody()).isTrue(); + List spans = Codec.JSON.readSpans(response.getBody().getBytes()); + List serviceNamesNotFoundInZipkin = serviceNamesNotFoundInZipkin(spans); + List spanNamesNotFoundInZipkin = annotationsNotFoundInZipkin(spans); + log.info("The following services were not found in Zipkin {}", serviceNamesNotFoundInZipkin); + log.info("The following spans were not found in Zipkin {}", spanNamesNotFoundInZipkin); + then(serviceNamesNotFoundInZipkin).isEmpty(); + then(spanNamesNotFoundInZipkin).isEmpty(); + log.info("Zipkin tracing is working! Sleuth is working! Let's be happy!"); + } + + private List serviceNamesNotFoundInZipkin(List spans) { + List serviceNamesFoundInAnnotations = spans.stream() + .filter(span -> span.annotations != null) + .map(span -> span.annotations) + .flatMap(Collection::stream) + .filter(span -> span.endpoint != null) + .map(annotation -> annotation.endpoint) + .map(endpoint -> endpoint.serviceName) + .distinct() + .collect(Collectors.toList()); + List serviceNamesFoundInBinaryAnnotations = spans.stream() + .filter(span -> span.binaryAnnotations != null) + .map(span -> span.binaryAnnotations) + .flatMap(Collection::stream) + .filter(span -> span.endpoint != null) + .map(annotation -> annotation.endpoint) + .map(endpoint -> endpoint.serviceName) + .distinct() + .collect(Collectors.toList()); + List names = new ArrayList<>(); + names.addAll(serviceNamesFoundInAnnotations); + names.addAll(serviceNamesFoundInBinaryAnnotations); + return names.contains(APP_NAME) ? Collections.EMPTY_LIST : names; + } + + private List annotationsNotFoundInZipkin(List spans) { + String binaryAnnotationName = "random-sleep-millis"; + Optional names = spans.stream() + .filter(span -> span.binaryAnnotations != null) + .map(span -> span.binaryAnnotations) + .flatMap(Collection::stream) + .filter(span -> span.endpoint != null) + .map(annotation -> annotation.key) + .filter(binaryAnnotationName::equals) + .findFirst(); + return names.isPresent() ? Collections.EMPTY_LIST : Collections.singletonList(binaryAnnotationName); + } + + private void httpMessageWithTraceIdInHeadersIsSuccessfullySent(String endpoint, String traceId) { + new RequestSendingRunnable(restTemplate, endpoint, traceId).run(); + } + +} diff --git a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/src/test/resources/docker-compose.yml b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/src/test/resources/docker-compose.yml new file mode 100644 index 000000000..22c392f76 --- /dev/null +++ b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/src/test/resources/docker-compose.yml @@ -0,0 +1,33 @@ +collector: + image: openzipkin/zipkin-collector:1.25.0 + environment: + - TRANSPORT_TYPE=scribe + - STORAGE_TYPE=mysql + ports: + - 9410:9410 + - 9900:9900 + links: + - mysql:storage + +query: + image: openzipkin/zipkin-query:1.25.0 + environment: + # Remove TRANSPORT_TYPE to disable tracing + - TRANSPORT_TYPE=http + - STORAGE_TYPE=mysql + ports: + - 9411:9411 + - 9901:9901 + links: + - mysql:storage + +rabbitmq: + image: rabbitmq:management + ports: + - 5672 + - 15672 + +mysql: + image: openzipkin/zipkin-mysql:1.25.0 + ports: + - 3306:3306 \ No newline at end of file