From aaa00a9754965f31b7015299912b9cdd822b31e8 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Wed, 23 Dec 2015 09:43:04 +0100 Subject: [PATCH] [#75] Working implementation of Hystrix + Feign + Sleuth. Fixes #75 --- .../SleuthHystrixInvocationHandler.java | 90 +++++++++++++++++++ .../TraceFeignClientAutoConfiguration.java | 20 ++++- .../instrument/web/client/FeignTraceTest.java | 13 +-- 3 files changed, 116 insertions(+), 7 deletions(-) create mode 100644 spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/SleuthHystrixInvocationHandler.java diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/SleuthHystrixInvocationHandler.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/SleuthHystrixInvocationHandler.java new file mode 100644 index 000000000..ed51922b2 --- /dev/null +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/SleuthHystrixInvocationHandler.java @@ -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 dispatch; + private final TraceManager traceManager; + + SleuthHystrixInvocationHandler(Target target, Map 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 hystrixCommand = new TraceCommand(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 dispatch) { + return new SleuthHystrixInvocationHandler(target, dispatch, traceManager); + } + } +} \ No newline at end of file diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/TraceFeignClientAutoConfiguration.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/TraceFeignClientAutoConfiguration.java index 6420e12a3..0772ac0e9 100644 --- a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/TraceFeignClientAutoConfiguration.java +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/TraceFeignClientAutoConfiguration.java @@ -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() { diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/FeignTraceTest.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/FeignTraceTest.java index 3f86c67ee..aa97e4231 100644 --- a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/FeignTraceTest.java +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/FeignTraceTest.java @@ -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 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 response, String name) { List headers = response.getHeaders().get(name); return headers == null || headers.isEmpty() ? null : headers.get(0);