From 162237206136bcee7d298436ee3d3e6f5bf2cdd3 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Mon, 10 Aug 2015 08:01:24 +0200 Subject: [PATCH] adding traceid and other headers for feign fixes gh-20 --- pom.xml | 11 ++ spring-cloud-sleuth-core/pom.xml | 31 ++++- .../TraceFeignClientAutoConfiguration.java | 130 ++++++++++++++++++ .../main/resources/META-INF/spring.factories | 3 +- .../instrument/web/client/FeignTraceTest.java | 115 ++++++++++++++++ 5 files changed, 287 insertions(+), 3 deletions(-) create mode 100644 spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/TraceFeignClientAutoConfiguration.java create mode 100644 spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/FeignTraceTest.java diff --git a/pom.xml b/pom.xml index 117b7898e..0afbf4733 100644 --- a/pom.xml +++ b/pom.xml @@ -144,6 +144,12 @@ ${spring-cloud-netflix.version} true + + org.springframework.cloud + spring-cloud-starter-feign + ${spring-cloud-netflix.version} + true + org.projectlombok lombok @@ -160,6 +166,11 @@ zuul-core 1.0.28 + + com.netflix.feign + feign-core + 8.1.1 + org.aspectj aspectjrt diff --git a/spring-cloud-sleuth-core/pom.xml b/spring-cloud-sleuth-core/pom.xml index c57d9aec3..114587dfc 100644 --- a/spring-cloud-sleuth-core/pom.xml +++ b/spring-cloud-sleuth-core/pom.xml @@ -44,8 +44,14 @@ true - org.springframework.boot - spring-boot-starter-integration + org.springframework.cloud + spring-cloud-starter-feign + true + + + org.springframework.integration + spring-integration-core + ${spring-integration.version} true @@ -53,6 +59,11 @@ hystrix-core true + + com.netflix.feign + feign-core + true + com.netflix.zuul zuul-core @@ -79,6 +90,22 @@ spring-boot-starter-test test + + org.aspectj + aspectjweaver + test + + + com.netflix.archaius + archaius-core + 0.7.1 + test + + + org.assertj + assertj-core + 2.1.0 + 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 new file mode 100644 index 000000000..e8ff821c2 --- /dev/null +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/TraceFeignClientAutoConfiguration.java @@ -0,0 +1,130 @@ +/* + * Copyright 2013-2015 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; + +import static java.util.Collections.singletonList; +import static org.springframework.cloud.sleuth.Trace.PARENT_ID_NAME; +import static org.springframework.cloud.sleuth.Trace.SPAN_ID_NAME; +import static org.springframework.cloud.sleuth.Trace.TRACE_ID_NAME; +import static org.springframework.cloud.sleuth.TraceContextHolder.getCurrentSpan; +import static org.springframework.cloud.sleuth.TraceContextHolder.isTracing; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.web.HttpMessageConverters; +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.event.ClientSentEvent; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +import feign.Client; +import feign.FeignException; +import feign.RequestInterceptor; +import feign.RequestTemplate; +import feign.Response; +import feign.codec.Decoder; + +/** + * + * Configuration for ensuring that TraceID is set on the response + * + * @author Marcin Grzejszczak, 4financeIT + */ +@Configuration +@ConditionalOnProperty(value = "spring.sleuth.feign.enabled", matchIfMissing = true) +@ConditionalOnClass(Client.class) +public class TraceFeignClientAutoConfiguration { + + @Autowired + private ObjectFactory messageConverters; + + @Autowired + private ApplicationEventPublisher publisher; + + @Bean + @Primary + public Decoder feignDecoder() { + return new ResponseEntityDecoder(new SpringDecoder(messageConverters)) { + @Override + public Object decode(Response response, Type type) throws IOException, + FeignException { + return super.decode(Response.create(response.status(), response.reason(), + headersWithTraceId(response.headers()), response.body()), type); + } + }; + } + + @Bean + public RequestInterceptor traceIdRequestInterceptor() { + return new RequestInterceptor() { + @Override + public void apply(RequestTemplate template) { + template.header(TRACE_ID_NAME, getCurrentSpan().getTraceId()); + setHeader(template, TRACE_ID_NAME, getCurrentSpan().getTraceId()); + setHeader(template, SPAN_ID_NAME, getCurrentSpan().getSpanId()); + setHeader(template, PARENT_ID_NAME, getParentId(getCurrentSpan())); + publish(new ClientSentEvent(this, getCurrentSpan())); + } + }; + } + + private void publish(ApplicationEvent event) { + if (this.publisher != null) { + this.publisher.publishEvent(event); + } + } + + private String getParentId(Span span) { + return span.getParents() != null && !span.getParents().isEmpty() ? span + .getParents().get(0) : null; + } + + public void setHeader(RequestTemplate request, String name, String value) { + if (value != null && !request.headers().containsKey(name) && isTracing()) { + request.header(name, value); + } + } + + private Map> headersWithTraceId( + Map> headers) { + Map> newHeaders = new HashMap<>(); + newHeaders.putAll(headers); + setHeader(newHeaders, TRACE_ID_NAME, getCurrentSpan().getTraceId()); + setHeader(newHeaders, SPAN_ID_NAME, getCurrentSpan().getSpanId()); + setHeader(newHeaders, PARENT_ID_NAME, getParentId(getCurrentSpan())); + return newHeaders; + } + + public void setHeader(Map> headers, String name, String value) { + if (value != null && !headers.containsKey(name) && isTracing()) { + headers.put(name, singletonList(value)); + } + } +} diff --git a/spring-cloud-sleuth-core/src/main/resources/META-INF/spring.factories b/spring-cloud-sleuth-core/src/main/resources/META-INF/spring.factories index 6c92c6089..30ebb35be 100644 --- a/spring-cloud-sleuth-core/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-sleuth-core/src/main/resources/META-INF/spring.factories @@ -7,4 +7,5 @@ org.springframework.cloud.sleuth.instrument.async.AsyncCustomAutoConfiguration,\ org.springframework.cloud.sleuth.instrument.async.AsyncDefaultAutoConfiguration,\ org.springframework.cloud.sleuth.instrument.scheduling.TraceSchedulingAutoConfiguration,\ org.springframework.cloud.sleuth.instrument.web.TraceWebAutoConfiguration,\ -org.springframework.cloud.sleuth.instrument.web.client.TraceWebClientAutoConfiguration +org.springframework.cloud.sleuth.instrument.web.client.TraceWebClientAutoConfiguration,\ +org.springframework.cloud.sleuth.instrument.web.client.TraceFeignClientAutoConfiguration 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 new file mode 100644 index 000000000..2dde3a41a --- /dev/null +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/FeignTraceTest.java @@ -0,0 +1,115 @@ +package org.springframework.cloud.sleuth.instrument.web.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.cloud.sleuth.Trace.PARENT_ID_NAME; +import static org.springframework.cloud.sleuth.Trace.SPAN_ID_NAME; +import static org.springframework.cloud.sleuth.Trace.TRACE_ID_NAME; + +import java.util.Arrays; +import java.util.List; + +import com.netflix.loadbalancer.BaseLoadBalancer; +import com.netflix.loadbalancer.ILoadBalancer; +import com.netflix.loadbalancer.Server; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +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.netflix.ribbon.RibbonClient; +import org.springframework.cloud.sleuth.MilliSpan; +import org.springframework.cloud.sleuth.TraceContextHolder; +import org.springframework.cloud.sleuth.instrument.web.TraceWebAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = { TraceWebAutoConfiguration.class, + FeignTraceTest.TestConfiguration.class }) +@WebIntegrationTest(value = "spring.application.name=fooservice", randomPort = true) +public class FeignTraceTest { + + @Autowired + TestFeignInterface testFeignInterface; + + @Test + public void shouldAttachTraceIdWhenUsingFeignClient() { + // given + String currentTraceId = "currentTraceId"; + String currentSpanId = "currentSpanId"; + String currentParentId = "currentParentId"; + TraceContextHolder.setCurrentSpan(MilliSpan.builder().traceId(currentTraceId) + .spanId(currentSpanId).parent(currentParentId).build()); + + // when + ResponseEntity response = testFeignInterface.getHealth(); + + // then + assertThat(getHeader(response, TRACE_ID_NAME)).isEqualTo(currentTraceId); + assertThat(getHeader(response, SPAN_ID_NAME)).isEqualTo(currentSpanId); + assertThat(getHeader(response, PARENT_ID_NAME)).isEqualTo(currentParentId); + } + + private String getHeader(ResponseEntity response, String name) { + List headers = response.getHeaders().get(name); + assertThat(headers).asList().isNotEmpty(); + return headers.get(0); + } + + @FeignClient("fooservice") + public interface TestFeignInterface { + @RequestMapping(method = RequestMethod.GET, value = "/traceid") + ResponseEntity getHealth(); + } + + @Configuration + @EnableAutoConfiguration + @EnableFeignClients + @RibbonClient(name = "fooservice", configuration = SimpleRibbonClientConfiguration.class) + public static class TestConfiguration { + + @Bean + FooController fooController() { + return new FooController(); + } + } + + @RestController + public static class FooController { + + @RequestMapping(value = "/traceid", method = RequestMethod.GET) + public String foo(@RequestHeader(TRACE_ID_NAME) String traceId, + @RequestHeader(SPAN_ID_NAME) String spanId, + @RequestHeader(PARENT_ID_NAME) String parentId) { + assertThat(traceId).isNotEmpty(); + assertThat(parentId).isNotEmpty(); + assertThat(spanId).isNotEmpty(); + return traceId; + } + } + + @Configuration + public static class SimpleRibbonClientConfiguration { + + @Value("${local.server.port}") + private int port = 0; + + @Bean + public ILoadBalancer ribbonLoadBalancer() { + BaseLoadBalancer balancer = new BaseLoadBalancer(); + balancer.setServersList(Arrays.asList(new Server("localhost", port))); + return balancer; + } + + } +}