Simplified Feign (#372)
with this change we no longer treat retries as a continuation of a previous span. That way the Feign code simplifies a lot. RequestInterceptor starts a span and the TraceFeignClient will always close it no matter what's happening. fixes #202
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<SpanHolder> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<FeignResponseHeadersHolder> {
|
||||
|
||||
@Override
|
||||
public void inject(Span span, FeignResponseHeadersHolder carrier) {
|
||||
Map<String, Collection<String>> headers = carrier.responseHeaders;
|
||||
headersWithTraceId(span, headers);
|
||||
}
|
||||
|
||||
private Map<String, Collection<String>> headersWithTraceId(Span span,
|
||||
Map<String, Collection<String>> headers) {
|
||||
Map<String, Collection<String>> 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<String, Collection<String>> headers, String name,
|
||||
String value) {
|
||||
if (StringUtils.hasText(value) && !headers.containsKey(name)) {
|
||||
headers.put(name, singletonList(value));
|
||||
}
|
||||
}
|
||||
|
||||
void setHeader(Map<String, Collection<String>> headers, String name,
|
||||
Long value) {
|
||||
if (value != null) {
|
||||
setHeader(headers, name, Span.idToHex(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<HttpMessageConverters> 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<RequestTemplate> feignRequestTemplateInjector() {
|
||||
return new FeignRequestTemplateInjector();
|
||||
}
|
||||
|
||||
private SpanInjector<FeignResponseHeadersHolder> feignResponseHeadersInjector() {
|
||||
return new FeignResponseHeadersInjector();
|
||||
return new TraceFeignRequestInterceptor(tracer, new FeignRequestTemplateInjector());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<RequestTemplate> spanInjector;
|
||||
private final FeignRequestContext feignRequestContext = FeignRequestContext.getInstance();
|
||||
|
||||
TraceFeignRequestInterceptor(Tracer tracer,
|
||||
SpanInjector<RequestTemplate> 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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -146,6 +146,18 @@ public class SpanAssert extends AbstractAssert<SpanAssert, Span> {
|
||||
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()) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<String> 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user