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,