From dad0505d5a03fe1df00e787671ced3605834dca8 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Fri, 29 May 2015 08:02:23 +0200 Subject: [PATCH] CorrelationID setting by 4financeIT Below you can find people who helped to create this solution Tomasz Nurkiewicz Marcin Zajaczkowski Kamil Szymanski Michal Chmielarz Marcin Grzejszczak Jakub Nabdralik Urszula Choromanska Tomasz Dziurko Tomasz Szymanski Adam Chudzik --- pom.xml | 120 ++++++++++++-- spring-cloud-sleuth-core/pom.xml | 58 +++++++ .../DefaultRestTemplateConfigurer.java | 46 ++++++ .../resttemplate/RestTemplateConfigurer.java | 30 ++++ .../SleuthRestTemplateAutoConfiguration.java | 63 ++++++++ .../main/resources/META-INF/spring.factories | 3 + spring-cloud-sleuth-correlation/pom.xml | 132 +++++++++++++++ .../correlation/CorrelationIdAspect.java | 134 ++++++++++++++++ .../CorrelationIdAutoConfiguration.java | 74 +++++++++ .../correlation/CorrelationIdFilter.java | 151 ++++++++++++++++++ .../correlation/CorrelationIdHolder.java | 39 +++++ ...ationIdSettingRestTemplateInterceptor.java | 56 +++++++ .../correlation/CorrelationIdUpdater.java | 120 ++++++++++++++ .../sleuth/correlation/UuidGenerator.java | 29 ++++ .../hystrix/CorrelatedCommand.java | 57 +++++++ .../ScheduledTaskWithCorrelationIdAspect.java | 67 ++++++++ .../TaskSchedulingConfiguration.java | 40 +++++ .../main/resources/META-INF/spring.factories | 3 + .../CorrelationIdAspectISpec.groovy | 118 ++++++++++++++ .../CorrelationIdFilterISpec.groovy | 57 +++++++ .../CorrelationIdFilterSkipPatternSpec.groovy | 65 ++++++++ .../CorrelationIdUpdaterSpec.groovy | 66 ++++++++ .../correlation/base/BaseConfiguration.groovy | 17 ++ .../correlation/base/HttpMockServer.groovy | 31 ++++ .../base/MockServerConfiguration.groovy | 25 +++ ...CorrelationIdSettingIntegrationSpec.groovy | 13 ++ .../base/MvcIntegrationSpec.groovy | 45 ++++++ .../base/MvcWiremockIntegrationSpec.groovy | 37 +++++ .../hystrix/CorrelatedCommandSpec.groovy | 40 +++++ ...CorrelationIdOnScheduledMethodISpec.groovy | 24 +++ .../ScheduledBeanConfiguration.groovy | 14 ++ .../TestBeanWithScheduledMethod.groovy | 15 ++ spring-cloud-sleuth-zipkin/pom.xml | 21 ++- .../zipkin/ZipkinAutoConfiguration.java | 23 +-- .../zipkin/ZipkinRestTemplateInterceptor.java | 3 +- .../web/ZipkinWebAutoConfiguration.java | 40 ++--- 36 files changed, 1825 insertions(+), 51 deletions(-) create mode 100644 spring-cloud-sleuth-core/pom.xml create mode 100644 spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/resttemplate/DefaultRestTemplateConfigurer.java create mode 100644 spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/resttemplate/RestTemplateConfigurer.java create mode 100644 spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/resttemplate/SleuthRestTemplateAutoConfiguration.java create mode 100644 spring-cloud-sleuth-core/src/main/resources/META-INF/spring.factories create mode 100644 spring-cloud-sleuth-correlation/pom.xml create mode 100644 spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/CorrelationIdAspect.java create mode 100644 spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/CorrelationIdAutoConfiguration.java create mode 100644 spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/CorrelationIdFilter.java create mode 100644 spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/CorrelationIdHolder.java create mode 100644 spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/CorrelationIdSettingRestTemplateInterceptor.java create mode 100644 spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/CorrelationIdUpdater.java create mode 100644 spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/UuidGenerator.java create mode 100644 spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/hystrix/CorrelatedCommand.java create mode 100644 spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/scheduling/ScheduledTaskWithCorrelationIdAspect.java create mode 100644 spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/scheduling/TaskSchedulingConfiguration.java create mode 100644 spring-cloud-sleuth-correlation/src/main/resources/META-INF/spring.factories create mode 100644 spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/CorrelationIdAspectISpec.groovy create mode 100644 spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/CorrelationIdFilterISpec.groovy create mode 100644 spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/CorrelationIdFilterSkipPatternSpec.groovy create mode 100644 spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/CorrelationIdUpdaterSpec.groovy create mode 100644 spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/base/BaseConfiguration.groovy create mode 100755 spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/base/HttpMockServer.groovy create mode 100755 spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/base/MockServerConfiguration.groovy create mode 100644 spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/base/MvcCorrelationIdSettingIntegrationSpec.groovy create mode 100755 spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/base/MvcIntegrationSpec.groovy create mode 100755 spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/base/MvcWiremockIntegrationSpec.groovy create mode 100644 spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/hystrix/CorrelatedCommandSpec.groovy create mode 100644 spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/scheduling/CorrelationIdOnScheduledMethodISpec.groovy create mode 100644 spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/scheduling/ScheduledBeanConfiguration.groovy create mode 100644 spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/scheduling/TestBeanWithScheduledMethod.groovy diff --git a/pom.xml b/pom.xml index 92a23bf27..149b2d1b9 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,6 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - org.springframework.cloud spring-cloud-sleuth 1.0.0.BUILD-SNAPSHOT pom @@ -27,31 +26,62 @@ + spring-cloud-sleuth-core + spring-cloud-sleuth-correlation spring-cloud-sleuth-zipkin spring-cloud-sleuth-sample docs - - - org.apache.maven.plugins - maven-compiler-plugin - 3.1 - - 1.7 - 1.7 - - - + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.7 + 1.7 + + + + org.codehaus.gmavenplus + gmavenplus-plugin + 1.4 + + + + testCompile + + + + + + maven-surefire-plugin + 2.6 + + false + + **/*Spec.java + + + + + + + org.springframework.cloud + spring-cloud-sleuth-core + ${project.version} + org.springframework.cloud spring-cloud-sleuth-zipkin - 1.0.0.BUILD-SNAPSHOT + ${project.version} com.github.kristofa @@ -99,6 +129,16 @@ guava 18.0 + + commons-lang + commons-lang + 2.5 + + + io.reactivex + rxjava + 1.0.10 + org.springframework.cloud spring-cloud-starter-zuul @@ -111,12 +151,66 @@ 1.12.6 provided + + com.netflix.hystrix + hystrix-core + 1.4.5 + + + org.aspectj + aspectjrt + ${aspectj.version} + + + org.aspectj + aspectjweaver + ${aspectj.version} + + + + org.spockframework + spock-core + ${spock.version} + test + + + org.spockframework + spock-spring + ${spock.version} + test + + + cglib + cglib-nodep + 3.1 + test + + + org.objenesis + objenesis + 2.1 + test + + + org.hamcrest + hamcrest-core + 1.3 + test + + + org.codehaus.groovy + groovy-all + 2.4.3 + test + 2.4.2 1.0.1.BUILD-SNAPSHOT + 1.8.4 + 1.0-groovy-2.4 diff --git a/spring-cloud-sleuth-core/pom.xml b/spring-cloud-sleuth-core/pom.xml new file mode 100644 index 000000000..edcea1b3f --- /dev/null +++ b/spring-cloud-sleuth-core/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + spring-cloud-sleuth-core + jar + Spring Cloud Sleuth Core + Spring Cloud Sleuth Core + + + org.springframework.cloud + spring-cloud-sleuth + 1.0.0.BUILD-SNAPSHOT + .. + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.codehaus.gmavenplus + gmavenplus-plugin + + + maven-surefire-plugin + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + true + + + org.projectlombok + lombok + + provided + + + org.springframework.boot + spring-boot-starter-test + test + + + + diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/resttemplate/DefaultRestTemplateConfigurer.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/resttemplate/DefaultRestTemplateConfigurer.java new file mode 100644 index 000000000..f7e33be80 --- /dev/null +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/resttemplate/DefaultRestTemplateConfigurer.java @@ -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 clientHttpRequestInterceptors; + + public DefaultRestTemplateConfigurer(List clientHttpRequestInterceptors) { + this.clientHttpRequestInterceptors = clientHttpRequestInterceptors; + } + + @Override + public void modifyRestTemplate(RestTemplate restTemplate) { + for (ClientHttpRequestInterceptor clientHttpRequestInterceptor : clientHttpRequestInterceptors) { + restTemplate.getInterceptors().add(clientHttpRequestInterceptor); + } + } +} diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/resttemplate/RestTemplateConfigurer.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/resttemplate/RestTemplateConfigurer.java new file mode 100644 index 000000000..54b643d40 --- /dev/null +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/resttemplate/RestTemplateConfigurer.java @@ -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); +} diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/resttemplate/SleuthRestTemplateAutoConfiguration.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/resttemplate/SleuthRestTemplateAutoConfiguration.java new file mode 100644 index 000000000..662176624 --- /dev/null +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/resttemplate/SleuthRestTemplateAutoConfiguration.java @@ -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 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; + } + } + +} diff --git a/spring-cloud-sleuth-core/src/main/resources/META-INF/spring.factories b/spring-cloud-sleuth-core/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..9d7ec500c --- /dev/null +++ b/spring-cloud-sleuth-core/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Auto Configuration +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.sleuth.resttemplate.SleuthRestTemplateAutoConfiguration diff --git a/spring-cloud-sleuth-correlation/pom.xml b/spring-cloud-sleuth-correlation/pom.xml new file mode 100644 index 000000000..697ebf14a --- /dev/null +++ b/spring-cloud-sleuth-correlation/pom.xml @@ -0,0 +1,132 @@ + + + 4.0.0 + + spring-cloud-sleuth-correlation + jar + Spring Cloud Sleuth Correlation + Spring Cloud Sleuth Correlation + + + org.springframework.cloud + spring-cloud-sleuth + 1.0.0.BUILD-SNAPSHOT + .. + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.codehaus.gmavenplus + gmavenplus-plugin + + + maven-surefire-plugin + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.cloud + spring-cloud-sleuth-core + + + org.springframework.boot + spring-boot-starter-actuator + true + + + commons-lang + commons-lang + + + org.projectlombok + lombok + + provided + + + org.springframework.boot + spring-boot-starter-test + test + + + com.netflix.hystrix + hystrix-core + + + org.aspectj + aspectjrt + + + org.aspectj + aspectjweaver + runtime + + + io.reactivex + rxjava + + + + org.spockframework + spock-core + test + + + org.spockframework + spock-spring + test + + + cglib + cglib-nodep + test + + + org.objenesis + objenesis + test + + + org.hamcrest + hamcrest-core + test + + + org.codehaus.groovy + groovy-all + test + + + org.codehaus.gpars + gpars + 1.2.1 + test + + + com.github.tomakehurst + wiremock + 1.53 + test + + + com.github.stefanbirkner + system-rules + 1.9.0 + test + + + + diff --git a/spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/CorrelationIdAspect.java b/spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/CorrelationIdAspect.java new file mode 100644 index 000000000..b73665aba --- /dev/null +++ b/spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/CorrelationIdAspect.java @@ -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 + *

+ *

    + *
  • {@link RestController} annotated classes + * with public {@link Callable} methods
  • + *
  • {@link Controller} annotated classes + * with public {@link Callable} methods
  • + *
  • explicit {@link RestOperations}.exchange(..) method calls
  • + *
+ *

+ * For controllers an around aspect is created that wraps the {@link Callable#call()} method execution + * in {@link CorrelationIdUpdater#wrapCallableWithId(Callable)} + *

+ * For {@link RestOperations} we are wrapping all executions of the + * exchange 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 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 modifyHttpEntityInMethodArguments(ProceedingJoinPoint pjp, HttpEntity newHttpEntity) { + List 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; + } +} diff --git a/spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/CorrelationIdAutoConfiguration.java b/spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/CorrelationIdAutoConfiguration.java new file mode 100644 index 000000000..51e71dfac --- /dev/null +++ b/spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/CorrelationIdAutoConfiguration.java @@ -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(); + } +} diff --git a/spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/CorrelationIdFilter.java b/spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/CorrelationIdFilter.java new file mode 100644 index 000000000..96a6aef07 --- /dev/null +++ b/spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/CorrelationIdFilter.java @@ -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() { + @Override + public String call() throws Exception { + return response.getHeader(CORRELATION_ID_HEADER); + } + }); + } + + private String getCorrelationIdFrom(final HttpServletRequest request) { + return withLoggingAs("request", new Callable() { + @Override + public String call() throws Exception { + return request.getHeader(CORRELATION_ID_HEADER); + } + }); + } + + private String withLoggingAs(String whereWasFound, Callable 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 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; + } +} diff --git a/spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/CorrelationIdHolder.java b/spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/CorrelationIdHolder.java new file mode 100644 index 000000000..6474887a5 --- /dev/null +++ b/spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/CorrelationIdHolder.java @@ -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 id = new ThreadLocal(); + + public static void set(String correlationId) { + id.set(correlationId); + } + + public static String get() { + return id.get(); + } + + public static void remove() { + id.remove(); + } +} diff --git a/spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/CorrelationIdSettingRestTemplateInterceptor.java b/spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/CorrelationIdSettingRestTemplateInterceptor.java new file mode 100644 index 000000000..3f1b6fcb8 --- /dev/null +++ b/spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/CorrelationIdSettingRestTemplateInterceptor.java @@ -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()); + } + } + +} diff --git a/spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/CorrelationIdUpdater.java b/spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/CorrelationIdUpdater.java new file mode 100644 index 000000000..0becfca32 --- /dev/null +++ b/spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/CorrelationIdUpdater.java @@ -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 withId(String temporaryCorrelationId, Callable 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. + *

+ *

+ * Useful in a situation when a Callable should be executed in a separate thread, for example in aspects. + *

+ *


+	 * @Around('...')
+	 * Object wrapWithCorrelationId(ProceedingJoinPoint pjp) throws Throwable {
+	 *     Callable callable = pjp.proceed() as Callable
+	 *     return CorrelationIdUpdater.wrapCallableWithId {
+	 *         callable.call()
+	 *     }
+	 * }
+	 * 
+ *

+ * Note: 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 Callable wrapCallableWithId(final Callable block) { + final String temporaryCorrelationId = CorrelationIdHolder.get(); + // unchecked assignment due to groovyc issues with + return new Callable() { + @Override + public Object call() throws Exception { + final String oldCorrelationId = CorrelationIdHolder.get(); + try { + updateCorrelationId(temporaryCorrelationId); + return block.call(); + } finally { + updateCorrelationId(oldCorrelationId); + } + } + }; + } +} diff --git a/spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/UuidGenerator.java b/spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/UuidGenerator.java new file mode 100644 index 000000000..e130097a8 --- /dev/null +++ b/spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/UuidGenerator.java @@ -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 extends HystrixCommand { + 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() { + @Override + public R call() throws Exception { + return doRun(); + } + + }); + } + + public abstract R doRun() throws Exception; +} diff --git a/spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/scheduling/ScheduledTaskWithCorrelationIdAspect.java b/spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/scheduling/ScheduledTaskWithCorrelationIdAspect.java new file mode 100644 index 000000000..6af07d5a7 --- /dev/null +++ b/spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/scheduling/ScheduledTaskWithCorrelationIdAspect.java @@ -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); + } + } + }); + } +} diff --git a/spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/scheduling/TaskSchedulingConfiguration.java b/spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/scheduling/TaskSchedulingConfiguration.java new file mode 100644 index 000000000..e0ace7fbe --- /dev/null +++ b/spring-cloud-sleuth-correlation/src/main/java/org/springframework/cloud/sleuth/correlation/scheduling/TaskSchedulingConfiguration.java @@ -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); + } + +} diff --git a/spring-cloud-sleuth-correlation/src/main/resources/META-INF/spring.factories b/spring-cloud-sleuth-correlation/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..387f34393 --- /dev/null +++ b/spring-cloud-sleuth-correlation/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Auto Configuration +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.sleuth.correlation.CorrelationIdAutoConfiguration diff --git a/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/CorrelationIdAspectISpec.groovy b/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/CorrelationIdAspectISpec.groovy new file mode 100644 index 000000000..a5a368a2f --- /dev/null +++ b/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/CorrelationIdAspectISpec.groovy @@ -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 hasCorrelationIdSet = new TypeSafeMatcher() { + @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 asyncPing() { + return { + callWiremockAndReturnOk() + } + } + + private String callWiremockAndReturnOk() { + restTemplate.getForObject("http://localhost:${httpMockServer.port()}", String) + return "OK" + } + } + +} diff --git a/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/CorrelationIdFilterISpec.groovy b/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/CorrelationIdFilterISpec.groovy new file mode 100644 index 000000000..f825632aa --- /dev/null +++ b/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/CorrelationIdFilterISpec.groovy @@ -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) + } +} diff --git a/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/CorrelationIdFilterSkipPatternSpec.groovy b/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/CorrelationIdFilterSkipPatternSpec.groovy new file mode 100644 index 000000000..b4841a435 --- /dev/null +++ b/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/CorrelationIdFilterSkipPatternSpec.groovy @@ -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' | _ + } + + +} diff --git a/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/CorrelationIdUpdaterSpec.groovy b/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/CorrelationIdUpdaterSpec.groovy new file mode 100644 index 000000000..b6cd61c7f --- /dev/null +++ b/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/CorrelationIdUpdaterSpec.groovy @@ -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 callable = new CorrelationIdTestCallable() + when: + Callable 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 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 { + @Override + String call() throws Exception { + CorrelationIdHolder.get() + } + } +} diff --git a/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/base/BaseConfiguration.groovy b/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/base/BaseConfiguration.groovy new file mode 100644 index 000000000..d3fc8744f --- /dev/null +++ b/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/base/BaseConfiguration.groovy @@ -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() + } + +} diff --git a/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/base/HttpMockServer.groovy b/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/base/HttpMockServer.groovy new file mode 100755 index 000000000..629190b34 --- /dev/null +++ b/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/base/HttpMockServer.groovy @@ -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() + } +} diff --git a/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/base/MockServerConfiguration.groovy b/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/base/MockServerConfiguration.groovy new file mode 100755 index 000000000..7e9a97c61 --- /dev/null +++ b/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/base/MockServerConfiguration.groovy @@ -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 + } + +} diff --git a/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/base/MvcCorrelationIdSettingIntegrationSpec.groovy b/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/base/MvcCorrelationIdSettingIntegrationSpec.groovy new file mode 100644 index 000000000..6c55d7e19 --- /dev/null +++ b/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/base/MvcCorrelationIdSettingIntegrationSpec.groovy @@ -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()) + } +} diff --git a/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/base/MvcIntegrationSpec.groovy b/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/base/MvcIntegrationSpec.groovy new file mode 100755 index 000000000..4cd63fd9e --- /dev/null +++ b/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/base/MvcIntegrationSpec.groovy @@ -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) { + } +} diff --git a/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/base/MvcWiremockIntegrationSpec.groovy b/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/base/MvcWiremockIntegrationSpec.groovy new file mode 100755 index 000000000..5e84ff830 --- /dev/null +++ b/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/base/MvcWiremockIntegrationSpec.groovy @@ -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)) + } + +} diff --git a/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/hystrix/CorrelatedCommandSpec.groovy b/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/hystrix/CorrelatedCommandSpec.groovy new file mode 100644 index 000000000..e97c65761 --- /dev/null +++ b/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/hystrix/CorrelatedCommandSpec.groovy @@ -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(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(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(""))) { + String doRun() { + return Thread.currentThread().name + } + } + when: + def threadName = command.execute() + then: + Thread.currentThread().name != threadName + } + +} diff --git a/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/scheduling/CorrelationIdOnScheduledMethodISpec.groovy b/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/scheduling/CorrelationIdOnScheduledMethodISpec.groovy new file mode 100644 index 000000000..1d58c1c50 --- /dev/null +++ b/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/scheduling/CorrelationIdOnScheduledMethodISpec.groovy @@ -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 + } + } + +} diff --git a/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/scheduling/ScheduledBeanConfiguration.groovy b/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/scheduling/ScheduledBeanConfiguration.groovy new file mode 100644 index 000000000..7bddf1cdb --- /dev/null +++ b/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/scheduling/ScheduledBeanConfiguration.groovy @@ -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() + } + +} diff --git a/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/scheduling/TestBeanWithScheduledMethod.groovy b/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/scheduling/TestBeanWithScheduledMethod.groovy new file mode 100644 index 000000000..66eb128ed --- /dev/null +++ b/spring-cloud-sleuth-correlation/src/test/groovy/org/springframework/cloud/sleuth/correlation/scheduling/TestBeanWithScheduledMethod.groovy @@ -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() + } + +} diff --git a/spring-cloud-sleuth-zipkin/pom.xml b/spring-cloud-sleuth-zipkin/pom.xml index a1cd2f182..262b5359d 100644 --- a/spring-cloud-sleuth-zipkin/pom.xml +++ b/spring-cloud-sleuth-zipkin/pom.xml @@ -16,11 +16,31 @@ .. + + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.codehaus.gmavenplus + gmavenplus-plugin + + + maven-surefire-plugin + + + + org.springframework.boot spring-boot-starter-web + + org.springframework.cloud + spring-cloud-sleuth-core + org.springframework.boot spring-boot-starter-actuator @@ -65,7 +85,6 @@ spring-boot-starter-test test - diff --git a/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/ZipkinAutoConfiguration.java b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/ZipkinAutoConfiguration.java index 472d2d483..c4a3e69f4 100644 --- a/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/ZipkinAutoConfiguration.java +++ b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/ZipkinAutoConfiguration.java @@ -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 }) diff --git a/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/ZipkinRestTemplateInterceptor.java b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/ZipkinRestTemplateInterceptor.java index 718feae85..bb6128ddf 100644 --- a/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/ZipkinRestTemplateInterceptor.java +++ b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/ZipkinRestTemplateInterceptor.java @@ -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.absent()); + RequestAdapter requestAdapter = new RequestAdapter(request); + clientRequestInterceptor.handle(requestAdapter, Optional.absent()); ClientHttpResponse response = null; Exception exception = null; diff --git a/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/web/ZipkinWebAutoConfiguration.java b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/web/ZipkinWebAutoConfiguration.java index 56c2d1c34..ba22a6999 100644 --- a/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/web/ZipkinWebAutoConfiguration.java +++ b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin/web/ZipkinWebAutoConfiguration.java @@ -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,