diff --git a/docs/src/main/asciidoc/spring-cloud-sleuth.adoc b/docs/src/main/asciidoc/spring-cloud-sleuth.adoc index 0f1b10947..96f4009cc 100644 --- a/docs/src/main/asciidoc/spring-cloud-sleuth.adoc +++ b/docs/src/main/asciidoc/spring-cloud-sleuth.adoc @@ -1083,6 +1083,11 @@ include::{project-root}/spring-cloud-sleuth-core/src/test/java/org/springframewo That way, you ensure that a new span is created and closed for each execution. +=== Spring Cloud CircuitBreaker + +If you have Spring Cloud CircuitBreaker on the classpath, we will wrap the passed command `Supplier` and the fallback `Function` in its trace representations. In order to disable this instrumentation set `spring.sleuth.circuitbreaker.enabled` to `false`. + + === Hystrix ==== Custom Concurrency Strategy diff --git a/pom.xml b/pom.xml index 6c513597b..d86748f89 100644 --- a/pom.xml +++ b/pom.xml @@ -176,6 +176,13 @@ pom import + + org.springframework.cloud + spring-cloud-circuitbreaker-dependencies + ${spring-cloud-circuitbreaker.version} + pom + import + org.springframework.cloud spring-cloud-stream-dependencies @@ -255,6 +262,7 @@ 2.2.1.BUILD-SNAPSHOT 2.2.1.BUILD-SNAPSHOT 2.2.1.BUILD-SNAPSHOT + 1.0.1.BUILD-SNAPSHOT Horsham.RELEASE 2.2.1.BUILD-SNAPSHOT 2.2.1.BUILD-SNAPSHOT diff --git a/spring-cloud-sleuth-core/pom.xml b/spring-cloud-sleuth-core/pom.xml index 4aeb423b0..27caad81e 100644 --- a/spring-cloud-sleuth-core/pom.xml +++ b/spring-cloud-sleuth-core/pom.xml @@ -95,6 +95,11 @@ spring-cloud-starter-gateway true + + org.springframework.cloud + spring-cloud-starter-circuitbreaker-resilience4j + true + org.springframework.cloud spring-cloud-starter-openfeign diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/SleuthCircuitBreakerAutoConfiguration.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/SleuthCircuitBreakerAutoConfiguration.java new file mode 100644 index 000000000..95886d5c0 --- /dev/null +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/SleuthCircuitBreakerAutoConfiguration.java @@ -0,0 +1,105 @@ +/* + * 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.circuitbreaker; + +import java.util.function.Function; +import java.util.function.Supplier; + +import brave.Tracer; +import brave.Tracing; +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.boot.autoconfigure.AutoConfigureAfter; +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.autoconfig.TraceAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration + * Auto-configuration} that registers instrumentation for circuit breakers. + * + * @author Marcin Grzejszczak + * @since 2.2.1 + */ +@Configuration(proxyBeanMethods = false) +@AutoConfigureAfter(TraceAutoConfiguration.class) +@ConditionalOnClass(CircuitBreaker.class) +@ConditionalOnBean(Tracing.class) +@ConditionalOnProperty(value = "spring.sleuth.circuitbreaker.enabled", + matchIfMissing = true) +@EnableConfigurationProperties(SleuthCircuitBreakerProperties.class) +public class SleuthCircuitBreakerAutoConfiguration { + + @Bean + TraceCircuitBreakerFactoryAspect traceCircuitBreakerFactoryAspect(Tracer tracer) { + return new TraceCircuitBreakerFactoryAspect(tracer); + } + +} + +@Aspect +class TraceCircuitBreakerFactoryAspect { + + private final Tracer tracer; + + TraceCircuitBreakerFactoryAspect(Tracer tracer) { + this.tracer = tracer; + } + + @Pointcut("execution(public * org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory.create(..))") + private void anyCircuitBreakerFactoryCreate() { + } // NOSONAR + + @Around("anyCircuitBreakerFactoryCreate()") + public Object wrapFactory(ProceedingJoinPoint pjp) throws Throwable { + CircuitBreaker circuitBreaker = (CircuitBreaker) pjp.proceed(); + return new TraceCircuitBreaker(circuitBreaker, this.tracer); + } + +} + +class TraceCircuitBreaker implements CircuitBreaker { + + private final CircuitBreaker delegate; + + private final Tracer tracer; + + TraceCircuitBreaker(CircuitBreaker delegate, Tracer tracer) { + this.delegate = delegate; + this.tracer = tracer; + } + + @Override + public T run(Supplier toRun, Function fallback) { + return this.delegate.run(new TraceSupplier<>(this.tracer, toRun), + new TraceFunction<>(this.tracer, fallback)); + } + + @Override + public T run(Supplier toRun) { + return this.delegate.run(new TraceSupplier<>(this.tracer, toRun)); + } + +} diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/SleuthCircuitBreakerProperties.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/SleuthCircuitBreakerProperties.java new file mode 100644 index 000000000..1cb9665a7 --- /dev/null +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/SleuthCircuitBreakerProperties.java @@ -0,0 +1,43 @@ +/* + * 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.circuitbreaker; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Sleuth Circuit Breaker settings. + * + * @author Marcin Grzejszczak + * @since 2.2.1 + */ +@ConfigurationProperties("spring.sleuth.circuitbreaker") +public class SleuthCircuitBreakerProperties { + + /** + * Enable Spring Cloud CircuitBreaker instrumentation. + */ + private boolean enabled = true; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + +} diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceFunction.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceFunction.java new file mode 100644 index 000000000..a450a2344 --- /dev/null +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceFunction.java @@ -0,0 +1,68 @@ +/* + * Copyright 2018-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.circuitbreaker; + +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + +import brave.Span; +import brave.Tracer; + +/** + * Trace representation of a {@link Function}. + * + * @param type returned by the fallback + * @since 2.2.1 + */ +public class TraceFunction implements Function { + + private final Tracer tracer; + + private final Function delegate; + + private final AtomicReference span; + + public TraceFunction(Tracer tracer, Function delegate) { + this.tracer = tracer; + this.delegate = delegate; + this.span = new AtomicReference<>(this.tracer.nextSpan()); + } + + @Override + public T apply(Throwable throwable) { + String name = this.delegate.getClass().getSimpleName(); + Span span = this.span.get().name(name); + Throwable tr = null; + try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span.start())) { + return this.delegate.apply(throwable); + } + catch (Throwable t) { + tr = t; + throw t; + } + finally { + if (tr != null) { + String message = tr.getMessage() == null ? tr.getClass().getSimpleName() + : tr.getMessage(); + span.tag("error", message); + } + span.finish(); + this.span.set(null); + } + } + +} diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceSupplier.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceSupplier.java new file mode 100644 index 000000000..2ca6b2633 --- /dev/null +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceSupplier.java @@ -0,0 +1,68 @@ +/* + * Copyright 2018-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.circuitbreaker; + +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +import brave.Span; +import brave.Tracer; + +/** + * Trace representation of a {@link Supplier}. + * + * @param type returned by the supplier + * @since 2.2.1 + */ +public class TraceSupplier implements Supplier { + + private final Tracer tracer; + + private final Supplier delegate; + + private final AtomicReference span; + + public TraceSupplier(Tracer tracer, Supplier delegate) { + this.tracer = tracer; + this.delegate = delegate; + this.span = new AtomicReference<>(this.tracer.nextSpan()); + } + + @Override + public T get() { + String name = this.delegate.getClass().getSimpleName(); + Span span = this.span.get().name(name); + Throwable tr = null; + try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span.start())) { + return this.delegate.get(); + } + catch (Throwable t) { + tr = t; + throw t; + } + finally { + if (tr != null) { + String message = tr.getMessage() == null ? tr.getClass().getSimpleName() + : tr.getMessage(); + span.tag("error", message); + } + span.finish(); + this.span.set(null); + } + } + +} 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 9290a35c7..00b2c3099 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 @@ -16,6 +16,7 @@ org.springframework.cloud.sleuth.instrument.async.AsyncDefaultAutoConfiguration, org.springframework.cloud.sleuth.instrument.scheduling.TraceSchedulingAutoConfiguration,\ org.springframework.cloud.sleuth.instrument.web.client.feign.TraceFeignClientAutoConfiguration,\ org.springframework.cloud.sleuth.instrument.hystrix.SleuthHystrixAutoConfiguration,\ +org.springframework.cloud.sleuth.instrument.circuitbreaker.SleuthCircuitBreakerAutoConfiguration,\ org.springframework.cloud.sleuth.instrument.rxjava.RxJavaAutoConfiguration,\ org.springframework.cloud.sleuth.instrument.reactor.TraceReactorAutoConfiguration,\ org.springframework.cloud.sleuth.instrument.web.TraceWebFluxAutoConfiguration,\ diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/CircuitBreakerIntegrationTests.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/CircuitBreakerIntegrationTests.java new file mode 100644 index 000000000..8346cbcb0 --- /dev/null +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/CircuitBreakerIntegrationTests.java @@ -0,0 +1,139 @@ +/* + * 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.circuitbreaker; + +import java.util.concurrent.atomic.AtomicReference; + +import brave.ScopedSpan; +import brave.Span; +import brave.Tracer; +import brave.sampler.Sampler; +import org.assertj.core.api.BDDAssertions; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JCircuitBreakerFactory; +import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; +import org.springframework.cloud.sleuth.util.ArrayListSpanReporter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.BDDAssertions.then; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = CircuitBreakerIntegrationTests.Config.class) +public class CircuitBreakerIntegrationTests { + + @Autowired + ArrayListSpanReporter reporter; + + @Autowired + Tracer tracer; + + @Autowired + CircuitBreakerFactory factory; + + @Before + public void setup() { + this.reporter.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(tracer::currentSpan); + + then(span).isNotNull(); + then(scopedSpan.context().traceIdString()) + .isEqualTo(span.context().traceIdString()); + } + finally { + scopedSpan.finish(); + } + } + + @Test + public void should_pass_tracing_information_when_using_circuit_breaker_with_fallback() { + // given + Tracer tracer = this.tracer; + AtomicReference first = new AtomicReference<>(); + AtomicReference second = new AtomicReference<>(); + ScopedSpan scopedSpan = null; + try { + scopedSpan = tracer.startScopedSpan("start"); + // when + BDDAssertions.thenThrownBy(() -> this.factory.create("name").run(() -> { + first.set(tracer.currentSpan()); + throw new IllegalStateException("boom"); + }, throwable -> { + second.set(tracer.currentSpan()); + throw new IllegalStateException("boom2"); + })).isInstanceOf(IllegalStateException.class).hasMessageContaining("boom2"); + + then(this.reporter.getSpans()).hasSize(2); + then(scopedSpan.context().traceIdString()) + .isEqualTo(first.get().context().traceIdString()); + then(scopedSpan.context().traceIdString()) + .isEqualTo(second.get().context().traceIdString()); + then(first.get().context().spanIdString()) + .isNotEqualTo(second.get().context().spanIdString()); + + zipkin2.Span reportedSpan = this.reporter.getSpans().get(0); + then(reportedSpan.name()).contains("circuitbreakerintegrationtests"); + then(reportedSpan.tags().get("error")).contains("boom"); + + reportedSpan = this.reporter.getSpans().get(1); + then(reportedSpan.name()).contains("circuitbreakerintegrationtests"); + then(reportedSpan.tags().get("error")).contains("boom2"); + } + finally { + scopedSpan.finish(); + } + } + + @Configuration + @EnableAutoConfiguration + static class Config { + + @Bean + ArrayListSpanReporter arrayListSpanReporter() { + return new ArrayListSpanReporter(); + } + + @Bean + Resilience4JCircuitBreakerFactory resilience4JCircuitBreakerFactory() { + return new Resilience4JCircuitBreakerFactory(); + } + + @Bean + Sampler sampler() { + return Sampler.ALWAYS_SAMPLE; + } + + } + +} diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/CircuitBreakerTests.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/CircuitBreakerTests.java new file mode 100644 index 000000000..a47e66ad2 --- /dev/null +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/CircuitBreakerTests.java @@ -0,0 +1,110 @@ +/* + * 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.circuitbreaker; + +import java.util.concurrent.atomic.AtomicReference; + +import brave.ScopedSpan; +import brave.Span; +import brave.Tracer; +import brave.Tracing; +import brave.propagation.StrictScopeDecorator; +import brave.propagation.ThreadLocalCurrentTraceContext; +import brave.sampler.Sampler; +import org.assertj.core.api.BDDAssertions; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JCircuitBreakerFactory; +import org.springframework.cloud.sleuth.util.ArrayListSpanReporter; + +import static org.assertj.core.api.BDDAssertions.then; + +public class CircuitBreakerTests { + + ArrayListSpanReporter reporter = new ArrayListSpanReporter(); + + Tracing tracing = Tracing.newBuilder() + .currentTraceContext(ThreadLocalCurrentTraceContext.newBuilder() + .addScopeDecorator(StrictScopeDecorator.create()).build()) + .spanReporter(this.reporter).sampler(Sampler.ALWAYS_SAMPLE).build(); + + Tracer tracer = this.tracing.tracer(); + + @Before + public void setup() { + this.reporter.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 = new Resilience4JCircuitBreakerFactory().create("name") + .run(new TraceSupplier<>(tracer, tracer::currentSpan)); + + then(span).isNotNull(); + then(scopedSpan.context().traceIdString()) + .isEqualTo(span.context().traceIdString()); + } + finally { + scopedSpan.finish(); + } + } + + @Test + public void should_pass_tracing_information_when_using_circuit_breaker_with_fallback() { + // given + Tracer tracer = this.tracer; + AtomicReference first = new AtomicReference<>(); + AtomicReference second = new AtomicReference<>(); + ScopedSpan scopedSpan = null; + try { + scopedSpan = tracer.startScopedSpan("start"); + // when + BDDAssertions.thenThrownBy(() -> new Resilience4JCircuitBreakerFactory() + .create("name").run(new TraceSupplier<>(tracer, () -> { + first.set(tracer.currentSpan()); + throw new IllegalStateException("boom"); + }), new TraceFunction<>(tracer, throwable -> { + second.set(tracer.currentSpan()); + throw new IllegalStateException("boom2"); + }))).isInstanceOf(IllegalStateException.class) + .hasMessageContaining("boom2"); + + then(this.reporter.getSpans()).hasSize(2); + then(scopedSpan.context().traceIdString()) + .isEqualTo(first.get().context().traceIdString()); + then(scopedSpan.context().traceIdString()) + .isEqualTo(second.get().context().traceIdString()); + then(first.get().context().spanIdString()) + .isNotEqualTo(second.get().context().spanIdString()); + + zipkin2.Span reportedSpan = this.reporter.getSpans().get(1); + then(reportedSpan.name()).contains("circuitbreakertests"); + then(reportedSpan.tags().get("error")).contains("boom2"); + } + finally { + scopedSpan.finish(); + } + } + +}