Fixed Feign integration

This commit is contained in:
Marcin Grzejszczak
2016-03-07 11:18:45 +01:00
parent 1043fd83c1
commit 19249cdc23
16 changed files with 726 additions and 181 deletions

View File

@@ -269,4 +269,14 @@ Currently Spring Cloud Sleuth registers very simple metrics related to spans.
It's using the http://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-metrics.html#production-ready-recording-metrics[Spring Boot's metrics support]
to calculate the number of accepted and dropped spans. Each time a span gets
sent to Zipkin the number of accepted spans will increase. If there's an error then
the number of dropped spans will get increased.
the number of dropped spans will get increased.
== Integrations
=== Feign
By default Spring Cloud Sleuth provides integration with feign via the `TraceFeignClientAutoConfiguration`. You can disable it
by setting `spring.sleuth.feign.enabled` to false.
We're taking care of Feign instrumentation by means of a `FeignBeanPostProcessor`.
You can disable the post processor by providing the `spring.sleuth.feign.processor.enabled` equal to `false`.

View File

@@ -27,4 +27,5 @@ public class SleuthHystrixAutoConfiguration {
SleuthHystrixConcurrencyStrategy sleuthHystrixConcurrencyStrategy(Tracer tracer, TraceKeys traceKeys) {
return new SleuthHystrixConcurrencyStrategy(tracer, traceKeys);
}
}

View File

@@ -7,8 +7,8 @@ import javax.annotation.PreDestroy;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.cloud.sleuth.TraceKeys;
import org.springframework.cloud.sleuth.Tracer;
import com.netflix.hystrix.strategy.HystrixPlugins;
import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy;

View File

@@ -1,100 +0,0 @@
/*
* Copyright 2015 Netflix, Inc.
*
* 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 java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;
import org.springframework.cloud.sleuth.TraceKeys;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.cloud.sleuth.instrument.hystrix.TraceCommand;
import org.springframework.util.ReflectionUtils;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandKey;
import feign.InvocationHandlerFactory;
import feign.InvocationHandlerFactory.MethodHandler;
import feign.Target;
import static feign.Util.checkNotNull;
/**
* Wraps {@link HystrixCommand} execution in Sleuth's {@link TraceCommand}
*
* @since 1.0.0
*/
final class SleuthHystrixInvocationHandler implements InvocationHandler {
private final Target<?> target;
private final Map<Method, MethodHandler> dispatch;
private final Tracer tracer;
private final TraceKeys traceKeys;
SleuthHystrixInvocationHandler(Target<?> target, Map<Method, MethodHandler> dispatch,
Tracer tracer, TraceKeys traceKeys) {
this.tracer = checkNotNull(tracer, "traceManager");
this.target = checkNotNull(target, "target");
this.dispatch = checkNotNull(dispatch, "dispatch");
this.traceKeys = checkNotNull(traceKeys, "traceKeys");
}
@Override public Object invoke(final Object proxy, final Method method,
final Object[] args) throws Throwable {
String groupKey = this.target.name();
String commandKey = method.getName();
HystrixCommand.Setter setter = HystrixCommand.Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey(groupKey))
.andCommandKey(HystrixCommandKey.Factory.asKey(commandKey));
HystrixCommand<Object> hystrixCommand = new TraceCommand<Object>(this.tracer, this.traceKeys,
setter) {
@Override public Object doRun() throws Exception {
try {
return SleuthHystrixInvocationHandler.this.dispatch.get(method)
.invoke(args);
}
catch (Throwable throwable) {
ReflectionUtils.rethrowException(throwable);
}
return null;
}
};
if (HystrixCommand.class.isAssignableFrom(method.getReturnType())) {
return hystrixCommand;
}
return hystrixCommand.execute();
}
static final class Factory implements InvocationHandlerFactory {
private final Tracer tracer;
private final TraceKeys traceKeys;
public Factory(Tracer tracer, TraceKeys traceKeys) {
this.tracer = tracer;
this.traceKeys = traceKeys;
}
@Override public InvocationHandler create(
@SuppressWarnings("rawtypes") Target target,
Map<Method, MethodHandler> dispatch) {
return new SleuthHystrixInvocationHandler(target, dispatch, this.tracer,
this.traceKeys);
}
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2013-2016 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.feign;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.context.ApplicationEventPublisher;
import feign.Client;
import feign.Retryer;
import feign.codec.Decoder;
/**
* Post processor that wraps Feign related classes {@link Decoder},
* {@link Retryer}
*
* @author Marcin Grzejszczak
*
* @since 1.0.0
*/
class FeignBeanPostProcessor implements BeanPostProcessor {
private final ApplicationEventPublisher publisher;
private final Tracer tracer;
FeignBeanPostProcessor(ApplicationEventPublisher publisher, Tracer tracer) {
this.publisher = publisher;
this.tracer = tracer;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
if (bean instanceof Decoder && !(bean instanceof TraceFeignDecoder)) {
return new TraceFeignDecoder(this.publisher, this.tracer, (Decoder) bean);
} else if (bean instanceof Retryer && !(bean instanceof TraceFeignRetryer)) {
return new TraceFeignRetryer(this.tracer, (Retryer) bean);
} else if (bean instanceof Client && !(bean instanceof TraceFeignClient)) {
return new TraceFeignClient(this.publisher, this.tracer, (Client) bean);
}
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2013-2016 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.feign;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.cloud.sleuth.event.ClientReceivedEvent;
import org.springframework.context.ApplicationEventPublisher;
/**
* Abstract class for publishing {@link org.springframework.cloud.sleuth.event.ClientReceivedEvent}
*
* @author Marcin Grzejszczak
*
* @since 1.0.0
*/
abstract class FeignEventPublisher {
private final FeignRequestContext feignRequestContext = FeignRequestContext.getInstance();
private final ApplicationEventPublisher publisher;
private final Tracer tracer;
protected FeignEventPublisher(ApplicationEventPublisher publisher, Tracer tracer) {
this.publisher = publisher;
this.tracer = tracer;
}
protected void finish() {
Span span = this.feignRequestContext.getCurrentSpan();
if (span != null) {
this.publisher.publishEvent(new ClientReceivedEvent(this, span));
this.tracer.close(span);
this.feignRequestContext.clearContext();
}
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2013-2016 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.feign;
import org.springframework.cloud.sleuth.Span;
/**
* Class that holds the information for the span processed by the current
* request. It also knows whether the request has already been retried.
*
* The implementation works on a {@link ThreadLocal} thus is thread-safe.
*
* @author Marcin Grzejszczak
*
* @since 1.0.0
*/
class FeignRequestContext {
private static final FeignRequestContext INSTANCE = new FeignRequestContext();
private FeignRequestContext() {}
private static final ThreadLocal<SpanHolder> THREAD_LOCAL = new ThreadLocal<>();
private static class SpanHolder {
final Span span;
final boolean retried;
private SpanHolder(Span span, boolean retried) {
this.span = span;
this.retried = retried;
}
}
boolean hasSpanInProcess() {
return THREAD_LOCAL.get() != null;
}
Span getCurrentSpan() {
if (hasSpanInProcess()) {
return THREAD_LOCAL.get().span;
}
return null;
}
boolean wasSpanRetried() {
return hasSpanInProcess() && THREAD_LOCAL.get().retried;
}
void putSpan(Span span, boolean retried) {
THREAD_LOCAL.set(new SpanHolder(span, retried));
}
void clearContext() {
THREAD_LOCAL.remove();
}
static FeignRequestContext getInstance() {
return INSTANCE;
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2013-2016 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.feign;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.context.ApplicationEventPublisher;
import feign.Feign;
import feign.hystrix.HystrixFeign;
/**
* Contains {@link feign.Feign.Builder} implementation that delegates execution
* {@link feign.hystrix.HystrixFeign} with custom retryer and decoder
* that close spans on exceptions / success and continues them on retries.
*
* @author Marcin Grzejszczak
*
* @since 1.0.0
*/
class SleuthFeignBuilder {
static Feign.Builder builder(ApplicationEventPublisher publisher, Tracer tracer) {
return HystrixFeign.builder()
.client(new TraceFeignClient(publisher, tracer))
.retryer(new TraceFeignRetryer(tracer))
.decoder(new TraceFeignDecoder(publisher, tracer));
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2013-2016 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.feign;
import java.io.IOException;
import java.util.Objects;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.context.ApplicationEventPublisher;
import feign.Client;
import feign.Request;
import feign.Response;
/**
* A Feign Client that closes a Span if there is no response body.
* In other cases Span will not get closed cause the Decoder will not get called
*
* @author Marcin Grzejszczak
*
* @since 1.0.0
*/
class TraceFeignClient extends FeignEventPublisher implements Client {
private final Client delegate;
TraceFeignClient(ApplicationEventPublisher publisher, Tracer tracer) {
super(publisher, tracer);
this.delegate = new Client.Default(null, null);
}
TraceFeignClient(ApplicationEventPublisher publisher, Tracer tracer, Client delegate) {
super(publisher, tracer);
this.delegate = delegate;
}
@Override
public Response execute(Request request, Request.Options options) throws IOException {
Response response = this.delegate.execute(request, options);
if (response.body() == null || (response.body() != null && Objects.equals(response.body().length(), 0))) {
finish();
}
return response;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2015 the original author or authors.
* Copyright 2013-2016 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.
@@ -14,13 +14,12 @@
* limitations under the License.
*/
package org.springframework.cloud.sleuth.instrument.web.client;
package org.springframework.cloud.sleuth.instrument.web.client.feign;
import static java.util.Collections.singletonList;
import java.io.IOException;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
@@ -30,7 +29,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
import org.springframework.cloud.netflix.feign.FeignAutoConfiguration;
@@ -38,12 +36,8 @@ import org.springframework.cloud.netflix.feign.support.ResponseEntityDecoder;
import org.springframework.cloud.netflix.feign.support.SpringDecoder;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.cloud.sleuth.event.ClientReceivedEvent;
import org.springframework.cloud.sleuth.event.ClientSentEvent;
import org.springframework.cloud.sleuth.TraceKeys;
import org.springframework.cloud.sleuth.instrument.hystrix.SleuthHystrixAutoConfiguration;
import org.springframework.cloud.sleuth.instrument.hystrix.SleuthHystrixConcurrencyStrategy;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -57,10 +51,8 @@ import feign.Client;
import feign.Feign;
import feign.FeignException;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import feign.Response;
import feign.codec.Decoder;
import feign.hystrix.HystrixFeign;
/**
* {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration Auto-configuration}
@@ -86,93 +78,50 @@ public class TraceFeignClientAutoConfiguration {
@Autowired
private Tracer tracer;
private final FeignRequestContext feignRequestContext = FeignRequestContext.getInstance();
@Bean
@Scope("prototype")
@ConditionalOnClass(HystrixCommand.class)
@ConditionalOnMissingBean(SleuthHystrixConcurrencyStrategy.class)
@ConditionalOnProperty(name = "feign.hystrix.enabled", matchIfMissing = true)
public Feign.Builder feignHystrixBuilder(Tracer tracer, TraceKeys traceKeys) {
return HystrixFeign.builder().invocationHandlerFactory(
new SleuthHystrixInvocationHandler.Factory(tracer, traceKeys));
return SleuthFeignBuilder.builder(this.publisher, tracer);
}
@Bean
@ConditionalOnProperty(name = "spring.sleuth.feign.processor.enabled", matchIfMissing = true)
public FeignBeanPostProcessor feignBeanPostProcessor(Tracer tracer) {
return new FeignBeanPostProcessor(this.publisher, tracer);
}
@Bean
@Primary
public Decoder feignDecoder() {
return new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)) {
public Decoder feignDecoder(final Tracer tracer) {
return new TraceFeignDecoder(this.publisher, tracer, new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)) {
@Override
public Object decode(Response response, Type type)
throws IOException, FeignException {
try {
return super.decode(Response.create(response.status(),
response.reason(), headersWithTraceId(response.headers()),
response.body()), type);
}
finally {
Span span = getCurrentSpan();
if (span != null) {
publish(new ClientReceivedEvent(this, span));
TraceFeignClientAutoConfiguration.this.tracer.close(span);
}
}
}
};
});
}
/**
* Sleuth {@link feign.RequestInterceptor} that either starts a new Span
* or continues an existing one if a retry takes place.
*/
@Bean
public RequestInterceptor traceIdRequestInterceptor() {
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
URI uri = URI.create(template.url());
String spanName = uriScheme(uri) + ":" + uri.getPath();
Span span = TraceFeignClientAutoConfiguration.this.tracer.createSpan(spanName);
if (span == null) {
setHeader(template, Span.NOT_SAMPLED_NAME, "true");
return;
}
template.header(Span.TRACE_ID_NAME, Span.idToHex(span.getTraceId()));
setHeader(template, Span.SPAN_NAME_NAME, span.getName());
setHeader(template, Span.SPAN_ID_NAME, Span.idToHex(span.getSpanId()));
if (!span.isExportable()) {
setHeader(template, Span.NOT_SAMPLED_NAME, "true");
}
Long parentId = getParentId(span);
if (parentId != null) {
setHeader(template, Span.PARENT_ID_NAME, Span.idToHex(parentId));
}
setHeader(template, Span.PROCESS_ID_NAME, span.getProcessId());
publish(new ClientSentEvent(this, span));
}
};
}
private String uriScheme(URI uri) {
return uri.getScheme() == null ? "http" : uri.getScheme();
}
private void publish(ApplicationEvent event) {
if (this.publisher != null) {
this.publisher.publishEvent(event);
}
}
private Long getParentId(Span span) {
return !span.getParents().isEmpty() ? span.getParents().get(0) : null;
}
public void setHeader(RequestTemplate request, String name, String value) {
if (StringUtils.hasText(value) && !request.headers().containsKey(name)
&& this.tracer.isTracing()) {
request.header(name, value);
}
public RequestInterceptor traceIdRequestInterceptor(Tracer tracer) {
return new TraceFeignRequestInterceptor(tracer);
}
private Map<String, Collection<String>> headersWithTraceId(
Map<String, Collection<String>> headers) {
Map<String, Collection<String>> newHeaders = new HashMap<>();
newHeaders.putAll(headers);
Span span = getCurrentSpan();
Span span = this.feignRequestContext.getCurrentSpan();
if (span == null) {
setHeader(newHeaders, Span.NOT_SAMPLED_NAME, "true");
return newHeaders;
@@ -183,6 +132,10 @@ public class TraceFeignClientAutoConfiguration {
return newHeaders;
}
private Long getParentId(Span span) {
return !span.getParents().isEmpty() ? span.getParents().get(0) : null;
}
public void setHeader(Map<String, Collection<String>> headers, String name,
String value) {
if (StringUtils.hasText(value) && !headers.containsKey(name)
@@ -198,8 +151,4 @@ public class TraceFeignClientAutoConfiguration {
}
}
private Span getCurrentSpan() {
return this.tracer.getCurrentSpan();
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2013-2016 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.feign;
import java.io.IOException;
import java.lang.reflect.Type;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.context.ApplicationEventPublisher;
import feign.FeignException;
import feign.Response;
import feign.codec.DecodeException;
import feign.codec.Decoder;
/**
* A decoder that closes a span upon decoding the response.
*
* @author Marcin Grzejszczak
*
* @since 1.0.0
*/
class TraceFeignDecoder extends FeignEventPublisher implements Decoder {
private final Decoder delegate;
public TraceFeignDecoder(ApplicationEventPublisher publisher, Tracer tracer) {
super(publisher, tracer);
this.delegate = new Decoder.Default();
}
public TraceFeignDecoder(ApplicationEventPublisher publisher, Tracer tracer, Decoder delegate) {
super(publisher, tracer);
this.delegate = delegate;
}
@Override
public Object decode(Response response, Type type)
throws IOException, DecodeException, FeignException {
try {
return this.delegate.decode(response, type);
} finally {
finish();
}
}
}

View File

@@ -0,0 +1,122 @@
/*
* Copyright 2013-2016 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.feign;
import java.net.URI;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.cloud.sleuth.event.ClientSentEvent;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.util.StringUtils;
import feign.RequestInterceptor;
import feign.RequestTemplate;
/**
* A request interceptor that sets tracing information in the headers
* and retrieves the span from the current {@link FeignRequestContext}.
*
* @author Marcin Grzejszczak
*
* @since 1.0.0
*/
class TraceFeignRequestInterceptor implements RequestInterceptor,
ApplicationEventPublisherAware {
private final Tracer tracer;
private final FeignRequestContext feignRequestContext = FeignRequestContext.getInstance();
private ApplicationEventPublisher publisher;
public TraceFeignRequestInterceptor(Tracer tracer) {
this.tracer = tracer;
}
@Override
public void apply(RequestTemplate template) {
String spanName = getSpanName(template);
Span span = getSpan(spanName);
if (span == null) {
setHeader(template, Span.NOT_SAMPLED_NAME, "true");
return;
}
template.header(Span.TRACE_ID_NAME, Span.idToHex(span.getTraceId()));
setHeader(template, Span.SPAN_NAME_NAME, span.getName());
setHeader(template, Span.SPAN_ID_NAME, Span.idToHex(span.getSpanId()));
if (!span.isExportable()) {
setHeader(template, Span.NOT_SAMPLED_NAME, "true");
}
Long parentId = getParentId(span);
if (parentId != null) {
setHeader(template, Span.PARENT_ID_NAME, Span.idToHex(parentId));
}
setHeader(template, Span.PROCESS_ID_NAME, span.getProcessId());
publish(new ClientSentEvent(this, span));
}
protected String getSpanName(RequestTemplate template) {
URI uri = URI.create(template.url());
return uriScheme(uri) + ":" + uri.getPath();
}
/**
* Depending on the presence of a Span in context, either starts a new Span
* or continues an existing one.
*/
protected Span getSpan(String spanName) {
if (!this.feignRequestContext.hasSpanInProcess()) {
Span span = this.tracer.createSpan(spanName);
this.feignRequestContext.putSpan(span, false);
return span;
} else {
if (this.feignRequestContext.wasSpanRetried()) {
return this.tracer.continueSpan(this.feignRequestContext.getCurrentSpan());
}
}
return this.tracer.createSpan(spanName);
}
private String uriScheme(URI uri) {
return uri.getScheme() == null ? "http" : uri.getScheme();
}
private Long getParentId(Span span) {
return !span.getParents().isEmpty() ? span.getParents().get(0) : null;
}
protected void setHeader(RequestTemplate request, String name, String value) {
if (StringUtils.hasText(value) && !request.headers().containsKey(name)
&& this.tracer.isTracing()) {
request.header(name, value);
}
}
private void publish(ApplicationEvent event) {
if (this.publisher != null) {
this.publisher.publishEvent(event);
}
}
@Override
public void setApplicationEventPublisher(
ApplicationEventPublisher applicationEventPublisher) {
this.publisher = applicationEventPublisher;
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2013-2016 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.feign;
import org.springframework.cloud.sleuth.Tracer;
import feign.RetryableException;
import feign.Retryer;
/**
* Execution of this retryer means that an exception occurred while trying to
* send the request. In that case we need to put information about this span
* into the {@link FeignRequestContext} in order for the {@link feign.RequestInterceptor}
* to know that it should be continued or a new one should be created.
*
* @author Marcin Grzejszczak
*
* @since 1.0.0
*/
class TraceFeignRetryer implements Retryer {
private final Tracer tracer;
private final FeignRequestContext feignRequestContext = FeignRequestContext.getInstance();
private final Retryer delegate;
public TraceFeignRetryer(Tracer tracer) {
this(tracer, new Retryer.Default());
}
public TraceFeignRetryer(Tracer tracer, Retryer delegate) {
this.tracer = tracer;
this.delegate = delegate;
}
@Override
public void continueOrPropagate(RetryableException e) {
try {
this.feignRequestContext.putSpan(this.tracer.getCurrentSpan(), true);
this.tracer.getCurrentSpan().logEvent("feign.retry");
this.delegate.continueOrPropagate(e);
}
catch (RetryableException e2) {
this.tracer.close(this.tracer.getCurrentSpan());
throw e2;
}
}
@Override
public Retryer clone() {
return new TraceFeignRetryer(this.tracer);
}
}

View File

@@ -12,7 +12,7 @@ org.springframework.cloud.sleuth.instrument.scheduling.TraceSchedulingAutoConfig
org.springframework.cloud.sleuth.instrument.web.TraceWebAutoConfiguration,\
org.springframework.cloud.sleuth.instrument.web.client.TraceWebClientAutoConfiguration,\
org.springframework.cloud.sleuth.instrument.web.client.TraceWebAsyncClientAutoConfiguration,\
org.springframework.cloud.sleuth.instrument.web.client.TraceFeignClientAutoConfiguration,\
org.springframework.cloud.sleuth.instrument.web.client.feign.TraceFeignClientAutoConfiguration,\
org.springframework.cloud.sleuth.instrument.zuul.TraceZuulAutoConfiguration
# Environment Post Processor

View File

@@ -0,0 +1,98 @@
/*
* Copyright 2013-2016 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.feign;
import java.io.IOException;
import java.net.UnknownHostException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.cloud.sleuth.trace.TestSpanContextHolder;
import org.springframework.cloud.sleuth.util.ExceptionUtils;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import com.netflix.config.ConfigurationManager;
import com.netflix.hystrix.HystrixCommandProperties;
import static org.assertj.core.api.BDDAssertions.then;
import static org.junit.Assert.fail;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = { FeignTraceExceptionTests.TestConfiguration.class })
@DirtiesContext
public class FeignTraceExceptionTests {
@Autowired
TestFeignInterfaceWithException testFeignInterfaceWithException;
@Autowired
Tracer tracer;
@Before
public void before() {
ExceptionUtils.setFail(true);
}
@After
public void close() {
TestSpanContextHolder.removeCurrentSpan();
}
@Test
public void shouldRemoveSpanFromThreadUponConnectionException() throws IOException {
Span span = this.tracer.createSpan("new trace");
ConfigurationManager
.getConfigInstance().setProperty("hystrix.command.shouldFailToConnect.execution.isolation.strategy",
HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE);
try {
this.testFeignInterfaceWithException.shouldFailToConnect();
fail("should throw an exception");
} catch (Exception e) {
then(e).hasRootCauseInstanceOf(UnknownHostException.class);
}
then(this.tracer.getCurrentSpan()).isEqualTo(span);
this.tracer.close(span);
}
@FeignClient(name = "exceptionService", url = "http://invalid.host.to.break.tests")
public interface TestFeignInterfaceWithException {
@RequestMapping(method = RequestMethod.GET, value = "/")
String shouldFailToConnect();
}
@Configuration
@EnableAutoConfiguration
@EnableFeignClients
public static class TestConfiguration {
}
}

View File

@@ -1,4 +1,20 @@
package org.springframework.cloud.sleuth.instrument.web.client;
/*
* Copyright 2013-2016 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.feign;
import java.util.ArrayList;
import java.util.Arrays;
@@ -90,7 +106,7 @@ public class FeignTraceTests {
public void shouldAttachTraceIdWhenUsingFeignClient() {
Long currentTraceId = 1L;
Long currentParentId = 2L;
Long currentSpanId = generatedId();
Long currentSpanId = 100L;
this.tracer.continueSpan(Span.builder().traceId(currentTraceId)
.spanId(currentSpanId).parent(currentParentId).build());
@@ -100,7 +116,28 @@ public class FeignTraceTests {
.isEqualTo(currentTraceId);
then(Span.hexToId(getHeader(response, Span.PARENT_ID_NAME)))
.isEqualTo(currentSpanId);
thenRegisteredClientSentAndReceivedEvents();
}
@Test
public void shouldAttachTraceIdWhenUsingFeignClientWithoutResponseBody() {
Long currentTraceId = 1L;
Long currentParentId = 2L;
Long currentSpanId = generatedId();
Span span = Span.builder().traceId(currentTraceId)
.spanId(currentSpanId).parent(currentParentId).build();
this.tracer.continueSpan(span);
this.testFeignInterface.noResponseBody();
thenRegisteredClientSentAndReceivedEvents();
then(this.tracer.getCurrentSpan()).isEqualTo(span);
}
private void thenRegisteredClientSentAndReceivedEvents() {
then(this.listener.getEvents().size()).isEqualTo(2);
then(this.listener.getEvents().get(0)).isExactlyInstanceOf(ClientSentEvent.class);
then(this.listener.getEvents().get(1)).isExactlyInstanceOf(ClientReceivedEvent.class);
}
private Long generatedId() {
@@ -122,6 +159,9 @@ public class FeignTraceTests {
@RequestMapping(method = RequestMethod.GET, value = "/")
ResponseEntity<Map<String, String>> headers();
@RequestMapping(method = RequestMethod.GET, value = "/noresponse")
void noResponseBody();
}
@Configuration
@@ -189,6 +229,15 @@ public class FeignTraceTests {
return map;
}
@RequestMapping("/noresponse")
public void noResponse(@RequestHeader(Span.TRACE_ID_NAME) String traceId,
@RequestHeader(Span.SPAN_ID_NAME) String spanId,
@RequestHeader(Span.PARENT_ID_NAME) String parentId) {
then(traceId).isNotEmpty();
then(parentId).isNotEmpty();
then(spanId).isNotEmpty();
}
}
@Configuration