diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/FeignContextBeanPostProcessor.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/FeignContextBeanPostProcessor.java index 632a6d306..d616696ac 100644 --- a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/FeignContextBeanPostProcessor.java +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/FeignContextBeanPostProcessor.java @@ -40,9 +40,8 @@ final class FeignContextBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { - if (bean instanceof FeignContext) { - return new TraceFeignContext(getTraceFeignObjectWrapper(), - (FeignContext) bean); + if (bean instanceof FeignContext && !(bean instanceof TraceFeignContext)) { + return new TraceFeignContext(getTraceFeignObjectWrapper(), (FeignContext) bean); } return bean; } @@ -53,7 +52,7 @@ final class FeignContextBeanPostProcessor implements BeanPostProcessor { return bean; } - TraceFeignObjectWrapper getTraceFeignObjectWrapper() { + private TraceFeignObjectWrapper getTraceFeignObjectWrapper() { if (this.traceFeignObjectWrapper == null) { this.traceFeignObjectWrapper = this.beanFactory.getBean(TraceFeignObjectWrapper.class); } diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/FeignEventPublisher.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/FeignEventPublisher.java deleted file mode 100644 index d66ccba03..000000000 --- a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/FeignEventPublisher.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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.factory.BeanFactory; -import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.Tracer; - -/** - * Abstract class for logging the client received event - * - * @author Marcin Grzejszczak - * - * @since 1.0.0 - */ -abstract class FeignEventPublisher { - - private final FeignRequestContext feignRequestContext = FeignRequestContext.getInstance(); - - protected final BeanFactory beanFactory; - private Tracer tracer; - - protected FeignEventPublisher(BeanFactory beanFactory) { - this.beanFactory = beanFactory; - } - - protected void finish() { - Span span = this.feignRequestContext.getCurrentSpan(); - if (span != null) { - span.logEvent(Span.CLIENT_RECV); - getTracer().close(span); - this.feignRequestContext.clearContext(); - } - } - - Tracer getTracer() { - if (this.tracer == null) { - this.tracer = this.beanFactory.getBean(Tracer.class); - } - return this.tracer; - } -} diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/FeignRequestContext.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/FeignRequestContext.java deleted file mode 100644 index 93bfdc694..000000000 --- a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/FeignRequestContext.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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 - */ -final class FeignRequestContext { - - private static final FeignRequestContext INSTANCE = new FeignRequestContext(); - - private FeignRequestContext() {} - - private static final ThreadLocal THREAD_LOCAL = new ThreadLocal<>(); - - private static final 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; - } -} diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/FeignResponseHeadersInjector.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/FeignResponseHeadersInjector.java deleted file mode 100644 index 9040570bb..000000000 --- a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/FeignResponseHeadersInjector.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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.Collection; -import java.util.HashMap; -import java.util.Map; - -import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.SpanInjector; -import org.springframework.util.StringUtils; - -import static java.util.Collections.singletonList; - -/** - * Span injector that injects tracing info to - * {@link FeignResponseHeadersHolder#responseHeaders} - * - * @author Marcin Grzejszczak - * - * @since 1.0.0 - */ -class FeignResponseHeadersInjector implements SpanInjector { - - @Override - public void inject(Span span, FeignResponseHeadersHolder carrier) { - Map> headers = carrier.responseHeaders; - headersWithTraceId(span, headers); - } - - private Map> headersWithTraceId(Span span, - Map> headers) { - Map> newHeaders = new HashMap<>(); - newHeaders.putAll(headers); - if (span == null) { - setHeader(newHeaders, Span.SAMPLED_NAME, Span.SPAN_NOT_SAMPLED); - return newHeaders; - } - setHeader(newHeaders, Span.SAMPLED_NAME, span.isExportable() ? - Span.SPAN_SAMPLED : Span.SPAN_NOT_SAMPLED); - setHeader(newHeaders, Span.TRACE_ID_NAME, span.getTraceId()); - setHeader(newHeaders, Span.SPAN_ID_NAME, span.getSpanId()); - return newHeaders; - } - - void setHeader(Map> headers, String name, - String value) { - if (StringUtils.hasText(value) && !headers.containsKey(name)) { - headers.put(name, singletonList(value)); - } - } - - void setHeader(Map> headers, String name, - Long value) { - if (value != null) { - setHeader(headers, name, Span.idToHex(value)); - } - } -} diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/SleuthFeignBuilder.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/SleuthFeignBuilder.java index cf4513062..b8486118e 100644 --- a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/SleuthFeignBuilder.java +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/SleuthFeignBuilder.java @@ -22,7 +22,7 @@ import feign.Feign; /** * Contains {@link feign.Feign.Builder} implementation with tracing components - * that close spans on exceptions / success and continues them on retries. + * that close spans on completion of request processing. * * @author Marcin Grzejszczak * @@ -34,9 +34,6 @@ final class SleuthFeignBuilder { static Feign.Builder builder(BeanFactory beanFactory) { return Feign.builder() - .client(new TraceFeignClient(beanFactory)) - .retryer(new TraceFeignRetryer(beanFactory)) - .decoder(new TraceFeignDecoder(beanFactory)) - .errorDecoder(new TraceFeignErrorDecoder(beanFactory)); + .client(new TraceFeignClient(beanFactory)); } } diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/SleuthHystrixFeignBuilder.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/SleuthHystrixFeignBuilder.java index e7f9c59f1..8b69adae8 100644 --- a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/SleuthHystrixFeignBuilder.java +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/SleuthHystrixFeignBuilder.java @@ -24,7 +24,7 @@ import feign.hystrix.HystrixFeign; /** * Contains {@link Feign.Builder} implementation that delegates execution * {@link HystrixFeign} with tracing components - * that close spans on exceptions / success and continues them on retries. + * that close spans upon completion of request processing. * * @author Marcin Grzejszczak * @@ -36,9 +36,6 @@ final class SleuthHystrixFeignBuilder { static Feign.Builder builder(BeanFactory beanFactory) { return HystrixFeign.builder() - .client(new TraceFeignClient(beanFactory)) - .retryer(new TraceFeignRetryer(beanFactory)) - .decoder(new TraceFeignDecoder(beanFactory)) - .errorDecoder(new TraceFeignErrorDecoder(beanFactory)); + .client(new TraceFeignClient(beanFactory)); } } diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignClient.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignClient.java index d0807d197..a29623769 100644 --- a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignClient.java +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignClient.java @@ -17,16 +17,19 @@ package org.springframework.cloud.sleuth.instrument.web.client.feign; import java.io.IOException; +import java.lang.invoke.MethodHandles; import java.net.URI; -import java.util.Objects; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.BeanFactory; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.Tracer; import org.springframework.cloud.sleuth.instrument.web.HttpTraceKeysInjector; import feign.Client; import feign.Request; import feign.Response; -import feign.RetryableException; /** * A Feign Client that closes a Span if there is no response body. In other cases Span @@ -36,42 +39,38 @@ import feign.RetryableException; * * @since 1.0.0 */ -final class TraceFeignClient extends FeignEventPublisher implements Client { +final class TraceFeignClient implements Client { + + private static final Log log = LogFactory.getLog(MethodHandles.lookup().lookupClass()); private final Client delegate; private HttpTraceKeysInjector keysInjector; + private final BeanFactory beanFactory; + private Tracer tracer; TraceFeignClient(BeanFactory beanFactory) { - super(beanFactory); + this.beanFactory = beanFactory; this.delegate = new Client.Default(null, null); } TraceFeignClient(BeanFactory beanFactory, Client delegate) { - super(beanFactory); this.delegate = delegate; + this.beanFactory = beanFactory; } @Override public Response execute(Request request, Request.Options options) throws IOException { - Response response; try { addRequestTags(request); - response = this.delegate.execute(request, options); - } - catch (RetryableException | IOException e) { - // IOException will be wrapped into a RetryableException in the caller + Response response = this.delegate.execute(request, options); + logCr(); + return response; + } catch (RuntimeException | IOException e) { + logError(e); throw e; + } finally { + closeSpan(); } - catch (RuntimeException e) { - // Any other exception is going to be propagated so we need to tidy up - finish(); - throw e; - } - if (response != null && response.body() == null || (response.body() != null - && Objects.equals(response.body().length(), 0))) { - finish(); - } - return response; } /** @@ -83,10 +82,42 @@ final class TraceFeignClient extends FeignEventPublisher implements Client { request.method(), request.headers()); } - HttpTraceKeysInjector getKeysInjector() { + private HttpTraceKeysInjector getKeysInjector() { if (this.keysInjector == null) { this.keysInjector = this.beanFactory.getBean(HttpTraceKeysInjector.class); } return this.keysInjector; } + + private void closeSpan() { + Span span = getTracer().getCurrentSpan(); + if (span != null) { + log.debug("Closing Feign span " + span); + getTracer().close(span); + } + } + + private void logCr() { + Span span = getTracer().getCurrentSpan(); + if (span != null) { + log.debug("Closing Feign span and logging CR" + span); + span.logEvent(Span.CLIENT_RECV); + } + } + + private void logError(Exception e) { + Span span = getTracer().getCurrentSpan(); + if (span != null) { + String message = e.getMessage() != null ? e.getMessage() : e.toString(); + log.debug("Appending exception [" + message + "] to span " + span); + getTracer().addTag("error", message); + } + } + + private Tracer getTracer() { + if (this.tracer == null) { + this.tracer = this.beanFactory.getBean(Tracer.class); + } + return this.tracer; + } } diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignClientAutoConfiguration.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignClientAutoConfiguration.java index 4f74db019..10da31b1b 100644 --- a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignClientAutoConfiguration.java +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignClientAutoConfiguration.java @@ -16,37 +16,23 @@ package org.springframework.cloud.sleuth.instrument.web.client.feign; -import java.io.IOException; -import java.lang.reflect.Type; - import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.ObjectFactory; -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.ConditionalOnBean; 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; -import org.springframework.cloud.netflix.feign.support.ResponseEntityDecoder; -import org.springframework.cloud.netflix.feign.support.SpringDecoder; -import org.springframework.cloud.sleuth.SpanInjector; import org.springframework.cloud.sleuth.Tracer; import org.springframework.cloud.sleuth.instrument.hystrix.SleuthHystrixAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Scope; import feign.Client; import feign.Feign; -import feign.FeignException; import feign.RequestInterceptor; -import feign.RequestTemplate; -import feign.Response; -import feign.codec.Decoder; /** * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration @@ -63,9 +49,6 @@ import feign.codec.Decoder; @AutoConfigureAfter(SleuthHystrixAutoConfiguration.class) public class TraceFeignClientAutoConfiguration { - @Autowired - private ObjectFactory messageConverters; - @Bean @Scope("prototype") @ConditionalOnClass(name = "com.netflix.hystrix.HystrixCommand") @@ -102,44 +85,12 @@ public class TraceFeignClientAutoConfiguration { return new TraceFeignObjectWrapper(beanFactory); } - @Bean - @Primary - Decoder feignDecoder(BeanFactory beanFactory) { - return new TraceFeignDecoder(beanFactory, - new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)) { - @Override - public Object decode(Response response, Type type) - throws IOException, FeignException { - FeignRequestContext feignRequestContext = FeignRequestContext - .getInstance(); - FeignResponseHeadersHolder feignResponseHeadersHolder = new FeignResponseHeadersHolder( - response.headers()); - feignResponseHeadersInjector().inject( - feignRequestContext.getCurrentSpan(), - feignResponseHeadersHolder); - return super.decode( - Response.create(response.status(), response.reason(), - feignResponseHeadersHolder.responseHeaders, - response.body()), - type); - } - }); - } - /** * Sleuth {@link feign.RequestInterceptor} that either starts a new Span or continues * an existing one if a retry takes place. */ @Bean RequestInterceptor traceIdRequestInterceptor(Tracer tracer) { - return new TraceFeignRequestInterceptor(tracer, feignRequestTemplateInjector()); - } - - private SpanInjector feignRequestTemplateInjector() { - return new FeignRequestTemplateInjector(); - } - - private SpanInjector feignResponseHeadersInjector() { - return new FeignResponseHeadersInjector(); + return new TraceFeignRequestInterceptor(tracer, new FeignRequestTemplateInjector()); } } diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignDecoder.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignDecoder.java deleted file mode 100644 index e4d130154..000000000 --- a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignDecoder.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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.beans.factory.BeanFactory; - -import feign.FeignException; -import feign.Response; -import feign.codec.Decoder; - -/** - * A decoder that closes a span upon decoding the response. - * - * @author Marcin Grzejszczak - * - * @since 1.0.0 - */ -final class TraceFeignDecoder extends FeignEventPublisher implements Decoder { - - private final Decoder delegate; - - TraceFeignDecoder(BeanFactory beanFactory) { - super(beanFactory); - this.delegate = new Decoder.Default(); - } - - TraceFeignDecoder(BeanFactory beanFactory, Decoder delegate) { - super(beanFactory); - this.delegate = delegate; - } - - @Override - public Object decode(Response response, Type type) - throws IOException, FeignException { - try { - return this.delegate.decode(response, type); - } finally { - finish(); - } - } -} diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignErrorDecoder.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignErrorDecoder.java deleted file mode 100644 index ec634fe8d..000000000 --- a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignErrorDecoder.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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.factory.BeanFactory; - -import feign.Response; -import feign.codec.ErrorDecoder; - -/** - * An {@link ErrorDecoder} that closes a span before returning the exception type. - * - * @author Marcin Grzejszczak - * - * @since 1.0.0 - */ -final class TraceFeignErrorDecoder extends FeignEventPublisher implements ErrorDecoder { - - private final ErrorDecoder delegate; - - TraceFeignErrorDecoder(BeanFactory beanFactory) { - super(beanFactory); - this.delegate = new ErrorDecoder.Default(); - } - - TraceFeignErrorDecoder(BeanFactory beanFactory, ErrorDecoder delegate) { - super(beanFactory); - this.delegate = delegate; - } - - @Override public Exception decode(String methodKey, Response response) { - try { - return this.delegate.decode(methodKey, response); - } finally { - finish(); - } - } -} diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignObjectWrapper.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignObjectWrapper.java index be7bc6a48..dae2e6b06 100644 --- a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignObjectWrapper.java +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignObjectWrapper.java @@ -3,9 +3,6 @@ package org.springframework.cloud.sleuth.instrument.web.client.feign; import org.springframework.beans.factory.BeanFactory; import feign.Client; -import feign.Retryer; -import feign.codec.Decoder; -import feign.codec.ErrorDecoder; /** * Class that wraps Feign related classes into their Trace representative @@ -22,14 +19,8 @@ final class TraceFeignObjectWrapper { } Object wrap(Object bean) { - if (bean instanceof Decoder && !(bean instanceof TraceFeignDecoder)) { - return new TraceFeignDecoder(this.beanFactory, (Decoder) bean); - } else if (bean instanceof Retryer && !(bean instanceof TraceFeignRetryer)) { - return new TraceFeignRetryer(this.beanFactory, (Retryer) bean); - } else if (bean instanceof Client && !(bean instanceof TraceFeignClient)) { + if (bean instanceof Client && !(bean instanceof TraceFeignClient)) { return new TraceFeignClient(this.beanFactory, (Client) bean); - } else if (bean instanceof ErrorDecoder && !(bean instanceof TraceFeignErrorDecoder)) { - return new TraceFeignErrorDecoder(this.beanFactory, (ErrorDecoder) bean); } return bean; } diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignRequestInterceptor.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignRequestInterceptor.java index b48fa9f2e..425e5bda6 100644 --- a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignRequestInterceptor.java +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignRequestInterceptor.java @@ -26,8 +26,7 @@ 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}. + * A request interceptor that sets tracing information in the headers. * * @author Marcin Grzejszczak * @@ -37,7 +36,6 @@ final class TraceFeignRequestInterceptor implements RequestInterceptor { private final Tracer tracer; private final SpanInjector spanInjector; - private final FeignRequestContext feignRequestContext = FeignRequestContext.getInstance(); TraceFeignRequestInterceptor(Tracer tracer, SpanInjector spanInjector) { @@ -48,33 +46,16 @@ final class TraceFeignRequestInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate template) { String spanName = getSpanName(template); - Span span = getSpan(spanName); + Span span = this.tracer.createSpan(spanName); this.spanInjector.inject(span, template); span.logEvent(Span.CLIENT_SEND); } - protected String getSpanName(RequestTemplate template) { + private 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(); } diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignRetryer.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignRetryer.java deleted file mode 100644 index 4fa8cbe4a..000000000 --- a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignRetryer.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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.factory.BeanFactory; -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 - */ -final class TraceFeignRetryer implements Retryer { - - private final BeanFactory beanFactory; - private Tracer tracer; - private final FeignRequestContext feignRequestContext = FeignRequestContext - .getInstance(); - private final Retryer delegate; - - TraceFeignRetryer(BeanFactory beanFactory) { - this(beanFactory, new Retryer.Default()); - } - - TraceFeignRetryer(BeanFactory beanFactory, Retryer delegate) { - this.beanFactory = beanFactory; - this.delegate = delegate; - } - - @Override - public void continueOrPropagate(RetryableException e) { - try { - this.feignRequestContext.putSpan(getTracer().getCurrentSpan(), true); - getTracer().getCurrentSpan().logEvent("feign.retry"); - this.delegate.continueOrPropagate(e); - } - catch (RetryableException e2) { - getTracer().close(getTracer().getCurrentSpan()); - throw e2; - } - } - - @Override - public Retryer clone() { - return new TraceFeignRetryer(this.beanFactory, this.delegate.clone()); - } - - Tracer getTracer() { - if (this.tracer == null) { - this.tracer = this.beanFactory.getBean(Tracer.class); - } - return this.tracer; - } -} \ No newline at end of file diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/assertions/SpanAssert.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/assertions/SpanAssert.java index 3503e64df..f0f5bd5ef 100644 --- a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/assertions/SpanAssert.java +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/assertions/SpanAssert.java @@ -146,6 +146,18 @@ public class SpanAssert extends AbstractAssert { return this; } + public SpanAssert hasNotLoggedAnEvent(String event) { + isNotNull(); + if (this.actual.logs().stream().map(org.springframework.cloud.sleuth.Log::getEvent) + .filter(s -> s.equals(event)).findAny().isPresent()) { + String message = String.format("Expected span NOT to have the event with event value <%s>. " + + "Found logs are <%s>", event, this.actual.logs()); + log.error(message); + failWithMessage(message); + } + return this; + } + public SpanAssert isExportable() { isNotNull(); if (!this.actual.isExportable()) { diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/feign/FeignRetriesTests.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/feign/FeignRetriesTests.java new file mode 100644 index 000000000..05365dab8 --- /dev/null +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/feign/FeignRetriesTests.java @@ -0,0 +1,150 @@ +/* + * 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.nio.charset.Charset; +import java.util.HashMap; +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.BDDMockito; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.cloud.sleuth.DefaultSpanNamer; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.TraceKeys; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.instrument.web.HttpTraceKeysInjector; +import org.springframework.cloud.sleuth.log.NoOpSpanLogger; +import org.springframework.cloud.sleuth.sampler.AlwaysSampler; +import org.springframework.cloud.sleuth.trace.DefaultTracer; +import org.springframework.cloud.sleuth.trace.TestSpanContextHolder; +import org.springframework.cloud.sleuth.util.ArrayListSpanAccumulator; +import org.springframework.cloud.sleuth.util.ExceptionUtils; + +import feign.Client; +import feign.Feign; +import feign.FeignException; +import feign.RequestInterceptor; +import feign.RequestLine; +import feign.Response; +import okhttp3.mockwebserver.MockWebServer; + +import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; +import static org.springframework.cloud.sleuth.assertions.SleuthAssertions.then; + +/** + * @author Marcin Grzejszczak + */ +@RunWith(MockitoJUnitRunner.class) +public class FeignRetriesTests { + + @Rule + public final MockWebServer server = new MockWebServer(); + + @Mock BeanFactory beanFactory; + + ArrayListSpanAccumulator spanAccumulator = new ArrayListSpanAccumulator(); + Tracer tracer = new DefaultTracer(new AlwaysSampler(), new Random(), new DefaultSpanNamer(), + new NoOpSpanLogger(), this.spanAccumulator); + FeignRequestTemplateInjector injector = new FeignRequestTemplateInjector(); + TraceFeignRequestInterceptor interceptor = new TraceFeignRequestInterceptor(tracer, injector); + + @Before + @After + public void setup() { + ExceptionUtils.setFail(true); + TestSpanContextHolder.removeCurrentSpan(); + BDDMockito.given(this.beanFactory.getBean(HttpTraceKeysInjector.class)) + .willReturn(new HttpTraceKeysInjector(this.tracer, new TraceKeys())); + BDDMockito.given(this.beanFactory.getBean(Tracer.class)).willReturn(this.tracer); + } + + @Test + public void testRetriedWhenExceededNumberOfRetries() throws Exception { + Client client = (request, options) -> { + throw new IOException(); + }; + String url = "http://localhost:" + server.getPort(); + + TestInterface api = + Feign.builder() + .client(new TraceFeignClient(beanFactory, client)) + .requestInterceptor(interceptor) + .target(TestInterface.class, url); + + try { + api.decodedPost(); + failBecauseExceptionWasNotThrown(FeignException.class); + } catch (FeignException e) { } + + then(this.tracer.getCurrentSpan()).isNull(); + then(ExceptionUtils.getLastException()).isNull(); + } + + @Test + public void testRetriedWhenRequestEventuallyIsSent() throws Exception { + String url = "http://localhost:" + server.getPort(); + final AtomicInteger atomicInteger = new AtomicInteger(); + // Client to simulate a retry scenario + Client client = (request, options) -> { + // we simulate an exception only for the first request + if (atomicInteger.get() == 1) { + throw new IOException(); + } else { + // with the second retry (first retry) we send back good result + return Response.create(200, "OK", new HashMap<>(), + "OK", Charset.defaultCharset()); + } + }; + RequestInterceptor requestInterceptor = template -> { + atomicInteger.incrementAndGet(); + interceptor.apply(template); + }; + TestInterface api = + Feign.builder() + .client(new TraceFeignClient(beanFactory, client)) + .requestInterceptor(requestInterceptor) + .target(TestInterface.class, url); + + then(api.decodedPost()).isEqualTo("OK"); + // request interception should take place only twice (1st request & 2nd retry) + then(atomicInteger.get()).isEqualTo(2); + then(this.tracer.getCurrentSpan()).isNull(); + then(ExceptionUtils.getLastException()).isNull(); + then(this.spanAccumulator.getSpans().get(0)) + .hasNotLoggedAnEvent(Span.CLIENT_RECV) + .hasATag("error", "java.io.IOException"); + then(this.spanAccumulator.getSpans().get(1)) + .hasLoggedAnEvent(Span.CLIENT_RECV); + } + + interface TestInterface { + + @RequestLine("POST /") + String decodedPost(); + } + + +} diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignClientTests.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignClientTests.java new file mode 100644 index 000000000..ba6905f22 --- /dev/null +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignClientTests.java @@ -0,0 +1,105 @@ +/* + * 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.nio.charset.Charset; +import java.util.HashMap; +import java.util.Random; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.BDDMockito; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.cloud.sleuth.DefaultSpanNamer; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.TraceKeys; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.assertions.SleuthAssertions; +import org.springframework.cloud.sleuth.instrument.web.HttpTraceKeysInjector; +import org.springframework.cloud.sleuth.log.NoOpSpanLogger; +import org.springframework.cloud.sleuth.sampler.AlwaysSampler; +import org.springframework.cloud.sleuth.trace.DefaultTracer; +import org.springframework.cloud.sleuth.trace.TestSpanContextHolder; +import org.springframework.cloud.sleuth.util.ArrayListSpanAccumulator; +import org.springframework.cloud.sleuth.util.ExceptionUtils; + +import feign.Client; +import feign.Request; +import feign.Response; + +import static org.springframework.cloud.sleuth.assertions.SleuthAssertions.then; + +/** + * @author Marcin Grzejszczak + */ +@RunWith(MockitoJUnitRunner.class) +public class TraceFeignClientTests { + + ArrayListSpanAccumulator spanAccumulator = new ArrayListSpanAccumulator(); + @Mock BeanFactory beanFactory; + Tracer tracer = new DefaultTracer(new AlwaysSampler(), new Random(), new DefaultSpanNamer(), + new NoOpSpanLogger(), this.spanAccumulator); + @Mock Client client; + @InjectMocks TraceFeignClient traceFeignClient; + + @Before + @After + public void setup() { + TestSpanContextHolder.removeCurrentSpan(); + ExceptionUtils.setFail(true); + BDDMockito.given(this.beanFactory.getBean(HttpTraceKeysInjector.class)) + .willReturn(new HttpTraceKeysInjector(this.tracer, new TraceKeys())); + BDDMockito.given(this.beanFactory.getBean(Tracer.class)).willReturn(this.tracer); + } + + @Test + public void should_log_cr_when_response_successful() throws IOException { + this.tracer.createSpan("foo"); + Response response = this.traceFeignClient.execute( + Request.create("GET", "http://foo", new HashMap<>(), "".getBytes(), + Charset.defaultCharset()), new Request.Options()); + + then(this.tracer.getCurrentSpan()).isNull(); + then(this.spanAccumulator.getSpans().get(0)).hasLoggedAnEvent(Span.CLIENT_RECV); + } + + @Test + public void should_log_error_when_exception_thrown() throws IOException { + this.tracer.createSpan("foo"); + BDDMockito.given(this.client.execute(BDDMockito.any(), BDDMockito.any())) + .willThrow(new RuntimeException("exception has occurred")); + + try { + this.traceFeignClient.execute( + Request.create("GET", "http://foo", new HashMap<>(), "".getBytes(), + Charset.defaultCharset()), new Request.Options()); + SleuthAssertions.fail("Exception should have been thrown"); + } catch (Exception e) {} + + then(this.tracer.getCurrentSpan()).isNull(); + then(this.spanAccumulator.getSpans().get(0)) + .hasNotLoggedAnEvent(Span.CLIENT_RECV) + .hasATag("error", "exception has occurred"); + } + +} \ No newline at end of file diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignObjectWrapperTests.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignObjectWrapperTests.java index 383bf71f3..946ac1201 100644 --- a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignObjectWrapperTests.java +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignObjectWrapperTests.java @@ -1,9 +1,5 @@ package org.springframework.cloud.sleuth.instrument.web.client.feign; -import feign.Client; -import feign.Retryer; -import feign.codec.Decoder; -import feign.codec.ErrorDecoder; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -13,6 +9,8 @@ import org.mockito.runners.MockitoJUnitRunner; import org.springframework.beans.factory.BeanFactory; import org.springframework.cloud.sleuth.Tracer; +import feign.Client; + import static org.assertj.core.api.BDDAssertions.then; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -32,26 +30,11 @@ public class TraceFeignObjectWrapperTests { given(this.beanFactory.getBean(Tracer.class)).willReturn(this.tracer); } - @Test - public void should_wrap_a_decoder_into_trace_decoder() throws Exception { - then(this.traceFeignObjectWrapper.wrap(mock(Decoder.class))).isExactlyInstanceOf(TraceFeignDecoder.class); - } - - @Test - public void should_wrap_a_retryer_into_trace_retryer() throws Exception { - then(this.traceFeignObjectWrapper.wrap(mock(Retryer.class))).isExactlyInstanceOf(TraceFeignRetryer.class); - } - @Test public void should_wrap_a_client_into_trace_client() throws Exception { then(this.traceFeignObjectWrapper.wrap(mock(Client.class))).isExactlyInstanceOf(TraceFeignClient.class); } - @Test - public void should_wrap_a_error_decoder_into_trace_error_decoder() throws Exception { - then(this.traceFeignObjectWrapper.wrap(mock(ErrorDecoder.class))).isExactlyInstanceOf(TraceFeignErrorDecoder.class); - } - @Test public void should_not_wrap_a_bean_that_is_not_feign_related() throws Exception { String notFeignRelatedObject = "object"; diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/feign/issues/Issue350Tests.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/feign/issues/issue350/Issue350Tests.java similarity index 99% rename from spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/feign/issues/Issue350Tests.java rename to spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/feign/issues/issue350/Issue350Tests.java index fd26a1b00..da379bd46 100644 --- a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/feign/issues/Issue350Tests.java +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/feign/issues/issue350/Issue350Tests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.sleuth.instrument.web.client.feign.issues; +package org.springframework.cloud.sleuth.instrument.web.client.feign.issues.issue350; import java.util.concurrent.ExecutionException; diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/feign/issues/issue362/Issue362Tests.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/feign/issues/issue362/Issue362Tests.java new file mode 100644 index 000000000..ef8f23bcc --- /dev/null +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/feign/issues/issue362/Issue362Tests.java @@ -0,0 +1,202 @@ +/* + * 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.issues.issue362; + +import java.util.Date; +import java.util.concurrent.ExecutionException; + +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.boot.test.WebIntegrationTest; +import org.springframework.cloud.netflix.feign.EnableFeignClients; +import org.springframework.cloud.netflix.feign.FeignClient; +import org.springframework.cloud.sleuth.assertions.SleuthAssertions; +import org.springframework.cloud.sleuth.sampler.AlwaysSampler; +import org.springframework.cloud.sleuth.util.ExceptionUtils; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; + +import feign.Client; +import feign.Logger; +import feign.Response; +import feign.RetryableException; +import feign.Retryer; +import feign.codec.ErrorDecoder; + +import static org.assertj.core.api.Assertions.fail; +import static org.assertj.core.api.BDDAssertions.then; + +/** + * @author Marcin Grzejszczak + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(Application.class) +@WebIntegrationTest +@TestPropertySource(properties = {"ribbon.eureka.enabled=false", "feign.hystrix.enabled=false", "server.port=9998"}) +public class Issue362Tests { + + RestTemplate template = new RestTemplate(); + + @Before + public void setup() { + ExceptionUtils.setFail(true); + } + + @Test + public void should_successfully_work_with_custom_error_decoder_when_sending_successful_request() { + String securedURl = "http://localhost:9998/sleuth/test-ok"; + + ResponseEntity response = this.template.getForEntity(securedURl, String.class); + + SleuthAssertions.then(response.getBody()).isEqualTo("I'm OK"); + then(ExceptionUtils.getLastException()).isNull(); + } + + @Test + public void should_successfully_work_with_custom_error_decoder_when_sending_failing_request() { + String securedURl = "http://localhost:9998/sleuth/test-not-ok"; + + try { + this.template.getForEntity(securedURl, String.class); + fail("should propagate an exception"); + } catch (Exception e) { } + + then(ExceptionUtils.getLastException()).isNull(); + } +} + +@Configuration +@EnableAutoConfiguration +@EnableFeignClients(basePackageClasses = { + SleuthTestController.class}) +class Application { + + @Bean + public ServiceTestController serviceTestController() { + return new ServiceTestController(); + } + + @Bean + public SleuthTestController sleuthTestController() { + return new SleuthTestController(); + } + + @Bean + public Logger.Level feignLoggerLevel() { + return feign.Logger.Level.FULL; + } + + @Bean + public AlwaysSampler defaultSampler() { + return new AlwaysSampler(); + } + + @Bean + public Client client() { + return new Client.Default(null, null); + } +} + +@Configuration +class CustomConfig { + + @Bean + public ErrorDecoder errorDecoder() { + return new CustomErrorDecoder(); + } + + @Bean + public Retryer retryer() { + return new Retryer.Default(); + } + + public static class CustomErrorDecoder extends ErrorDecoder.Default { + + public CustomErrorDecoder() { + } + + @Override + public Exception decode(String methodKey, Response response) { + if (response.status() == 409) { + return new RetryableException("Article not Ready", new Date()); + } else { + return super.decode(methodKey, response); + } + } + + } +} + +@FeignClient(value="myFeignClient", url="http://localhost:9998", + configuration = CustomConfig.class) +interface MyFeignClient { + + @RequestMapping("/service/ok") + String ok(); + + @RequestMapping("/service/not-ok") + String exp(); +} + +@RestController +@RequestMapping(path = "/service") +class ServiceTestController { + + @RequestMapping("/ok") + public String ok() throws InterruptedException, ExecutionException { + String result = "I'm OK"; + return result; + } + + @RequestMapping("/not-ok") + @ResponseStatus(HttpStatus.CONFLICT) + public String notOk() throws InterruptedException, ExecutionException { + return "Not OK"; + } +} + +@RestController +@RequestMapping(path = "/sleuth") +class SleuthTestController { + + @Autowired + private MyFeignClient myFeignClient; + + @RequestMapping("/test-ok") + public String ok() throws InterruptedException, ExecutionException { + String result = myFeignClient.ok(); + return result; + } + + @RequestMapping("/test-not-ok") + public String notOk() throws InterruptedException, ExecutionException { + String result = myFeignClient.exp(); + return result; + } +} \ No newline at end of file