From 6e8f86ed352d167276cb957b17d757268832f516 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Tue, 20 Apr 2021 10:03:36 +0000 Subject: [PATCH] Adds support for SC CircuitBreaker Reactive (#1914) * Adds support for SC CircuitBreaker Reactive * Changes following the review * Lazilly initializes the TraceFunction * Updated version fixes gh-1910 --- docs/src/main/asciidoc/integrations.adoc | 2 +- .../TraceCircuitBreakerAutoConfiguration.java | 13 +- .../TraceCircuitBreakerFactoryAspect.java | 7 +- .../circuitbreaker/TraceFunction.java | 1 + .../TraceReactiveCircuitBreaker.java | 138 +++++++++++++++++ ...ceReactiveCircuitBreakerFactoryAspect.java | 45 ++++++ .../circuitbreaker/TraceSupplier.java | 1 + tests/brave/pom.xml | 1 + .../pom.xml | 89 +++++++++++ ...eactiveCircuitBreakerIntegrationTests.java | 60 +++++++ .../ReactiveCircuitBreakerTests.java | 43 ++++++ .../src/test/resources/application.yml | 5 + tests/common/pom.xml | 5 + ...eactiveCircuitBreakerIntegrationTests.java | 146 ++++++++++++++++++ .../ReactiveCircuitBreakerTests.java | 94 +++++++++++ 15 files changed, 641 insertions(+), 9 deletions(-) create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceReactiveCircuitBreaker.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceReactiveCircuitBreakerFactoryAspect.java create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests/pom.xml create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/circuitbreaker/ReactiveCircuitBreakerIntegrationTests.java create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/circuitbreaker/ReactiveCircuitBreakerTests.java create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests/src/test/resources/application.yml create mode 100644 tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/ReactiveCircuitBreakerIntegrationTests.java create mode 100644 tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/ReactiveCircuitBreakerTests.java diff --git a/docs/src/main/asciidoc/integrations.adoc b/docs/src/main/asciidoc/integrations.adoc index b56360cb0..6da99eafc 100644 --- a/docs/src/main/asciidoc/integrations.adoc +++ b/docs/src/main/asciidoc/integrations.adoc @@ -565,5 +565,5 @@ IMPORTANT: The suggested approach to reactive programming and Sleuth is to use t This feature is available for all tracer implementations. -If you have Spring Cloud CircuitBreaker on the classpath, we will wrap the passed command `Supplier` and the fallback `Function` in its trace representations. +If you have Spring Cloud CircuitBreaker on the classpath, we will wrap the passed command `Supplier` and the fallback `Function` in its trace representations. We will also instrument the reactive implementation of the CircuitBreaker. In order to disable this instrumentation set `spring.sleuth.circuitbreaker.enabled` to `false`. \ No newline at end of file diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/circuitbreaker/TraceCircuitBreakerAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/circuitbreaker/TraceCircuitBreakerAutoConfiguration.java index e80b7d20c..f724943c5 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/circuitbreaker/TraceCircuitBreakerAutoConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/circuitbreaker/TraceCircuitBreakerAutoConfiguration.java @@ -21,10 +21,11 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cloud.client.circuitbreaker.CircuitBreaker; +import org.springframework.cloud.sleuth.CurrentTraceContext; import org.springframework.cloud.sleuth.Tracer; import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; import org.springframework.cloud.sleuth.instrument.circuitbreaker.TraceCircuitBreakerFactoryAspect; +import org.springframework.cloud.sleuth.instrument.circuitbreaker.TraceReactiveCircuitBreakerFactoryAspect; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -36,7 +37,6 @@ import org.springframework.context.annotation.Configuration; * @since 2.2.1 */ @Configuration(proxyBeanMethods = false) -@ConditionalOnClass(CircuitBreaker.class) @ConditionalOnBean(Tracer.class) @ConditionalOnProperty(value = "spring.sleuth.circuitbreaker.enabled", matchIfMissing = true) @EnableConfigurationProperties(SleuthCircuitBreakerProperties.class) @@ -44,8 +44,17 @@ import org.springframework.context.annotation.Configuration; public class TraceCircuitBreakerAutoConfiguration { @Bean + @ConditionalOnClass(name = "org.springframework.cloud.client.circuitbreaker.CircuitBreaker") TraceCircuitBreakerFactoryAspect traceCircuitBreakerFactoryAspect(Tracer tracer) { return new TraceCircuitBreakerFactoryAspect(tracer); } + @Bean + @ConditionalOnClass(name = { "reactor.core.publisher.Mono", + "org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker" }) + TraceReactiveCircuitBreakerFactoryAspect traceReactiveCircuitBreakerFactoryAspect(Tracer tracer, + CurrentTraceContext currentTraceContext) { + return new TraceReactiveCircuitBreakerFactoryAspect(tracer, currentTraceContext); + } + } diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceCircuitBreakerFactoryAspect.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceCircuitBreakerFactoryAspect.java index d5f3fb3a5..32505fbc7 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceCircuitBreakerFactoryAspect.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceCircuitBreakerFactoryAspect.java @@ -19,7 +19,6 @@ package org.springframework.cloud.sleuth.instrument.circuitbreaker; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.annotation.Pointcut; import org.springframework.cloud.client.circuitbreaker.CircuitBreaker; import org.springframework.cloud.sleuth.Tracer; @@ -39,11 +38,7 @@ public class TraceCircuitBreakerFactoryAspect { this.tracer = tracer; } - @Pointcut("execution(public * org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory.create(..))") - private void anyCircuitBreakerFactoryCreate() { - } // NOSONAR - - @Around("anyCircuitBreakerFactoryCreate()") + @Around("execution(public * org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory.create(..))") public Object wrapFactory(ProceedingJoinPoint pjp) throws Throwable { CircuitBreaker circuitBreaker = (CircuitBreaker) pjp.proceed(); return new TraceCircuitBreaker(circuitBreaker, this.tracer); diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceFunction.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceFunction.java index 361400b51..d5f8f6df8 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceFunction.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceFunction.java @@ -44,6 +44,7 @@ class TraceFunction implements Function { @Override public T apply(Throwable throwable) { + // TODO: This name needs to be better String name = this.delegate.getClass().getSimpleName(); Span span = this.span.get().name(name); Throwable tr = null; diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceReactiveCircuitBreaker.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceReactiveCircuitBreaker.java new file mode 100644 index 000000000..b283db6e8 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceReactiveCircuitBreaker.java @@ -0,0 +1,138 @@ +/* + * Copyright 2018-2021 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.circuitbreaker; + +import java.util.function.Function; +import java.util.function.Supplier; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.context.Context; + +import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker; +import org.springframework.cloud.sleuth.CurrentTraceContext; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.TraceContext; +import org.springframework.cloud.sleuth.Tracer; + +class TraceReactiveCircuitBreaker implements ReactiveCircuitBreaker { + + private static final Log log = LogFactory.getLog(TraceReactiveCircuitBreaker.class); + + private final ReactiveCircuitBreaker delegate; + + private final Tracer tracer; + + private final CurrentTraceContext currentTraceContext; + + TraceReactiveCircuitBreaker(ReactiveCircuitBreaker delegate, Tracer tracer, + CurrentTraceContext currentTraceContext) { + this.delegate = delegate; + this.tracer = tracer; + this.currentTraceContext = currentTraceContext; + } + + @Override + public Mono run(Mono toRun) { + return runAndTraceMono(() -> this.delegate.run(toRun)); + } + + @Override + public Mono run(Mono toRun, Function> fallback) { + return runAndTraceMono( + () -> this.delegate.run(toRun, fallback != null ? new TraceFunction<>(this.tracer, fallback) : null)); + } + + @Override + public Flux run(Flux toRun) { + return runAndTraceFlux(() -> this.delegate.run(toRun)); + } + + @Override + public Flux run(Flux toRun, Function> fallback) { + return runAndTraceFlux( + () -> this.delegate.run(toRun, fallback != null ? new TraceFunction<>(this.tracer, fallback) : null)); + } + + private Mono runAndTraceMono(Supplier> mono) { + return Mono.deferContextual(contextView -> { + Span span = contextView.get(Span.class); + Tracer.SpanInScope scope = contextView.get(Tracer.SpanInScope.class); + return mono.get().doOnError(span::error).doFinally(signalType -> { + span.end(); + scope.close(); + }); + }).contextWrite(this::enhanceContext); + } + + private Flux runAndTraceFlux(Supplier> flux) { + return Flux.deferContextual(contextView -> { + Span span = contextView.get(Span.class); + Tracer.SpanInScope scope = contextView.get(Tracer.SpanInScope.class); + return flux.get().doOnError(span::error).doFinally(signalType -> { + span.end(); + scope.close(); + }); + }).contextWrite(this::enhanceContext); + } + + private Span spanFromContext(reactor.util.context.Context context) { + TraceContext traceContext = context.getOrDefault(TraceContext.class, null); + Span span = null; + if (traceContext == null) { + span = context.getOrDefault(Span.class, null); + } + if (traceContext == null && span == null) { + span = this.tracer.nextSpan(); + if (log.isDebugEnabled()) { + log.debug("There was no previous span in reactor context, created a new one [" + span + "]"); + } + } + else if (traceContext != null) { + // there was a previous span - we create a child one + try (CurrentTraceContext.Scope scope = this.currentTraceContext.maybeScope(traceContext)) { + if (log.isDebugEnabled()) { + log.debug("Found a trace context in reactor context [" + traceContext + "]"); + } + span = this.tracer.nextSpan(); + if (log.isDebugEnabled()) { + log.debug("Created a child span [" + span + "]"); + } + } + } + else { + if (log.isDebugEnabled()) { + log.debug("Found a span in reactor context [" + span + "]"); + } + span = this.tracer.nextSpan(span); + if (log.isDebugEnabled()) { + log.debug("Created a child span [" + span + "]"); + } + } + // TODO: Better name? + return span.name("function"); + } + + private Context enhanceContext(Context context) { + Span span = spanFromContext(context); + return context.put(Span.class, span).put(TraceContext.class, span.context()).put(Tracer.SpanInScope.class, + this.tracer.withSpan(span)); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceReactiveCircuitBreakerFactoryAspect.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceReactiveCircuitBreakerFactoryAspect.java new file mode 100644 index 000000000..ea2d99f1e --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceReactiveCircuitBreakerFactoryAspect.java @@ -0,0 +1,45 @@ +/* + * Copyright 2018-2021 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.circuitbreaker; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; + +import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker; +import org.springframework.cloud.sleuth.CurrentTraceContext; +import org.springframework.cloud.sleuth.Tracer; + +@Aspect +public class TraceReactiveCircuitBreakerFactoryAspect { + + private final Tracer tracer; + + private final CurrentTraceContext currentTraceContext; + + public TraceReactiveCircuitBreakerFactoryAspect(Tracer tracer, CurrentTraceContext currentTraceContext) { + this.tracer = tracer; + this.currentTraceContext = currentTraceContext; + } + + @Around("execution(public * org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory.create(..))") + public Object wrapFactory(ProceedingJoinPoint pjp) throws Throwable { + ReactiveCircuitBreaker circuitBreaker = (ReactiveCircuitBreaker) pjp.proceed(); + return new TraceReactiveCircuitBreaker(circuitBreaker, this.tracer, this.currentTraceContext); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceSupplier.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceSupplier.java index 3e6ffa2d5..503229151 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceSupplier.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceSupplier.java @@ -44,6 +44,7 @@ class TraceSupplier implements Supplier { @Override public T get() { + // TODO: This name needs to be better String name = this.delegate.getClass().getSimpleName(); Span span = this.span.get().name(name); Throwable tr = null; diff --git a/tests/brave/pom.xml b/tests/brave/pom.xml index eb3531c41..09478bd12 100644 --- a/tests/brave/pom.xml +++ b/tests/brave/pom.xml @@ -39,6 +39,7 @@ spring-cloud-sleuth-instrumentation-async-tests spring-cloud-sleuth-instrumentation-baggage-tests spring-cloud-sleuth-instrumentation-circuitbreaker-tests + spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests spring-cloud-sleuth-instrumentation-feign-tests spring-cloud-sleuth-instrumentation-gateway-tests spring-cloud-sleuth-instrumentation-grpc-tests diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests/pom.xml new file mode 100644 index 000000000..43d9e1ab6 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests/pom.xml @@ -0,0 +1,89 @@ + + + + + 4.0.0 + + spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests + jar + Spring Cloud Sleuth Brave Circuitbreaker Reactive Instrumentation Tests + Spring Cloud Sleuth Brave Circuitbreaker Reactive Instrumentation Tests + + + org.springframework.cloud + spring-cloud-sleuth-tests-brave + 3.1.0-SNAPSHOT + .. + + + + true + + + + + + + maven-deploy-plugin + + true + + + + + + + + org.springframework.cloud + spring-cloud-sleuth-tests-common + ${project.version} + + + io.projectreactor + reactor-core + + + org.springframework.boot + spring-boot-starter-aop + + + org.springframework.cloud + spring-cloud-starter-circuitbreaker-reactor-resilience4j + + + org.springframework.cloud + spring-cloud-starter-sleuth + + + org.springframework.boot + spring-boot-starter-test + + + io.zipkin.brave + brave-tests + + + org.awaitility + awaitility + + + + diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/circuitbreaker/ReactiveCircuitBreakerIntegrationTests.java b/tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/circuitbreaker/ReactiveCircuitBreakerIntegrationTests.java new file mode 100644 index 000000000..df5d1a819 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/circuitbreaker/ReactiveCircuitBreakerIntegrationTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2013-2021 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.brave.instrument.circuitbreaker; + +import brave.sampler.Sampler; +import org.assertj.core.api.BDDAssertions; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.sleuth.brave.BraveTestSpanHandler; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; + +@SpringBootTest +@ContextConfiguration(classes = ReactiveCircuitBreakerIntegrationTests.Config.class) +public class ReactiveCircuitBreakerIntegrationTests + extends org.springframework.cloud.sleuth.instrument.circuitbreaker.ReactiveCircuitBreakerIntegrationTests { + + @Override + public void assertException(FinishedSpan finishedSpan) { + BDDAssertions.then(finishedSpan.getTags().get("error")).contains("boom"); + } + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + TestSpanHandler testSpanHandlerSupplier(brave.test.TestSpanHandler testSpanHandler) { + return new BraveTestSpanHandler(testSpanHandler); + } + + @Bean + Sampler alwaysSampler() { + return Sampler.ALWAYS_SAMPLE; + } + + @Bean + brave.test.TestSpanHandler braveTestSpanHandler() { + return new brave.test.TestSpanHandler(); + } + + } + +} diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/circuitbreaker/ReactiveCircuitBreakerTests.java b/tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/circuitbreaker/ReactiveCircuitBreakerTests.java new file mode 100644 index 000000000..7ac516cce --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/circuitbreaker/ReactiveCircuitBreakerTests.java @@ -0,0 +1,43 @@ +/* + * Copyright 2013-2021 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.brave.instrument.circuitbreaker; + +import org.assertj.core.api.BDDAssertions; + +import org.springframework.cloud.sleuth.brave.BraveTestTracing; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.test.TestTracingAware; + +public class ReactiveCircuitBreakerTests + extends org.springframework.cloud.sleuth.instrument.circuitbreaker.ReactiveCircuitBreakerTests { + + BraveTestTracing testTracing; + + @Override + public TestTracingAware tracerTest() { + if (this.testTracing == null) { + this.testTracing = new BraveTestTracing(); + } + return this.testTracing; + } + + @Override + public void additionalAssertions(FinishedSpan finishedSpan) { + BDDAssertions.then(finishedSpan.getTags().get("error")).contains("boom2"); + } + +} diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests/src/test/resources/application.yml b/tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests/src/test/resources/application.yml new file mode 100644 index 000000000..f5756fce9 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests/src/test/resources/application.yml @@ -0,0 +1,5 @@ +logging.level.org.springframework.cloud: DEBUG +logging.level.com.netflix.discovery.InstanceInfoReplicator: ERROR +logging.level.org.springframework.cloud.sleuth.brave.instrument.web.client.feign: TRACE + +spring.autoconfigure.exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration, org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration, org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration, org.springframework.cloud.gateway.config.GatewayAutoConfiguration, org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration, org.springframework.cloud.gateway.config.GatewayMetricsAutoConfiguration diff --git a/tests/common/pom.xml b/tests/common/pom.xml index 1b79278cb..628288865 100644 --- a/tests/common/pom.xml +++ b/tests/common/pom.xml @@ -89,6 +89,11 @@ spring-cloud-starter-circuitbreaker-resilience4j true + + org.springframework.cloud + spring-cloud-starter-circuitbreaker-reactor-resilience4j + true + org.springframework.boot spring-boot-starter-quartz diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/ReactiveCircuitBreakerIntegrationTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/ReactiveCircuitBreakerIntegrationTests.java new file mode 100644 index 000000000..3ee120771 --- /dev/null +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/ReactiveCircuitBreakerIntegrationTests.java @@ -0,0 +1,146 @@ +/* + * Copyright 2013-2021 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.circuitbreaker; + +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.cloud.circuitbreaker.resilience4j.ReactiveResilience4JCircuitBreakerFactory; +import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory; +import org.springframework.cloud.sleuth.ScopedSpan; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; + +@ContextConfiguration(classes = ReactiveCircuitBreakerIntegrationTests.TestConfig.class) +public abstract class ReactiveCircuitBreakerIntegrationTests { + + @Autowired + TestSpanHandler spans; + + @Autowired + Tracer tracer; + + @Autowired + ReactiveCircuitBreakerFactory factory; + + @Autowired + CircuitService circuitService; + + @BeforeEach + public void setup() { + this.spans.clear(); + } + + @Test + public void should_pass_tracing_information_when_using_circuit_breaker() { + // given + Tracer tracer = this.tracer; + ScopedSpan scopedSpan = null; + try { + scopedSpan = tracer.startScopedSpan("start"); + // when + Span span = this.factory.create("name").run(Mono.defer(() -> Mono.just(tracer.currentSpan()))).block(); + + BDDAssertions.then(span).isNotNull(); + BDDAssertions.then(scopedSpan.context().traceId()).isEqualTo(span.context().traceId()); + } + finally { + scopedSpan.end(); + } + } + + @Test + public void should_pass_tracing_information_when_using_circuit_breaker_with_fallback() { + // when + BDDAssertions.then(this.circuitService.call().block()).isEqualTo("fallback"); + + BDDAssertions.then(this.spans).hasSize(2); + String traceId = this.circuitService.firstSpan.context().traceId(); + BDDAssertions.then(this.circuitService.secondSpan.context().traceId()).isEqualTo(traceId); + + FinishedSpan finishedSpan = this.spans.get(0); + BDDAssertions.then(finishedSpan.getName()).contains("CircuitBreakerIntegrationTests"); + + finishedSpan = this.spans.get(1); + BDDAssertions.then(finishedSpan.getName()).contains("function"); + } + + public void assertException(FinishedSpan finishedSpan) { + throw new UnsupportedOperationException("Implement this assertion"); + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + public static class TestConfig { + + @Bean + ReactiveResilience4JCircuitBreakerFactory reactiveResilience4JCircuitBreakerFactory() { + return new ReactiveResilience4JCircuitBreakerFactory(); + } + + @Bean + CircuitService circuitService(ReactiveCircuitBreakerFactory reactiveCircuitBreakerFactory, Tracer tracer) { + return new CircuitService(reactiveCircuitBreakerFactory, tracer); + } + + } + + static class CircuitService { + + private static final Logger log = LoggerFactory.getLogger(CircuitService.class); + + private final ReactiveCircuitBreakerFactory factory; + + private final Tracer tracer; + + Span firstSpan; + + Span secondSpan; + + CircuitService(ReactiveCircuitBreakerFactory factory, Tracer tracer) { + this.factory = factory; + this.tracer = tracer; + } + + Mono call() { + return this.factory.create("circuit").run(Mono.defer(() -> { + this.firstSpan = this.tracer.currentSpan(); + log.info(" Hello from consumer", + this.tracer.currentSpan().context().traceId()); + return Mono.error(new IllegalStateException("boom")); + }), throwable -> { + this.secondSpan = this.tracer.currentSpan(); + log.info(" Hello from producer", + this.tracer.currentSpan().context().traceId()); + return Mono.just("fallback"); + }); + } + + } + +} diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/ReactiveCircuitBreakerTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/ReactiveCircuitBreakerTests.java new file mode 100644 index 000000000..021fbcd9e --- /dev/null +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/ReactiveCircuitBreakerTests.java @@ -0,0 +1,94 @@ +/* + * Copyright 2013-2021 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.circuitbreaker; + +import java.util.concurrent.atomic.AtomicReference; + +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import org.springframework.cloud.circuitbreaker.resilience4j.ReactiveResilience4JCircuitBreakerFactory; +import org.springframework.cloud.sleuth.ScopedSpan; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.test.TestTracingAwareSupplier; + +public abstract class ReactiveCircuitBreakerTests implements TestTracingAwareSupplier { + + @Test + public void should_pass_tracing_information_when_using_circuit_breaker() { + // given + Tracer tracer = tracerTest().tracing().tracer(); + ScopedSpan scopedSpan = null; + try { + scopedSpan = tracer.startScopedSpan("start"); + // when + Span span = new TraceReactiveCircuitBreaker(new ReactiveResilience4JCircuitBreakerFactory().create("name"), + tracer, tracerTest().tracing().currentTraceContext()) + .run(Mono.defer(() -> Mono.just(tracer.currentSpan()))).block(); + + BDDAssertions.then(span).isNotNull(); + BDDAssertions.then(scopedSpan.context().traceId()).isEqualTo(span.context().traceId()); + } + finally { + scopedSpan.end(); + } + } + + @Test + public void should_pass_tracing_information_when_using_circuit_breaker_with_fallback() { + // given + Tracer tracer = tracerTest().tracing().tracer(); + AtomicReference first = new AtomicReference<>(); + AtomicReference second = new AtomicReference<>(); + ScopedSpan scopedSpan = null; + try { + scopedSpan = tracer.startScopedSpan("start"); + // when + BDDAssertions.thenThrownBy(() -> new TraceReactiveCircuitBreaker( + new ReactiveResilience4JCircuitBreakerFactory().create("name"), tracer, + tracerTest().tracing().currentTraceContext()).run(Mono.defer(() -> { + first.set(tracer.currentSpan()); + throw new IllegalStateException("boom"); + }), throwable -> { + second.set(tracer.currentSpan()); + throw new IllegalStateException("boom2"); + }).block()).isInstanceOf(IllegalStateException.class).hasMessageContaining("boom2"); + + BDDAssertions.then(tracerTest().handler().reportedSpans()).hasSize(2); + BDDAssertions.then(first.get()).isNotNull(); + BDDAssertions.then(second.get()).isNotNull(); + BDDAssertions.then(scopedSpan.context().traceId()).isEqualTo(first.get().context().traceId()); + BDDAssertions.then(scopedSpan.context().traceId()).isEqualTo(second.get().context().traceId()); + BDDAssertions.then(first.get().context().spanId()).isNotEqualTo(second.get().context().spanId()); + + FinishedSpan finishedSpan = tracerTest().handler().reportedSpans().get(1); + BDDAssertions.then(finishedSpan.getName()).contains("function"); + additionalAssertions(finishedSpan); + } + finally { + scopedSpan.end(); + } + } + + public void additionalAssertions(FinishedSpan finishedSpan) { + throw new UnsupportedOperationException("Assert errors"); + } + +}