Merge pull request #4 from spring-cloud-incubator/correlation-abstraction2

Tracing abstraction
This commit is contained in:
Spencer Gibb
2015-07-04 01:58:57 -05:00
75 changed files with 1969 additions and 1651 deletions

View File

@@ -1,2 +1,13 @@
TODO: intro Spring Cloud Sleuth
=== Terminology
Spring Cloud Sleuth borrows http://research.google.com/pubs/pub36356.html[Dapper's] terminology.
*Span:* The basic unit of work. For example, sending an RPC is a new span, as is sending a response to an RPC. Span's are identified by a unique 64-bit ID for the span and another 64-bit ID for the trace the span is a part of. Spans also have other data, such as descriptions, key-value annotations, the ID of the span that caused them, and process ID's (normally IP address).
Spans are started and stopped, and they keep track of their timing information. Once you create a span, you must stop it at some point in the future.
*Trace:* A set of spans forming a tree-like structure. For example, if you are running a distributed big-data store, a trace might be formed by a put request.

View File

@@ -27,8 +27,6 @@
<modules>
<module>spring-cloud-sleuth-core</module>
<module>spring-cloud-sleuth-correlation</module>
<module>spring-cloud-sleuth-zipkin</module>
<module>spring-cloud-sleuth-sample</module>
<module>docs</module>
</modules>
@@ -148,7 +146,7 @@
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.12.6</version>
<version>1.16.4</version>
<scope>provided</scope>
</dependency>
<dependency>
@@ -156,6 +154,11 @@
<artifactId>hystrix-core</artifactId>
<version>1.4.5</version>
</dependency>
<dependency>
<groupId>com.netflix.zuul</groupId>
<artifactId>zuul-core</artifactId>
<version>1.0.28</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>

View File

@@ -42,6 +42,26 @@
<artifactId>spring-boot-starter-actuator</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-core</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.netflix.zuul</groupId>
<artifactId>zuul-core</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.reactivex</groupId>
<artifactId>rxjava</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>

View File

@@ -0,0 +1,21 @@
package org.springframework.cloud.sleuth;
import java.util.ArrayList;
import lombok.Value;
import org.springframework.cloud.sleuth.event.SpanStoppedEvent;
import org.springframework.context.ApplicationListener;
/**
* @author Spencer Gibb
*/
@Value
public class ArrayListSpanAccumulator implements ApplicationListener<SpanStoppedEvent> {
private final ArrayList<Span> spans = new ArrayList<>();
@Override
public void onApplicationEvent(SpanStoppedEvent event) {
spans.add(event.getSpan());
}
}

View File

@@ -0,0 +1,156 @@
package org.springframework.cloud.sleuth;
import static org.springframework.cloud.sleuth.Utils.error;
import java.util.Collections;
import java.util.concurrent.Callable;
import org.springframework.cloud.sleuth.event.SpanStartedEvent;
import org.springframework.cloud.sleuth.instrument.TraceCallable;
import org.springframework.cloud.sleuth.instrument.TraceRunnable;
import org.springframework.context.ApplicationEventPublisher;
/**
* @author Spencer Gibb
*/
public class DefaultTrace implements Trace {
private final Sampler<?> defaultSampler;
private final IdGenerator idGenerator;
private final ApplicationEventPublisher publisher;
public DefaultTrace(Sampler<?> defaultSampler, IdGenerator idGenerator,
ApplicationEventPublisher publisher) {
this.defaultSampler = defaultSampler;
this.idGenerator = idGenerator;
this.publisher = publisher;
}
@Override
public TraceScope startSpan(String name) {
return this.startSpan(name, defaultSampler);
}
@Override
public TraceScope startSpan(String name, TraceInfo tinfo) {
if (tinfo == null) return doStart(null);
MilliSpan span = MilliSpan.builder()
.begin(System.currentTimeMillis())
.name(name)
.traceId(tinfo.getTraceId())
.spanId(idGenerator.create())
.parent(tinfo.getSpanId())
.build();
return doStart(span);
}
@Override
public TraceScope startSpan(String name, Span parent) {
if (parent == null) {
return startSpan(name);
}
Span currentSpan = getCurrentSpan();
if ((currentSpan != null) && (currentSpan != parent)) {
error("HTrace client error: thread " +
Thread.currentThread().getName() + " tried to start a new Span " +
"with parent " + parent.toString() + ", but there is already a " +
"currentSpan " + currentSpan);
}
return doStart(createChild(parent, name));
}
@Override
public <T> TraceScope startSpan(String name, Sampler<T> s) {
return startSpan(name, s, null);
}
@Override
public <T> TraceScope startSpan(String name, Sampler<T> s, T info) {
Span span = null;
if (TraceContextHolder.isTracing() || s.next(info)) {
span = createNew(name);
}
return doStart(span);
}
protected Span createNew(String name) {
Span parent = getCurrentSpan();
if (parent == null) {
return MilliSpan.builder()
.begin(System.currentTimeMillis())
.name(name)
.traceId(idGenerator.create())
.spanId(idGenerator.create())
.build();
} else {
return createChild(parent, name);
}
}
protected Span createChild(Span parent, String childname) {
return MilliSpan.builder()
.begin(System.currentTimeMillis())
.name(childname)
.traceId(parent.getTraceId())
.parent(parent.getSpanId())
.spanId(idGenerator.create())
.processId(parent.getProcessId())
.build();
}
protected TraceScope doStart(Span span) {
if (span != null) {
publisher.publishEvent(new SpanStartedEvent(this, span));
}
return continueSpan(span);
}
@Override
public TraceScope continueSpan(Span span) {
// Return an empty TraceScope that does nothing on close
if (span == null) return NullScope.INSTANCE;
Span oldSpan = getCurrentSpan();
TraceContextHolder.setCurrentSpan(span);
return new TraceScope(this.publisher, span, oldSpan);
}
protected Span getCurrentSpan() {
return TraceContextHolder.getCurrentSpan();
}
@Override
public void addKVAnnotation(String key, String value) {
Span s = getCurrentSpan();
if (s != null) {
s.addKVAnnotation(key, value);
}
}
/**
* Wrap the callable in a TraceCallable, if tracing.
*
* @return The callable provided, wrapped if tracing, 'callable' if not.
*/
@Override
public <V> Callable<V> wrap(Callable<V> callable) {
if (TraceContextHolder.isTracing()) {
return new TraceCallable<>(this, callable, TraceContextHolder.getCurrentSpan());
}
return callable;
}
/**
* Wrap the runnable in a TraceRunnable, if tracing.
*
* @return The runnable provided, wrapped if tracing, 'runnable' if not.
*/
@Override
public Runnable wrap(Runnable runnable) {
if (TraceContextHolder.isTracing()) {
return new TraceRunnable(this, runnable, TraceContextHolder.getCurrentSpan());
}
return runnable;
}
}

View File

@@ -0,0 +1,8 @@
package org.springframework.cloud.sleuth;
/**
* @author Spencer Gibb
*/
public interface IdGenerator {
String create();
}

View File

@@ -0,0 +1,67 @@
package org.springframework.cloud.sleuth;
import java.util.List;
import java.util.Map;
import lombok.Builder;
import lombok.Singular;
import lombok.Value;
import lombok.experimental.NonFinal;
/**
* @author Spencer Gibb
*/
@Value
@Builder
public class MilliSpan implements Span {
private long begin;
@NonFinal
private long end = 0;
private String name;
private String traceId;
@Singular
private List<String> parents;
private String spanId;
private Map<String, String> kVAnnotations;
private String processId;
@Singular
private List<TimelineAnnotation> timelineAnnotations;
@Override
public synchronized void stop() {
if (end == 0) {
if (begin == 0) {
throw new IllegalStateException("Span for " + name
+ " has not been started");
}
end = System.currentTimeMillis();
}
}
@Override
public synchronized long getAccumulatedMillis() {
if (begin == 0) {
return 0;
}
if (end > 0) {
return end - begin;
}
return System.currentTimeMillis() - begin;
}
@Override
public synchronized boolean isRunning() {
return begin != 0 && end == 0;
}
@Override
public void addKVAnnotation(String key, String value) {
kVAnnotations.put(key, value);
}
@Override
public void addTimelineAnnotation(String msg) {
timelineAnnotations.add(new TimelineAnnotation(System.currentTimeMillis(), msg));
}
}

View File

@@ -0,0 +1,31 @@
package org.springframework.cloud.sleuth;
/**
* @author Spencer Gibb
*/
/**
* Singleton instance representing an empty {@link TraceScope}.
*/
public final class NullScope extends TraceScope {
public static final TraceScope INSTANCE = new NullScope();
private NullScope() {
super(null, null, null);
}
@Override
public Span detach() {
return null;
}
@Override
public void close() {
return;
}
@Override
public String toString() {
return "NullScope";
}
}

View File

@@ -0,0 +1,14 @@
package org.springframework.cloud.sleuth;
import java.util.UUID;
/**
* @author Spencer Gibb
*/
public class RandomUuidGenerator implements IdGenerator {
@Override
public String create() {
return UUID.randomUUID().toString();
}
}

View File

@@ -0,0 +1,33 @@
package org.springframework.cloud.sleuth;
/**
* Extremely simple callback to determine the frequency that an action should be
* performed.
* <p/>
* 'T' is the object type you require to create a more advanced sampling
* function. For example if there is some RPC information in a 'Call' object,
* you might implement Sampler<Call>. Then when the RPC is received you can call
* one of the Trace.java functions that takes the extra 'info' parameter, which
* will be passed into the next function you implemented.
* <p/>
* For the example above, the next(T info) function may look like this
* <p/>
* <pre>
* <code>public boolean next(T info) {
* if (info == null) {
* return false;
* } else if (info.getName().equals("get")) {
* return Math.random() > 0.5;
* } else if (info.getName().equals("put")) {
* return Math.random() > 0.25;
* } else {
* return false;
* }
* }
* </code>
* </pre>
* This would trace 50% of all gets, 75% of all puts and would not trace any other requests.
*/
public interface Sampler<T> {
boolean next(T info);
}

View File

@@ -0,0 +1,99 @@
package org.springframework.cloud.sleuth;
import java.util.List;
import java.util.Map;
/**
* Base interface for gathering and reporting statistics about a block of
* execution.
* <p/>
* Spans should form a directed acyclic graph structure. It should be possible
* to keep following the parents of a span until you arrive at a span with no
* parents.<p/>
*/
public interface Span {
/**
* The block has completed, stop the clock
*/
void stop();
/**
* Get the start time, in milliseconds
*/
long getBegin();
/**
* Get the stop time, in milliseconds
*/
long getEnd();
/**
* Return the total amount of time elapsed since start was called, if running,
* or difference between stop and start
*/
long getAccumulatedMillis();
/**
* Has the span been started and not yet stopped?
*/
boolean isRunning();
/**
* Return a textual name of this span.<p/>
* <p/>
* Will never be null.
*/
String getName();
/**
* A pseudo-unique (random) number assigned to this span instance.<p/>
* <p/>
* The spanId is immutable and cannot be changed. It is safe to access this
* from multiple threads.
*/
String getSpanId();
/**
* A pseudo-unique (random) number assigned to the trace associated with this
* span
*/
String getTraceId();
/**
* Returns the parent IDs of the span.<p/>
* <p/>
* The collection will be empty if there are no parents.
*/
List<String> getParents();
/**
* Add a data annotation associated with this span
*/
void addKVAnnotation(String key, String value);
/**
* Add a timeline annotation associated with this span
*/
void addTimelineAnnotation(String msg);
/**
* Get data associated with this span (read only)<p/>
* <p/>
* Will never be null.
*/
Map<String, String> getKVAnnotations();
/**
* Get any timeline annotations (read only)<p/>
* <p/>
* Will never be null.
*/
List<TimelineAnnotation> getTimelineAnnotations();
/**
* Return a unique id for the process from which this Span originated.<p/>
* <p/>
* Will never be null.
*/
String getProcessId();
}

View File

@@ -0,0 +1,12 @@
package org.springframework.cloud.sleuth;
import lombok.Data;
/**
* @author Spencer Gibb
*/
@Data
public class TimelineAnnotation {
private final long time;
private final String msg;
}

View File

@@ -0,0 +1,85 @@
package org.springframework.cloud.sleuth;
import java.util.concurrent.Callable;
/**
* The Trace class is the primary way to interact with the library. It provides
* methods to create and manipulate spans.
*
* A 'Span' represents a length of time. It has many other attributes such as a
* name, ID, and even potentially a set of key/value strings attached to
* it.
*
* Each thread in your application has a single currently active currentSpan
* associated with it. When this is non-null, it represents the current
* operation that the thread is doing. Spans are NOT thread-safe, and must
* never be used by multiple threads at once. With care, it is possible to
* safely pass a Span object between threads, but in most cases this is not
* necessary.
*
* A 'TraceScope' can either be empty, or contain a Span. TraceScope objects
* implement the Java's Closeable interface. Similar to file descriptors, they
* must be closed after they are created. When a TraceScope contains a Span,
* this span is closed when the scope is closed.
*
* The 'startSpan' methods in this class do a few things:
* <ul>
* <li>Create a new Span which has this thread's currentSpan as one of its parents.</li>
* <li>Set currentSpan to the new Span.</li>
* <li>Create a TraceSpan object to manage the new Span.</li>
* </ul>
*
* Closing a TraceScope does a few things:
* <ul>
* <li>It closes the span which the scope was managing.</li>
* <li>Set currentSpan to the previous currentSpan (which may be null).</li>
* </ul>
*/
public interface Trace {
String SPAN_ID_NAME = "Span-Id";
String TRACE_ID_NAME = "Trace-Id";
/**
* Creates a new trace scope.
* <p/>
* If this thread has a currently active trace span, the trace scope we create
* here will contain a new span descending from the currently active span.
* If there is no currently active trace span, the trace scope we create will
* be empty.
*
* @param name The name field for the new span to create.
*/
TraceScope startSpan(String name);
TraceScope startSpan(String name, TraceInfo tinfo);
/**
* Creates a new trace scope.
* <p/>
* If this thread has a currently active trace span, it must be the 'parent'
* span that you pass in here as a parameter. The trace scope we create here
* will contain a new span which is a child of 'parent'.
*
* @param name The name field for the new span to create.
*/
TraceScope startSpan(String name, Span parent);
<T> TraceScope startSpan(String name, Sampler<T> s);
<T> TraceScope startSpan(String name, Sampler<T> s, T info);
/**
* Pick up an existing span from another thread.
*/
TraceScope continueSpan(Span s);
/**
* Adds a data annotation to the current span if tracing is currently on.
*/
void addKVAnnotation(String key, String value);
<V> Callable<V> wrap(Callable<V> callable);
Runnable wrap(Runnable runnable);
}

View File

@@ -0,0 +1,33 @@
package org.springframework.cloud.sleuth;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.sleuth.sampler.IsTracingSampler;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author Spencer Gibb
*/
@Configuration
public class TraceAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public IdGenerator idGenerator() {
return new RandomUuidGenerator();
}
@Bean
@ConditionalOnMissingBean
public Sampler defaultSampler() {
return new IsTracingSampler();
}
@Bean
@ConditionalOnMissingBean
public Trace trace(Sampler sampler, IdGenerator idGenerator,
ApplicationEventPublisher publisher) {
return new DefaultTrace(sampler, idGenerator, publisher);
}
}

View File

@@ -0,0 +1,27 @@
package org.springframework.cloud.sleuth;
import lombok.extern.apachecommons.CommonsLog;
import org.springframework.core.NamedThreadLocal;
/**
* @author Spencer Gibb
*/
@CommonsLog
public class TraceContextHolder {
private static final ThreadLocal<Span> currentSpan = new NamedThreadLocal<>("Trace Context");
public static Span getCurrentSpan() {
return currentSpan.get();
}
public static void setCurrentSpan(Span span) {
if (log.isTraceEnabled()) {
log.trace("Setting current span " + span);
}
currentSpan.set(span);
}
public static boolean isTracing() {
return currentSpan.get() != null;
}
}

View File

@@ -0,0 +1,12 @@
package org.springframework.cloud.sleuth;
import lombok.Data;
/**
* @author Spencer Gibb
*/
@Data
public class TraceInfo {
private final String traceId;
private final String spanId;
}

View File

@@ -0,0 +1,86 @@
package org.springframework.cloud.sleuth;
import java.io.Closeable;
import lombok.SneakyThrows;
import lombok.Value;
import lombok.experimental.NonFinal;
import org.springframework.cloud.sleuth.event.SpanStoppedEvent;
import org.springframework.context.ApplicationEventPublisher;
/**
* @author Spencer Gibb
*/
@Value
@NonFinal
public class TraceScope implements Closeable {
private final ApplicationEventPublisher publisher;
/**
* the span for this scope
*/
private final Span span;
/**
* the span that was "current" before this scope was entered
*/
private final Span savedSpan;
@NonFinal
private boolean detached = false;
public TraceScope(ApplicationEventPublisher publisher, Span span, Span savedSpan) {
this.publisher = publisher;
this.span = span;
this.savedSpan = savedSpan;
}
/**
* Remove this span as the current thread, but don't stop it yet or
* send it for collection. This is useful if the span object is then
* passed to another thread for use with Trace.continueTrace().
*
* @return the same Span object
*/
public Span detach() {
if (detached) {
Utils.error("Tried to detach trace span " + span + " but " +
"it has already been detached.");
}
detached = true;
Span cur = TraceContextHolder.getCurrentSpan();
if (cur != span) {
Utils.error("Tried to detach trace span " + span + " but " +
"it is not the current span for the " +
Thread.currentThread().getName() + " thread. You have " +
"probably forgotten to close or detach " + cur);
} else {
TraceContextHolder.setCurrentSpan(savedSpan);
}
return span;
}
@Override
@SneakyThrows
public void close() {
if (detached) {
return;
}
detached = true;
Span cur = TraceContextHolder.getCurrentSpan();
if (cur != span) {
Utils.error("Tried to close trace span " + span + " but " +
"it is not the current span for the " +
Thread.currentThread().getName() + " thread. You have " +
"probably forgotten to close or detach " + cur);
} else {
span.stop();
this.publisher.publishEvent(new SpanStoppedEvent(this, span));
TraceContextHolder.setCurrentSpan(savedSpan);
}
}
}

View File

@@ -0,0 +1,14 @@
package org.springframework.cloud.sleuth;
import lombok.extern.apachecommons.CommonsLog;
/**
* @author Spencer Gibb
*/
@CommonsLog
public abstract class Utils {
public static void error(String msg) {
log.error(msg);
throw new RuntimeException(msg);
}
}

View File

@@ -0,0 +1,19 @@
package org.springframework.cloud.sleuth.event;
import lombok.Value;
import org.springframework.cloud.sleuth.Span;
import org.springframework.context.ApplicationEvent;
/**
* @author Spencer Gibb
*/
@Value
public class SpanStartedEvent extends ApplicationEvent {
private final Span span;
public SpanStartedEvent(Object source, Span span) {
super(source);
this.span = span;
}
}

View File

@@ -0,0 +1,19 @@
package org.springframework.cloud.sleuth.event;
import lombok.Value;
import org.springframework.cloud.sleuth.Span;
import org.springframework.context.ApplicationEvent;
/**
* @author Spencer Gibb
*/
@Value
public class SpanStoppedEvent extends ApplicationEvent {
private final Span span;
public SpanStoppedEvent(Object source, Span span) {
super(source);
this.span = span;
}
}

View File

@@ -0,0 +1,47 @@
package org.springframework.cloud.sleuth.instrument;
import java.util.concurrent.Callable;
import lombok.Value;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.Trace;
import org.springframework.cloud.sleuth.TraceScope;
/**
* @author Spencer Gibb
*/
@Value
public class TraceCallable<V> extends TraceDelegate<Callable<V>> implements Callable<V> {
public TraceCallable(Trace trace, Callable<V> delagate) {
super(trace, delagate);
}
public TraceCallable(Trace trace, Callable<V> delagate, Span parent) {
super(trace, delagate, parent);
}
public TraceCallable(Trace trace, Callable<V> delagate, Span parent, String name) {
super(trace, delagate, parent, name);
}
@Override
public V call() throws Exception {
if (this.parent != null) {
TraceScope scope = startSpan();
try {
return this.delagate.call();
}
finally {
scope.close();
}
}
else {
return this.delagate.call();
}
}
}

View File

@@ -0,0 +1,40 @@
package org.springframework.cloud.sleuth.instrument;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.Trace;
import org.springframework.cloud.sleuth.TraceContextHolder;
import org.springframework.cloud.sleuth.TraceScope;
/**
* @author Spencer Gibb
*/
public abstract class TraceDelegate<T> {
protected final Trace trace;
protected final T delagate;
protected final Span parent;
protected final String name;
public TraceDelegate(Trace trace, T delagate) {
this(trace, delagate, TraceContextHolder.getCurrentSpan(), null);
}
public TraceDelegate(Trace trace, T delagate, Span parent) {
this(trace, delagate, parent, null);
}
public TraceDelegate(Trace trace, T delagate, Span parent, String name) {
this.trace = trace;
this.delagate = delagate;
this.parent = parent;
this.name = name;
}
protected TraceScope startSpan() {
return this.trace.startSpan(getSpanName(), this.parent);
}
protected String getSpanName() {
return this.name == null ? Thread.currentThread().getName() : name;
}
}

View File

@@ -0,0 +1,43 @@
package org.springframework.cloud.sleuth.instrument;
import lombok.Value;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.Trace;
import org.springframework.cloud.sleuth.TraceScope;
/**
* @author Spencer Gibb
*/
@Value
public class TraceRunnable extends TraceDelegate<Runnable> implements Runnable {
public TraceRunnable(Trace trace, Runnable delagate) {
super(trace, delagate);
}
public TraceRunnable(Trace trace, Runnable delagate, Span parent) {
super(trace, delagate, parent);
}
public TraceRunnable(Trace trace, Runnable delagate, Span parent, String name) {
super(trace, delagate, parent, name);
}
@Override
public void run() {
if (this.parent != null) {
TraceScope scope = startSpan();
try {
this.delagate.run();
}
finally {
scope.close();
}
}
else {
this.delagate.run();
}
}
}

View File

@@ -0,0 +1,59 @@
package org.springframework.cloud.sleuth.instrument.circuitbreaker;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixThreadPoolKey;
import org.springframework.cloud.sleuth.Trace;
import org.springframework.cloud.sleuth.TraceScope;
/**
* Abstraction over {@code HystrixCommand} that wraps command execution with Trace setting
*
* @see HystrixCommand
* @see Trace
*
* @author Tomasz Nurkiewicz, 4financeIT
* @author Marcin Grzejszczak, 4financeIT
* @author Spencer Gibb
*/
public abstract class TraceCommand<R> extends HystrixCommand<R> {
private Trace trace;
protected TraceCommand(Trace trace, HystrixCommandGroupKey group) {
super(group);
this.trace = trace;
}
protected TraceCommand(Trace trace, HystrixCommandGroupKey group, HystrixThreadPoolKey threadPool) {
super(group, threadPool);
this.trace = trace;
}
protected TraceCommand(Trace trace, HystrixCommandGroupKey group, int executionIsolationThreadTimeoutInMilliseconds) {
super(group, executionIsolationThreadTimeoutInMilliseconds);
this.trace = trace;
}
protected TraceCommand(Trace trace, HystrixCommandGroupKey group, HystrixThreadPoolKey threadPool, int executionIsolationThreadTimeoutInMilliseconds) {
super(group, threadPool, executionIsolationThreadTimeoutInMilliseconds);
this.trace = trace;
}
protected TraceCommand(Trace trace, Setter setter) {
super(setter);
this.trace = trace;
}
@Override
protected R run() throws Exception {
TraceScope scope = trace.startSpan(getCommandKey().name());
try {
return doRun();
} finally {
scope.close();
}
}
public abstract R doRun() throws Exception;
}

View File

@@ -0,0 +1,39 @@
package org.springframework.cloud.sleuth.instrument.scheduling;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.cloud.sleuth.Trace;
import org.springframework.cloud.sleuth.TraceScope;
import org.springframework.scheduling.annotation.Scheduled;
/**
* Aspect that creates a new Span for running threads executing methods annotated with {@link Scheduled} annotation.
* For every execution of scheduled method a new trace will be started.
*
* @author Tomasz Nurkewicz, 4financeIT
* @author Michal Chmielarz, 4financeIT
* @author Marcin Grzejszczak, 4financeIT
* @author Spencer Gibb
*
* @see Trace
*/
@Aspect
public class TraceSchedulingAspect {
private final Trace trace;
public TraceSchedulingAspect(Trace trace) {
this.trace = trace;
}
@Around("execution (@org.springframework.scheduling.annotation.Scheduled * *.*(..))")
public Object traceSceduledThread(final ProceedingJoinPoint pjp) throws Throwable {
TraceScope scope = trace.startSpan(pjp.toShortString());
try {
return pjp.proceed();
} finally {
scope.close();
}
}
}

View File

@@ -0,0 +1,33 @@
package org.springframework.cloud.sleuth.instrument.scheduling;
/**
* @author Spencer Gibb
*/
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.cloud.sleuth.Trace;
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 TraceSchedulingAspect
*
* @author Michal Chmielarz, 4financeIT
* @author Spencer Gibb
*/
@Configuration
@EnableScheduling
@EnableAspectJAutoProxy
@ConditionalOnClass(ProceedingJoinPoint.class)
public class TraceSchedulingAutoConfiguration {
@Bean
public TraceSchedulingAspect traceSchedulingAspect(Trace trace) {
return new TraceSchedulingAspect(trace);
}
}

View File

@@ -0,0 +1,118 @@
/*
* 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.instrument.web;
import static org.springframework.cloud.sleuth.Trace.SPAN_ID_NAME;
import static org.springframework.cloud.sleuth.Trace.TRACE_ID_NAME;
import static org.springframework.util.StringUtils.hasText;
import java.io.IOException;
import java.util.regex.Pattern;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.cloud.sleuth.Trace;
import org.springframework.cloud.sleuth.TraceInfo;
import org.springframework.cloud.sleuth.TraceScope;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* Filter that takes the value of the {@link Trace#SPAN_ID_NAME} and
* {@link Trace#TRACE_ID_NAME} header from either request or response and uses them to
* create a new span.
*
* @see Trace
*
* @author Jakub Nabrdalik, 4financeIT
* @author Tomasz Nurkiewicz, 4financeIT
* @author Marcin Grzejszczak, 4financeIT
* @author Spencer Gibb
*/
public class TraceFilter extends OncePerRequestFilter {
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 Trace trace;
private final Pattern skipPattern;
public TraceFilter(Trace trace) {
this.trace = trace;
this.skipPattern = DEFAULT_SKIP_PATTERN;
}
public TraceFilter(Trace trace, Pattern skipPattern) {
this.trace = trace;
this.skipPattern = skipPattern;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String uri = hasText(request.getRequestURI()) ? request.getRequestURI() : "";
boolean skip = skipPattern.matcher(uri).matches();
TraceScope traceScope = null;
if (!skip) {
String spanId = getHeader(request, response, SPAN_ID_NAME);
String traceId = getHeader(request, response, TRACE_ID_NAME);
if (hasText(spanId) && hasText(traceId)) {
TraceInfo traceInfo = new TraceInfo(traceId, spanId);
// TODO: trace description?
traceScope = trace.startSpan("traceFilter", traceInfo);
// Send new span id back
addToResponseIfNotPresent(response, SPAN_ID_NAME, traceScope.getSpan()
.getSpanId());
}
else {
traceScope = trace.startSpan("traceFilter");
}
}
try {
filterChain.doFilter(request, response);
}
finally {
if (traceScope != null) {
traceScope.close();
}
}
}
private String getHeader(HttpServletRequest request, HttpServletResponse response,
String name) {
String value = request.getHeader(name);
return hasText(value) ? value : response.getHeader(name);
}
private void addToResponseIfNotPresent(HttpServletResponse response, String name,
String value) {
if (!hasText(response.getHeader(name))) {
response.addHeader(name, value);
}
}
@Override
protected boolean shouldNotFilterAsyncDispatch() {
return false;
}
}

View File

@@ -0,0 +1,45 @@
package org.springframework.cloud.sleuth.instrument.web;
import org.springframework.cloud.sleuth.Trace;
import org.springframework.cloud.sleuth.TraceScope;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author Spencer Gibb
*/
public class TraceHandlerInterceptor implements HandlerInterceptor {
private static final String ATTR_NAME = "__CURRENT_TRACE_HANDLER_TRACE_SCOPE_ATTR___";
private final Trace trace;
public TraceHandlerInterceptor(Trace trace) {
this.trace = trace;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//TODO: get trace data from request?
//TODO: what is the description?
TraceScope scope = trace.startSpan("traceHandlerInterceptor");
request.setAttribute(ATTR_NAME, scope);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
TraceScope scope = TraceScope.class.cast(request.getAttribute(ATTR_NAME));
if (scope != null) {
scope.close();
}
}
}

View File

@@ -0,0 +1,82 @@
package org.springframework.cloud.sleuth.instrument.web;
import java.util.concurrent.Callable;
import lombok.extern.apachecommons.CommonsLog;
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.springframework.cloud.sleuth.Trace;
import org.springframework.cloud.sleuth.TraceContextHolder;
import org.springframework.cloud.sleuth.instrument.TraceCallable;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestOperations;
/**
* Aspect that adds correlation id to
* <p/>
* <ul>
* <li>{@link RestController} annotated classes with public {@link Callable} methods</li>
* <li>{@link Controller} annotated classes with public {@link Callable} methods</li>
* </ul>
* <p/>
* For controllers an around aspect is created that wraps the {@link Callable#call()}
* method execution in {@link TraceCallable}
* <p/>
*
* @see RestController
* @see Controller
* @see RestOperations
* @see TraceCallable
* @see Trace
*
* @author Tomasz Nurkewicz, 4financeIT
* @author Marcin Grzejszczak, 4financeIT
* @author Michal Chmielarz, 4financeIT
* @author Spencer Gibb
*/
@Aspect
@CommonsLog
public class TraceWebAspect {
private final Trace trace;
public TraceWebAspect(Trace trace) {
this.trace = trace;
}
@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()")
@SuppressWarnings("unchecked")
public Object wrapWithCorrelationId(ProceedingJoinPoint pjp) throws Throwable {
Callable callable = (Callable) pjp.proceed();
if (TraceContextHolder.isTracing()) {
log.debug("Wrapping callable with span ["
+ TraceContextHolder.getCurrentSpan() + "]");
return new TraceCallable(this.trace, callable);
}
else {
return callable;
}
}
}

View File

@@ -0,0 +1,95 @@
/*
* 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.instrument.web;
import java.util.regex.Pattern;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.embedded.FilterRegistrationBean;
import org.springframework.cloud.sleuth.Trace;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
/**
* Registers beans that add tracing to requests
*
* @author Tomasz Nurkewicz, 4financeIT
* @author Marcin Grzejszczak, 4financeIT
* @author Michal Chmielarz, 4financeIT
* @author Spencer Gibb
*/
@Configuration
@ConditionalOnProperty(value = "spring.cloud.sleuth.trace.web.enabled", matchIfMissing = true)
@ConditionalOnWebApplication
public class TraceWebAutoConfiguration {
/**
* Pattern for URLs that should be skipped in tracing
*/
@Value("${spring.cloud.sleuth.instrument.web.skipPattern:}")
private String skipPattern;
@Autowired
private Trace trace;
@Bean
@ConditionalOnMissingBean
public TraceWebAspect traceWebAspect() {
return new TraceWebAspect(trace);
}
//TODO: I don't think TraceHandlerInterceptor is needed with TraceFilter
/*@Bean
@ConditionalOnMissingBean
public TraceHandlerInterceptor traceHandlerInterceptor() {
return new TraceHandlerInterceptor(trace);
}
@Bean
public WebMvcConfigurerAdapter webMvcConfigurerAdapter(
TraceHandlerInterceptor handlerInterceptor) {
return new TraceWebConfigurer(handlerInterceptor);
}
protected static class TraceWebConfigurer extends WebMvcConfigurerAdapter {
private TraceHandlerInterceptor interceptor;
public TraceWebConfigurer(TraceHandlerInterceptor interceptor) {
this.interceptor = interceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptor).addPathPatterns("/**");
}
}*/
@Bean
@ConditionalOnMissingBean
public FilterRegistrationBean traceFilter() {
Pattern pattern = StringUtils.hasText(skipPattern) ? Pattern.compile(skipPattern)
: TraceFilter.DEFAULT_SKIP_PATTERN;
return new FilterRegistrationBean(new TraceFilter(trace, pattern));
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.sleuth.instrument.web.client;
import static org.springframework.cloud.sleuth.Trace.SPAN_ID_NAME;
import static org.springframework.cloud.sleuth.Trace.TRACE_ID_NAME;
import static org.springframework.cloud.sleuth.TraceContextHolder.getCurrentSpan;
import static org.springframework.cloud.sleuth.TraceContextHolder.isTracing;
import java.io.IOException;
import org.springframework.cloud.sleuth.Trace;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
/**
* Interceptor that verifies whether the trance and span id has been set on the
* request and sets them if one or both of them are missing.
*
* @see org.springframework.web.client.RestTemplate
* @see Trace
*
* @author Marcin Grzejszczak, 4financeIT
* @author Spencer Gibb
*/
public class TraceRestTemplateInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
setHeader(request, SPAN_ID_NAME, getCurrentSpan().getSpanId());
setHeader(request, TRACE_ID_NAME, getCurrentSpan().getTraceId());
return execution.execute(request, body);
}
public void setHeader(HttpRequest request, String name, String value) {
if (!request.getHeaders().containsKey(name) && isTracing()) {
request.getHeaders().add(name, value);
}
}
}

View File

@@ -0,0 +1,50 @@
package org.springframework.cloud.sleuth.instrument.web.client;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
import javax.annotation.PostConstruct;
/**
* @author Spencer Gibb
*/
@Configuration
@ConditionalOnProperty(value = "spring.cloud.sleuth.trace.web.client.enabled", matchIfMissing = true)
@ConditionalOnClass(RestTemplate.class)
public class TraceWebClientAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public TraceRestTemplateInterceptor traceRestTemplateInterceptor() {
return new TraceRestTemplateInterceptor();
}
@Bean
@ConditionalOnMissingBean
public RestTemplate restTemplate() {
return new RestTemplate();
}
@Configuration
protected static class TraceInterceptorConfiguration {
@Autowired(required = false)
private RestTemplate restTemplate;
@Autowired
private TraceRestTemplateInterceptor traceRestTemplateInterceptor;
@PostConstruct
public void init() {
if (restTemplate != null) {
restTemplate.getInterceptors().add(traceRestTemplateInterceptor);
}
}
}
}

View File

@@ -0,0 +1,46 @@
package org.springframework.cloud.sleuth.instrument.zuul;
import org.springframework.cloud.sleuth.Trace;
import org.springframework.cloud.sleuth.TraceScope;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
/**
* @author Spencer Gibb
*/
public class TracePostFilter extends ZuulFilter {
private Trace trace;
public TracePostFilter(Trace trace) {
this.trace = trace;
}
@Override
public String filterType() {
return "post";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
TraceScope traceScope = (TraceScope) RequestContext.getCurrentContext().get(
"traceScope");
if (traceScope != null) {
traceScope.close();
}
return null;
}
}

View File

@@ -0,0 +1,65 @@
package org.springframework.cloud.sleuth.instrument.zuul;
import static org.springframework.cloud.sleuth.Trace.SPAN_ID_NAME;
import static org.springframework.cloud.sleuth.Trace.TRACE_ID_NAME;
import static org.springframework.util.StringUtils.hasText;
import javax.servlet.http.HttpServletRequest;
import org.springframework.cloud.sleuth.Trace;
import org.springframework.cloud.sleuth.TraceInfo;
import org.springframework.cloud.sleuth.TraceScope;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
/**
* @author Spencer Gibb
*/
public class TracePreFilter extends ZuulFilter {
private Trace trace;
public TracePreFilter(Trace trace) {
this.trace = trace;
}
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
String spanId = request.getHeader(SPAN_ID_NAME);
String traceId = request.getHeader(TRACE_ID_NAME);
TraceScope traceScope = null;
if (hasText(spanId) && hasText(traceId)) {
TraceInfo traceInfo = new TraceInfo(traceId, spanId);
// TODO: trace description?
traceScope = trace.startSpan("traceZuulFilter", traceInfo);
}
else {
traceScope = trace.startSpan("traceZuulFilter");
}
context.set("traceScope", traceScope);
return null;
}
}

View File

@@ -0,0 +1,28 @@
package org.springframework.cloud.sleuth.instrument.zuul;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.cloud.sleuth.Trace;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.netflix.zuul.ZuulFilter;
/**
* @author Spencer Gibb
*/
@Configuration
@ConditionalOnClass(ZuulFilter.class)
@ConditionalOnBean(Trace.class)
public class TraceZuulAutoConfiguration {
@Bean
public TracePreFilter tracePreFilter(Trace trace) {
return new TracePreFilter(trace);
}
@Bean
public TracePostFilter tracePostFilter(Trace trace) {
return new TracePostFilter(trace);
}
}

View File

@@ -1,46 +0,0 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.sleuth.resttemplate;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.web.client.RestTemplate;
import java.util.List;
/**
* Default configure of {@code RestTemplate} that adds a list {@code ClientHttpRequestInterceptor}
* to {@code RestTemplate}
*
* @see ClientHttpRequestInterceptor
* @see RestTemplate
*
* @author Marcin Grzejszczak, 4financeIT
*/
public class DefaultRestTemplateConfigurer implements RestTemplateConfigurer {
private final List<ClientHttpRequestInterceptor> clientHttpRequestInterceptors;
public DefaultRestTemplateConfigurer(List<ClientHttpRequestInterceptor> clientHttpRequestInterceptors) {
this.clientHttpRequestInterceptors = clientHttpRequestInterceptors;
}
@Override
public void modifyRestTemplate(RestTemplate restTemplate) {
for (ClientHttpRequestInterceptor clientHttpRequestInterceptor : clientHttpRequestInterceptors) {
restTemplate.getInterceptors().add(clientHttpRequestInterceptor);
}
}
}

View File

@@ -1,30 +0,0 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.sleuth.resttemplate;
import org.springframework.web.client.RestTemplate;
/**
* Interface that allows to modify the {@code RestTemplate} parameters
*
* @see RestTemplate
*
* @author Marcin Grzejszczak, 4financeIT
*/
public interface RestTemplateConfigurer {
void modifyRestTemplate(RestTemplate restTemplate);
}

View File

@@ -1,63 +0,0 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.sleuth.resttemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.List;
/**
*
* Autoconfiguration that sets up a {@code RestTemplate} as a bean if one is not present
* and performs its additional modification via a {@code RestTemplateConfigurer}.
*
* @author Marcin Grzejszczak, 4financeIT
*/
@Configuration
@ConditionalOnWebApplication
@ConditionalOnProperty(value = "spring.cloud.sleuth.resttemplate.enabled", matchIfMissing = true)
public class SleuthRestTemplateAutoConfiguration {
@Configuration
protected static class RestTemplateConfig {
@Autowired
private List<ClientHttpRequestInterceptor> clientHttpRequestInterceptors = new ArrayList<>();
@Bean
@ConditionalOnMissingBean
public RestTemplate restTemplate() {
return new RestTemplate();
}
@Bean
@ConditionalOnMissingBean
public RestTemplateConfigurer restTemplateConfigurer(RestTemplate restTemplate) {
DefaultRestTemplateConfigurer configurer = new DefaultRestTemplateConfigurer(clientHttpRequestInterceptors);
configurer.modifyRestTemplate(restTemplate);
return configurer;
}
}
}

View File

@@ -0,0 +1,13 @@
package org.springframework.cloud.sleuth.sampler;
import org.springframework.cloud.sleuth.Sampler;
/**
* @author Spencer Gibb
*/
public class AlwaysSampler implements Sampler<Object> {
@Override
public boolean next(Object info) {
return true;
}
}

View File

@@ -0,0 +1,15 @@
package org.springframework.cloud.sleuth.sampler;
import org.springframework.cloud.sleuth.Sampler;
import org.springframework.cloud.sleuth.TraceContextHolder;
/**
* @author Spencer Gibb
*/
public class IsTracingSampler implements Sampler<Object> {
@Override
public boolean next(Object info) {
return TraceContextHolder.getCurrentSpan() != null;
}
}

View File

@@ -0,0 +1,24 @@
package org.springframework.cloud.sleuth.slf4j;
import org.slf4j.MDC;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author Spencer Gibb
*/
@Configuration
@ConditionalOnClass(MDC.class)
public class SleuthSlf4jAutoConfiguration {
@Bean
public Slf4jSpanStartedListener slf4jSpanStartedListener() {
return new Slf4jSpanStartedListener();
}
@Bean
public Slf4jSpanStoppedListener slf4jSpanStoppedListener() {
return new Slf4jSpanStoppedListener();
}
}

View File

@@ -0,0 +1,25 @@
package org.springframework.cloud.sleuth.slf4j;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.Trace;
import org.springframework.cloud.sleuth.event.SpanStartedEvent;
import org.springframework.context.ApplicationListener;
/**
* @author Spencer Gibb
*/
@Slf4j
public class Slf4jSpanStartedListener implements ApplicationListener<SpanStartedEvent> {
@Override
public void onApplicationEvent(SpanStartedEvent event) {
Span span = event.getSpan();
//TODO: what log level?
log.info("Starting span: {}", span);
MDC.put(Trace.SPAN_ID_NAME, span.getSpanId());
MDC.put(Trace.TRACE_ID_NAME, span.getTraceId());
}
}

View File

@@ -0,0 +1,24 @@
package org.springframework.cloud.sleuth.slf4j;
import static org.springframework.cloud.sleuth.Trace.SPAN_ID_NAME;
import static org.springframework.cloud.sleuth.Trace.TRACE_ID_NAME;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.cloud.sleuth.event.SpanStoppedEvent;
import org.springframework.context.ApplicationListener;
/**
* @author Spencer Gibb
*/
@Slf4j
public class Slf4jSpanStoppedListener implements ApplicationListener<SpanStoppedEvent> {
@Override
public void onApplicationEvent(SpanStoppedEvent event) {
//TODO: what should this log level be?
log.info("Received span: {}", event.getSpan());
MDC.remove(SPAN_ID_NAME);
MDC.remove(TRACE_ID_NAME);
}
}

View File

@@ -1,3 +1,6 @@
# Auto Configuration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.sleuth.resttemplate.SleuthRestTemplateAutoConfiguration
org.springframework.cloud.sleuth.TraceAutoConfiguration,\
org.springframework.cloud.sleuth.slf4j.SleuthSlf4jAutoConfiguration,\
org.springframework.cloud.sleuth.instrument.web.TraceWebAutoConfiguration,\
org.springframework.cloud.sleuth.instrument.web.client.TraceWebClientAutoConfiguration

View File

@@ -0,0 +1,120 @@
package org.springframework.cloud.sleuth;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.isA;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.cloud.sleuth.event.SpanStartedEvent;
import org.springframework.cloud.sleuth.event.SpanStoppedEvent;
import org.springframework.cloud.sleuth.sampler.AlwaysSampler;
import org.springframework.cloud.sleuth.sampler.IsTracingSampler;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
/**
* @author Spencer Gibb
*/
public class DefaultTraceTests {
public static final String CREATE_SIMPLE_TRACE = "createSimpleTrace";
public static final String IMPORTANT_WORK_1 = "important work 1";
public static final String IMPORTANT_WORK_2 = "important work 2";
public static final int NUM_SPANS = 3;
@Test
public void tracingWorks() {
ApplicationEventPublisher publisher = mock(ApplicationEventPublisher.class);
DefaultTrace trace = new DefaultTrace(new IsTracingSampler(),
new RandomUuidGenerator(), publisher);
TraceScope scope = trace.startSpan(CREATE_SIMPLE_TRACE, new AlwaysSampler());
try {
importantWork1(trace);
}
finally {
scope.close();
}
verify(publisher, times(NUM_SPANS)).publishEvent(isA(SpanStartedEvent.class));
verify(publisher, times(NUM_SPANS)).publishEvent(isA(SpanStoppedEvent.class));
ArgumentCaptor<ApplicationEvent> captor = ArgumentCaptor
.forClass(ApplicationEvent.class);
verify(publisher, atLeast(NUM_SPANS)).publishEvent(captor.capture());
List<Span> spans = new ArrayList<>();
for (ApplicationEvent event : captor.getAllValues()) {
if (event instanceof SpanStoppedEvent) {
spans.add(((SpanStoppedEvent) event).getSpan());
}
}
assertThat("spans was wrong size", spans.size(), is(NUM_SPANS));
Span root = assertSpan(spans, null, CREATE_SIMPLE_TRACE);
Span child = assertSpan(spans, root.getSpanId(), IMPORTANT_WORK_1);
Span grandChild = assertSpan(spans, child.getSpanId(), IMPORTANT_WORK_2);
List<Span> gen4 = findSpans(spans, grandChild.getSpanId());
assertThat("gen4 was non-empty", gen4.isEmpty(), is(true));
}
private Span assertSpan(List<Span> spans, String parentId, String name) {
List<Span> found = findSpans(spans, parentId);
assertThat("more than one span with parentId " + parentId, found.size(), is(1));
Span span = found.get(0);
assertThat("name is wrong for span with parentId " + parentId,
span.getName(), is(name));
return span;
}
private List<Span> findSpans(List<Span> spans, String parentId) {
List<Span> found = new ArrayList<>();
for (Span span : spans) {
if (parentId == null && span.getParents().isEmpty()) {
found.add(span);
}
else if (span.getParents().contains(parentId)) {
found.add(span);
}
}
return found;
}
private void importantWork1(Trace trace) {
TraceScope cur = trace.startSpan(IMPORTANT_WORK_1);
try {
Thread.sleep((long) (50 * Math.random()));
importantWork2(trace);
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
finally {
cur.close();
}
}
private void importantWork2(Trace trace) {
TraceScope cur = trace.startSpan(IMPORTANT_WORK_2);
try {
Thread.sleep((long) (50 * Math.random()));
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
finally {
cur.close();
}
}
}

View File

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

View File

@@ -1,134 +0,0 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.sleuth.correlation;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestOperations;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import static org.springframework.cloud.sleuth.correlation.CorrelationIdHolder.CORRELATION_ID_HEADER;
/**
* Aspect that adds correlation id to
* <p/>
* <ul>
* <li>{@link RestController} annotated classes
* with public {@link Callable} methods</li>
* <li>{@link Controller} annotated classes
* with public {@link Callable} methods</li>
* <li>explicit {@link RestOperations}.exchange(..) method calls</li>
* </ul>
* <p/>
* For controllers an around aspect is created that wraps the {@link Callable#call()} method execution
* in {@link CorrelationIdUpdater#wrapCallableWithId(Callable)}
* <p/>
* For {@link RestOperations} we are wrapping all executions of the
* <b>exchange</b> methods and we are extracting {@link HttpHeaders} from the passed {@link HttpEntity}.
* Next we are adding correlation id header {@link CorrelationIdHolder#CORRELATION_ID_HEADER} with
* the value taken from {@link CorrelationIdHolder}. Finally the method execution proceeds.
*
* @see RestController
* @see Controller
* @see RestOperations
* @see CorrelationIdHolder
* @see CorrelationIdFilter
*
* @author Tomasz Nurkewicz, 4financeIT
* @author Marcin Grzejszczak, 4financeIT
* @author Michal Chmielarz, 4financeIT
*/
@Aspect
public class CorrelationIdAspect {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final int HTTP_ENTITY_PARAM_INDEX = 2;
@Pointcut("@target(org.springframework.web.bind.annotation.RestController)")
private void anyRestControllerAnnotated() {
}
@Pointcut("@target(org.springframework.stereotype.Controller)")
private void anyControllerAnnotated() {
}
@Pointcut("execution(public java.util.concurrent.Callable *(..))")
private void anyPublicMethodReturningCallable() {
}
@Pointcut("(anyRestControllerAnnotated() || anyControllerAnnotated()) && anyPublicMethodReturningCallable()")
private void anyControllerOrRestControllerWithPublicAsyncMethod() {
}
@Around("anyControllerOrRestControllerWithPublicAsyncMethod()")
public Object wrapWithCorrelationId(ProceedingJoinPoint pjp) throws Throwable {
final Callable callable = (Callable) pjp.proceed();
log.debug("Wrapping callable with correlation id [" + CorrelationIdHolder.get() + "]");
return CorrelationIdUpdater.wrapCallableWithId(new Callable() {
@Override
public Object call() throws Exception {
return callable.call();
}
});
}
@Pointcut("execution(public * org.springframework.web.client.RestOperations.exchange(..))")
private void anyExchangeRestOperationsMethod() {
}
@Around("anyExchangeRestOperationsMethod()")
public Object wrapWithCorrelationIdForRestOperations(ProceedingJoinPoint pjp) throws Throwable {
String correlationId = CorrelationIdHolder.get();
log.debug("Wrapping RestTemplate call with correlation id [" + correlationId + "]");
HttpEntity httpEntity = (HttpEntity) pjp.getArgs()[HTTP_ENTITY_PARAM_INDEX];
HttpEntity newHttpEntity = createNewHttpEntity(httpEntity, correlationId);
List<Object> newArgs = modifyHttpEntityInMethodArguments(pjp, newHttpEntity);
return pjp.proceed(newArgs.toArray());
}
@SuppressWarnings("unchecked")
private HttpEntity createNewHttpEntity(HttpEntity httpEntity, String correlationId) {
HttpHeaders newHttpHeaders = new HttpHeaders();
newHttpHeaders.putAll(httpEntity.getHeaders());
newHttpHeaders.add(CORRELATION_ID_HEADER, correlationId);
return new HttpEntity(httpEntity.getBody(), newHttpHeaders);
}
private List<Object> modifyHttpEntityInMethodArguments(ProceedingJoinPoint pjp, HttpEntity newHttpEntity) {
List<Object> newArgs = new ArrayList<>();
for (int i = 0; i < pjp.getArgs().length; i++) {
Object arg = pjp.getArgs()[i];
if (i != HTTP_ENTITY_PARAM_INDEX) {
newArgs.add(i, arg);
} else {
newArgs.add(i, newHttpEntity);
}
}
return newArgs;
}
}

View File

@@ -1,74 +0,0 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.sleuth.correlation;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.embedded.FilterRegistrationBean;
import org.springframework.cloud.sleuth.resttemplate.SleuthRestTemplateAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.regex.Pattern;
/**
* Registers beans that add correlation id to requests
*
* @see CorrelationIdAspect
* @see CorrelationIdFilter
*
* @author Tomasz Nurkewicz, 4financeIT
* @author Marcin Grzejszczak, 4financeIT
* @author Michal Chmielarz, 4financeIT
*/
@Configuration
@ConditionalOnProperty(value = "spring.cloud.sleuth.correlation.enabled", matchIfMissing = true)
@AutoConfigureAfter(SleuthRestTemplateAutoConfiguration.class)
public class CorrelationIdAutoConfiguration {
/**
* Pattern for URLs that should be skipped in correlationID setting
*/
@Value("${spring.cloud.sleuth.correlation.skipPattern:}")
private String skipPattern;
@Bean
@ConditionalOnMissingBean
public CorrelationIdAspect correlationIdAspect() {
return new CorrelationIdAspect();
}
@Bean
@ConditionalOnMissingBean
public FilterRegistrationBean correlationHeaderFilter(UuidGenerator uuidGenerator) {
Pattern pattern = StringUtils.isBlank(skipPattern) ? Pattern.compile(skipPattern) : CorrelationIdFilter.DEFAULT_SKIP_PATTERN;
return new FilterRegistrationBean(new CorrelationIdFilter(uuidGenerator, pattern));
}
@Bean
@ConditionalOnMissingBean
public UuidGenerator uuidGenerator() {
return new UuidGenerator();
}
@Bean
public CorrelationIdSettingRestTemplateInterceptor correlationIdSettingRestTemplateInterceptor() {
return new CorrelationIdSettingRestTemplateInterceptor();
}
}

View File

@@ -1,151 +0,0 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.sleuth.correlation;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.concurrent.Callable;
import java.util.regex.Pattern;
import static org.springframework.cloud.sleuth.correlation.CorrelationIdHolder.CORRELATION_ID_HEADER;
import static org.springframework.util.StringUtils.hasText;
/**
* Filter that takes the value of the {@link CorrelationIdHolder#CORRELATION_ID_HEADER} header
* from either request or response and sets it in the {@link CorrelationIdHolder}. It also provides
* that value in {@link MDC} logging related class so that logger prints the value of
* correlation id at each log.
*
* @see CorrelationIdHolder
* @see MDC
*
* @author Jakub Nabrdalik, 4financeIT
* @author Tomasz Nurkiewicz, 4financeIT
* @author Marcin Grzejszczak, 4financeIT
*/
public class CorrelationIdFilter extends OncePerRequestFilter {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
public static final Pattern DEFAULT_SKIP_PATTERN = Pattern.compile("/api-docs.*|/autoconfig|/configprops|/dump|/info|/metrics.*|/mappings|/trace|/swagger.*|.*\\.png|.*\\.css|.*\\.js|.*\\.html");
private final Pattern skipCorrId;
private final UuidGenerator uuidGenerator;
public CorrelationIdFilter() {
this.uuidGenerator = new UuidGenerator();
this.skipCorrId = null;
}
public CorrelationIdFilter(UuidGenerator uuidGenerator, Pattern skipCorrId) {
this.uuidGenerator = uuidGenerator;
this.skipCorrId = skipCorrId;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
setupCorrelationId(request, response);
try {
filterChain.doFilter(request, response);
} finally {
cleanupCorrelationId();
}
}
private void setupCorrelationId(HttpServletRequest request, HttpServletResponse response) {
String correlationIdFromRequest = getCorrelationIdFrom(request);
String correlationId = (hasText(correlationIdFromRequest)) ? correlationIdFromRequest : getCorrelationIdFrom(response);
if (!hasText(correlationId) && shouldGenerateCorrId(request)) {
correlationId = createNewCorrIdIfEmpty();
}
CorrelationIdHolder.set(correlationId);
addCorrelationIdToResponseIfNotPresent(response, correlationId);
}
private String getCorrelationIdFrom(final HttpServletResponse response) {
return withLoggingAs("response", new Callable<String>() {
@Override
public String call() throws Exception {
return response.getHeader(CORRELATION_ID_HEADER);
}
});
}
private String getCorrelationIdFrom(final HttpServletRequest request) {
return withLoggingAs("request", new Callable<String>() {
@Override
public String call() throws Exception {
return request.getHeader(CORRELATION_ID_HEADER);
}
});
}
private String withLoggingAs(String whereWasFound, Callable<String> correlationIdGetter) {
String correlationId = tryToGetCorrelationId(correlationIdGetter);
if (hasText(correlationId)) {
MDC.put(CORRELATION_ID_HEADER, correlationId);
log.debug("Found correlationId in " + whereWasFound + ": " + correlationId);
}
return correlationId;
}
private String tryToGetCorrelationId(Callable<String> correlationIdGetter) {
try {
return correlationIdGetter.call();
} catch (Exception e) {
log.error("Exception occurred while trying to retrieve request header", e);
return StringUtils.EMPTY;
}
}
private String createNewCorrIdIfEmpty() {
String currentCorrId = uuidGenerator.create();
MDC.put(CORRELATION_ID_HEADER, currentCorrId);
log.debug("Generating new correlationId: " + currentCorrId);
return currentCorrId;
}
protected boolean shouldGenerateCorrId(HttpServletRequest request) {
final String i = request.getRequestURI();
final String uri = StringUtils.defaultIfEmpty(i, StringUtils.EMPTY);
boolean skip = skipCorrId != null && skipCorrId.matcher(uri).matches();
return !skip;
}
private void addCorrelationIdToResponseIfNotPresent(HttpServletResponse response, String correlationId) {
if (!hasText(response.getHeader(CORRELATION_ID_HEADER))) {
response.addHeader(CORRELATION_ID_HEADER, correlationId);
}
}
private void cleanupCorrelationId() {
MDC.remove(CORRELATION_ID_HEADER);
CorrelationIdHolder.remove();
}
@Override
protected boolean shouldNotFilterAsyncDispatch() {
return false;
}
}

View File

@@ -1,39 +0,0 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.sleuth.correlation;
/**
* Component that stores correlation id using {@link ThreadLocal}
*
* @author Jakub Nabrdalik, 4financeIT
* @author Marcin Zajaczkowski, 4financeIT
*/
public class CorrelationIdHolder {
public static final String CORRELATION_ID_HEADER = "Correlation-Id";
private static final ThreadLocal<String> id = new ThreadLocal<String>();
public static void set(String correlationId) {
id.set(correlationId);
}
public static String get() {
return id.get();
}
public static void remove() {
id.remove();
}
}

View File

@@ -1,56 +0,0 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.sleuth.correlation;
import lombok.SneakyThrows;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import java.io.IOException;
/**
* Interceptor that verifies whether the correlation id has been
* set on the request and sets it if it's missing.
*
* @see org.springframework.web.client.RestTemplate
* @see CorrelationIdHolder
*
* @author Marcin Grzejszczak, 4financeIT
*/
public class CorrelationIdSettingRestTemplateInterceptor implements ClientHttpRequestInterceptor {
@SneakyThrows
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
appendCorrelationIdToRequestIfMissing(request);
ClientHttpResponse response = null;
try {
response = execution.execute(request, body);
} catch (final Exception e) {
throw new RuntimeException(e);
}
return response;
}
private void appendCorrelationIdToRequestIfMissing(HttpRequest request) {
if (!request.getHeaders().containsKey(CorrelationIdHolder.CORRELATION_ID_HEADER)) {
request.getHeaders().add(CorrelationIdHolder.CORRELATION_ID_HEADER, CorrelationIdHolder.get());
}
}
}

View File

@@ -1,120 +0,0 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.sleuth.correlation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.util.StringUtils;
import java.lang.invoke.MethodHandles;
import java.util.concurrent.Callable;
import static org.springframework.cloud.sleuth.correlation.CorrelationIdHolder.CORRELATION_ID_HEADER;
/**
* Class that takes care of updating all necessary components with new value
* of correlation id.
* It sets correlationId on {@link ThreadLocal} in {@link CorrelationIdHolder}
* and in {@link MDC}.
*
* @see CorrelationIdHolder
* @see MDC
*
* @author Jakub Nabrdalik, 4financeIT
* @author Michal Chmielarz, 4financeIT
*/
public class CorrelationIdUpdater {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
public static void updateCorrelationId(String correlationId) {
if (StringUtils.hasText(correlationId)) {
log.debug("Updating correlationId with value: [" + correlationId + "]");
CorrelationIdHolder.set(correlationId);
MDC.put(CORRELATION_ID_HEADER, correlationId);
}
}
/**
* Temporarily updates correlation ID inside block of code.
* Makes sure previous ID is restored after block's execution
*
* @param temporaryCorrelationId - correlationID to passed to the executed block of code
* @param block Callable to be executed with new ID
* @return the result of Callable block execution
*/
public static <T> T withId(String temporaryCorrelationId, Callable<T> block) {
final String oldCorrelationId = CorrelationIdHolder.get();
try {
updateCorrelationId(temporaryCorrelationId);
return block.call();
} catch (RuntimeException e) {
logException(e);
throw e;
} catch (Exception e) {
logException(e);
throw new RuntimeException(e);
} finally {
updateCorrelationId(oldCorrelationId);
}
}
private static void logException(Throwable e) {
log.error("Exception occurred while trying to execute the function", e);
}
/**
* Wraps given {@link Callable} with another {@link Callable Callable} propagating correlation ID inside nested
* Callable/Closure.
* <p/>
* <p/>
* Useful in a situation when a Callable should be executed in a separate thread, for example in aspects.
* <p/>
* <pre><code>
* &#64;Around('...')
* Object wrapWithCorrelationId(ProceedingJoinPoint pjp) throws Throwable {
* Callable callable = pjp.proceed() as Callable
* return CorrelationIdUpdater.wrapCallableWithId {
* callable.call()
* }
* }
* </code></pre>
* <p/>
* <b>Note</b>: Passing only one input parameter currently is supported.
*
* @param block code block to execute in a thread with a correlation ID taken from the original thread
* @return wrapping block as Callable
*/
@SuppressWarnings("unchecked")
public static <T> Callable<T> wrapCallableWithId(final Callable<T> block) {
final String temporaryCorrelationId = CorrelationIdHolder.get();
// unchecked assignment due to groovyc issues with <T>
return new Callable() {
@Override
public Object call() throws Exception {
final String oldCorrelationId = CorrelationIdHolder.get();
try {
updateCorrelationId(temporaryCorrelationId);
return block.call();
} finally {
updateCorrelationId(oldCorrelationId);
}
}
};
}
}

View File

@@ -1,29 +0,0 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.sleuth.correlation;
import java.util.UUID;
/**
* Default Uuid generator
*
* @author <a href="http://www.4financeit.com>4financeIT</a>
*/
public class UuidGenerator {
public String create() {
return UUID.randomUUID().toString();
}
}

View File

@@ -1,57 +0,0 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.sleuth.correlation.hystrix;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import org.springframework.cloud.sleuth.correlation.CorrelationIdHolder;
import org.springframework.cloud.sleuth.correlation.CorrelationIdUpdater;
import java.util.concurrent.Callable;
/**
* Abstraction over {@code HystrixCommand} that wraps command execution with CorrelationID setting
*
* @see HystrixCommand
* @see CorrelationIdUpdater
*
* @author Tomasz Nurkiewicz, 4financeIT
* @author Marcin Grzejszczak, 4financeIT
*/
public abstract class CorrelatedCommand<R> extends HystrixCommand<R> {
private final String clientCorrelationId = CorrelationIdHolder.get();
protected CorrelatedCommand(HystrixCommandGroupKey group) {
super(group);
}
protected CorrelatedCommand(Setter setter) {
super(setter);
}
@Override
protected final R run() throws Exception {
return CorrelationIdUpdater.withId(clientCorrelationId, new Callable<R>() {
@Override
public R call() throws Exception {
return doRun();
}
});
}
public abstract R doRun() throws Exception;
}

View File

@@ -1,67 +0,0 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.sleuth.correlation.scheduling;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.sleuth.correlation.CorrelationIdUpdater;
import org.springframework.cloud.sleuth.correlation.UuidGenerator;
import org.springframework.scheduling.annotation.Scheduled;
import java.lang.invoke.MethodHandles;
import java.util.concurrent.Callable;
/**
* Aspect that sets correlationId for running threads executing methods annotated with {@link Scheduled} annotation.
* For every execution of scheduled method a new, i.e. unique one, value of correlationId will be set.
*
* @author Tomasz Nurkewicz, 4financeIT
* @author Michal Chmielarz, 4financeIT
* @author Marcin Grzejszczak, 4financeIT
*
* @see UuidGenerator
* @see CorrelationIdUpdater
*/
@Aspect
public class ScheduledTaskWithCorrelationIdAspect {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private final UuidGenerator uuidGenerator;
public ScheduledTaskWithCorrelationIdAspect(UuidGenerator uuidGenerator) {
this.uuidGenerator = uuidGenerator;
}
@Around("execution (@org.springframework.scheduling.annotation.Scheduled * *.*(..))")
public Object setNewCorrelationIdOnThread(final ProceedingJoinPoint pjp) throws Throwable {
String correlationId = uuidGenerator.create();
return CorrelationIdUpdater.withId(correlationId, new Callable() {
@Override
public Object call() throws Exception {
try {
return pjp.proceed();
} catch (Throwable throwable) {
log.error("Didn't manage to proceed with the pointcut", throwable);
throw new RuntimeException(throwable);
}
}
});
}
}

View File

@@ -1,40 +0,0 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.sleuth.correlation.scheduling;
import org.springframework.cloud.sleuth.correlation.UuidGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* Registers beans related to task scheduling.
*
* @see ScheduledTaskWithCorrelationIdAspect
*
* @author Michal Chmielarz, 4financeIT
*/
@Configuration
@EnableScheduling
@EnableAspectJAutoProxy
public class TaskSchedulingConfiguration {
@Bean
public ScheduledTaskWithCorrelationIdAspect scheduledTaskPointcut(UuidGenerator uuidGenerator) {
return new ScheduledTaskWithCorrelationIdAspect(uuidGenerator);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -47,7 +47,11 @@
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin</artifactId>
<artifactId>spring-cloud-sleuth-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>

View File

@@ -1,64 +1,45 @@
package org.springframework.cloud.sleuth.sample;
import java.util.Random;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.embedded.EmbeddedServletContainerInitializedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.cloud.sleuth.Sampler;
import org.springframework.cloud.sleuth.sampler.AlwaysSampler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.scheduling.annotation.EnableAsync;
/**
* @author Spencer Gibb
*/
@Configuration
@EnableAutoConfiguration
@RestController
@EnableAspectJAutoProxy(proxyTargetClass = true)
@EnableAsync
@Slf4j
public class SampleApplication implements ApplicationListener<EmbeddedServletContainerInitializedEvent> {
public class SampleApplication {
public static final String CLIENT_NAME = "testApp";
@Autowired
private RestTemplate restTemplate;
private int port;
@SneakyThrows
@RequestMapping("/")
public String hi() {
final Random random = new Random();
Thread.sleep(random.nextInt(1000));
String s = restTemplate.getForObject("http://localhost:" + port + "/hi2", String.class);
return "hi/"+s;
@Bean
public Sampler defaultSampler() {
return new AlwaysSampler();
}
@SneakyThrows
@RequestMapping("/hi2")
public String hi2() {
final Random random = new Random();
Thread.sleep(random.nextInt(1000));
return "hi2";
}
@Bean
public SampleController sampleController() {
return new SampleController();
}
public static void main(String[] args) {
SpringApplication.run(SampleApplication.class, args);
}
/*@Bean
public SpanCollector spanCollector() {
return new LoggingSpanCollectorImpl();
}*/
/*
* @Bean public SpanCollector spanCollector() { return new LoggingSpanCollectorImpl();
* }
*/
@Override
public void onApplicationEvent(EmbeddedServletContainerInitializedEvent event) {
port = event.getEmbeddedServletContainer().getPort();
}
}

View File

@@ -0,0 +1,82 @@
package org.springframework.cloud.sleuth.sample;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.EmbeddedServletContainerInitializedEvent;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.Trace;
import org.springframework.cloud.sleuth.TraceContextHolder;
import org.springframework.cloud.sleuth.TraceScope;
import org.springframework.cloud.sleuth.sampler.AlwaysSampler;
import org.springframework.context.ApplicationListener;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.Random;
import java.util.concurrent.Callable;
/**
* @author Spencer Gibb
*/
@Slf4j
@RestController
class SampleController implements
ApplicationListener<EmbeddedServletContainerInitializedEvent> {
@Autowired
private RestTemplate restTemplate;
@Autowired
private Trace trace;
private int port;
@SneakyThrows
@RequestMapping("/")
public String hi() {
final Random random = new Random();
Thread.sleep(random.nextInt(1000));
String s = restTemplate.getForObject("http://localhost:" + port + "/hi2",
String.class);
return "hi/" + s;
}
@RequestMapping("/call")
public Callable<String> call() {
return new Callable<String>() {
@Override
public String call() throws Exception {
Span currentSpan = TraceContextHolder.getCurrentSpan();
return "async hi: "+currentSpan;
}
};
}
@SneakyThrows
@RequestMapping("/hi2")
public String hi2() {
final Random random = new Random();
Thread.sleep(random.nextInt(1000));
return "hi2";
}
@SneakyThrows
@RequestMapping("/traced")
public String traced() {
TraceScope scope = trace.startSpan("customTraceEndpoint", new AlwaysSampler());
final Random random = new Random();
int millis = random.nextInt(1000);
log.info("Sleeping for {} millis", millis);
Thread.sleep(millis);
String s = restTemplate.getForObject("http://localhost:" + port + "/hi2", String.class);
scope.close();
return "hi/" + s;
}
@Override
public void onApplicationEvent(EmbeddedServletContainerInitializedEvent event) {
port = event.getEmbeddedServletContainer().getPort();
}
}

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<property name="LOG_FILE" value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}/}spring.log}"/>
<property name="CONSOLE_LOG_PATTERN" value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%X{Trace-Id:-} %X{Span-Id:-}]){yellow} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wex"/>
<property name="FILE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %5p ${PID:- } --- [%X{Trace-Id:-} %X{Span-Id:-}] [%t] %-40.40logger{39} : %m%n%wex"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml" />
<include resource="org/springframework/boot/logging/logback/file-appender.xml" />
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
<logger name="org.springframework.web" level="DEBUG"/>
</configuration>