@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user