[#75] Working implementation of Hystrix + Feign + Sleuth. Fixes #75

This commit is contained in:
Marcin Grzejszczak
2015-12-23 09:43:04 +01:00
parent 08f79397ab
commit aaa00a9754
3 changed files with 116 additions and 7 deletions

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.sleuth.instrument.web.client;
import static feign.Util.checkNotNull;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;
import org.springframework.cloud.sleuth.TraceManager;
import org.springframework.cloud.sleuth.instrument.hystrix.TraceCommand;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandKey;
import feign.InvocationHandlerFactory;
import feign.InvocationHandlerFactory.MethodHandler;
import feign.Target;
/**
* Wraps execution in Sleuth's TraceCommand
*/
final class SleuthHystrixInvocationHandler implements InvocationHandler {
private final Target target;
private final Map<Method, MethodHandler> dispatch;
private final TraceManager traceManager;
SleuthHystrixInvocationHandler(Target target, Map<Method, MethodHandler> dispatch, TraceManager traceManager) {
this.traceManager = checkNotNull(traceManager, "traceManager");
this.target = checkNotNull(target, "target");
this.dispatch = checkNotNull(dispatch, "dispatch");
}
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
String groupKey = this.target.name();
String commandKey = method.getName();
HystrixCommand.Setter setter = HystrixCommand.Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey(groupKey))
.andCommandKey(HystrixCommandKey.Factory.asKey(commandKey));
HystrixCommand<Object> hystrixCommand = new TraceCommand<Object>(traceManager, setter) {
@Override
public Object doRun() throws Exception {
try {
return SleuthHystrixInvocationHandler.this.dispatch.get(method).invoke(args);
} catch (Exception e) {
throw e;
} catch (Throwable t) {
throw (Error)t;
}
}
};
if (HystrixCommand.class.isAssignableFrom(method.getReturnType())) {
return hystrixCommand;
}
return hystrixCommand.execute();
}
static final class Factory implements InvocationHandlerFactory {
private final TraceManager traceManager;
public Factory(TraceManager traceManager) {
this.traceManager = traceManager;
}
@Override
public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) {
return new SleuthHystrixInvocationHandler(target, dispatch, traceManager);
}
}
}

View File

@@ -26,14 +26,18 @@ import java.util.Map;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
import org.springframework.cloud.netflix.feign.FeignAutoConfiguration;
import org.springframework.cloud.netflix.feign.support.ResponseEntityDecoder;
import org.springframework.cloud.netflix.feign.support.SpringDecoder;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.Trace;
import org.springframework.cloud.sleuth.TraceAccessor;
import org.springframework.cloud.sleuth.TraceManager;
import org.springframework.cloud.sleuth.event.ClientReceivedEvent;
import org.springframework.cloud.sleuth.event.ClientSentEvent;
import org.springframework.context.ApplicationEvent;
@@ -41,23 +45,28 @@ import org.springframework.context.ApplicationEventPublisher;
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 com.netflix.hystrix.HystrixCommand;
import feign.Client;
import feign.Feign;
import feign.FeignException;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import feign.Response;
import feign.codec.Decoder;
import feign.hystrix.HystrixFeign;
/**
*
* Configuration for ensuring that TraceID is set on the response
* Configuration for ensuring that Spans are propagated while using Feign
*
* @author Marcin Grzejszczak, 4financeIT
*/
@Configuration
@ConditionalOnProperty(value = "spring.sleuth.feign.enabled", matchIfMissing = true)
@ConditionalOnClass(Client.class)
@AutoConfigureBefore(FeignAutoConfiguration.class)
public class TraceFeignClientAutoConfiguration {
@Autowired
@@ -69,6 +78,15 @@ public class TraceFeignClientAutoConfiguration {
@Autowired
private TraceAccessor accessor;
@Bean
@Scope("prototype")
@ConditionalOnClass(HystrixCommand.class)
@ConditionalOnProperty(name = "feign.hystrix.enabled", matchIfMissing = true)
public Feign.Builder feignHystrixBuilder(TraceManager traceManager) {
return HystrixFeign.builder()
.invocationHandlerFactory(new SleuthHystrixInvocationHandler.Factory(traceManager));
}
@Bean
@Primary
public Decoder feignDecoder() {

View File

@@ -31,6 +31,7 @@ import org.springframework.context.event.EventListener;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.util.JdkIdGenerator;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@@ -43,9 +44,7 @@ import com.netflix.loadbalancer.Server;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = { TraceWebAutoConfiguration.class,
FeignTraceTest.TestConfiguration.class })
// TODO: make it work with default isolation
@WebIntegrationTest(value = { "spring.application.name=fooservice",
"hystrix.command.default.execution.isolation.strategy=SEMAPHORE" }, randomPort = true)
@WebIntegrationTest(value = { "spring.application.name=fooservice" }, randomPort = true)
public class FeignTraceTest {
@Autowired
@@ -77,21 +76,23 @@ public class FeignTraceTest {
public void shouldAttachTraceIdWhenUsingFeignClient() {
// given
String currentTraceId = "currentTraceId";
String currentSpanId = "currentSpanId";
String currentParentId = "currentParentId";
this.traceManager.continueSpan(MilliSpan.builder().traceId(currentTraceId)
.spanId(currentSpanId).parent(currentParentId).build());
.spanId(generatedId()).parent(currentParentId).build());
// when
ResponseEntity<String> response = this.testFeignInterface.getTraceId();
// then
then(getHeader(response, Trace.TRACE_ID_NAME)).isEqualTo(currentTraceId);
then(getHeader(response, Trace.SPAN_ID_NAME)).isEqualTo(currentSpanId);
then(getHeader(response, Trace.PARENT_ID_NAME)).isEqualTo(currentParentId);
then(this.listener.getEvents().size()).isEqualTo(2);
}
private String generatedId() {
return new JdkIdGenerator().generateId().toString();
}
private String getHeader(ResponseEntity<String> response, String name) {
List<String> headers = response.getHeaders().get(name);
return headers == null || headers.isEmpty() ? null : headers.get(0);