Improve support for reactor #1027 (#1028)

* reactor Mono/Flux enhancement : post review fixes (methods renamed to reactor)
* new span started on @ContinueSpan should be closed on completion
* #1027 tests for reactor Mono/Flux support enhancement

fixes gh-1027
This commit is contained in:
Sergii Karpenko
2018-08-09 12:01:17 +03:00
committed by Marcin Grzejszczak
parent 04440fa959
commit 85761191d8
9 changed files with 1193 additions and 43 deletions

View File

@@ -35,7 +35,7 @@ class DefaultNewSpanParser implements NewSpanParser {
@Override
public void parse(MethodInvocation pjp, NewSpan newSpan, SpanCustomizer span) {
String name = StringUtils.isEmpty(newSpan.name()) ?
String name = newSpan == null || StringUtils.isEmpty(newSpan.name()) ?
pjp.getMethod().getName() : newSpan.name();
String changedName = SpanNameUtil.toLowerHyphen(name);
if (log.isDebugEnabled()) {

View File

@@ -16,17 +16,13 @@
package org.springframework.cloud.sleuth.annotation;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.PostConstruct;
import brave.Span;
import brave.Tracer;
import org.aopalliance.aop.Advice;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Publisher;
import org.springframework.aop.ClassFilter;
import org.springframework.aop.IntroductionInterceptor;
import org.springframework.aop.Pointcut;
@@ -40,6 +36,15 @@ import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Supplier;
/**
* Custom pointcut advisor that picks all classes / interfaces that
@@ -143,19 +148,14 @@ class SleuthAdvisorConfig extends AbstractPointcutAdvisor implements BeanFactory
public boolean hasAnnotatedMethods(Class<?> clazz) {
final AtomicBoolean found = new AtomicBoolean(false);
ReflectionUtils.doWithMethods(clazz,
new ReflectionUtils.MethodCallback() {
@Override
public void doWith(Method method) throws IllegalArgumentException,
IllegalAccessException {
if (found.get()) {
return;
}
Annotation annotation = AnnotationUtils.findAnnotation(method,
SleuthAdvisorConfig.AnnotationMethodsResolver.this.annotationType);
if (annotation != null) { found.set(true); }
}
});
ReflectionUtils.doWithMethods(clazz, method -> {
if (found.get()) {
return;
}
Annotation annotation = AnnotationUtils.findAnnotation(method,
AnnotationMethodsResolver.this.annotationType);
if (annotation != null) { found.set(true); }
});
return found.get();
}
@@ -183,6 +183,7 @@ class SleuthInterceptor implements IntroductionInterceptor, BeanFactoryAware {
if (method == null) {
return invocation.proceed();
}
Method mostSpecificMethod = AopUtils
.getMostSpecificMethod(method, invocation.getThis().getClass());
NewSpan newSpan = SleuthAnnotationUtils.findAnnotation(mostSpecificMethod, NewSpan.class);
@@ -190,39 +191,135 @@ class SleuthInterceptor implements IntroductionInterceptor, BeanFactoryAware {
if (newSpan == null && continueSpan == null) {
return invocation.proceed();
}
if(isReactorReturnType(method.getReturnType())){
return proceedUnderReactorSpan(invocation, newSpan, continueSpan);
} else {
return proceedUnderSynchronousSpan(invocation, newSpan, continueSpan);
}
}
private boolean isReactorReturnType(Class<?> returnType) {
return Flux.class.equals(returnType) || Mono.class.equals(returnType);
}
private Object proceedUnderSynchronousSpan(
MethodInvocation invocation, NewSpan newSpan, ContinueSpan continueSpan) throws Throwable {
Span span = tracer().currentSpan();
if (newSpan != null || span == null) {
span = tracer().nextSpan().start();
//in case of @ContinueSpan and no span in tracer we start new span and should close it on completion
boolean startNewSpan = newSpan != null || span == null;
if (startNewSpan) {
span = tracer().nextSpan();
newSpanParser().parse(invocation, newSpan, span);
span.start();
}
String log = log(continueSpan);
boolean hasLog = StringUtils.hasText(log);
try (Tracer.SpanInScope ws = tracer().withSpanInScope(span)) {
if (hasLog) {
logEvent(span, log + ".before");
}
spanTagAnnotationHandler().addAnnotatedParameters(invocation);
addTags(invocation, span);
before(invocation, span, log, hasLog);
return invocation.proceed();
} catch (Exception e) {
if (logger.isDebugEnabled()) {
logger.debug("Exception occurred while trying to continue the pointcut", e);
}
if (hasLog) {
logEvent(span, log + ".afterFailure");
}
span.error(e);
onFailure(span, log, hasLog, e);
throw e;
} finally {
if (hasLog) {
logEvent(span, log + ".after");
after(span, startNewSpan, log, hasLog);
}
}
private Object proceedUnderReactorSpan(
MethodInvocation invocation, NewSpan newSpan, ContinueSpan continueSpan) throws Throwable{
Span spanPrevious = tracer().currentSpan();
//in case of @ContinueSpan and no span in tracer we start new span and should close it on completion
boolean startNewSpan = newSpan != null || spanPrevious == null;
Span span;
if (startNewSpan) {
span = tracer().nextSpan();
newSpanParser().parse(invocation, newSpan, span);
} else {
span = spanPrevious;
}
String log = log(continueSpan);
boolean hasLog = StringUtils.hasText(log);
try(Tracer.SpanInScope ws = tracer().withSpanInScope(span)) {
Publisher<?> publisher = (Publisher) invocation.proceed();
Mono<Span> startSpan = Mono.defer(() -> withSpanInScope(span, () -> {
if (startNewSpan) {
span.start();
}
before(invocation, span, log, hasLog);
return Mono.just(span);
}));
if(publisher instanceof Mono){
return startSpan.flatMap(spanStarted -> ((Mono<?>)publisher)
.doOnError(onFailureReactor(log, hasLog, spanStarted))
.doOnTerminate(afterReactor(startNewSpan, log, hasLog, spanStarted)));
}
if (newSpan != null) {
span.finish();
else if(publisher instanceof Flux){
return startSpan.flatMapMany(spanStarted -> ((Flux<?>)publisher)
.doOnError(onFailureReactor(log, hasLog, spanStarted))
.doOnTerminate(afterReactor(startNewSpan, log, hasLog, spanStarted)));
}
else {
throw new IllegalArgumentException("Unexpected type of publisher: "+publisher.getClass());
}
}
}
private <T> T withSpanInScope(Span span, Supplier<T> supplier) {
try(Tracer.SpanInScope ws1 = tracer().withSpanInScope(span)) {
return supplier.get();
}
}
private Runnable afterReactor(boolean isNewSpan, String log, boolean hasLog, Span span) {
return () -> {
try(Tracer.SpanInScope ws = tracer().withSpanInScope(span)) {
after(span, isNewSpan, log, hasLog);
}
};
}
private Consumer<Throwable> onFailureReactor(String log, boolean hasLog, Span span) {
return throwable -> {
try(Tracer.SpanInScope ws = tracer().withSpanInScope(span)) {
onFailure(span, log, hasLog, throwable);
}
};
}
private void before(MethodInvocation invocation, Span span, String log, boolean hasLog) {
if (hasLog) {
logEvent(span, log + ".before");
}
spanTagAnnotationHandler().addAnnotatedParameters(invocation);
addTags(invocation, span);
}
private void after(Span span, boolean isNewSpan, String log, boolean hasLog) {
if (hasLog) {
logEvent(span, log + ".after");
}
if (isNewSpan) {
span.finish();
}
}
private void onFailure(Span span, String log, boolean hasLog, Throwable e) {
if (logger.isDebugEnabled()) {
logger.debug("Exception occurred while trying to continue the pointcut", e);
}
if (hasLog) {
logEvent(span, log + ".afterFailure");
}
span.error(e);
}
private void addTags(MethodInvocation invocation, Span span) {
span.tag(CLASS_KEY, invocation.getThis().getClass().getSimpleName());
span.tag(METHOD_KEY, invocation.getMethod().getName());

View File

@@ -0,0 +1,505 @@
/*
* Copyright 2013-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.sleuth.annotation;
import brave.Span;
import brave.Tracer;
import brave.sampler.Sampler;
import org.junit.Before;
import org.junit.Ignore;
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.sleuth.util.ArrayListSpanReporter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import zipkin2.Annotation;
import zipkin2.reporter.Reporter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import static org.assertj.core.api.BDDAssertions.then;
import static org.springframework.cloud.sleuth.annotation.SleuthSpanCreatorAspectFluxTests.TestBean.TEST_STRING1;
import static org.springframework.cloud.sleuth.annotation.SleuthSpanCreatorAspectFluxTests.TestBean.TEST_STRING2;
@SpringBootTest(classes = SleuthSpanCreatorAspectFluxTests.TestConfiguration.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class SleuthSpanCreatorAspectFluxTests {
@Autowired TestBeanInterface testBean;
@Autowired Tracer tracer;
@Autowired ArrayListSpanReporter reporter;
@Before
public void setup() {
this.reporter.clear();
testBean.reset();
}
@Test
public void shouldCreateSpanWhenAnnotationOnInterfaceMethod() {
Flux<String> flux = this.testBean.testMethod();
verifyNoSpansUntilFluxComplete(flux);
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("test-method");
then(spans.get(0).duration()).isNotZero();
}
@Test
public void shouldCreateSpanWhenAnnotationOnClassMethod() {
Flux<String> flux = this.testBean.testMethod2();
verifyNoSpansUntilFluxComplete(flux);
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("test-method2");
then(spans.get(0).duration()).isNotZero();
}
@Test
public void shouldCreateSpanWithCustomNameWhenAnnotationOnClassMethod() {
Flux<String> flux = this.testBean.testMethod3();
verifyNoSpansUntilFluxComplete(flux);
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("custom-name-on-test-method3");
then(spans.get(0).duration()).isNotZero();
}
@Test
public void shouldCreateSpanWithCustomNameWhenAnnotationOnInterfaceMethod() {
Flux<String> flux = this.testBean.testMethod4();
verifyNoSpansUntilFluxComplete(flux);
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("custom-name-on-test-method4");
then(spans.get(0).duration()).isNotZero();
}
@Test
public void shouldCreateSpanWithTagWhenAnnotationOnInterfaceMethod() {
// tag::execution[]
Flux<String> flux = this.testBean.testMethod5("test");
// end::execution[]
verifyNoSpansUntilFluxComplete(flux);
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("custom-name-on-test-method5");
then(spans.get(0).tags()).containsEntry("testTag", "test");
then(spans.get(0).duration()).isNotZero();
}
@Test
public void shouldCreateSpanWithTagWhenAnnotationOnClassMethod() {
Flux<String> flux = this.testBean.testMethod6("test");
verifyNoSpansUntilFluxComplete(flux);
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("custom-name-on-test-method6");
then(spans.get(0).tags()).containsEntry("testTag6", "test");
then(spans.get(0).duration()).isNotZero();
}
@Test
public void shouldCreateSpanWithLogWhenAnnotationOnInterfaceMethod() {
Flux<String> flux = this.testBean.testMethod8("test");
verifyNoSpansUntilFluxComplete(flux);
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("custom-name-on-test-method8");
then(spans.get(0).duration()).isNotZero();
}
@Test
public void shouldCreateSpanWithLogWhenAnnotationOnClassMethod() {
Flux<String> flux = this.testBean.testMethod9("test");
verifyNoSpansUntilFluxComplete(flux);
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("custom-name-on-test-method9");
then(spans.get(0).tags())
.containsEntry("class", "TestBean")
.containsEntry("method", "testMethod9");
then(spans.get(0).duration()).isNotZero();
}
@Test
public void shouldContinueSpanWithLogWhenAnnotationOnInterfaceMethod() {
Span span = this.tracer.nextSpan().name("foo");
try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span.start())) {
Flux<String> flux = this.testBean.testMethod10("test");
verifyNoSpansUntilFluxComplete(flux);
} finally {
span.finish();
}
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("foo");
then(spans.get(0).tags())
.containsEntry("customTestTag10", "test");
then(spans.get(0).annotations()
.stream().map(Annotation::value).collect(Collectors.toList()))
.contains("customTest.before", "customTest.after");
then(spans.get(0).duration()).isNotZero();
}
@Test
public void shouldStartAndCloseSpanOnContinueSpanIfSpanNotSet() {
Flux<String> flux = this.testBean.testMethod10("test");
verifyNoSpansUntilFluxComplete(flux);
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("test-method10");
then(spans.get(0).tags())
.containsEntry("customTestTag10", "test");
then(spans.get(0).annotations()
.stream().map(Annotation::value).collect(Collectors.toList()))
.contains("customTest.before", "customTest.after");
then(spans.get(0).duration()).isNotZero();
}
@Test
public void shouldContinueSpanWhenKeyIsUsedOnSpanTagWhenAnnotationOnInterfaceMethod() {
Span span = this.tracer.nextSpan().name("foo");
try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span.start())) {
Flux<String> flux = this.testBean.testMethod10_v2("test");
verifyNoSpansUntilFluxComplete(flux);
} finally {
span.finish();
}
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("foo");
then(spans.get(0).tags())
.containsEntry("customTestTag10", "test");
then(spans.get(0).annotations()
.stream().map(Annotation::value).collect(Collectors.toList()))
.contains("customTest.before", "customTest.after");
then(spans.get(0).duration()).isNotZero();
}
@Test
public void shouldContinueSpanWithLogWhenAnnotationOnClassMethod() {
Span span = this.tracer.nextSpan().name("foo");
try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span.start())) {
// tag::continue_span_execution[]
Flux<String> flux = this.testBean.testMethod11("test");
// end::continue_span_execution[]
verifyNoSpansUntilFluxComplete(flux);
} finally {
span.finish();
}
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("foo");
then(spans.get(0).tags())
.containsEntry("class", "TestBean")
.containsEntry("method", "testMethod11")
.containsEntry("customTestTag11", "test");
then(spans.get(0).annotations()
.stream().map(Annotation::value).collect(Collectors.toList()))
.contains("customTest.before", "customTest.after");
then(spans.get(0).duration()).isNotZero();
}
@Test
public void shouldAddErrorTagWhenExceptionOccurredInNewSpan() {
try {
Flux<String> flux = this.testBean.testMethod12("test");
then(this.reporter.getSpans()).isEmpty();
flux.toIterable().iterator().next();
} catch (RuntimeException ignored) {
}
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("test-method12");
then(spans.get(0).tags())
.containsEntry("testTag12", "test")
.containsEntry("error", "test exception 12");
then(spans.get(0).duration()).isNotZero();
}
@Test
public void shouldAddErrorTagWhenExceptionOccurredInContinueSpan() {
Span span = this.tracer.nextSpan().name("foo");
try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span.start())) {
// tag::continue_span_execution[]
Flux<String> flux = this.testBean.testMethod13();
then(this.reporter.getSpans()).isEmpty();
flux.toIterable().iterator().next();
// end::continue_span_execution[]
} catch (RuntimeException ignored) {
} finally {
span.finish();
}
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("foo");
then(spans.get(0).tags())
.containsEntry("error", "test exception 13");
then(spans.get(0).annotations()
.stream().map(Annotation::value).collect(Collectors.toList()))
.contains("testMethod13.before", "testMethod13.afterFailure",
"testMethod13.after");
then(spans.get(0).duration()).isNotZero();
}
@Test
public void shouldNotCreateSpanWhenNotAnnotated() {
Flux<String> flux = this.testBean.testMethod7();
verifyNoSpansUntilFluxComplete(flux);
List<zipkin2.Span> spans = new ArrayList<>(this.reporter.getSpans());
then(spans).isEmpty();
}
private void verifyNoSpansUntilFluxComplete(Flux<String> flux) {
Iterator<String> iterator = flux.toIterable().iterator();
then(this.reporter.getSpans()).isEmpty();
testBean.proceed();
String result1 = iterator.next();
then(result1).isEqualTo(TEST_STRING1);
then(this.reporter.getSpans()).isEmpty();
testBean.proceed();
String result2 = iterator.next();
then(result2).isEqualTo(TEST_STRING2);
}
protected interface TestBeanInterface {
// tag::annotated_method[]
@NewSpan
Flux<String> testMethod();
// end::annotated_method[]
Flux<String> testMethod2();
@NewSpan(name = "interfaceCustomNameOnTestMethod3")
Flux<String> testMethod3();
// tag::custom_name_on_annotated_method[]
@NewSpan("customNameOnTestMethod4")
Flux<String> testMethod4();
// end::custom_name_on_annotated_method[]
// tag::custom_name_and_tag_on_annotated_method[]
@NewSpan(name = "customNameOnTestMethod5")
Flux<String> testMethod5(@SpanTag("testTag") String param);
// end::custom_name_and_tag_on_annotated_method[]
Flux<String> testMethod6(String test);
Flux<String> testMethod7();
@NewSpan(name = "customNameOnTestMethod8")
Flux<String> testMethod8(String param);
@NewSpan(name = "testMethod9")
Flux<String> testMethod9(String param);
@ContinueSpan(log = "customTest")
Flux<String> testMethod10(@SpanTag(value = "testTag10") String param);
@ContinueSpan(log = "customTest")
Flux<String> testMethod10_v2(@SpanTag(key = "testTag10") String param);
// tag::continue_span[]
@ContinueSpan(log = "testMethod11")
Flux<String> testMethod11(@SpanTag("testTag11") String param);
// end::continue_span[]
@NewSpan
Flux<String> testMethod12(@SpanTag("testTag12") String param);
@ContinueSpan(log = "testMethod13")
Flux<String> testMethod13();
@ContinueSpan
Flux<String> testMethod14(String param);
void proceed();
void reset();
}
protected static class TestBean implements TestBeanInterface {
public static final String TEST_STRING1 = "Test String 1";
public static final String TEST_STRING2 = "Test String 2";
private AtomicReference<CompletableFuture<Void>> proceed
= new AtomicReference<>(new CompletableFuture<>());
private Flux<String> testFlux = Flux.defer(() -> Flux.just(TEST_STRING1, TEST_STRING2))
.delayUntil(s -> Mono.fromFuture(proceed.get()))
.doOnNext(s -> proceed.set(new CompletableFuture<>()));
@Override
public void reset(){
proceed.set(new CompletableFuture<>());
}
public void proceed(){
proceed.get().complete(null);
}
@Override
public Flux<String> testMethod() {
return testFlux;
}
@NewSpan
@Override
public Flux<String> testMethod2() {
return testFlux;
}
// tag::name_on_implementation[]
@NewSpan(name = "customNameOnTestMethod3")
@Override
public Flux<String> testMethod3() {
return testFlux;
}
// end::name_on_implementation[]
@Override
public Flux<String> testMethod4() {
return testFlux;
}
@Override
public Flux<String> testMethod5(String test) {
return testFlux;
}
@NewSpan(name = "customNameOnTestMethod6")
@Override
public Flux<String> testMethod6(@SpanTag("testTag6") String test) {
return testFlux;
}
@Override
public Flux<String> testMethod7() {
return testFlux;
}
@Override
public Flux<String> testMethod8(String param) {
return testFlux;
}
@NewSpan(name = "customNameOnTestMethod9")
@Override
public Flux<String> testMethod9(String param) {
return testFlux;
}
@Override
public Flux<String> testMethod10(@SpanTag(value = "customTestTag10") String param) {
return testFlux;
}
@Override
public Flux<String> testMethod10_v2(@SpanTag(key = "customTestTag10") String param) {
return testFlux;
}
@ContinueSpan(log = "customTest")
@Override
public Flux<String> testMethod11(@SpanTag("customTestTag11") String param) {
return testFlux;
}
@Override
public Flux<String> testMethod12(String param) {
return Flux.defer(() -> Flux.error(new RuntimeException("test exception 12")));
}
@Override
public Flux<String> testMethod13() {
return Flux.defer(() -> Flux.error(new RuntimeException("test exception 13")));
}
@Override
public Flux<String> testMethod14(String param) {
return Flux.just(TEST_STRING1, TEST_STRING2);
}
}
@Configuration
@EnableAutoConfiguration
protected static class TestConfiguration {
@Bean
public TestBeanInterface testBean() {
return new TestBean();
}
@Bean Reporter<zipkin2.Span> spanReporter() {
return new ArrayListSpanReporter();
}
@Bean Sampler alwaysSampler() {
return Sampler.ALWAYS_SAMPLE;
}
}
}

View File

@@ -0,0 +1,480 @@
/*
* Copyright 2013-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.sleuth.annotation;
import brave.Span;
import brave.Tracer;
import brave.sampler.Sampler;
import org.junit.Before;
import org.junit.Ignore;
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.sleuth.util.ArrayListSpanReporter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import reactor.core.publisher.Mono;
import zipkin2.Annotation;
import zipkin2.reporter.Reporter;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import static org.assertj.core.api.BDDAssertions.then;
import static org.springframework.cloud.sleuth.annotation.SleuthSpanCreatorAspectMonoTests.TestBean.TEST_STRING;
import static reactor.core.publisher.Mono.just;
@SpringBootTest(classes = SleuthSpanCreatorAspectMonoTests.TestConfiguration.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class SleuthSpanCreatorAspectMonoTests {
@Autowired TestBeanInterface testBean;
@Autowired Tracer tracer;
@Autowired ArrayListSpanReporter reporter;
@Before
public void setup() {
this.reporter.clear();
}
@Test
public void shouldCreateSpanWhenAnnotationOnInterfaceMethod() {
Mono<String> mono = this.testBean.testMethod();
then(this.reporter.getSpans()).isEmpty();
mono.block();
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("test-method");
then(spans.get(0).duration()).isNotZero();
then(this.tracer.currentSpan()).isNull();
}
@Test
public void shouldCreateSpanWhenAnnotationOnClassMethod() {
Mono<String> mono = this.testBean.testMethod2();
then(this.reporter.getSpans()).isEmpty();
mono.block();
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("test-method2");
then(spans.get(0).duration()).isNotZero();
}
@Test
public void shouldCreateSpanWithCustomNameWhenAnnotationOnClassMethod() {
Mono<String> mono = this.testBean.testMethod3();
then(this.reporter.getSpans()).isEmpty();
String result = mono.block();
then(result).isEqualTo(TEST_STRING);
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("custom-name-on-test-method3");
then(spans.get(0).duration()).isNotZero();
}
@Test
public void shouldCreateSpanWithCustomNameWhenAnnotationOnInterfaceMethod() {
Mono<String> mono = this.testBean.testMethod4();
then(this.reporter.getSpans()).isEmpty();
mono.block();
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("custom-name-on-test-method4");
then(spans.get(0).duration()).isNotZero();
}
@Test
public void shouldCreateSpanWithTagWhenAnnotationOnInterfaceMethod() {
// tag::execution[]
Mono<String> mono = this.testBean.testMethod5("test");
// end::execution[]
then(this.reporter.getSpans()).isEmpty();
mono.block();
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("custom-name-on-test-method5");
then(spans.get(0).tags()).containsEntry("testTag", "test");
then(spans.get(0).duration()).isNotZero();
}
@Test
public void shouldCreateSpanWithTagWhenAnnotationOnClassMethod() {
Mono<String> mono = this.testBean.testMethod6("test");
then(this.reporter.getSpans()).isEmpty();
mono.block();
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("custom-name-on-test-method6");
then(spans.get(0).tags()).containsEntry("testTag6", "test");
then(spans.get(0).duration()).isNotZero();
}
@Test
public void shouldCreateSpanWithLogWhenAnnotationOnInterfaceMethod() {
Mono<String> mono = this.testBean.testMethod8("test");
then(this.reporter.getSpans()).isEmpty();
mono.block();
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("custom-name-on-test-method8");
then(spans.get(0).duration()).isNotZero();
}
@Test
public void shouldCreateSpanWithLogWhenAnnotationOnClassMethod() {
Mono<String> mono = this.testBean.testMethod9("test");
then(this.reporter.getSpans()).isEmpty();
mono.block();
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("custom-name-on-test-method9");
then(spans.get(0).tags())
.containsEntry("class", "TestBean")
.containsEntry("method", "testMethod9");
then(spans.get(0).duration()).isNotZero();
}
@Test
public void shouldContinueSpanWithLogWhenAnnotationOnInterfaceMethod() {
Span span = this.tracer.nextSpan().name("foo");
try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span.start())) {
Mono<String> mono = this.testBean.testMethod10("test");
then(this.reporter.getSpans()).isEmpty();
mono.block();
} finally {
span.finish();
}
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("foo");
then(spans.get(0).tags())
.containsEntry("customTestTag10", "test");
then(spans.get(0).annotations()
.stream().map(Annotation::value).collect(Collectors.toList()))
.contains("customTest.before", "customTest.after");
then(spans.get(0).duration()).isNotZero();
}
@Test
public void shouldStartAndCloseSpanOnContinueSpanIfSpanNotSet() {
this.testBean.testMethod10("test").block();
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("test-method10");
then(spans.get(0).tags())
.containsEntry("customTestTag10", "test");
then(spans.get(0).annotations()
.stream().map(Annotation::value).collect(Collectors.toList()))
.contains("customTest.before", "customTest.after");
then(spans.get(0).duration()).isNotZero();
}
@Test
public void shouldContinueSpanWhenKeyIsUsedOnSpanTagWhenAnnotationOnInterfaceMethod() {
Span span = this.tracer.nextSpan().name("foo");
try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span.start())) {
Mono<String> mono = this.testBean.testMethod10_v2("test");
then(this.reporter.getSpans()).isEmpty();
mono.block();
} finally {
span.finish();
}
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("foo");
then(spans.get(0).tags())
.containsEntry("customTestTag10", "test");
then(spans.get(0).annotations()
.stream().map(Annotation::value).collect(Collectors.toList()))
.contains("customTest.before", "customTest.after");
then(spans.get(0).duration()).isNotZero();
}
@Test
public void shouldContinueSpanWithLogWhenAnnotationOnClassMethod() {
Span span = this.tracer.nextSpan().name("foo");
try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span.start())) {
// tag::continue_span_execution[]
Mono<String> mono = this.testBean.testMethod11("test");
// end::continue_span_execution[]
then(this.reporter.getSpans()).isEmpty();
mono.block();
} finally {
span.finish();
}
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("foo");
then(spans.get(0).tags())
.containsEntry("class", "TestBean")
.containsEntry("method", "testMethod11")
.containsEntry("customTestTag11", "test");
then(spans.get(0).annotations()
.stream().map(Annotation::value).collect(Collectors.toList()))
.contains("customTest.before", "customTest.after");
then(spans.get(0).duration()).isNotZero();
}
@Test
public void shouldAddErrorTagWhenExceptionOccurredInNewSpan() {
try {
Mono<String> mono = this.testBean.testMethod12("test");
then(this.reporter.getSpans()).isEmpty();
mono.block();
} catch (RuntimeException ignored) {
}
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("test-method12");
then(spans.get(0).tags())
.containsEntry("testTag12", "test")
.containsEntry("error", "test exception 12");
then(spans.get(0).duration()).isNotZero();
}
@Test
public void shouldAddErrorTagWhenExceptionOccurredInContinueSpan() {
Span span = this.tracer.nextSpan().name("foo");
try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span.start())) {
// tag::continue_span_execution[]
Mono<String> mono = this.testBean.testMethod13();
then(this.reporter.getSpans()).isEmpty();
mono.block();
// end::continue_span_execution[]
} catch (RuntimeException ignored) {
} finally {
span.finish();
}
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("foo");
then(spans.get(0).tags())
.containsEntry("error", "test exception 13");
then(spans.get(0).annotations()
.stream().map(Annotation::value).collect(Collectors.toList()))
.contains("testMethod13.before", "testMethod13.afterFailure",
"testMethod13.after");
then(spans.get(0).duration()).isNotZero();
}
@Test
public void shouldNotCreateSpanWhenNotAnnotated() {
Mono<String> mono = this.testBean.testMethod7();
mono.block();
List<zipkin2.Span> spans = new ArrayList<>(this.reporter.getSpans());
then(spans).isEmpty();
}
protected interface TestBeanInterface {
// tag::annotated_method[]
@NewSpan
Mono<String> testMethod();
// end::annotated_method[]
Mono<String> testMethod2();
@NewSpan(name = "interfaceCustomNameOnTestMethod3")
Mono<String> testMethod3();
// tag::custom_name_on_annotated_method[]
@NewSpan("customNameOnTestMethod4")
Mono<String> testMethod4();
// end::custom_name_on_annotated_method[]
// tag::custom_name_and_tag_on_annotated_method[]
@NewSpan(name = "customNameOnTestMethod5")
Mono<String> testMethod5(@SpanTag("testTag") String param);
// end::custom_name_and_tag_on_annotated_method[]
Mono<String> testMethod6(String test);
Mono<String> testMethod7();
@NewSpan(name = "customNameOnTestMethod8")
Mono<String> testMethod8(String param);
@NewSpan(name = "testMethod9")
Mono<String> testMethod9(String param);
@ContinueSpan(log = "customTest")
Mono<String> testMethod10(@SpanTag(value = "testTag10") String param);
@ContinueSpan(log = "customTest")
Mono<String> testMethod10_v2(@SpanTag(key = "testTag10") String param);
// tag::continue_span[]
@ContinueSpan(log = "testMethod11")
Mono<String> testMethod11(@SpanTag("testTag11") String param);
// end::continue_span[]
@NewSpan
Mono<String> testMethod12(@SpanTag("testTag12") String param);
@ContinueSpan(log = "testMethod13")
Mono<String> testMethod13();
}
protected static class TestBean implements TestBeanInterface {
public static final String TEST_STRING = "Test String";
public static final Mono<String> TEST_MONO = Mono.defer(() -> just(TEST_STRING));
@Override
public Mono<String> testMethod() {
return TEST_MONO;
}
@NewSpan
@Override
public Mono<String> testMethod2() {
return TEST_MONO;
}
// tag::name_on_implementation[]
@NewSpan(name = "customNameOnTestMethod3")
@Override
public Mono<String> testMethod3() {
return TEST_MONO;
}
// end::name_on_implementation[]
@Override
public Mono<String> testMethod4() {
return TEST_MONO;
}
@Override
public Mono<String> testMethod5(String test) {
return TEST_MONO;
}
@NewSpan(name = "customNameOnTestMethod6")
@Override
public Mono<String> testMethod6(@SpanTag("testTag6") String test) {
return TEST_MONO;
}
@Override
public Mono<String> testMethod7() {
return TEST_MONO;
}
@Override
public Mono<String> testMethod8(String param) {
return TEST_MONO;
}
@NewSpan(name = "customNameOnTestMethod9")
@Override
public Mono<String> testMethod9(String param) {
return TEST_MONO;
}
@Override
public Mono<String> testMethod10(@SpanTag(value = "customTestTag10") String param) {
return TEST_MONO;
}
@Override
public Mono<String> testMethod10_v2(@SpanTag(key = "customTestTag10") String param) {
return TEST_MONO;
}
@ContinueSpan(log = "customTest")
@Override
public Mono<String> testMethod11(@SpanTag("customTestTag11") String param) {
return TEST_MONO;
}
@Override
public Mono<String> testMethod12(String param) {
return Mono.defer(() -> Mono.error(new RuntimeException("test exception 12")));
}
@Override
public Mono<String> testMethod13() {
return Mono.defer(() -> Mono.error(new RuntimeException("test exception 13")));
}
}
@Configuration
@EnableAutoConfiguration
protected static class TestConfiguration {
@Bean
public TestBeanInterface testBean() {
return new TestBean();
}
@Bean Reporter<zipkin2.Span> spanReporter() {
return new ArrayListSpanReporter();
}
@Bean Sampler alwaysSampler() {
return Sampler.ALWAYS_SAMPLE;
}
}
}

View File

@@ -60,6 +60,7 @@ public class SleuthSpanCreatorAspectTests {
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("test-method");
then(spans.get(0).duration()).isNotZero();
then(this.tracer.currentSpan()).isNull();
}
@Test
@@ -70,6 +71,7 @@ public class SleuthSpanCreatorAspectTests {
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("test-method2");
then(spans.get(0).duration()).isNotZero();
then(this.tracer.currentSpan()).isNull();
}
@Test
@@ -80,6 +82,7 @@ public class SleuthSpanCreatorAspectTests {
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("custom-name-on-test-method3");
then(spans.get(0).duration()).isNotZero();
then(this.tracer.currentSpan()).isNull();
}
@Test
@@ -90,6 +93,7 @@ public class SleuthSpanCreatorAspectTests {
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("custom-name-on-test-method4");
then(spans.get(0).duration()).isNotZero();
then(this.tracer.currentSpan()).isNull();
}
@Test
@@ -103,6 +107,7 @@ public class SleuthSpanCreatorAspectTests {
then(spans.get(0).name()).isEqualTo("custom-name-on-test-method5");
then(spans.get(0).tags()).containsEntry("testTag", "test");
then(spans.get(0).duration()).isNotZero();
then(this.tracer.currentSpan()).isNull();
}
@Test
@@ -114,6 +119,7 @@ public class SleuthSpanCreatorAspectTests {
then(spans.get(0).name()).isEqualTo("custom-name-on-test-method6");
then(spans.get(0).tags()).containsEntry("testTag6", "test");
then(spans.get(0).duration()).isNotZero();
then(this.tracer.currentSpan()).isNull();
}
@Test
@@ -124,6 +130,7 @@ public class SleuthSpanCreatorAspectTests {
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("custom-name-on-test-method8");
then(spans.get(0).duration()).isNotZero();
then(this.tracer.currentSpan()).isNull();
}
@Test
@@ -137,6 +144,7 @@ public class SleuthSpanCreatorAspectTests {
.containsEntry("class", "TestBean")
.containsEntry("method", "testMethod9");
then(spans.get(0).duration()).isNotZero();
then(this.tracer.currentSpan()).isNull();
}
@Test
@@ -149,7 +157,7 @@ public class SleuthSpanCreatorAspectTests {
span.finish();
}
List<zipkin2.Span> spans = new ArrayList<>(this.reporter.getSpans());
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("foo");
then(spans.get(0).tags())
@@ -158,6 +166,23 @@ public class SleuthSpanCreatorAspectTests {
.stream().map(Annotation::value).collect(Collectors.toList()))
.contains("customTest.before", "customTest.after");
then(spans.get(0).duration()).isNotZero();
then(this.tracer.currentSpan()).isNull();
}
@Test
public void shouldStartAndCloseSpanOnContinueSpanIfSpanNotSet() {
this.testBean.testMethod10("test");
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("test-method10");
then(spans.get(0).tags())
.containsEntry("customTestTag10", "test");
then(spans.get(0).annotations()
.stream().map(Annotation::value).collect(Collectors.toList()))
.contains("customTest.before", "customTest.after");
then(spans.get(0).duration()).isNotZero();
then(this.tracer.currentSpan()).isNull();
}
@Test
@@ -170,7 +195,7 @@ public class SleuthSpanCreatorAspectTests {
span.finish();
}
List<zipkin2.Span> spans = new ArrayList<>(this.reporter.getSpans());
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("foo");
then(spans.get(0).tags())
@@ -179,6 +204,7 @@ public class SleuthSpanCreatorAspectTests {
.stream().map(Annotation::value).collect(Collectors.toList()))
.contains("customTest.before", "customTest.after");
then(spans.get(0).duration()).isNotZero();
then(this.tracer.currentSpan()).isNull();
}
@Test
@@ -193,7 +219,7 @@ public class SleuthSpanCreatorAspectTests {
span.finish();
}
List<zipkin2.Span> spans = new ArrayList<>(this.reporter.getSpans());
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("foo");
then(spans.get(0).tags())
@@ -204,6 +230,7 @@ public class SleuthSpanCreatorAspectTests {
.stream().map(Annotation::value).collect(Collectors.toList()))
.contains("customTest.before", "customTest.after");
then(spans.get(0).duration()).isNotZero();
then(this.tracer.currentSpan()).isNull();
}
@Test
@@ -213,13 +240,14 @@ public class SleuthSpanCreatorAspectTests {
} catch (RuntimeException ignored) {
}
List<zipkin2.Span> spans = new ArrayList<>(this.reporter.getSpans());
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("test-method12");
then(spans.get(0).tags())
.containsEntry("testTag12", "test")
.containsEntry("error", "test exception 12");
then(spans.get(0).duration()).isNotZero();
then(this.tracer.currentSpan()).isNull();
}
@Test
@@ -235,7 +263,7 @@ public class SleuthSpanCreatorAspectTests {
span.finish();
}
List<zipkin2.Span> spans = new ArrayList<>(this.reporter.getSpans());
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).hasSize(1);
then(spans.get(0).name()).isEqualTo("foo");
then(spans.get(0).tags())
@@ -245,14 +273,16 @@ public class SleuthSpanCreatorAspectTests {
.contains("testMethod13.before", "testMethod13.afterFailure",
"testMethod13.after");
then(spans.get(0).duration()).isNotZero();
then(this.tracer.currentSpan()).isNull();
}
@Test
public void shouldNotCreateSpanWhenNotAnnotated() {
this.testBean.testMethod7();
List<zipkin2.Span> spans = new ArrayList<>(this.reporter.getSpans());
List<zipkin2.Span> spans = this.reporter.getSpans();
then(spans).isEmpty();
then(this.tracer.currentSpan()).isNull();
}
protected interface TestBeanInterface {

View File

@@ -90,6 +90,16 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.reactivestreams</groupId>
<artifactId>reactive-streams</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -109,6 +109,14 @@
<groupId>io.zipkin.zipkin2</groupId>
<artifactId>zipkin</artifactId>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
</dependency>
<dependency>
<groupId>org.reactivestreams</groupId>
<artifactId>reactive-streams</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -77,6 +77,16 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.reactivestreams</groupId>
<artifactId>reactive-streams</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -156,6 +156,16 @@
<artifactId>aspectjweaver</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.reactivestreams</groupId>
<artifactId>reactive-streams</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>