[#89] Added test for zipkin integration

This commit is contained in:
Marcin Grzejszczak
2016-01-11 17:38:24 +01:00
parent 4d3300aab6
commit 3084eab3eb
9 changed files with 355 additions and 28 deletions

View File

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

View File

@@ -63,6 +63,12 @@
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
<version>1.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
@@ -102,6 +108,12 @@
<version>${testcontainers.jackson.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.zipkin</groupId>
<artifactId>zipkin-java-core</artifactId>
<version>0.1.1</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@@ -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<String> 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<String> 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<String> endpointToCheckZipkinQueryHealth() {
URI uri = URI.create(getZipkinServicesQueryUrl());
log.info("Sending request to the Zipkin query service [{}]", uri);
return exchangeRequest(uri);
}
protected ResponseEntity<String> endpointToCheckZipkinCollectorHealth() {
URI uri = URI.create(getZipkinCollectorHealthUrl());
log.info("Sending request to the Zipkin collector service [{}]", uri);
return exchangeRequest(uri);
}
protected ResponseEntity<String> 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<String> 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());
}
}
}

View File

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

View File

@@ -1,7 +0,0 @@
package tools;
/**
* @author Marcin Grzejszczak
*/
interface DockerTests {
}

View File

@@ -69,6 +69,29 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-sample-test-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${testcontainers.jackson.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${testcontainers.jackson.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${testcontainers.jackson.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/base.xml"/>
<logger name="org.springframework.cloud.sleuth" level="DEBUG"/>
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
</configuration>

View File

@@ -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<String> 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<io.zipkin.Span> spans = Codec.JSON.readSpans(response.getBody().getBytes());
List<String> serviceNamesNotFoundInZipkin = serviceNamesNotFoundInZipkin(spans);
List<String> 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<String> serviceNamesNotFoundInZipkin(List<io.zipkin.Span> spans) {
List<String> 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<String> 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<String> names = new ArrayList<>();
names.addAll(serviceNamesFoundInAnnotations);
names.addAll(serviceNamesFoundInBinaryAnnotations);
return names.contains(APP_NAME) ? Collections.EMPTY_LIST : names;
}
private List<String> annotationsNotFoundInZipkin(List<io.zipkin.Span> spans) {
String binaryAnnotationName = "random-sleep-millis";
Optional<String> 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();
}
}

View File

@@ -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