From d7b7e812356335b04a27e8d4867bdcdacc6f78c9 Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Tue, 21 Jan 2020 19:57:23 +0100 Subject: [PATCH] Gh 1528 instrument feign sc loadbalancer client (#1531) * Add optional SC LoadBalancer dependency. Create TraceFeignBlockingLoadBalancerClient. * Instrument Spring Cloud LoadBalancer in TraceFeignObjectWrapper. * Do not use class in field in order to avoid NoClassDefFound. fixes #1528 --- spring-cloud-sleuth-core/pom.xml | 5 + .../TraceFeignBlockingLoadBalancerClient.java | 125 ++++++++++++++++++ .../client/feign/TraceFeignObjectWrapper.java | 113 ++++++++++++---- .../feign/TracingFeignObjectWrapperTests.java | 55 ++++++-- ...dDelegateLoadBalancerFeignClientTests.java | 6 +- 5 files changed, 265 insertions(+), 39 deletions(-) create mode 100644 spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignBlockingLoadBalancerClient.java diff --git a/spring-cloud-sleuth-core/pom.xml b/spring-cloud-sleuth-core/pom.xml index e5dabfe58..4189ba781 100644 --- a/spring-cloud-sleuth-core/pom.xml +++ b/spring-cloud-sleuth-core/pom.xml @@ -156,6 +156,11 @@ spring-cloud-context true + + org.springframework.cloud + spring-cloud-starter-loadbalancer + true + com.netflix.hystrix hystrix-core diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignBlockingLoadBalancerClient.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignBlockingLoadBalancerClient.java new file mode 100644 index 000000000..29ad789c2 --- /dev/null +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignBlockingLoadBalancerClient.java @@ -0,0 +1,125 @@ +/* + * Copyright 2013-2019 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 + * + * https://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.util.HashMap; + +import brave.Span; +import brave.Tracer; +import brave.http.HttpTracing; +import com.netflix.client.ClientException; +import feign.Client; +import feign.Request; +import feign.Response; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient; +import org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient; + +/** + * A trace representation of {@link FeignBlockingLoadBalancerClient}. Needed due to casts + * in {@link org.springframework.cloud.openfeign.FeignClientFactoryBean}. Based on + * {@link TraceLoadBalancerFeignClient}. + * + * @author Olga Maciaszek-Sharma + * @since 2.2.0 + * @see TraceLoadBalancerFeignClient + * @see FeignBlockingLoadBalancerClient + */ +public class TraceFeignBlockingLoadBalancerClient + extends FeignBlockingLoadBalancerClient { + + private static final Log LOG = LogFactory + .getLog(TraceFeignBlockingLoadBalancerClient.class); + + private final BeanFactory beanFactory; + + Tracer tracer; + + HttpTracing httpTracing; + + TracingFeignClient tracingFeignClient; + + TraceFeignBlockingLoadBalancerClient(Client delegate, + BlockingLoadBalancerClient loadBalancerClient, BeanFactory beanFactory) { + super(delegate, loadBalancerClient); + this.beanFactory = beanFactory; + } + + @Override + public Response execute(Request request, Request.Options options) throws IOException { + if (LOG.isDebugEnabled()) { + LOG.debug("Before send"); + } + Response response = null; + Span fallbackSpan = tracer().nextSpan().start(); + try { + response = super.execute(request, options); + if (LOG.isDebugEnabled()) { + LOG.debug("After receive"); + } + return response; + } + catch (Exception e) { + if (LOG.isDebugEnabled()) { + LOG.debug("Exception thrown", e); + } + if (e instanceof IOException || e.getCause() != null + && e.getCause() instanceof ClientException + && ((ClientException) e.getCause()) + .getErrorType() == ClientException.ErrorType.GENERAL) { + if (LOG.isDebugEnabled()) { + LOG.debug( + "General exception was thrown, so most likely the traced client wasn't called. Falling back to a manual span"); + } + fallbackSpan = tracingFeignClient().handleSend( + new HashMap<>(request.headers()), request, fallbackSpan); + tracingFeignClient().handleReceive(fallbackSpan, response, e); + } + throw e; + } + finally { + fallbackSpan.abandon(); + } + } + + private Tracer tracer() { + if (tracer == null) { + tracer = beanFactory.getBean(Tracer.class); + } + return tracer; + } + + private HttpTracing httpTracing() { + if (httpTracing == null) { + httpTracing = beanFactory.getBean(HttpTracing.class); + } + return httpTracing; + } + + private TracingFeignClient tracingFeignClient() { + if (tracingFeignClient == null) { + tracingFeignClient = (TracingFeignClient) TracingFeignClient + .create(httpTracing(), getDelegate()); + } + return tracingFeignClient; + } + +} diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignObjectWrapper.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignObjectWrapper.java index 637a2f74c..a5c3c15f1 100644 --- a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignObjectWrapper.java +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignObjectWrapper.java @@ -24,7 +24,9 @@ import org.apache.commons.logging.LogFactory; import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.BeanFactory; +import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient; import org.springframework.cloud.netflix.ribbon.SpringClientFactory; +import org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient; import org.springframework.cloud.openfeign.ribbon.CachingSpringLoadBalancerFactory; import org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient; import org.springframework.util.ClassUtils; @@ -33,14 +35,21 @@ import org.springframework.util.ClassUtils; * Class that wraps Feign related classes into their Trace representative. * * @author Marcin Grzejszczak + * @author Olga Maciaszek-Sharma * @since 1.0.1 */ final class TraceFeignObjectWrapper { - private static final Log log = LogFactory.getLog(TraceFeignObjectWrapper.class); + public static final String EXCEPTION_WARNING = "Exception occurred while trying to access the delegate's field. Will fallback to default instrumentation mechanism, which means that the delegate might not be instrumented"; private static final boolean ribbonPresent; + private static final Log log = LogFactory.getLog(TraceFeignObjectWrapper.class); + + private static final boolean loadBalancerPresent; + + private static final String DELEGATE = "delegate"; + static { ribbonPresent = ClassUtils.isPresent( "org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient", @@ -48,6 +57,12 @@ final class TraceFeignObjectWrapper { && ClassUtils.isPresent( "org.springframework.cloud.netflix.ribbon.SpringClientFactory", null); + loadBalancerPresent = ClassUtils.isPresent( + "org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient", + null) + && ClassUtils.isPresent( + "org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient", + null); } private final BeanFactory beanFactory; @@ -56,6 +71,8 @@ final class TraceFeignObjectWrapper { private Object springClientFactory; + private Object loadBalancerClient; + TraceFeignObjectWrapper(BeanFactory beanFactory) { this.beanFactory = beanFactory; } @@ -64,34 +81,16 @@ final class TraceFeignObjectWrapper { if (bean instanceof Client && !(bean instanceof TracingFeignClient)) { if (ribbonPresent && bean instanceof LoadBalancerFeignClient && !(bean instanceof TraceLoadBalancerFeignClient)) { - if (AopUtils.getTargetClass(bean).equals(LoadBalancerFeignClient.class)) { - LoadBalancerFeignClient client = ((LoadBalancerFeignClient) bean); - return new TraceLoadBalancerFeignClient( - (Client) new TraceFeignObjectWrapper(this.beanFactory) - .wrap(client.getDelegate()), - factory(), (SpringClientFactory) clientFactory(), - this.beanFactory); - } - else { - LoadBalancerFeignClient client = ((LoadBalancerFeignClient) bean); - try { - Field delegate = LoadBalancerFeignClient.class - .getDeclaredField("delegate"); - delegate.setAccessible(true); - delegate.set(client, new TraceFeignObjectWrapper(this.beanFactory) - .wrap(client.getDelegate())); - } - catch (NoSuchFieldException | IllegalArgumentException - | IllegalAccessException | SecurityException e) { - log.warn( - "Exception occurred while trying to access the delegate's field. Will fallback to default instrumentation mechanism, which means that the delegate might not be instrumented", - e); - } - return new TraceLoadBalancerFeignClient(client, factory(), - (SpringClientFactory) clientFactory(), this.beanFactory); - } + return instrumentedFeignRibbonClient(bean); } - else if (ribbonPresent && bean instanceof TraceLoadBalancerFeignClient) { + if (ribbonPresent && bean instanceof TraceLoadBalancerFeignClient) { + return bean; + } + if (loadBalancerPresent && bean instanceof FeignBlockingLoadBalancerClient + && !(bean instanceof TraceFeignBlockingLoadBalancerClient)) { + return instrumentedFeignLoadBalancerClient(bean); + } + if (ribbonPresent && bean instanceof TraceFeignBlockingLoadBalancerClient) { return bean; } return new LazyTracingFeignClient(this.beanFactory, (Client) bean); @@ -99,6 +98,57 @@ final class TraceFeignObjectWrapper { return bean; } + private Object instrumentedFeignLoadBalancerClient(Object bean) { + if (AopUtils.getTargetClass(bean).equals(FeignBlockingLoadBalancerClient.class)) { + FeignBlockingLoadBalancerClient client = ((FeignBlockingLoadBalancerClient) bean); + return new TraceFeignBlockingLoadBalancerClient( + (Client) new TraceFeignObjectWrapper(this.beanFactory) + .wrap(client.getDelegate()), + (BlockingLoadBalancerClient) loadBalancerClient(), this.beanFactory); + } + else { + FeignBlockingLoadBalancerClient client = ((FeignBlockingLoadBalancerClient) bean); + try { + Field delegate = FeignBlockingLoadBalancerClient.class + .getDeclaredField(DELEGATE); + delegate.setAccessible(true); + delegate.set(client, new TraceFeignObjectWrapper(this.beanFactory) + .wrap(client.getDelegate())); + } + catch (NoSuchFieldException | IllegalArgumentException + | IllegalAccessException | SecurityException e) { + log.warn(EXCEPTION_WARNING, e); + } + return new TraceFeignBlockingLoadBalancerClient(client, + (BlockingLoadBalancerClient) loadBalancerClient(), this.beanFactory); + } + } + + private Object instrumentedFeignRibbonClient(Object bean) { + if (AopUtils.getTargetClass(bean).equals(LoadBalancerFeignClient.class)) { + LoadBalancerFeignClient client = ((LoadBalancerFeignClient) bean); + return new TraceLoadBalancerFeignClient( + (Client) new TraceFeignObjectWrapper(this.beanFactory) + .wrap(client.getDelegate()), + factory(), (SpringClientFactory) clientFactory(), this.beanFactory); + } + else { + LoadBalancerFeignClient client = ((LoadBalancerFeignClient) bean); + try { + Field delegate = LoadBalancerFeignClient.class.getDeclaredField(DELEGATE); + delegate.setAccessible(true); + delegate.set(client, new TraceFeignObjectWrapper(this.beanFactory) + .wrap(client.getDelegate())); + } + catch (NoSuchFieldException | IllegalArgumentException + | IllegalAccessException | SecurityException e) { + log.warn(EXCEPTION_WARNING, e); + } + return new TraceLoadBalancerFeignClient(client, factory(), + (SpringClientFactory) clientFactory(), this.beanFactory); + } + } + private CachingSpringLoadBalancerFactory factory() { if (this.cachingSpringLoadBalancerFactory == null) { this.cachingSpringLoadBalancerFactory = this.beanFactory @@ -115,4 +165,11 @@ final class TraceFeignObjectWrapper { return this.springClientFactory; } + private Object loadBalancerClient() { + if (loadBalancerClient == null) { + loadBalancerClient = beanFactory.getBean(BlockingLoadBalancerClient.class); + } + return loadBalancerClient; + } + } diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TracingFeignObjectWrapperTests.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TracingFeignObjectWrapperTests.java index a90aee096..de2b9a50e 100644 --- a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TracingFeignObjectWrapperTests.java +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/feign/TracingFeignObjectWrapperTests.java @@ -16,8 +16,6 @@ package org.springframework.cloud.sleuth.instrument.web.client.feign; -import brave.Tracing; -import brave.http.HttpTracing; import feign.Client; import org.junit.Test; import org.junit.runner.RunWith; @@ -26,9 +24,13 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.beans.factory.BeanFactory; +import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient; +import org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.BDDAssertions.then; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * @author Marcin Grzejszczak @@ -36,10 +38,6 @@ import static org.mockito.Mockito.mock; @RunWith(MockitoJUnitRunner.class) public class TracingFeignObjectWrapperTests { - Tracing tracing = Tracing.newBuilder().build(); - - HttpTracing httpTracing = HttpTracing.create(this.tracing); - @Mock BeanFactory beanFactory; @@ -47,16 +45,57 @@ public class TracingFeignObjectWrapperTests { TraceFeignObjectWrapper traceFeignObjectWrapper; @Test - public void should_wrap_a_client_into_lazy_trace_client() throws Exception { + public void should_wrap_a_client_into_lazy_trace_client() { then(this.traceFeignObjectWrapper.wrap(mock(Client.class))) .isExactlyInstanceOf(LazyTracingFeignClient.class); } @Test - public void should_not_wrap_a_bean_that_is_not_feign_related() throws Exception { + public void should_not_wrap_a_bean_that_is_not_feign_related() { String notFeignRelatedObject = "object"; then(this.traceFeignObjectWrapper.wrap(notFeignRelatedObject)) .isSameAs(notFeignRelatedObject); } + // gh-1528 + @Test + public void should_wrap_feign_loadbalancer_client() { + Client delegate = mock(Client.class); + BlockingLoadBalancerClient loadBalancerClient = mock( + BlockingLoadBalancerClient.class); + when(beanFactory.getBean(BlockingLoadBalancerClient.class)) + .thenReturn(loadBalancerClient); + + Object wrapped = traceFeignObjectWrapper + .wrap(new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient)); + + assertThat(wrapped).isInstanceOf(TraceFeignBlockingLoadBalancerClient.class); + } + + // gh-1528, gh-1125 + @Test + public void should_wrap_subclass_of_feign_loadbalancer_client() { + Client delegate = mock(Client.class); + BlockingLoadBalancerClient loadBalancerClient = mock( + BlockingLoadBalancerClient.class); + when(beanFactory.getBean(BlockingLoadBalancerClient.class)) + .thenReturn(loadBalancerClient); + + Object wrapped = traceFeignObjectWrapper.wrap( + new TestFeignBlockingLoadBalancerClient(delegate, loadBalancerClient)); + + assertThat(wrapped).isInstanceOf(TraceFeignBlockingLoadBalancerClient.class); + + } + + static class TestFeignBlockingLoadBalancerClient + extends FeignBlockingLoadBalancerClient { + + TestFeignBlockingLoadBalancerClient(Client delegate, + BlockingLoadBalancerClient loadBalancerClient) { + super(delegate, loadBalancerClient); + } + + } + } diff --git a/tests/spring-cloud-sleuth-instrumentation-feign-tests/src/test/java/org/springframework/cloud/sleuth/instrument/feign/issues/issue1125delegates/ManuallyCreatedDelegateLoadBalancerFeignClientTests.java b/tests/spring-cloud-sleuth-instrumentation-feign-tests/src/test/java/org/springframework/cloud/sleuth/instrument/feign/issues/issue1125delegates/ManuallyCreatedDelegateLoadBalancerFeignClientTests.java index 8fc639b98..e2a0df65e 100644 --- a/tests/spring-cloud-sleuth-instrumentation-feign-tests/src/test/java/org/springframework/cloud/sleuth/instrument/feign/issues/issue1125delegates/ManuallyCreatedDelegateLoadBalancerFeignClientTests.java +++ b/tests/spring-cloud-sleuth-instrumentation-feign-tests/src/test/java/org/springframework/cloud/sleuth/instrument/feign/issues/issue1125delegates/ManuallyCreatedDelegateLoadBalancerFeignClientTests.java @@ -61,7 +61,7 @@ import static org.assertj.core.api.BDDAssertions.then; @RunWith(SpringRunner.class) @SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.NONE, - properties = {"feign.hystrix.enabled=false"}) + properties = { "feign.hystrix.enabled=false" }) @DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD) public class ManuallyCreatedDelegateLoadBalancerFeignClientTests { @@ -138,8 +138,8 @@ class Application { public MyNameRemote myNameRemote(Client client, Decoder decoder, Encoder encoder, Contract contract) { return Feign.builder().client(client).encoder(encoder).decoder(decoder) - .contract(contract).target(new HardCodedTarget<>( - MyNameRemote.class, "foo", "https://non.existing.url")); + .contract(contract).target(new HardCodedTarget<>(MyNameRemote.class, + "foo", "https://non.existing.url")); } @Bean