CorrelationID setting by 4financeIT

Below you can find people who helped to create this solution

Tomasz Nurkiewicz <tomasz.nurkiewicz@4finance.com>
Marcin Zajaczkowski <marcin.zajaczkowski@4finance.com>
Kamil Szymanski <kamil.szymanski@4finance.com>
Michal Chmielarz <michal.chmielarz@4finance.com>
Marcin Grzejszczak <marcin.grzejszczak@4finance.com>
Jakub Nabdralik <jakub.nabrdalik@4finance.com>
Urszula Choromanska <urszula.choromanska@4finance.com>
Tomasz Dziurko <tomasz.dziurko@4finance.com>
Tomasz Szymanski <tomasz.szymanski@4finance.com>
Adam Chudzik <adam.chudzik@4finance.com>
This commit is contained in:
Marcin Grzejszczak
2015-05-29 08:02:23 +02:00
committed by Marcin Grzejszczak
parent b5b3a21a88
commit dad0505d5a
36 changed files with 1825 additions and 51 deletions

120
pom.xml
View File

@@ -4,7 +4,6 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<packaging>pom</packaging>
@@ -27,31 +26,62 @@
</scm>
<modules>
<module>spring-cloud-sleuth-core</module>
<module>spring-cloud-sleuth-correlation</module>
<module>spring-cloud-sleuth-zipkin</module>
<module>spring-cloud-sleuth-sample</module>
<module>docs</module>
</modules>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
</plugins>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
<version>1.4</version>
<executions>
<execution>
<goals>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.6</version>
<configuration>
<useFile>false</useFile>
<includes>
<include>**/*Spec.java</include>
</includes>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.github.kristofa</groupId>
@@ -99,6 +129,16 @@
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>io.reactivex</groupId>
<artifactId>rxjava</artifactId>
<version>1.0.10</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
@@ -111,12 +151,66 @@
<version>1.12.6</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-core</artifactId>
<version>1.4.5</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${aspectj.version}</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${aspectj.version}</version>
</dependency>
<!-- Spock -->
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<version>${spock.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-spring</artifactId>
<version>${spock.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<version>3.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.objenesis</groupId>
<artifactId>objenesis</artifactId>
<version>2.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
<version>1.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.4.3</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
<properties>
<brave.version>2.4.2</brave.version>
<spring-cloud.version>1.0.1.BUILD-SNAPSHOT</spring-cloud.version>
<aspectj.version>1.8.4</aspectj.version>
<spock.version>1.0-groovy-2.4</spock.version>
</properties>
</project>

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-sleuth-core</artifactId>
<packaging>jar</packaging>
<name>Spring Cloud Sleuth Core</name>
<description>Spring Cloud Sleuth Core</description>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<!-- Only needed at compile time -->
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2012-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.resttemplate;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.web.client.RestTemplate;
import java.util.List;
/**
* Default configure of {@code RestTemplate} that adds a list {@code ClientHttpRequestInterceptor}
* to {@code RestTemplate}
*
* @see ClientHttpRequestInterceptor
* @see RestTemplate
*
* @author Marcin Grzejszczak, 4financeIT
*/
public class DefaultRestTemplateConfigurer implements RestTemplateConfigurer {
private final List<ClientHttpRequestInterceptor> clientHttpRequestInterceptors;
public DefaultRestTemplateConfigurer(List<ClientHttpRequestInterceptor> clientHttpRequestInterceptors) {
this.clientHttpRequestInterceptors = clientHttpRequestInterceptors;
}
@Override
public void modifyRestTemplate(RestTemplate restTemplate) {
for (ClientHttpRequestInterceptor clientHttpRequestInterceptor : clientHttpRequestInterceptors) {
restTemplate.getInterceptors().add(clientHttpRequestInterceptor);
}
}
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2012-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.resttemplate;
import org.springframework.web.client.RestTemplate;
/**
* Interface that allows to modify the {@code RestTemplate} parameters
*
* @see RestTemplate
*
* @author Marcin Grzejszczak, 4financeIT
*/
public interface RestTemplateConfigurer {
void modifyRestTemplate(RestTemplate restTemplate);
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2012-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.resttemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.List;
/**
*
* Autoconfiguration that sets up a {@code RestTemplate} as a bean if one is not present
* and performs its additional modification via a {@code RestTemplateConfigurer}.
*
* @author Marcin Grzejszczak, 4financeIT
*/
@Configuration
@ConditionalOnWebApplication
@ConditionalOnProperty(value = "spring.cloud.sleuth.resttemplate.enabled", matchIfMissing = true)
public class SleuthRestTemplateAutoConfiguration {
@Configuration
protected static class RestTemplateConfig {
@Autowired
private List<ClientHttpRequestInterceptor> clientHttpRequestInterceptors = new ArrayList<>();
@Bean
@ConditionalOnMissingBean
public RestTemplate restTemplate() {
return new RestTemplate();
}
@Bean
@ConditionalOnMissingBean
public RestTemplateConfigurer restTemplateConfigurer(RestTemplate restTemplate) {
DefaultRestTemplateConfigurer configurer = new DefaultRestTemplateConfigurer(clientHttpRequestInterceptors);
configurer.modifyRestTemplate(restTemplate);
return configurer;
}
}
}

View File

@@ -0,0 +1,3 @@
# Auto Configuration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.sleuth.resttemplate.SleuthRestTemplateAutoConfiguration

View File

@@ -0,0 +1,132 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-sleuth-correlation</artifactId>
<packaging>jar</packaging>
<name>Spring Cloud Sleuth Correlation</name>
<description>Spring Cloud Sleuth Correlation</description>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<!-- Only needed at compile time -->
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-core</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.reactivex</groupId>
<artifactId>rxjava</artifactId>
</dependency>
<!-- Spock -->
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-spring</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.objenesis</groupId>
<artifactId>objenesis</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.codehaus.gpars</groupId>
<artifactId>gpars</artifactId>
<version>1.2.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock</artifactId>
<version>1.53</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.stefanbirkner</groupId>
<artifactId>system-rules</artifactId>
<version>1.9.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,134 @@
/*
* Copyright 2012-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.correlation;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestOperations;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import static org.springframework.cloud.sleuth.correlation.CorrelationIdHolder.CORRELATION_ID_HEADER;
/**
* Aspect that adds correlation id to
* <p/>
* <ul>
* <li>{@link RestController} annotated classes
* with public {@link Callable} methods</li>
* <li>{@link Controller} annotated classes
* with public {@link Callable} methods</li>
* <li>explicit {@link RestOperations}.exchange(..) method calls</li>
* </ul>
* <p/>
* For controllers an around aspect is created that wraps the {@link Callable#call()} method execution
* in {@link CorrelationIdUpdater#wrapCallableWithId(Callable)}
* <p/>
* For {@link RestOperations} we are wrapping all executions of the
* <b>exchange</b> methods and we are extracting {@link HttpHeaders} from the passed {@link HttpEntity}.
* Next we are adding correlation id header {@link CorrelationIdHolder#CORRELATION_ID_HEADER} with
* the value taken from {@link CorrelationIdHolder}. Finally the method execution proceeds.
*
* @see RestController
* @see Controller
* @see RestOperations
* @see CorrelationIdHolder
* @see CorrelationIdFilter
*
* @author Tomasz Nurkewicz, 4financeIT
* @author Marcin Grzejszczak, 4financeIT
* @author Michal Chmielarz, 4financeIT
*/
@Aspect
public class CorrelationIdAspect {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final int HTTP_ENTITY_PARAM_INDEX = 2;
@Pointcut("@target(org.springframework.web.bind.annotation.RestController)")
private void anyRestControllerAnnotated() {
}
@Pointcut("@target(org.springframework.stereotype.Controller)")
private void anyControllerAnnotated() {
}
@Pointcut("execution(public java.util.concurrent.Callable *(..))")
private void anyPublicMethodReturningCallable() {
}
@Pointcut("(anyRestControllerAnnotated() || anyControllerAnnotated()) && anyPublicMethodReturningCallable()")
private void anyControllerOrRestControllerWithPublicAsyncMethod() {
}
@Around("anyControllerOrRestControllerWithPublicAsyncMethod()")
public Object wrapWithCorrelationId(ProceedingJoinPoint pjp) throws Throwable {
final Callable callable = (Callable) pjp.proceed();
log.debug("Wrapping callable with correlation id [" + CorrelationIdHolder.get() + "]");
return CorrelationIdUpdater.wrapCallableWithId(new Callable() {
@Override
public Object call() throws Exception {
return callable.call();
}
});
}
@Pointcut("execution(public * org.springframework.web.client.RestOperations.exchange(..))")
private void anyExchangeRestOperationsMethod() {
}
@Around("anyExchangeRestOperationsMethod()")
public Object wrapWithCorrelationIdForRestOperations(ProceedingJoinPoint pjp) throws Throwable {
String correlationId = CorrelationIdHolder.get();
log.debug("Wrapping RestTemplate call with correlation id [" + correlationId + "]");
HttpEntity httpEntity = (HttpEntity) pjp.getArgs()[HTTP_ENTITY_PARAM_INDEX];
HttpEntity newHttpEntity = createNewHttpEntity(httpEntity, correlationId);
List<Object> newArgs = modifyHttpEntityInMethodArguments(pjp, newHttpEntity);
return pjp.proceed(newArgs.toArray());
}
@SuppressWarnings("unchecked")
private HttpEntity createNewHttpEntity(HttpEntity httpEntity, String correlationId) {
HttpHeaders newHttpHeaders = new HttpHeaders();
newHttpHeaders.putAll(httpEntity.getHeaders());
newHttpHeaders.add(CORRELATION_ID_HEADER, correlationId);
return new HttpEntity(httpEntity.getBody(), newHttpHeaders);
}
private List<Object> modifyHttpEntityInMethodArguments(ProceedingJoinPoint pjp, HttpEntity newHttpEntity) {
List<Object> newArgs = new ArrayList<>();
for (int i = 0; i < pjp.getArgs().length; i++) {
Object arg = pjp.getArgs()[i];
if (i != HTTP_ENTITY_PARAM_INDEX) {
newArgs.add(i, arg);
} else {
newArgs.add(i, newHttpEntity);
}
}
return newArgs;
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2012-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.correlation;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.embedded.FilterRegistrationBean;
import org.springframework.cloud.sleuth.resttemplate.SleuthRestTemplateAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.regex.Pattern;
/**
* Registers beans that add correlation id to requests
*
* @see CorrelationIdAspect
* @see CorrelationIdFilter
*
* @author Tomasz Nurkewicz, 4financeIT
* @author Marcin Grzejszczak, 4financeIT
* @author Michal Chmielarz, 4financeIT
*/
@Configuration
@ConditionalOnProperty(value = "spring.cloud.sleuth.correlation.enabled", matchIfMissing = true)
@AutoConfigureAfter(SleuthRestTemplateAutoConfiguration.class)
public class CorrelationIdAutoConfiguration {
/**
* Pattern for URLs that should be skipped in correlationID setting
*/
@Value("${spring.cloud.sleuth.correlation.skipPattern:}")
private String skipPattern;
@Bean
@ConditionalOnMissingBean
public CorrelationIdAspect correlationIdAspect() {
return new CorrelationIdAspect();
}
@Bean
@ConditionalOnMissingBean
public FilterRegistrationBean correlationHeaderFilter(UuidGenerator uuidGenerator) {
Pattern pattern = StringUtils.isBlank(skipPattern) ? Pattern.compile(skipPattern) : CorrelationIdFilter.DEFAULT_SKIP_PATTERN;
return new FilterRegistrationBean(new CorrelationIdFilter(uuidGenerator, pattern));
}
@Bean
@ConditionalOnMissingBean
public UuidGenerator uuidGenerator() {
return new UuidGenerator();
}
@Bean
public CorrelationIdSettingRestTemplateInterceptor correlationIdSettingRestTemplateInterceptor() {
return new CorrelationIdSettingRestTemplateInterceptor();
}
}

View File

@@ -0,0 +1,151 @@
/*
* Copyright 2012-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.correlation;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.concurrent.Callable;
import java.util.regex.Pattern;
import static org.springframework.cloud.sleuth.correlation.CorrelationIdHolder.CORRELATION_ID_HEADER;
import static org.springframework.util.StringUtils.hasText;
/**
* Filter that takes the value of the {@link CorrelationIdHolder#CORRELATION_ID_HEADER} header
* from either request or response and sets it in the {@link CorrelationIdHolder}. It also provides
* that value in {@link MDC} logging related class so that logger prints the value of
* correlation id at each log.
*
* @see CorrelationIdHolder
* @see MDC
*
* @author Jakub Nabrdalik, 4financeIT
* @author Tomasz Nurkiewicz, 4financeIT
* @author Marcin Grzejszczak, 4financeIT
*/
public class CorrelationIdFilter extends OncePerRequestFilter {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
public static final Pattern DEFAULT_SKIP_PATTERN = Pattern.compile("/api-docs.*|/autoconfig|/configprops|/dump|/info|/metrics.*|/mappings|/trace|/swagger.*|.*\\.png|.*\\.css|.*\\.js|.*\\.html");
private final Pattern skipCorrId;
private final UuidGenerator uuidGenerator;
public CorrelationIdFilter() {
this.uuidGenerator = new UuidGenerator();
this.skipCorrId = null;
}
public CorrelationIdFilter(UuidGenerator uuidGenerator, Pattern skipCorrId) {
this.uuidGenerator = uuidGenerator;
this.skipCorrId = skipCorrId;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
setupCorrelationId(request, response);
try {
filterChain.doFilter(request, response);
} finally {
cleanupCorrelationId();
}
}
private void setupCorrelationId(HttpServletRequest request, HttpServletResponse response) {
String correlationIdFromRequest = getCorrelationIdFrom(request);
String correlationId = (hasText(correlationIdFromRequest)) ? correlationIdFromRequest : getCorrelationIdFrom(response);
if (!hasText(correlationId) && shouldGenerateCorrId(request)) {
correlationId = createNewCorrIdIfEmpty();
}
CorrelationIdHolder.set(correlationId);
addCorrelationIdToResponseIfNotPresent(response, correlationId);
}
private String getCorrelationIdFrom(final HttpServletResponse response) {
return withLoggingAs("response", new Callable<String>() {
@Override
public String call() throws Exception {
return response.getHeader(CORRELATION_ID_HEADER);
}
});
}
private String getCorrelationIdFrom(final HttpServletRequest request) {
return withLoggingAs("request", new Callable<String>() {
@Override
public String call() throws Exception {
return request.getHeader(CORRELATION_ID_HEADER);
}
});
}
private String withLoggingAs(String whereWasFound, Callable<String> correlationIdGetter) {
String correlationId = tryToGetCorrelationId(correlationIdGetter);
if (hasText(correlationId)) {
MDC.put(CORRELATION_ID_HEADER, correlationId);
log.debug("Found correlationId in " + whereWasFound + ": " + correlationId);
}
return correlationId;
}
private String tryToGetCorrelationId(Callable<String> correlationIdGetter) {
try {
return correlationIdGetter.call();
} catch (Exception e) {
log.error("Exception occurred while trying to retrieve request header", e);
return StringUtils.EMPTY;
}
}
private String createNewCorrIdIfEmpty() {
String currentCorrId = uuidGenerator.create();
MDC.put(CORRELATION_ID_HEADER, currentCorrId);
log.debug("Generating new correlationId: " + currentCorrId);
return currentCorrId;
}
protected boolean shouldGenerateCorrId(HttpServletRequest request) {
final String i = request.getRequestURI();
final String uri = StringUtils.defaultIfEmpty(i, StringUtils.EMPTY);
boolean skip = skipCorrId != null && skipCorrId.matcher(uri).matches();
return !skip;
}
private void addCorrelationIdToResponseIfNotPresent(HttpServletResponse response, String correlationId) {
if (!hasText(response.getHeader(CORRELATION_ID_HEADER))) {
response.addHeader(CORRELATION_ID_HEADER, correlationId);
}
}
private void cleanupCorrelationId() {
MDC.remove(CORRELATION_ID_HEADER);
CorrelationIdHolder.remove();
}
@Override
protected boolean shouldNotFilterAsyncDispatch() {
return false;
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2012-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.correlation;
/**
* Component that stores correlation id using {@link ThreadLocal}
*
* @author Jakub Nabrdalik, 4financeIT
* @author Marcin Zajaczkowski, 4financeIT
*/
public class CorrelationIdHolder {
public static final String CORRELATION_ID_HEADER = "Correlation-Id";
private static final ThreadLocal<String> id = new ThreadLocal<String>();
public static void set(String correlationId) {
id.set(correlationId);
}
public static String get() {
return id.get();
}
public static void remove() {
id.remove();
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2012-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.correlation;
import lombok.SneakyThrows;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import java.io.IOException;
/**
* Interceptor that verifies whether the correlation id has been
* set on the request and sets it if it's missing.
*
* @see org.springframework.web.client.RestTemplate
* @see CorrelationIdHolder
*
* @author Marcin Grzejszczak, 4financeIT
*/
public class CorrelationIdSettingRestTemplateInterceptor implements ClientHttpRequestInterceptor {
@SneakyThrows
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
appendCorrelationIdToRequestIfMissing(request);
ClientHttpResponse response = null;
try {
response = execution.execute(request, body);
} catch (final Exception e) {
throw new RuntimeException(e);
}
return response;
}
private void appendCorrelationIdToRequestIfMissing(HttpRequest request) {
if (!request.getHeaders().containsKey(CorrelationIdHolder.CORRELATION_ID_HEADER)) {
request.getHeaders().add(CorrelationIdHolder.CORRELATION_ID_HEADER, CorrelationIdHolder.get());
}
}
}

View File

@@ -0,0 +1,120 @@
/*
* Copyright 2012-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.correlation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.util.StringUtils;
import java.lang.invoke.MethodHandles;
import java.util.concurrent.Callable;
import static org.springframework.cloud.sleuth.correlation.CorrelationIdHolder.CORRELATION_ID_HEADER;
/**
* Class that takes care of updating all necessary components with new value
* of correlation id.
* It sets correlationId on {@link ThreadLocal} in {@link CorrelationIdHolder}
* and in {@link MDC}.
*
* @see CorrelationIdHolder
* @see MDC
*
* @author Jakub Nabrdalik, 4financeIT
* @author Michal Chmielarz, 4financeIT
*/
public class CorrelationIdUpdater {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
public static void updateCorrelationId(String correlationId) {
if (StringUtils.hasText(correlationId)) {
log.debug("Updating correlationId with value: [" + correlationId + "]");
CorrelationIdHolder.set(correlationId);
MDC.put(CORRELATION_ID_HEADER, correlationId);
}
}
/**
* Temporarily updates correlation ID inside block of code.
* Makes sure previous ID is restored after block's execution
*
* @param temporaryCorrelationId - correlationID to passed to the executed block of code
* @param block Callable to be executed with new ID
* @return the result of Callable block execution
*/
public static <T> T withId(String temporaryCorrelationId, Callable<T> block) {
final String oldCorrelationId = CorrelationIdHolder.get();
try {
updateCorrelationId(temporaryCorrelationId);
return block.call();
} catch (RuntimeException e) {
logException(e);
throw e;
} catch (Exception e) {
logException(e);
throw new RuntimeException(e);
} finally {
updateCorrelationId(oldCorrelationId);
}
}
private static void logException(Throwable e) {
log.error("Exception occurred while trying to execute the function", e);
}
/**
* Wraps given {@link Callable} with another {@link Callable Callable} propagating correlation ID inside nested
* Callable/Closure.
* <p/>
* <p/>
* Useful in a situation when a Callable should be executed in a separate thread, for example in aspects.
* <p/>
* <pre><code>
* &#64;Around('...')
* Object wrapWithCorrelationId(ProceedingJoinPoint pjp) throws Throwable {
* Callable callable = pjp.proceed() as Callable
* return CorrelationIdUpdater.wrapCallableWithId {
* callable.call()
* }
* }
* </code></pre>
* <p/>
* <b>Note</b>: Passing only one input parameter currently is supported.
*
* @param block code block to execute in a thread with a correlation ID taken from the original thread
* @return wrapping block as Callable
*/
@SuppressWarnings("unchecked")
public static <T> Callable<T> wrapCallableWithId(final Callable<T> block) {
final String temporaryCorrelationId = CorrelationIdHolder.get();
// unchecked assignment due to groovyc issues with <T>
return new Callable() {
@Override
public Object call() throws Exception {
final String oldCorrelationId = CorrelationIdHolder.get();
try {
updateCorrelationId(temporaryCorrelationId);
return block.call();
} finally {
updateCorrelationId(oldCorrelationId);
}
}
};
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2012-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.correlation;
import java.util.UUID;
/**
* Default Uuid generator
*
* @author <a href="http://www.4financeit.com>4financeIT</a>
*/
public class UuidGenerator {
public String create() {
return UUID.randomUUID().toString();
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2012-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.correlation.hystrix;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import org.springframework.cloud.sleuth.correlation.CorrelationIdHolder;
import org.springframework.cloud.sleuth.correlation.CorrelationIdUpdater;
import java.util.concurrent.Callable;
/**
* Abstraction over {@code HystrixCommand} that wraps command execution with CorrelationID setting
*
* @see HystrixCommand
* @see CorrelationIdUpdater
*
* @author Tomasz Nurkiewicz, 4financeIT
* @author Marcin Grzejszczak, 4financeIT
*/
public abstract class CorrelatedCommand<R> extends HystrixCommand<R> {
private final String clientCorrelationId = CorrelationIdHolder.get();
protected CorrelatedCommand(HystrixCommandGroupKey group) {
super(group);
}
protected CorrelatedCommand(Setter setter) {
super(setter);
}
@Override
protected final R run() throws Exception {
return CorrelationIdUpdater.withId(clientCorrelationId, new Callable<R>() {
@Override
public R call() throws Exception {
return doRun();
}
});
}
public abstract R doRun() throws Exception;
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2012-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.correlation.scheduling;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.sleuth.correlation.CorrelationIdUpdater;
import org.springframework.cloud.sleuth.correlation.UuidGenerator;
import org.springframework.scheduling.annotation.Scheduled;
import java.lang.invoke.MethodHandles;
import java.util.concurrent.Callable;
/**
* Aspect that sets correlationId for running threads executing methods annotated with {@link Scheduled} annotation.
* For every execution of scheduled method a new, i.e. unique one, value of correlationId will be set.
*
* @author Tomasz Nurkewicz, 4financeIT
* @author Michal Chmielarz, 4financeIT
* @author Marcin Grzejszczak, 4financeIT
*
* @see UuidGenerator
* @see CorrelationIdUpdater
*/
@Aspect
public class ScheduledTaskWithCorrelationIdAspect {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private final UuidGenerator uuidGenerator;
public ScheduledTaskWithCorrelationIdAspect(UuidGenerator uuidGenerator) {
this.uuidGenerator = uuidGenerator;
}
@Around("execution (@org.springframework.scheduling.annotation.Scheduled * *.*(..))")
public Object setNewCorrelationIdOnThread(final ProceedingJoinPoint pjp) throws Throwable {
String correlationId = uuidGenerator.create();
return CorrelationIdUpdater.withId(correlationId, new Callable() {
@Override
public Object call() throws Exception {
try {
return pjp.proceed();
} catch (Throwable throwable) {
log.error("Didn't manage to proceed with the pointcut", throwable);
throw new RuntimeException(throwable);
}
}
});
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2012-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.correlation.scheduling;
import org.springframework.cloud.sleuth.correlation.UuidGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* Registers beans related to task scheduling.
*
* @see ScheduledTaskWithCorrelationIdAspect
*
* @author Michal Chmielarz, 4financeIT
*/
@Configuration
@EnableScheduling
@EnableAspectJAutoProxy
public class TaskSchedulingConfiguration {
@Bean
public ScheduledTaskWithCorrelationIdAspect scheduledTaskPointcut(UuidGenerator uuidGenerator) {
return new ScheduledTaskWithCorrelationIdAspect(uuidGenerator);
}
}

View File

@@ -0,0 +1,3 @@
# Auto Configuration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.sleuth.correlation.CorrelationIdAutoConfiguration

View File

@@ -0,0 +1,118 @@
package org.springframework.cloud.sleuth.correlation
import groovy.transform.CompileStatic
import groovy.transform.PackageScope
import groovy.transform.TypeChecked
import org.hamcrest.Description
import org.hamcrest.TypeSafeMatcher
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.boot.test.SpringApplicationContextLoader
import org.springframework.cloud.sleuth.correlation.base.HttpMockServer
import org.springframework.cloud.sleuth.correlation.base.MvcCorrelationIdSettingIntegrationSpec
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.EnableAspectJAutoProxy
import org.springframework.http.MediaType
import org.springframework.scheduling.annotation.EnableAsync
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.web.servlet.MvcResult
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
import org.springframework.test.web.servlet.result.MockMvcResultHandlers
import org.springframework.test.web.servlet.result.MockMvcResultMatchers
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestMethod
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.client.RestTemplate
import java.util.concurrent.Callable
import java.util.concurrent.TimeUnit
import static com.github.tomakehurst.wiremock.client.WireMock.*
import static org.springframework.cloud.sleuth.correlation.CorrelationIdHolder.CORRELATION_ID_HEADER
@ContextConfiguration(classes = [CorrelationIdAspectSpecConfiguration], loader = SpringApplicationContextLoader)
class CorrelationIdAspectISpec extends MvcCorrelationIdSettingIntegrationSpec {
public static final String CORRELATION_ID_PATTERN = /^(?!\s*$).+/
public static final TypeSafeMatcher<String> hasCorrelationIdSet = new TypeSafeMatcher<String>() {
@Override
protected boolean matchesSafely(String item) {
return item.matches(CORRELATION_ID_PATTERN)
}
@Override
void describeTo(Description description) {
}
}
def "should set correlationId on header via aspect in synchronous call"() {
given:
stubInteraction(get(urlMatching('.*')), aResponse().withStatus(200))
when:
mockMvc.perform(MockMvcRequestBuilders.get('/syncPing').accept(MediaType.TEXT_PLAIN))
.andExpect(MockMvcResultMatchers.header().string(CORRELATION_ID_HEADER, hasCorrelationIdSet))
then:
wireMock.verifyThat(getRequestedFor(urlMatching('.*')).withHeader(CORRELATION_ID_HEADER, matching(CORRELATION_ID_PATTERN)))
}
def "should set correlationId on header via aspect in asynchronous call"() {
given:
stubInteraction(get(urlMatching('.*')), aResponse().withStatus(200))
when:
MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get('/asyncPing').accept(MediaType.TEXT_PLAIN))
.andExpect(MockMvcResultMatchers.request().asyncStarted())
.andReturn()
and:
mvcResult.getAsyncResult(TimeUnit.SECONDS.toMillis(2))
then:
mockMvc.perform(MockMvcRequestBuilders.asyncDispatch(mvcResult)).
andDo(MockMvcResultHandlers.print()).
andExpect(MockMvcResultMatchers.status().isOk()).
andExpect(MockMvcResultMatchers.header().string(CORRELATION_ID_HEADER, hasCorrelationIdSet))
and:
wireMock.verifyThat(getRequestedFor(urlMatching('.*')).withHeader(CORRELATION_ID_HEADER, matching(CORRELATION_ID_PATTERN)))
}
@CompileStatic
@Configuration
@EnableAsync
@EnableAutoConfiguration
@EnableAspectJAutoProxy(proxyTargetClass = true)
static class CorrelationIdAspectSpecConfiguration {
@Bean
AspectTestingController aspectTestingController() {
return new AspectTestingController()
}
}
@RestController
@TypeChecked
@PackageScope
static class AspectTestingController {
@Autowired
private HttpMockServer httpMockServer
@Autowired
private RestTemplate restTemplate
@RequestMapping(value = "/syncPing", method = RequestMethod.GET, produces = MediaType.TEXT_PLAIN_VALUE)
String syncPing() {
callWiremockAndReturnOk()
}
@RequestMapping(value = "/asyncPing", method = RequestMethod.GET, produces = MediaType.TEXT_PLAIN_VALUE)
Callable<String> asyncPing() {
return {
callWiremockAndReturnOk()
}
}
private String callWiremockAndReturnOk() {
restTemplate.getForObject("http://localhost:${httpMockServer.port()}", String)
return "OK"
}
}
}

View File

@@ -0,0 +1,57 @@
package org.springframework.cloud.sleuth.correlation
import org.slf4j.MDC
import org.springframework.boot.test.SpringApplicationContextLoader
import org.springframework.cloud.sleuth.correlation.base.BaseConfiguration
import org.springframework.cloud.sleuth.correlation.base.MvcCorrelationIdSettingIntegrationSpec
import org.springframework.http.MediaType
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.web.servlet.MvcResult
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
@ContextConfiguration(classes = [BaseConfiguration, CorrelationIdAutoConfiguration], loader = SpringApplicationContextLoader)
class CorrelationIdFilterISpec extends MvcCorrelationIdSettingIntegrationSpec {
def "should create and return correlationId in HTTP header"() {
when:
MvcResult mvcResult = sendPingWithoutCorrelationId()
then:
getCorrelationIdFromResponseHeader(mvcResult) != null
}
def "when correlationId is sent, should not create a new one, but return the existing one instead"() {
given:
String passedCorrelationId = "passedCorId"
when:
MvcResult mvcResult = sendPingWithCorrelationId(passedCorrelationId)
then:
getCorrelationIdFromResponseHeader(mvcResult) == passedCorrelationId
}
def "should clean up MDC after the call"() {
given:
String passedCorrelationId = "passedCorId"
when:
sendPingWithCorrelationId(passedCorrelationId)
then:
MDC.get(CorrelationIdHolder.CORRELATION_ID_HEADER) == null
}
private MvcResult sendPingWithCorrelationId(String passedCorrelationId) {
mockMvc.perform(MockMvcRequestBuilders.get('/ping').accept(MediaType.TEXT_PLAIN)
.header(CorrelationIdHolder.CORRELATION_ID_HEADER, passedCorrelationId)).andReturn()
}
private MvcResult sendPingWithoutCorrelationId() {
mockMvc.perform(MockMvcRequestBuilders.get('/ping').accept(MediaType.TEXT_PLAIN)).andReturn()
}
private String getCorrelationIdFromResponseHeader(MvcResult mvcResult) {
mvcResult.response.getHeader(CorrelationIdHolder.CORRELATION_ID_HEADER)
}
}

View File

@@ -0,0 +1,65 @@
package org.springframework.cloud.sleuth.correlation
import spock.lang.Specification
import spock.lang.Unroll
import javax.servlet.FilterChain
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
@Unroll
class CorrelationIdFilterSkipPatternSpec extends Specification {
CorrelationIdFilter filter = new CorrelationIdFilter(Stub(UuidGenerator), CorrelationIdFilter.DEFAULT_SKIP_PATTERN)
def 'should skip meaningless URIs like #uri'() {
given:
HttpServletResponse responseMock = Mock(HttpServletResponse)
HttpServletRequest requestMock = Mock(HttpServletRequest)
and:
requestMock.getRequestURI() >> uri
when:
filter.doFilter(requestMock, responseMock, Stub(FilterChain))
then:
0 * responseMock.addHeader(CorrelationIdHolder.CORRELATION_ID_HEADER, _ as String)
where:
uri | _
'/api-docs' | _
'/api-docs/default' | _
'/swagger' | _
'/trace' | _
'/metrics' | _
'/metrics/foo' | _
'/mappings' | _
'/autoconfig' | _
'/configprops' | _
'/info' | _
'/dump' | _
'/swagger/foo' | _
'/foo.js' | _
'/foo/bar.png' | _
'/foo/bar.html' | _
'/foo/bar.css' | _
'/foo/bar.js' | _
'/foo/bar.js' | _
}
def 'should not skip #uri'() {
given:
HttpServletResponse responseMock = Mock(HttpServletResponse)
HttpServletRequest requestMock = Mock(HttpServletRequest)
and:
requestMock.getRequestURI() >> uri
when:
filter.doFilter(requestMock, responseMock, Stub(FilterChain))
then:
1 * responseMock.addHeader(CorrelationIdHolder.CORRELATION_ID_HEADER, _ as String)
where:
uri | _
'/business/api-docs' | _
'/business/swagger/foo' | _
'/foo.js/service' | _
}
}

View File

@@ -0,0 +1,66 @@
package org.springframework.cloud.sleuth.correlation
import groovyx.gpars.GParsPool
import spock.lang.Specification
import java.util.concurrent.Callable
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
class CorrelationIdUpdaterSpec extends Specification {
def cleanup() {
CorrelationIdHolder.remove()
}
def "correlation ID should not be propagated to other thread by default"() {
given:
CorrelationIdUpdater.updateCorrelationId('A')
expect:
GParsPool.withPool(1) {
["1"].eachParallel {
assert CorrelationIdHolder.get() == null
}
}
}
def "should propagate correlation ID into nested Callable"() {
given:
ExecutorService threadPool = Executors.newFixedThreadPool(1)
CorrelationIdUpdater.updateCorrelationId('A')
Callable<String> callable = new CorrelationIdTestCallable()
when:
Callable<String> wrappedCallable = CorrelationIdUpdater.wrapCallableWithId(callable)
String nestedCorrelationId = threadPool.submit(wrappedCallable).get(1, TimeUnit.SECONDS)
then:
nestedCorrelationId == 'A'
cleanup:
threadPool.shutdown()
}
def "should restore previous correlation ID after Callable execution in other thread"() {
given:
ExecutorService threadPool = Executors.newFixedThreadPool(1)
CorrelationIdUpdater.updateCorrelationId('A')
Callable<String> callable = new CorrelationIdTestCallable()
and:
threadPool.submit({ CorrelationIdHolder.set('B') }).get(1, TimeUnit.SECONDS)
when:
threadPool.submit(CorrelationIdUpdater.wrapCallableWithId(callable)).get(1, TimeUnit.SECONDS)
then:
def restoredCorrelationId = threadPool.submit({
CorrelationIdHolder.get()
} as Callable).get(1, TimeUnit.SECONDS)
restoredCorrelationId == 'B'
cleanup:
threadPool.shutdown()
}
private static class CorrelationIdTestCallable implements Callable<String> {
@Override
String call() throws Exception {
CorrelationIdHolder.get()
}
}
}

View File

@@ -0,0 +1,17 @@
package org.springframework.cloud.sleuth.correlation.base
import groovy.transform.CompileStatic
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer
@CompileStatic
@Configuration
class BaseConfiguration {
@Bean
static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer()
}
}

View File

@@ -0,0 +1,31 @@
package org.springframework.cloud.sleuth.correlation.base
import com.github.tomakehurst.wiremock.WireMockServer
import groovy.transform.CompileStatic
/**
* Custom implementation of {@link WireMockServer} that by default registers itself at port
* {@link HttpMockServer#DEFAULT_PORT}.
*
* @see WireMockServer
*/
@CompileStatic
class HttpMockServer extends WireMockServer {
public static final int DEFAULT_PORT = 8030
HttpMockServer(int port) {
super(port)
}
HttpMockServer() {
super(DEFAULT_PORT)
}
void shutdownServer() {
if (isRunning()) {
stop()
}
shutdown()
}
}

View File

@@ -0,0 +1,25 @@
package org.springframework.cloud.sleuth.correlation.base
import groovy.transform.CompileStatic
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.util.SocketUtils
/**
* Configuration that registers {@link HttpMockServer} as a Spring bean. Takes care
* of graceful shutdown process.
*
* @see HttpMockServer
*/
@CompileStatic
@Configuration
class MockServerConfiguration {
@Bean(destroyMethod = 'shutdownServer')
HttpMockServer httpMockServer() {
HttpMockServer httpMockServer = new HttpMockServer(SocketUtils.findAvailableTcpPort())
httpMockServer.start()
return httpMockServer
}
}

View File

@@ -0,0 +1,13 @@
package org.springframework.cloud.sleuth.correlation.base
import org.springframework.cloud.sleuth.correlation.CorrelationIdFilter
import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder
class MvcCorrelationIdSettingIntegrationSpec extends org.springframework.cloud.sleuth.correlation.base.MvcWiremockIntegrationSpec {
@Override
protected void configureMockMvcBuilder(ConfigurableMockMvcBuilder mockMvcBuilder) {
super.configureMockMvcBuilder(mockMvcBuilder)
mockMvcBuilder.addFilter(new CorrelationIdFilter())
}
}

View File

@@ -0,0 +1,45 @@
package org.springframework.cloud.sleuth.correlation.base
import groovy.transform.CompileStatic
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.test.context.web.WebAppConfiguration
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.web.context.WebApplicationContext
import spock.lang.Specification
/**
* Base for specifications that use Spring's {@link MockMvc}. Provides also {@link WebApplicationContext},
* {@link ApplicationContext}. The latter you can use to specify what
* kind of address should be returned for a given dependency name.
*
* @see WebApplicationContext
* @see ApplicationContext
*/
@CompileStatic
@WebAppConfiguration
abstract class MvcIntegrationSpec extends Specification {
@Autowired
protected WebApplicationContext webApplicationContext
@Autowired
protected ApplicationContext applicationContext
protected MockMvc mockMvc
void setup() {
ConfigurableMockMvcBuilder mockMvcBuilder = MockMvcBuilders.webAppContextSetup(webApplicationContext)
configureMockMvcBuilder(mockMvcBuilder)
mockMvc = mockMvcBuilder.build()
}
/**
* Override in a subclass to modify mockMvcBuilder configuration (e.g. add filter).
*
* The method from super class should be called.
*/
protected void configureMockMvcBuilder(ConfigurableMockMvcBuilder mockMvcBuilder) {
}
}

View File

@@ -0,0 +1,37 @@
package org.springframework.cloud.sleuth.correlation.base
import com.github.tomakehurst.wiremock.client.MappingBuilder
import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder
import com.github.tomakehurst.wiremock.client.WireMock
import groovy.transform.CompileStatic
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.test.context.ContextConfiguration
/**
* Base specification for tests that use Wiremock as HTTP server stub.
* By extending this specification you gain a bean with {@link HttpMockServer} and a {@link WireMock}
* instance that you can stub by using {@link MvcWiremockIntegrationSpec#stubInteraction(com.github.tomakehurst.wiremock.client.MappingBuilder, com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder)}
*
* @see MockServerConfiguration
* @see WireMock
* @see HttpMockServer
* @see MvcIntegrationSpec
*/
@CompileStatic
@ContextConfiguration(classes = [MockServerConfiguration])
abstract class MvcWiremockIntegrationSpec extends MvcIntegrationSpec {
@Autowired
protected HttpMockServer httpMockServer
protected WireMock wireMock
void setup() {
wireMock = new WireMock('localhost', httpMockServer.port())
wireMock.resetToDefaultMappings()
}
protected void stubInteraction(MappingBuilder mapping, ResponseDefinitionBuilder response) {
wireMock.register(mapping.willReturn(response))
}
}

View File

@@ -0,0 +1,40 @@
package org.springframework.cloud.sleuth.correlation.hystrix
import com.netflix.hystrix.HystrixCommand
import com.netflix.hystrix.HystrixCommandGroupKey
import org.springframework.cloud.sleuth.correlation.CorrelationIdHolder
import org.springframework.cloud.sleuth.correlation.CorrelationIdUpdater
import spock.lang.Specification
class CorrelatedCommandSpec extends Specification {
public static final String CORRELATION_ID = 'A'
def 'should run Hystrix command with client correlation ID'() {
given:
CorrelationIdUpdater.updateCorrelationId(CORRELATION_ID)
def command = new CorrelatedCommand<String>(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(""))) {
String doRun() {
return CorrelationIdHolder.get()
}
}
when:
def result = command.execute()
then:
result == CORRELATION_ID
}
def 'should run Hystrix command in different thread'() {
given:
def command = new CorrelatedCommand<String>(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(""))) {
String doRun() {
return Thread.currentThread().name
}
}
when:
def threadName = command.execute()
then:
Thread.currentThread().name != threadName
}
}

View File

@@ -0,0 +1,24 @@
package org.springframework.cloud.sleuth.correlation.scheduling
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.cloud.sleuth.correlation.base.BaseConfiguration
import org.springframework.cloud.sleuth.correlation.CorrelationIdAutoConfiguration
import org.springframework.test.context.ContextConfiguration
import spock.lang.Specification
import spock.util.concurrent.PollingConditions
@ContextConfiguration(classes = [TaskSchedulingConfiguration, ScheduledBeanConfiguration, CorrelationIdAutoConfiguration, BaseConfiguration])
class CorrelationIdOnScheduledMethodISpec extends Specification {
@Autowired
TestBeanWithScheduledMethod beanWithScheduledMethod
def "should have correlationId set after scheduled method has been called"() {
PollingConditions conditions = new PollingConditions(timeout: 1.5, initialDelay: 0.1, factor: 1.05)
expect:
conditions.eventually {
beanWithScheduledMethod.correlationId != null
}
}
}

View File

@@ -0,0 +1,14 @@
package org.springframework.cloud.sleuth.correlation.scheduling
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class ScheduledBeanConfiguration {
@Bean
TestBeanWithScheduledMethod testBeanWithScheduledMethod() {
return new TestBeanWithScheduledMethod()
}
}

View File

@@ -0,0 +1,15 @@
package org.springframework.cloud.sleuth.correlation.scheduling
import org.springframework.cloud.sleuth.correlation.CorrelationIdHolder
import org.springframework.scheduling.annotation.Scheduled
class TestBeanWithScheduledMethod {
String correlationId
@Scheduled(fixedDelay = 50L)
void scheduledMethod() {
correlationId = CorrelationIdHolder.get()
}
}

View File

@@ -16,11 +16,31 @@
<relativePath>..</relativePath>
</parent>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
@@ -65,7 +85,6 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -1,16 +1,5 @@
package org.springframework.cloud.sleuth.zipkin;
import java.util.List;
import com.github.kristofa.brave.zipkin.ZipkinSpanCollector;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import com.github.kristofa.brave.AnnotationSubmitterConfig;
import com.github.kristofa.brave.ClientTracer;
import com.github.kristofa.brave.ClientTracerConfig;
@@ -24,7 +13,18 @@ import com.github.kristofa.brave.TraceFilters;
import com.github.kristofa.brave.client.ClientRequestInterceptor;
import com.github.kristofa.brave.client.ClientResponseInterceptor;
import com.github.kristofa.brave.client.spanfilter.SpanNameFilter;
import com.github.kristofa.brave.zipkin.ZipkinSpanCollector;
import com.google.common.base.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import java.util.List;
/**
* @author Spencer Gibb
@@ -32,6 +32,7 @@ import com.google.common.base.Optional;
@Configuration
@EnableConfigurationProperties
@ConditionalOnClass(ServerTracerConfig.class)
@ConditionalOnProperty(value = "spring.cloud.sleuth.zipkin.enabled", matchIfMissing = true)
@Import({ AnnotationSubmitterConfig.class, ClientTracerConfig.class,
EndPointSubmitterConfig.class, ServerSpanThreadBinderConfig.class,
ServerTracerConfig.class })

View File

@@ -33,7 +33,8 @@ public class ZipkinRestTemplateInterceptor implements ClientHttpRequestIntercept
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
clientRequestInterceptor.handle(new RequestAdapter(request), Optional.<String>absent());
RequestAdapter requestAdapter = new RequestAdapter(request);
clientRequestInterceptor.handle(requestAdapter, Optional.<String>absent());
ClientHttpResponse response = null;
Exception exception = null;

View File

@@ -1,24 +1,22 @@
package org.springframework.cloud.sleuth.zipkin.web;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.cloud.sleuth.zipkin.ZipkinAutoConfiguration;
import org.springframework.cloud.sleuth.zipkin.ZipkinRestTemplateInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import com.github.kristofa.brave.EndPointSubmitter;
import com.github.kristofa.brave.ServerTracer;
import com.github.kristofa.brave.ServerTracerConfig;
import com.github.kristofa.brave.client.ClientRequestInterceptor;
import com.github.kristofa.brave.client.ClientResponseInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.cloud.sleuth.zipkin.ZipkinAutoConfiguration;
import org.springframework.cloud.sleuth.zipkin.ZipkinRestTemplateInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
/**
* @author Spencer Gibb
@@ -26,6 +24,7 @@ import com.github.kristofa.brave.client.ClientResponseInterceptor;
@Configuration
@ConditionalOnClass(ServerTracerConfig.class)
@ConditionalOnWebApplication
@ConditionalOnProperty(value = "spring.cloud.sleuth.zipkin.enabled", matchIfMissing = true)
@AutoConfigureAfter(ZipkinAutoConfiguration.class)
public class ZipkinWebAutoConfiguration {
@@ -35,10 +34,10 @@ public class ZipkinWebAutoConfiguration {
@Autowired
private ServerTracer serverTracer;
/*@Bean
@Bean
public ZipkinHandlerInterceptor zipkinHandlerInterceptor() {
return new ZipkinHandlerInterceptor(httpServletRequestInterceptor());
}*/
}
@Bean
public ZipkinFilter zipkinFilter() {
@@ -65,15 +64,6 @@ public class ZipkinWebAutoConfiguration {
@Autowired
private ClientResponseInterceptor clientResponseInterceptor;
@Bean
@ConditionalOnMissingBean
public RestTemplate restTemplate() {
//TODO: howto add this to an existing restTemplate without circular dependencies
RestTemplate restTemplate = new RestTemplate();
restTemplate.getInterceptors().add(zipkinRestTemplateInterceptor());
return restTemplate;
}
@Bean
public ZipkinRestTemplateInterceptor zipkinRestTemplateInterceptor() {
return new ZipkinRestTemplateInterceptor(clientRequestInterceptor,