Working Zipkin Stream tests
This commit is contained in:
@@ -63,6 +63,16 @@
|
||||
<artifactId>brave-core</artifactId>
|
||||
<version>${brave.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.kristofa</groupId>
|
||||
<artifactId>brave-http</artifactId>
|
||||
<version>${brave.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.kristofa</groupId>
|
||||
<artifactId>brave-spring-web-servlet-interceptor</artifactId>
|
||||
<version>${brave.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.aspectj</groupId>
|
||||
<artifactId>aspectjrt</artifactId>
|
||||
|
||||
@@ -46,11 +46,48 @@
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-sleuth-dependencies</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-sleuth-sample-test-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>docker-compose</artifactId>
|
||||
<version>0.9.8</version>
|
||||
<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>
|
||||
<dependency>
|
||||
<groupId>io.zipkin</groupId>
|
||||
<artifactId>zipkin-java-core</artifactId>
|
||||
<version>0.1.2</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
|
||||
@@ -118,6 +118,11 @@
|
||||
<version>${testcontainers.jackson.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>docker-compose</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -31,9 +31,8 @@ import org.springframework.util.JdkIdGenerator;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.testcontainers.containers.DockerComposeContainer;
|
||||
import sample.SampleMessagingApplication;
|
||||
import tools.AbstractIntegrationTest;
|
||||
import tools.AbstractDockerIntegrationTest;
|
||||
import tools.IntegrationTestSpanCollector;
|
||||
import tools.RequestSendingRunnable;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Collection;
|
||||
@@ -41,11 +40,11 @@ import java.util.Collection;
|
||||
import static org.assertj.core.api.BDDAssertions.then;
|
||||
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringApplicationConfiguration(classes = { AbstractIntegrationTest.Config.class, SampleMessagingApplication.class })
|
||||
@SpringApplicationConfiguration(classes = { AbstractDockerIntegrationTest.Config.class, SampleMessagingApplication.class })
|
||||
@WebIntegrationTest
|
||||
@TestPropertySource(properties="sample.zipkin.enabled=true")
|
||||
@Slf4j
|
||||
public class MessagingApplicationDockerTests extends AbstractIntegrationTest {
|
||||
public class MessagingApplicationDockerTests extends AbstractDockerIntegrationTest {
|
||||
|
||||
private static int port = 3381;
|
||||
private static String sampleAppUrl = "http://localhost:" + port;
|
||||
@@ -94,10 +93,6 @@ public class MessagingApplicationDockerTests extends AbstractIntegrationTest {
|
||||
.anyMatch(binaryAnnotationKey::equals)).isTrue();
|
||||
}
|
||||
|
||||
private RequestSendingRunnable httpMessageWithTraceIdInHeadersIsSuccessfullySent(String endpoint, String traceId) {
|
||||
return new RequestSendingRunnable(restTemplate, endpoint, traceId);
|
||||
}
|
||||
|
||||
private void thenAllSpansHaveTraceIdEqualTo(String traceId) {
|
||||
then(integrationTestSpanCollector.hashedSpans.stream().allMatch(span -> span.getTrace_id() == zipkinHashedTraceId(traceId))).isTrue();
|
||||
}
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
<relativePath>..</relativePath>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
@@ -28,6 +33,21 @@
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
<plugins>
|
||||
<!-- This is a test jar - it can have jdk8 -->
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>animal-sniffer-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
<signature>
|
||||
<groupId>org.codehaus.mojo.signature</groupId>
|
||||
<artifactId>java17</artifactId>
|
||||
<version>1.0</version>
|
||||
</signature>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<dependencies>
|
||||
@@ -35,10 +55,6 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-integration</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-sleuth-core</artifactId>
|
||||
@@ -69,11 +85,6 @@
|
||||
<version>1.3</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
@@ -84,34 +95,25 @@
|
||||
<artifactId>awaitility</artifactId>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>docker-compose</artifactId>
|
||||
<version>0.9.8</version>
|
||||
<scope>compile</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>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>docker-compose</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.zipkin</groupId>
|
||||
<artifactId>zipkin-java-core</artifactId>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
/*
|
||||
* 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 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 io.zipkin.Codec;
|
||||
import io.zipkin.Span;
|
||||
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 java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
import static org.assertj.core.api.BDDAssertions.then;
|
||||
|
||||
/**
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class AbstractDockerIntegrationTest {
|
||||
|
||||
protected static int pollInterval = 1;
|
||||
protected static int timeout = 120;
|
||||
protected RestTemplate restTemplate = new AssertingRestTemplate();
|
||||
|
||||
public static ConditionFactory await() {
|
||||
return Awaitility.await().pollInterval(pollInterval, SECONDS).atMost(timeout, SECONDS);
|
||||
}
|
||||
|
||||
protected long zipkinHashedTraceId(String string) {
|
||||
long h = 1125899906842597L;
|
||||
if (string == null) {
|
||||
return h;
|
||||
}
|
||||
int len = string.length();
|
||||
|
||||
for (int i = 0; i < len; i++) {
|
||||
h = 31 * h + string.charAt(i);
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
protected String zipkinHashedHexStringTraceId(String traceId) {
|
||||
long hashedTraceId = zipkinHashedTraceId(traceId);
|
||||
return Long.toHexString(hashedTraceId);
|
||||
}
|
||||
|
||||
public static String getDockerUrl() {
|
||||
URI dockerUri = getDockerURI();
|
||||
if (StringUtils.isEmpty(dockerUri.getScheme())) {
|
||||
return "http://localhost";
|
||||
}
|
||||
return "http://" + dockerUri.getHost();
|
||||
}
|
||||
|
||||
public 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 checkServerHealth("Zipkin Query Server", this::endpointToCheckZipkinQueryHealth);
|
||||
}
|
||||
|
||||
protected Runnable zipkinServerIsUp() {
|
||||
return checkServerHealth("Zipkin Stream Server", this::endpointToCheckZipkinServerHealth);
|
||||
}
|
||||
|
||||
protected Runnable zipkinCollectorServerIsUp() {
|
||||
return checkServerHealth("Zipkin collector", this::endpointToCheckZipkinCollectorHealth);
|
||||
}
|
||||
|
||||
protected Runnable checkServerHealth(String appName, RequestExchanger requestExchanger) {
|
||||
return () -> {
|
||||
ResponseEntity<String> response = requestExchanger.exchange();
|
||||
log.info("Response from the [{}] health endpoint is [{}]", appName, response);
|
||||
then(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
log.info("[{}] is up!", appName);
|
||||
};
|
||||
}
|
||||
|
||||
private interface RequestExchanger {
|
||||
ResponseEntity<String> exchange();
|
||||
}
|
||||
|
||||
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> endpointToCheckZipkinServerHealth() {
|
||||
URI uri = URI.create("http://localhost:9411/health");
|
||||
log.info("Sending request to the Zipkin Server [{}]", 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 getZipkinRootUrl() + ":9411/api/v1/trace/";
|
||||
}
|
||||
|
||||
protected String getZipkinRootUrl() {
|
||||
return getDockerUrl();
|
||||
}
|
||||
|
||||
protected String getZipkinServicesQueryUrl() {
|
||||
return getDockerUrl() + ":9411/api/v1/services";
|
||||
}
|
||||
|
||||
protected String getZipkinServerHealthUrl() {
|
||||
return getDockerUrl() + ":9411/health";
|
||||
}
|
||||
|
||||
protected String getZipkinCollectorHealthUrl() {
|
||||
return getDockerUrl() + ":9900/health";
|
||||
}
|
||||
|
||||
protected Runnable httpMessageWithTraceIdInHeadersIsSuccessfullySent(String endpoint, String traceId) {
|
||||
return new RequestSendingRunnable(restTemplate, endpoint, traceId);
|
||||
}
|
||||
|
||||
protected Runnable allSpansWereRegisteredInZipkinWithTraceIdEqualTo(String traceId) {
|
||||
return () -> {
|
||||
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<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!");
|
||||
};
|
||||
}
|
||||
|
||||
protected 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(getAppName()) ? Collections.EMPTY_LIST : names;
|
||||
}
|
||||
|
||||
protected String getAppName() {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
protected List<String> annotationsNotFoundInZipkin(List<io.zipkin.Span> spans) {
|
||||
String binaryAnnotationName = getRequiredBinaryAnnotationName();
|
||||
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);
|
||||
}
|
||||
|
||||
protected String getRequiredBinaryAnnotationName() {
|
||||
return "random-sleep-millis";
|
||||
}
|
||||
|
||||
@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(() -> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
/*
|
||||
* 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 tools;
|
||||
|
||||
import com.github.kristofa.brave.EmptySpanCollectorMetricsHandler;
|
||||
import com.github.kristofa.brave.HttpSpanCollector;
|
||||
import com.github.kristofa.brave.SpanCollector;
|
||||
import com.github.kristofa.brave.SpanCollectorMetricsHandler;
|
||||
import com.jayway.awaitility.Awaitility;
|
||||
import com.jayway.awaitility.core.ConditionFactory;
|
||||
import java.net.URI;
|
||||
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.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.RequestEntity;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
import static org.assertj.core.api.BDDAssertions.then;
|
||||
|
||||
/**
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class AbstractIntegrationTest {
|
||||
|
||||
protected static int pollInterval = 1;
|
||||
protected static int timeout = 120;
|
||||
protected RestTemplate restTemplate = new AssertingRestTemplate();
|
||||
|
||||
protected static ConditionFactory await() {
|
||||
return Awaitility.await().pollInterval(pollInterval, SECONDS).atMost(timeout, SECONDS);
|
||||
}
|
||||
|
||||
protected long zipkinHashedTraceId(String string) {
|
||||
long h = 1125899906842597L;
|
||||
if (string == null) {
|
||||
return h;
|
||||
}
|
||||
int len = string.length();
|
||||
|
||||
for (int i = 0; i < len; i++) {
|
||||
h = 31 * h + string.charAt(i);
|
||||
}
|
||||
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 ResponseEntity<String> endpointToCheckZipkinQueryHealth() {
|
||||
URI uri = URI.create(getZipkinServicesQueryUrl());
|
||||
log.info("Sending request to the Zipkin query 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";
|
||||
}
|
||||
|
||||
@Configuration
|
||||
public static class Config {
|
||||
@Bean
|
||||
SpanCollector integrationTestSpanCollector() {
|
||||
return new IntegrationTestSpanCollector();
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Slf4j
|
||||
public static class ZipkinConfig {
|
||||
@Bean
|
||||
@SneakyThrows
|
||||
public SpanCollector 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 SpanCollector getSpanCollector(ZipkinProperties zipkin) {
|
||||
String url = "http://" + getDockerURI().getHost() + ":" + zipkin.getPort();
|
||||
// TODO: parameterize this
|
||||
SpanCollectorMetricsHandler metrics = new EmptySpanCollectorMetricsHandler();
|
||||
return HttpSpanCollector.create(url, zipkin.getHttpConfig(), metrics);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ public class RequestSendingRunnable implements Runnable {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
log.info("Sending the request to url [{}] with trace id in headers [{}]", url, traceId);
|
||||
ResponseEntity<String> responseEntity = restTemplate.exchange(requestWithTraceId(traceId), String.class);
|
||||
then(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
log.info("Received the following response [{}]", responseEntity);
|
||||
|
||||
@@ -49,6 +49,18 @@
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.kristofa</groupId>
|
||||
<artifactId>brave-spancollector-scribe</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.kristofa</groupId>
|
||||
<artifactId>brave-http</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.kristofa</groupId>
|
||||
<artifactId>brave-spring-web-servlet-interceptor</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
@@ -75,6 +87,27 @@
|
||||
<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>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-annotations</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>docker-compose</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package integration;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
|
||||
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
|
||||
import org.springframework.amqp.rabbit.connection.RabbitConnectionFactoryBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.amqp.RabbitProperties;
|
||||
import org.springframework.cloud.sleuth.TraceManager;
|
||||
import org.springframework.cloud.sleuth.trace.TraceContextHolder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tools.AbstractDockerIntegrationTest;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
@RestController
|
||||
@Slf4j
|
||||
public class SampleApp {
|
||||
|
||||
@Autowired
|
||||
private TraceManager traceManager;
|
||||
|
||||
@SneakyThrows
|
||||
@RequestMapping("/hi2")
|
||||
public String hi2() {
|
||||
log.info("I'm in the sample app");
|
||||
final Random random = new Random();
|
||||
int millis = random.nextInt(1000);
|
||||
Thread.sleep(millis);
|
||||
this.traceManager.addAnnotation("random-sleep-millis", String.valueOf(millis));
|
||||
log.info("Current span is [{}]", TraceContextHolder.getCurrentSpan());
|
||||
return "hi2";
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableAutoConfiguration
|
||||
@Slf4j
|
||||
public static class Config {
|
||||
|
||||
public static final int RABBITMQ_PORT = 5672;
|
||||
|
||||
@Bean SampleApp sampleApp() {
|
||||
return new SampleApp();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@SneakyThrows
|
||||
ConnectionFactory connectionFactory() {
|
||||
RabbitProperties config = rabbitProperties();
|
||||
AbstractDockerIntegrationTest.await().until(() -> {
|
||||
try {
|
||||
ServerSocket serverSocket = new ServerSocket(RABBITMQ_PORT, 50,
|
||||
InetAddress.getByName(AbstractDockerIntegrationTest.getDockerURI().getHost()));
|
||||
serverSocket.close();
|
||||
} catch (IOException e) {
|
||||
log.info("RabbitMQ is up and running - proceeding");
|
||||
return true;
|
||||
}
|
||||
log.warn("RabbitMQ has not started yet...");
|
||||
return false;
|
||||
});
|
||||
RabbitConnectionFactoryBean factory = new RabbitConnectionFactoryBean();
|
||||
if (config.getHost() != null) {
|
||||
factory.setHost(config.getHost());
|
||||
factory.setPort(config.getPort());
|
||||
}
|
||||
if (config.getUsername() != null) {
|
||||
factory.setUsername(config.getUsername());
|
||||
}
|
||||
if (config.getPassword() != null) {
|
||||
factory.setPassword(config.getPassword());
|
||||
}
|
||||
if (config.getVirtualHost() != null) {
|
||||
factory.setVirtualHost(config.getVirtualHost());
|
||||
}
|
||||
if (config.getRequestedHeartbeat() != null) {
|
||||
factory.setRequestedHeartbeat(config.getRequestedHeartbeat());
|
||||
}
|
||||
RabbitProperties.Ssl ssl = config.getSsl();
|
||||
if (ssl.isEnabled()) {
|
||||
factory.setUseSSL(true);
|
||||
factory.setKeyStore(ssl.getKeyStore());
|
||||
factory.setKeyStorePassphrase(ssl.getKeyStorePassword());
|
||||
factory.setTrustStore(ssl.getTrustStore());
|
||||
factory.setTrustStorePassphrase(ssl.getTrustStorePassword());
|
||||
}
|
||||
factory.afterPropertiesSet();
|
||||
CachingConnectionFactory connectionFactory = new CachingConnectionFactory(
|
||||
factory.getObject());
|
||||
connectionFactory.setAddresses(config.getAddresses());
|
||||
return connectionFactory;
|
||||
}
|
||||
|
||||
RabbitProperties rabbitProperties() {
|
||||
RabbitProperties rabbitProperties = new RabbitProperties();
|
||||
rabbitProperties.setHost(AbstractDockerIntegrationTest.getDockerURI().getHost());
|
||||
return rabbitProperties;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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 example.ZipkinStreamServerApplication;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.util.JdkIdGenerator;
|
||||
import org.testcontainers.containers.DockerComposeContainer;
|
||||
import tools.AbstractDockerIntegrationTest;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringApplicationConfiguration(classes = { SampleApp.Config.class,
|
||||
AbstractDockerIntegrationTest.ZipkinConfig.class, ZipkinStreamServerApplication.class })
|
||||
@WebIntegrationTest()
|
||||
@Slf4j
|
||||
public class ZipkinStreamDockerTests extends AbstractDockerIntegrationTest {
|
||||
|
||||
private static int port = 9411;
|
||||
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("rabbitmq_1", 15672)
|
||||
.withExposedService("mysql_1", 3306);
|
||||
|
||||
@Test
|
||||
@SneakyThrows
|
||||
public void should_propagate_spans_to_zipkin() {
|
||||
await().until(zipkinServerIsUp());
|
||||
String traceId = new JdkIdGenerator().generateId().toString();
|
||||
|
||||
await().until(httpMessageWithTraceIdInHeadersIsSuccessfullySent(sampleAppUrl + "/hi2", traceId));
|
||||
|
||||
await().until(allSpansWereRegisteredInZipkinWithTraceIdEqualTo(traceId));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getZipkinRootUrl() {
|
||||
return "http://localhost";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
mysql:
|
||||
image: mysql
|
||||
ports:
|
||||
- 3306:3306
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=root
|
||||
- MYSQL_DATABASE=test
|
||||
|
||||
rabbitmq:
|
||||
image: rabbitmq:management
|
||||
ports:
|
||||
- 5672:5672
|
||||
- 15672:15672
|
||||
@@ -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>
|
||||
@@ -100,6 +100,23 @@
|
||||
<version>${testcontainers.jackson.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>docker-compose</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.kristofa</groupId>
|
||||
<artifactId>brave-spancollector-scribe</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.kristofa</groupId>
|
||||
<artifactId>brave-http</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.kristofa</groupId>
|
||||
<artifactId>brave-spring-web-servlet-interceptor</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
package integration;
|
||||
|
||||
import io.zipkin.Codec;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.Before;
|
||||
@@ -24,28 +23,21 @@ 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 tools.AbstractDockerIntegrationTest;
|
||||
|
||||
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 })
|
||||
@SpringApplicationConfiguration(classes = { AbstractDockerIntegrationTest.ZipkinConfig.class, SampleZipkinApplication.class })
|
||||
@WebIntegrationTest
|
||||
@TestPropertySource(properties="sample.zipkin.enabled=true")
|
||||
@Slf4j
|
||||
public class ZipkinDockerTests extends AbstractIntegrationTest {
|
||||
public class ZipkinDockerTests extends AbstractDockerIntegrationTest {
|
||||
|
||||
private static final String APP_NAME = "testsleuthzipkin";
|
||||
private static int port = 3380;
|
||||
@@ -68,65 +60,13 @@ public class ZipkinDockerTests extends AbstractIntegrationTest {
|
||||
public void should_propagate_spans_to_zipkin() {
|
||||
String traceId = new JdkIdGenerator().generateId().toString();
|
||||
|
||||
httpMessageWithTraceIdInHeadersIsSuccessfullySent(sampleAppUrl + "/hi2", traceId);
|
||||
await().until(httpMessageWithTraceIdInHeadersIsSuccessfullySent(sampleAppUrl + "/hi2", traceId));
|
||||
|
||||
await().until(() -> {
|
||||
allSpansWereRegisteredInZipkinWithTraceIdEqualTo(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!");
|
||||
@Override
|
||||
protected String getAppName() {
|
||||
return APP_NAME;
|
||||
}
|
||||
|
||||
private List<String> serviceNamesNotFoundInZipkin(List<io.zipkin.Span> spans) {
|
||||
List<String> serviceNamesFoundInAnnotations = spans.stream()
|
||||
.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()
|
||||
.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()
|
||||
.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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user