Spring Cloud Circuit Breaker tracing (#1505)

Adds support for Spring Cloud CircuitBreaker
This commit is contained in:
Marcin Grzejszczak
2019-12-13 11:49:43 +01:00
committed by GitHub
parent bc640791ab
commit 11c8090b94
10 changed files with 552 additions and 0 deletions

View File

@@ -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

View File

@@ -176,6 +176,13 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-circuitbreaker-dependencies</artifactId>
<version>${spring-cloud-circuitbreaker.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-dependencies</artifactId>
@@ -255,6 +262,7 @@
<spring-cloud-build.version>2.2.1.BUILD-SNAPSHOT</spring-cloud-build.version>
<spring-cloud-commons.version>2.2.1.BUILD-SNAPSHOT</spring-cloud-commons.version>
<spring-cloud-gateway.version>2.2.1.BUILD-SNAPSHOT</spring-cloud-gateway.version>
<spring-cloud-circuitbreaker.version>1.0.1.BUILD-SNAPSHOT</spring-cloud-circuitbreaker.version>
<spring-cloud-stream.version>Horsham.RELEASE</spring-cloud-stream.version>
<spring-cloud-netflix.version>2.2.1.BUILD-SNAPSHOT</spring-cloud-netflix.version>
<spring-cloud-openfeign.version>2.2.1.BUILD-SNAPSHOT</spring-cloud-openfeign.version>

View File

@@ -95,6 +95,11 @@
<artifactId>spring-cloud-starter-gateway</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>

View File

@@ -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> T run(Supplier<T> toRun, Function<Throwable, T> fallback) {
return this.delegate.run(new TraceSupplier<>(this.tracer, toRun),
new TraceFunction<>(this.tracer, fallback));
}
@Override
public <T> T run(Supplier<T> toRun) {
return this.delegate.run(new TraceSupplier<>(this.tracer, toRun));
}
}

View File

@@ -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;
}
}

View File

@@ -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 <T> type returned by the fallback
* @since 2.2.1
*/
public class TraceFunction<T> implements Function<Throwable, T> {
private final Tracer tracer;
private final Function<Throwable, T> delegate;
private final AtomicReference<Span> span;
public TraceFunction(Tracer tracer, Function<Throwable, T> 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);
}
}
}

View File

@@ -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 <T> type returned by the supplier
* @since 2.2.1
*/
public class TraceSupplier<T> implements Supplier<T> {
private final Tracer tracer;
private final Supplier<T> delegate;
private final AtomicReference<Span> span;
public TraceSupplier(Tracer tracer, Supplier<T> 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);
}
}
}

View File

@@ -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,\

View File

@@ -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<Span> first = new AtomicReference<>();
AtomicReference<Span> 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;
}
}
}

View File

@@ -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<Span> first = new AtomicReference<>();
AtomicReference<Span> 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();
}
}
}