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:
Marcin Grzejszczak
2016-08-09 18:54:28 +02:00
parent e408fd4190
commit ee23ea6465
19 changed files with 536 additions and 527 deletions

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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));
}
}
}

View File

@@ -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));
}
}

View File

@@ -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));
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}

View File

@@ -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();
}

View File

@@ -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;
}
}

View File

@@ -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()) {

View File

@@ -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();
}
}

View File

@@ -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");
}
}

View File

@@ -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";

View File

@@ -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;

View File

@@ -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;
}
}