From 0f29735c1112e8586103a274b1d8fcbdc8a7d189 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Mon, 27 Feb 2017 15:26:03 +0100 Subject: [PATCH] Managing spans with annotations (#526) The main arguments for these features are * api-agnostic means to collaborate with a span - use of annotations allows users to add to a span with no library dependency on a span api. This allows Sleuth to change its core api less impact to user code. * reduced surface area for basic span operations. - without this feature one has to use the span api, which has lifecycle commands that could be used incorrectly. By only exposing scope, tag and log functionality, users can collaborate without accidentally breaking span lifecycle. * collaboration with runtime generated code - with libraries such as Spring Data / Feign the implementations of interfaces are generated at runtime thus span wrapping of objects was tedious. Now you can provide annotations over interfaces and arguments of those interfaces This PR is an adoption of @Koizumi85 work started here - https://github.com/Koizumi85/spring-cloud-sleuth-annotation fixes #182 --- README.adoc | 2 + benchmarks/pom.xml | 2 +- .../app/SleuthBenchmarkingSpringApp.java | 75 +++- .../jmh/benchmarks/AnnotationBenchmarks.java | 81 ++++ .../jmh/benchmarks/HttpFilterBenchmarks.java | 1 - .../jmh/benchmarks/RestTemplateBenchmark.java | 1 - docs/src/main/asciidoc/features.adoc | 2 + .../main/asciidoc/spring-cloud-sleuth.adoc | 140 +++++++ scripts/runJmhBenchmarks.sh | 2 +- .../cloud/sleuth/annotation/ContinueSpan.java | 42 +++ .../sleuth/annotation/DefaultSpanCreator.java | 63 ++++ .../cloud/sleuth/annotation/NewSpan.java | 56 +++ .../annotation/NoOpTagValueResolver.java | 29 ++ .../annotation/SleuthAdvisorConfig.java | 324 ++++++++++++++++ .../annotation/SleuthAnnotatedParameter.java | 40 ++ .../SleuthAnnotationAutoConfiguration.java | 68 ++++ .../SleuthAnnotationProperties.java | 39 ++ .../annotation/SleuthAnnotationUtils.java | 82 +++++ .../cloud/sleuth/annotation/SpanCreator.java | 35 ++ .../cloud/sleuth/annotation/SpanTag.java | 68 ++++ .../annotation/SpanTagAnnotationHandler.java | 149 ++++++++ .../SpelTagValueExpressionResolver.java | 48 +++ .../TagValueExpressionResolver.java | 36 ++ .../sleuth/annotation/TagValueResolver.java | 19 + .../main/resources/META-INF/spring.factories | 3 +- .../annotation/NoOpTagValueResolverTests.java | 31 ++ ...euthSpanCreatorAnnotationDisableTests.java | 38 ++ ...uthSpanCreatorAnnotationNoSleuthTests.java | 41 +++ .../SleuthSpanCreatorAspectNegativeTests.java | 156 ++++++++ .../SleuthSpanCreatorAspectTests.java | 347 ++++++++++++++++++ ...uthSpanCreatorCircularDependencyTests.java | 64 ++++ .../SpanTagAnnotationHandlerTests.java | 127 +++++++ .../SpelTagValueExpressionResolverTests.java | 50 +++ .../sleuth/assertions/ListOfSpansAssert.java | 19 + 34 files changed, 2273 insertions(+), 7 deletions(-) create mode 100644 benchmarks/src/main/java/org/springframework/cloud/sleuth/benchmarks/jmh/benchmarks/AnnotationBenchmarks.java create mode 100644 spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/ContinueSpan.java create mode 100644 spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/DefaultSpanCreator.java create mode 100644 spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/NewSpan.java create mode 100644 spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/NoOpTagValueResolver.java create mode 100644 spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SleuthAdvisorConfig.java create mode 100644 spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SleuthAnnotatedParameter.java create mode 100644 spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SleuthAnnotationAutoConfiguration.java create mode 100644 spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SleuthAnnotationProperties.java create mode 100644 spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SleuthAnnotationUtils.java create mode 100644 spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SpanCreator.java create mode 100644 spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SpanTag.java create mode 100644 spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SpanTagAnnotationHandler.java create mode 100644 spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SpelTagValueExpressionResolver.java create mode 100644 spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/TagValueExpressionResolver.java create mode 100644 spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/TagValueResolver.java create mode 100644 spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/NoOpTagValueResolverTests.java create mode 100644 spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SleuthSpanCreatorAnnotationDisableTests.java create mode 100644 spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SleuthSpanCreatorAnnotationNoSleuthTests.java create mode 100644 spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SleuthSpanCreatorAspectNegativeTests.java create mode 100644 spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SleuthSpanCreatorAspectTests.java create mode 100644 spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SleuthSpanCreatorCircularDependencyTests.java create mode 100644 spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SpanTagAnnotationHandlerTests.java create mode 100644 spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SpelTagValueExpressionResolverTests.java diff --git a/README.adoc b/README.adoc index c7bf411e4..9a24e7ec1 100644 --- a/README.adoc +++ b/README.adoc @@ -629,6 +629,8 @@ works via Zipkin-compatible request headers. This propagation logic is defined a * Sleuth gives you the possibility to propagate context (also known as baggage) between processes. That means that if you set on a Span a baggage element then it will be sent downstream either via HTTP or messaging to other processes. +* Provides a way to create / continue spans and add tags and logs via annotations. + * Provides simple metrics of accepted / dropped spans. * If `spring-cloud-sleuth-zipkin` then the app will generate and collect Zipkin-compatible traces. diff --git a/benchmarks/pom.xml b/benchmarks/pom.xml index 52822c896..f25490e2f 100644 --- a/benchmarks/pom.xml +++ b/benchmarks/pom.xml @@ -248,7 +248,7 @@ org.springframework.boot spring-boot-maven-plugin - 1.3.3.RELEASE + 1.5.1.RELEASE diff --git a/benchmarks/src/main/java/org/springframework/cloud/sleuth/benchmarks/app/SleuthBenchmarkingSpringApp.java b/benchmarks/src/main/java/org/springframework/cloud/sleuth/benchmarks/app/SleuthBenchmarkingSpringApp.java index 83eb20103..9f112b678 100644 --- a/benchmarks/src/main/java/org/springframework/cloud/sleuth/benchmarks/app/SleuthBenchmarkingSpringApp.java +++ b/benchmarks/src/main/java/org/springframework/cloud/sleuth/benchmarks/app/SleuthBenchmarkingSpringApp.java @@ -26,6 +26,7 @@ import javax.annotation.PreDestroy; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -33,8 +34,15 @@ import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory import org.springframework.boot.context.embedded.EmbeddedServletContainerInitializedEvent; import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory; import org.springframework.cloud.sleuth.Sampler; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.annotation.ContinueSpan; +import org.springframework.cloud.sleuth.annotation.NewSpan; +import org.springframework.cloud.sleuth.annotation.SpanTag; import org.springframework.cloud.sleuth.sampler.AlwaysSampler; +import org.springframework.cloud.sleuth.util.ArrayListSpanAccumulator; import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.EnableAsync; @@ -57,6 +65,9 @@ public class SleuthBenchmarkingSpringApp implements public int port; + @Autowired(required = false) Tracer tracer; + @Autowired AClass aClass; + @RequestMapping("/foo") public String foo() { return "foo"; @@ -77,6 +88,14 @@ public class SleuthBenchmarkingSpringApp implements return this.pool.submit(() -> "async"); } + public String manualSpan() { + return this.aClass.manualSpan(); + } + + public String newSpan() { + return this.aClass.newSpan(); + } + @Override public void onApplicationEvent(EmbeddedServletContainerInitializedEvent event) { this.port = event.getEmbeddedServletContainer().getPort(); @@ -93,11 +112,18 @@ public class SleuthBenchmarkingSpringApp implements this.pool.shutdownNow(); } - @Bean - public Sampler alwaysSampler() { + @Bean Sampler alwaysSampler() { return new AlwaysSampler(); } + @Bean AnotherClass anotherClass() { + return new AnotherClass(this.tracer); + } + + @Bean AClass aClass() { + return new AClass(this.tracer, anotherClass()); + } + public ExecutorService getPool() { return this.pool; } @@ -106,3 +132,48 @@ public class SleuthBenchmarkingSpringApp implements SpringApplication.run(SleuthBenchmarkingSpringApp.class, args); } } +class AClass { + private final Tracer tracer; + private final AnotherClass anotherClass; + + AClass(Tracer tracer, AnotherClass anotherClass) { + this.tracer = tracer; + this.anotherClass = anotherClass; + } + + public String manualSpan() { + Span manual = this.tracer.createSpan("span-name"); + try { + return this.anotherClass.continuedSpan(); + } finally { + this.tracer.close(manual); + } + } + + @NewSpan + public String newSpan() { + return this.anotherClass.continuedAnnotation("bar"); + } +} + +class AnotherClass { + private final Tracer tracer; + + AnotherClass(Tracer tracer) { + this.tracer = tracer; + } + + @ContinueSpan(log = "continuedspan") + public String continuedAnnotation(@SpanTag("foo") String tagValue) { + return "continued"; + } + + public String continuedSpan() { + Span continuedSpan = this.tracer.continueSpan(this.tracer.getCurrentSpan()); + this.tracer.addTag("foo", "bar"); + continuedSpan.logEvent("continuedspan.before"); + String response = "continued"; + continuedSpan.logEvent("continuedspan.after"); + return response; + } +} diff --git a/benchmarks/src/main/java/org/springframework/cloud/sleuth/benchmarks/jmh/benchmarks/AnnotationBenchmarks.java b/benchmarks/src/main/java/org/springframework/cloud/sleuth/benchmarks/jmh/benchmarks/AnnotationBenchmarks.java new file mode 100644 index 000000000..88e59d232 --- /dev/null +++ b/benchmarks/src/main/java/org/springframework/cloud/sleuth/benchmarks/jmh/benchmarks/AnnotationBenchmarks.java @@ -0,0 +1,81 @@ +/* + * Copyright 2013-2016 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.benchmarks.jmh.benchmarks; + +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.springframework.boot.SpringApplication; +import org.springframework.cloud.sleuth.benchmarks.app.SleuthBenchmarkingSpringApp; +import org.springframework.cloud.sleuth.util.ExceptionUtils; +import org.springframework.context.ConfigurableApplicationContext; + +import static org.assertj.core.api.BDDAssertions.then; + +@Measurement(iterations = 5) +@Warmup(iterations = 10) +@Fork(3) +@BenchmarkMode(Mode.SampleTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@Threads(Threads.MAX) +public class AnnotationBenchmarks { + + @State(Scope.Benchmark) + public static class BenchmarkContext { + volatile ConfigurableApplicationContext withSleuth; + volatile SleuthBenchmarkingSpringApp sleuth; + + @Setup public void setup() { + this.withSleuth = new SpringApplication( + SleuthBenchmarkingSpringApp.class) + .run("--spring.jmx.enabled=false", + "--spring.application.name=withSleuth"); + this.sleuth = this.withSleuth.getBean( + SleuthBenchmarkingSpringApp.class); + } + + @TearDown public void clean() { + this.sleuth.clean(); + this.withSleuth.close(); + } + } + + @Benchmark + public void manuallyCreatedSpans(BenchmarkContext context) + throws Exception { + then(context.sleuth.manualSpan()).isEqualTo("continued"); + then(ExceptionUtils.getLastException()).isNull(); + } + + @Benchmark + public void spanCreatedWithAnnotations(BenchmarkContext context) + throws Exception { + then(context.sleuth.newSpan()).isEqualTo("continued"); + then(ExceptionUtils.getLastException()).isNull(); + } +} \ No newline at end of file diff --git a/benchmarks/src/main/java/org/springframework/cloud/sleuth/benchmarks/jmh/benchmarks/HttpFilterBenchmarks.java b/benchmarks/src/main/java/org/springframework/cloud/sleuth/benchmarks/jmh/benchmarks/HttpFilterBenchmarks.java index a42e5a607..1efa727d7 100644 --- a/benchmarks/src/main/java/org/springframework/cloud/sleuth/benchmarks/jmh/benchmarks/HttpFilterBenchmarks.java +++ b/benchmarks/src/main/java/org/springframework/cloud/sleuth/benchmarks/jmh/benchmarks/HttpFilterBenchmarks.java @@ -18,7 +18,6 @@ package org.springframework.cloud.sleuth.benchmarks.jmh.benchmarks; import java.io.IOException; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; - import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; diff --git a/benchmarks/src/main/java/org/springframework/cloud/sleuth/benchmarks/jmh/benchmarks/RestTemplateBenchmark.java b/benchmarks/src/main/java/org/springframework/cloud/sleuth/benchmarks/jmh/benchmarks/RestTemplateBenchmark.java index 76b2aa2b1..c585ffdf5 100644 --- a/benchmarks/src/main/java/org/springframework/cloud/sleuth/benchmarks/jmh/benchmarks/RestTemplateBenchmark.java +++ b/benchmarks/src/main/java/org/springframework/cloud/sleuth/benchmarks/jmh/benchmarks/RestTemplateBenchmark.java @@ -18,7 +18,6 @@ package org.springframework.cloud.sleuth.benchmarks.jmh.benchmarks; import java.io.IOException; import java.util.Collections; import java.util.concurrent.TimeUnit; - import javax.servlet.ServletException; import org.openjdk.jmh.annotations.Benchmark; diff --git a/docs/src/main/asciidoc/features.adoc b/docs/src/main/asciidoc/features.adoc index ed11c382a..2bf7d0027 100644 --- a/docs/src/main/asciidoc/features.adoc +++ b/docs/src/main/asciidoc/features.adoc @@ -37,6 +37,8 @@ works via Zipkin-compatible request headers. This propagation logic is defined a * Sleuth gives you the possibility to propagate context (also known as baggage) between processes. That means that if you set on a Span a baggage element then it will be sent downstream either via HTTP or messaging to other processes. +* Provides a way to create / continue spans and add tags and logs via annotations. + * Provides simple metrics of accepted / dropped spans. * If `spring-cloud-sleuth-zipkin` then the app will generate and collect Zipkin-compatible traces. diff --git a/docs/src/main/asciidoc/spring-cloud-sleuth.adoc b/docs/src/main/asciidoc/spring-cloud-sleuth.adoc index 467c8eed6..c6fe83ec1 100644 --- a/docs/src/main/asciidoc/spring-cloud-sleuth.adoc +++ b/docs/src/main/asciidoc/spring-cloud-sleuth.adoc @@ -201,6 +201,146 @@ include::../../../../spring-cloud-sleuth-core/src/test/java/org/springframework/ will lead in creating a span named `calculateTax`. +== Managing spans with annotations + +=== Rationale + +The main arguments for this features are + +* api-agnostic means to collaborate with a span + - use of annotations allows users to add to a span with no library dependency on a span api. + This allows Sleuth to change its core api less impact to user code. +* reduced surface area for basic span operations. + - without this feature one has to use the span api, which has lifecycle commands that + could be used incorrectly. By only exposing scope, tag and log functionality, users can + collaborate without accidentally breaking span lifecycle. +* collaboration with runtime generated code + - with libraries such as Spring Data / Feign the implementations of interfaces are generated + at runtime thus span wrapping of objects was tedious. Now you can provide annotations + over interfaces and arguments of those interfaces + +=== Creating new spans + +If you really don't want to take care of creating local spans manually you can profit from the +`@NewSpan` annotation. Also we give you the `@SpanTag` annotation to add tags in an automated +fashion. + +Let's look at some examples of usage. + +[source,java] +---- +include::../../../../spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SleuthSpanCreatorAspectTests.java[tags=annotated_method,indent=0] +---- + +Annotating the method without any parameter will lead to a creation of a new span whose name +will be equal to annotated method name. + +[source,java] +---- +include::../../../../spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SleuthSpanCreatorAspectTests.java[tags=custom_name_on_annotated_method,indent=0] +---- + +If you provide the value in the annotation (either directly or via the `name` parameter) then +the created span will have the name as the provided value. + +[source,java] +---- +// method declaration +include::../../../../spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SleuthSpanCreatorAspectTests.java[tags=custom_name_and_tag_on_annotated_method,indent=0] + +// and method execution +include::../../../../spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SleuthSpanCreatorAspectTests.java[tags=execution,indent=0] +---- + +You can combine both the name and a tag. Let's focus on the latter. In this case whatever the value of +the annotated method's parameter runtime value will be - that will be the value of the tag. In our sample +the tag key will be `testTag` and the tag value will be `test`. + +[source,java] +---- +include::../../../../spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SleuthSpanCreatorAspectTests.java[tags=name_on_implementation,indent=0] +---- + +You can place the `@NewSpan` annotation on both the class and an interface. If you override the +interface's method and provide a different value of the `@NewSpan` annotation then the most +concrete one wins (in this case `customNameOnTestMethod3` will be set). + +=== Continuing spans + +If you want to just add tags and annotations to an existing span it's enough +to use the `@ContinueSpan` annotation as presented below. Note that in contrast +with the `@NewSpan` annotation you can also add logs via the `log` parameter: + +[source,java] +---- +// method declaration +include::../../../../spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SleuthSpanCreatorAspectTests.java[tags=continue_span,indent=0] + +// method execution +include::../../../../spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SleuthSpanCreatorAspectTests.java[tags=continue_span_execution,indent=0] +---- + +That way the span will get continued and: + + - logs with name `testMethod11.before` and `testMethod11.after` will be created + - if an exception will be thrown a log `testMethod11.afterFailure` will also be created + - tag with key `testTag11` and value `test` will be created + +=== More advanced tag setting + +There are 3 different ways to add tags to a span. All of them are controlled by the `SpanTag` annotation. +Precedence is: + +- try with the bean of `TagValueResolver` type and provided name +- if one hasn't provided the bean name, try to evaluate an expression. We're searching for a `TagValueExpressionResolver` bean. +The default implementation uses SPEL expression resolution. +- if one hasn't provided any expression to evaluate just return a `toString()` value of the parameter + +==== Custom extractor + +The value of the tag for following method will be computed by an implementation of `TagValueResolver` interface. +Its class name has to be passed as the value of the `resolver` attribute. + +Having such an annotated method: + +[source,java] +---- +include::../../../../spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SpanTagAnnotationHandlerTests.java[tags=resolver_bean,indent=0] +---- + +and such a `TagValueResolver` bean implementation + +[source,java] +---- +include::../../../../spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SpanTagAnnotationHandlerTests.java[tags=custom_resolver,indent=0] +---- + +Will lead to setting of a tag value equal to `Value from myCustomTagValueResolver`. + +==== Resolving expressions for value + +Having such an annotated method: + +[source,java] +---- +include::../../../../spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SpanTagAnnotationHandlerTests.java[tags=spel,indent=0] +---- + +and no custom implementation of a `TagValueExpressionResolver` will lead to evaluation of the SPEL expression and a tag with value `4 characters` will be set on the span. +If you want to use some other expression resolution mechanism you can create your own implementation +of the bean. + +==== Using toString method + +Having such an annotated method: + +[source,java] +---- +include::../../../../spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SpanTagAnnotationHandlerTests.java[tags=toString,indent=0] +---- + +if executed with a value of `15` will lead to setting of a tag with a String value of `"15"`. + == Customizations Thanks to the `SpanInjector` and `SpanExtractor` you can customize the way spans diff --git a/scripts/runJmhBenchmarks.sh b/scripts/runJmhBenchmarks.sh index 3467056c4..d4e76f71b 100755 --- a/scripts/runJmhBenchmarks.sh +++ b/scripts/runJmhBenchmarks.sh @@ -2,4 +2,4 @@ echo "Running JMH Benchmarks" ./mvnw clean install -DskipTests --projects benchmarks --also-make -Pbenchmarks,jmh -java -Djmh.ignoreLock=true -jar benchmarks/target/benchmarks.jar org.springframework.cloud.sleuth.benchmarks.jmh.* | tee target/benchmarks.log \ No newline at end of file +java -Djmh.ignoreLock=true -jar benchmarks/target/benchmarks.jar org.springframework.cloud.sleuth.benchmarks.jmh.* -rf csv -rff target/jmh-result.csv | tee target/benchmarks.log \ No newline at end of file diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/ContinueSpan.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/ContinueSpan.java new file mode 100644 index 000000000..b013ffa65 --- /dev/null +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/ContinueSpan.java @@ -0,0 +1,42 @@ +/* + * Copyright 2013-2016 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 java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Tells Sleuth that all Sleuth related annotations should be applied + * to an existing span instead of creating a new one. + * + * @author Marcin Grzejszczak + * @since 1.2.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Target(value = { ElementType.METHOD, ElementType.TYPE }) +public @interface ContinueSpan { + + /** + * The value passed to the annotation will be used and the framework + * will create two events with the {@code .start} and {@code .end} suffixes + */ + String log() default ""; +} diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/DefaultSpanCreator.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/DefaultSpanCreator.java new file mode 100644 index 000000000..6ff198861 --- /dev/null +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/DefaultSpanCreator.java @@ -0,0 +1,63 @@ +/* + * Copyright 2013-2016 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 java.lang.invoke.MethodHandles; + +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.util.SpanNameUtil; +import org.springframework.util.StringUtils; + +/** + * Default implementation of the {@link SpanCreator} that creates + * a new span around the annotated method. + * + * @author Christian Schwerdtfeger + * @since 1.2.0 + */ +class DefaultSpanCreator implements SpanCreator { + + private static final Log log = LogFactory.getLog(MethodHandles.lookup().lookupClass()); + + private final Tracer tracer; + + DefaultSpanCreator(Tracer tracer) { + this.tracer = tracer; + } + + @Override public Span createSpan(MethodInvocation pjp, NewSpan newSpanAnnotation) { + String name = StringUtils.isEmpty(newSpanAnnotation.name()) ? + pjp.getMethod().getName() : newSpanAnnotation.name(); + String changedName = SpanNameUtil.toLowerHyphen(name); + if (log.isDebugEnabled()) { + log.debug("For the class [" + pjp.getThis().getClass() + "] method " + + "[" + pjp.getMethod().getName() + "] will name the span [" + changedName + "]"); + } + return createSpan(changedName); + } + + private Span createSpan(String name) { + if (this.tracer.isTracing()) { + return this.tracer.createSpan(name, this.tracer.getCurrentSpan()); + } + return this.tracer.createSpan(name); + } + +} diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/NewSpan.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/NewSpan.java new file mode 100644 index 000000000..fa97deaeb --- /dev/null +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/NewSpan.java @@ -0,0 +1,56 @@ +/* + * Copyright 2013-2016 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 java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +/** + * Allows to create a new span around a public method or a class. The new span + * will be either a child of an existing span if a trace is already in progress + * or a new span will be created if there was no previous trace. + *

+ * Method parameters can be annotated with {@link SpanTag}, which will end + * in adding the parameter value as a tag value to the span. The tag key will be + * the value of the {@code key} annotation from {@link SpanTag}. + * + * + * @author Christian Schwerdtfeger + * @since 1.2.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Target(value = { ElementType.METHOD, ElementType.TYPE }) +public @interface NewSpan { + + /** + * The name of the span which will be created. Default is the annotated method's name separated by hyphens. + */ + @AliasFor("value") + String name() default ""; + + /** + * The name of the span which will be created. Default is the annotated method's name separated by hyphens. + */ + @AliasFor("name") + String value() default ""; + +} diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/NoOpTagValueResolver.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/NoOpTagValueResolver.java new file mode 100644 index 000000000..13a20ab2d --- /dev/null +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/NoOpTagValueResolver.java @@ -0,0 +1,29 @@ +/* + * Copyright 2013-2016 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; + +/** + * Does nothing + * + * @author Marcin Grzejszczak + * @since 1.2.0 + */ +class NoOpTagValueResolver implements TagValueResolver { + @Override public String resolve(Object parameter) { + return null; + } +} diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SleuthAdvisorConfig.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SleuthAdvisorConfig.java new file mode 100644 index 000000000..19452465f --- /dev/null +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SleuthAdvisorConfig.java @@ -0,0 +1,324 @@ +/* + * Copyright 2013-2016 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 java.lang.annotation.Annotation; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Method; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.annotation.PostConstruct; + +import org.aopalliance.aop.Advice; +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.aop.ClassFilter; +import org.springframework.aop.IntroductionAdvisor; +import org.springframework.aop.IntroductionInterceptor; +import org.springframework.aop.Pointcut; +import org.springframework.aop.support.AbstractPointcutAdvisor; +import org.springframework.aop.support.AopUtils; +import org.springframework.aop.support.DynamicMethodMatcherPointcut; +import org.springframework.aop.support.annotation.AnnotationClassFilter; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.util.ExceptionUtils; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * Custom pointcut advisor that picks all classes / interfaces that + * have the Sleuth related annotations. + * + * @author Marcin Grzejszczak + * @since 1.2.0 + */ +class SleuthAdvisorConfig extends AbstractPointcutAdvisor implements + IntroductionAdvisor, BeanFactoryAware { + private static final Log log = LogFactory.getLog(MethodHandles.lookup().lookupClass()); + + private Advice advice; + + private Pointcut pointcut; + + private BeanFactory beanFactory; + + @PostConstruct + public void init() { + this.pointcut = buildPointcut(); + this.advice = buildAdvice(); + if (this.advice instanceof BeanFactoryAware) { + ((BeanFactoryAware) this.advice).setBeanFactory(this.beanFactory); + } + } + + /** + * Set the {@code BeanFactory} to be used when looking up executors by qualifier. + */ + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public ClassFilter getClassFilter() { + return this.pointcut.getClassFilter(); + } + + @Override + public Class[] getInterfaces() { + return new Class[] {}; + } + + @Override + public void validateInterfaces() throws IllegalArgumentException { + } + + @Override + public Advice getAdvice() { + return this.advice; + } + + @Override + public Pointcut getPointcut() { + return this.pointcut; + } + + private Advice buildAdvice() { + return new SleuthInterceptor(); + } + + private Pointcut buildPointcut() { + return new AnnotationClassOrMethodOrArgsPointcut(); + } + + /** + * Checks if a class or a method is is annotated with Sleuth related annotations + */ + private final class AnnotationClassOrMethodOrArgsPointcut extends + DynamicMethodMatcherPointcut { + + private final DynamicMethodMatcherPointcut methodResolver; + + AnnotationClassOrMethodOrArgsPointcut() { + this.methodResolver = new DynamicMethodMatcherPointcut() { + @Override public boolean matches(Method method, Class targetClass, + Object... args) { + if (SleuthAnnotationUtils.isMethodAnnotated(method)) { + if (log.isDebugEnabled()) { + log.debug("Found a method with Sleuth annotation"); + } + return true; + } + if (SleuthAnnotationUtils.hasAnnotatedParams(method, args)) { + if (log.isDebugEnabled()) { + log.debug("Found annotated arguments of the method"); + } + return true; + } + return false; + } + }; + } + + @Override + public boolean matches(Method method, Class targetClass, Object... args) { + return getClassFilter().matches(targetClass) || + this.methodResolver.matches(method, targetClass, args); + } + + @Override public ClassFilter getClassFilter() { + return new ClassFilter() { + @Override public boolean matches(Class clazz) { + return new AnnotationClassOrMethodFilter(NewSpan.class).matches(clazz) || + new AnnotationClassOrMethodFilter(ContinueSpan.class).matches(clazz); + } + }; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof AnnotationClassOrMethodOrArgsPointcut)) { + return false; + } + AnnotationClassOrMethodOrArgsPointcut otherAdvisor = (AnnotationClassOrMethodOrArgsPointcut) other; + return ObjectUtils.nullSafeEquals(this.methodResolver, otherAdvisor.methodResolver); + } + + } + + private final class AnnotationClassOrMethodFilter extends AnnotationClassFilter { + + private final AnnotationMethodsResolver methodResolver; + + AnnotationClassOrMethodFilter(Class annotationType) { + super(annotationType, true); + this.methodResolver = new AnnotationMethodsResolver(annotationType); + } + + @Override + public boolean matches(Class clazz) { + return super.matches(clazz) || this.methodResolver.hasAnnotatedMethods(clazz); + } + + } + + /** + * Checks if a method is properly annotated with a given Sleuth annotation + */ + private static class AnnotationMethodsResolver { + + private Class annotationType; + + public AnnotationMethodsResolver(Class annotationType) { + this.annotationType = annotationType; + } + + 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, + AnnotationMethodsResolver.this.annotationType); + if (annotation != null) { found.set(true); } + } + }); + return found.get(); + } + + } +} + +/** + * Interceptor that creates or continues a span depending on the provided + * annotation. Also it adds logs and tags if necessary. + */ +class SleuthInterceptor implements IntroductionInterceptor, BeanFactoryAware { + + private static final Log logger = LogFactory.getLog(MethodHandles.lookup().lookupClass()); + + private BeanFactory beanFactory; + private SpanCreator spanCreator; + private Tracer tracer; + private SpanTagAnnotationHandler spanTagAnnotationHandler; + + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + Method method = invocation.getMethod(); + if (method == null) { + return invocation.proceed(); + } + Method mostSpecificMethod = AopUtils + .getMostSpecificMethod(method, invocation.getThis().getClass()); + NewSpan newSpan = SleuthAnnotationUtils.findAnnotation(mostSpecificMethod, NewSpan.class); + ContinueSpan continueSpan = SleuthAnnotationUtils.findAnnotation(mostSpecificMethod, ContinueSpan.class); + if (newSpan == null && continueSpan == null) { + return invocation.proceed(); + } + Span span = tracer().getCurrentSpan(); + String log = log(continueSpan); + boolean hasLog = StringUtils.hasText(log); + try { + if (newSpan != null) { + span = spanCreator().createSpan(invocation, newSpan); + } + if (hasLog) { + logEvent(span, log + ".before"); + } + spanTagAnnotationHandler().addAnnotatedParameters(invocation); + 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"); + } + tracer().addTag(Span.SPAN_ERROR_TAG_NAME, ExceptionUtils.getExceptionMessage(e)); + throw e; + } finally { + if (span != null) { + if (hasLog) { + logEvent(span, log + ".after"); + } + if (newSpan != null) { + tracer().close(span); + } + } + } + } + + private void logEvent(Span span, String name) { + if (span == null) { + logger.warn("You were trying to continue a span which was null. Please " + + "remember that if two proxied methods are calling each other from " + + "the same class then the aspect will not be properly resolved"); + return; + } + span.logEvent(name); + } + + private String log(ContinueSpan continueSpan) { + if (continueSpan != null) { + return continueSpan.log(); + } + return ""; + } + + private Tracer tracer() { + if (this.tracer == null) { + this.tracer = this.beanFactory.getBean(Tracer.class); + } + return this.tracer; + } + + private SpanCreator spanCreator() { + if (this.spanCreator == null) { + this.spanCreator = this.beanFactory.getBean(SpanCreator.class); + } + return this.spanCreator; + } + + private SpanTagAnnotationHandler spanTagAnnotationHandler() { + if (this.spanTagAnnotationHandler == null) { + this.spanTagAnnotationHandler = new SpanTagAnnotationHandler(this.beanFactory); + } + return this.spanTagAnnotationHandler; + } + + @Override public boolean implementsInterface(Class intf) { + return true; + } + + @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } +} diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SleuthAnnotatedParameter.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SleuthAnnotatedParameter.java new file mode 100644 index 000000000..cf890fcd1 --- /dev/null +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SleuthAnnotatedParameter.java @@ -0,0 +1,40 @@ +/* + * Copyright 2013-2016 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; + +/** + * A container class that holds information about the parameter + * of the annotated method argument. + * + * @author Christian Schwerdtfeger + * @since 1.2.0 + */ +class SleuthAnnotatedParameter { + + int parameterIndex; + + SpanTag annotation; + + Object argument; + + SleuthAnnotatedParameter(int parameterIndex, SpanTag annotation, + Object argument) { + this.parameterIndex = parameterIndex; + this.annotation = annotation; + this.argument = argument; + } + +} diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SleuthAnnotationAutoConfiguration.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SleuthAnnotationAutoConfiguration.java new file mode 100644 index 000000000..3cf1cefa1 --- /dev/null +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SleuthAnnotationAutoConfiguration.java @@ -0,0 +1,68 @@ +/* + * Copyright 2013-2016 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 org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.sleuth.Tracer; +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 allows creating spans by means of a + * {@link NewSpan} annotation. You can annotate classes or just methods. + * You can also apply this annotation to an interface. + * + * @author Christian Schwerdtfeger + * @author Marcin Grzejszczak + * @since 1.2.0 + */ +@Configuration +@ConditionalOnBean(Tracer.class) +@ConditionalOnProperty(name = "spring.sleuth.annotation.enabled", matchIfMissing = true) +@AutoConfigureAfter(TraceAutoConfiguration.class) +@EnableConfigurationProperties(SleuthAnnotationProperties.class) +public class SleuthAnnotationAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + SpanCreator spanCreator(Tracer tracer) { + return new DefaultSpanCreator(tracer); + } + + @Bean + @ConditionalOnMissingBean + TagValueExpressionResolver spelTagValueExpressionResolver() { + return new SpelTagValueExpressionResolver(); + } + + @Bean + @ConditionalOnMissingBean + TagValueResolver noOpTagValueResolver() { + return new NoOpTagValueResolver(); + } + + @Bean + SleuthAdvisorConfig sleuthAdvisorConfig() { + return new SleuthAdvisorConfig(); + } + +} diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SleuthAnnotationProperties.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SleuthAnnotationProperties.java new file mode 100644 index 000000000..203ed2e5a --- /dev/null +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SleuthAnnotationProperties.java @@ -0,0 +1,39 @@ +/* + * Copyright 2013-2016 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 org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Sleuth annotation settings + * + * @author Marcin Grzejszczak + * @since 1.2.0 + */ +@ConfigurationProperties("spring.sleuth.annotation") +public class SleuthAnnotationProperties { + + 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/annotation/SleuthAnnotationUtils.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SleuthAnnotationUtils.java new file mode 100644 index 000000000..854b907d6 --- /dev/null +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SleuthAnnotationUtils.java @@ -0,0 +1,82 @@ +/* + * Copyright 2013-2016 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 java.lang.annotation.Annotation; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.annotation.AnnotationUtils; + +/** + * Utility class that can verify whether the method is annotated with + * the Sleuth annotations. + * + * @author Christian Schwerdtfeger + * @since 1.2.0 + */ +class SleuthAnnotationUtils { + + private static final Log log = LogFactory.getLog(MethodHandles.lookup().lookupClass()); + + static boolean isMethodAnnotated(Method method) { + return findAnnotation(method, NewSpan.class) != null || + findAnnotation(method, ContinueSpan.class) != null; + } + + static boolean hasAnnotatedParams(Method method, Object[] args) { + return !findAnnotatedParameters(method, args).isEmpty(); + } + + static List findAnnotatedParameters(Method method, Object[] args) { + Annotation[][] parameters = method.getParameterAnnotations(); + List result = new ArrayList<>(); + int i = 0; + for (Annotation[] parameter : parameters) { + for (Annotation parameter2 : parameter) { + if (parameter2 instanceof SpanTag) { + result.add(new SleuthAnnotatedParameter(i, (SpanTag) parameter2, args[i])); + } + } + i++; + } + return result; + } + + /** + * Searches for an annotation either on a method or inside the method parameters + */ + static T findAnnotation(Method method, Class clazz) { + T annotation = AnnotationUtils.findAnnotation(method, clazz); + if (annotation == null) { + try { + annotation = AnnotationUtils.findAnnotation( + method.getDeclaringClass().getMethod(method.getName(), + method.getParameterTypes()), clazz); + } catch (NoSuchMethodException | SecurityException e) { + if (log.isDebugEnabled()) { + log.debug("Exception occurred while tyring to find the annotation", e); + } + } + } + return annotation; + } +} diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SpanCreator.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SpanCreator.java new file mode 100644 index 000000000..f071f5953 --- /dev/null +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SpanCreator.java @@ -0,0 +1,35 @@ +/* + * Copyright 2013-2016 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 org.aopalliance.intercept.MethodInvocation; +import org.springframework.cloud.sleuth.Span; + +/** + * A contract for creating a new span for a given join point + * and the {@link NewSpan} annotation. + * + * @author Christian Schwerdtfeger + * @since 1.2.0 + */ +public interface SpanCreator { + + /** + * Returns a new {@link Span} for the join point and {@link NewSpan} + */ + Span createSpan(MethodInvocation methodInvocation, NewSpan newSpan); +} diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SpanTag.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SpanTag.java new file mode 100644 index 000000000..9da85d1f4 --- /dev/null +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SpanTag.java @@ -0,0 +1,68 @@ +/* + * Copyright 2013-2016 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 java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +/** + * There are 3 different ways to add tags to a span. All of them are controlled by the annotation values. + * Precedence is: + * + *

    + *
  • try with the {@link TagValueResolver} bean
  • + *
  • if the value of the bean wasn't set, try to evaluate a SPEL expression
  • + *
  • if there’s no SPEL expression just return a {@code toString()} value of the parameter
  • + *
+ * + * @author Christian Schwerdtfeger + * @since 1.2.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Target(value = { ElementType.PARAMETER }) +public @interface SpanTag { + + /** + * The name of the key of the tag which should be created. + */ + @AliasFor("key") + String value() default ""; + + /** + * The name of the key of the tag which should be created. + */ + @AliasFor("value") + String key() default ""; + + /** + * Execute this SPEL expression to calculate the tag value. Will be analyzed if no value of the + * {@link SpanTag#resolver()} was set. + */ + String expression() default ""; + + /** + * Use this bean to resolve the tag value. Has the highest precedence. + */ + Class resolver() default NoOpTagValueResolver.class; + +} diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SpanTagAnnotationHandler.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SpanTagAnnotationHandler.java new file mode 100644 index 000000000..e176eb106 --- /dev/null +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SpanTagAnnotationHandler.java @@ -0,0 +1,149 @@ +/* + * Copyright 2013-2016 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 java.lang.invoke.MethodHandles; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; + +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.util.StringUtils; + +/** + * This class is able to find all methods annotated with the + * Sleuth annotations. All methods mean that if you have both an interface + * and an implementation annotated with Sleuth annotations then this class is capable + * of finding both of them and merging into one set of tracing information. + * + * This information is then used to add proper tags to the span from the + * method arguments that are annotated with {@link SpanTag}. + * + * @author Christian Schwerdtfeger + * @since 1.2.0 + */ +class SpanTagAnnotationHandler { + + private static final Log log = LogFactory.getLog(MethodHandles.lookup().lookupClass()); + + private final BeanFactory beanFactory; + private Tracer tracer; + + SpanTagAnnotationHandler(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + void addAnnotatedParameters(MethodInvocation pjp) { + try { + Method method = pjp.getMethod(); + Method mostSpecificMethod = AopUtils.getMostSpecificMethod(method, + pjp.getThis().getClass()); + List annotatedParameters = + SleuthAnnotationUtils.findAnnotatedParameters(mostSpecificMethod, pjp.getArguments()); + getAnnotationsFromInterfaces(pjp, mostSpecificMethod, annotatedParameters); + mergeAnnotatedMethodsIfNecessary(pjp, method, mostSpecificMethod, + annotatedParameters); + addAnnotatedArguments(annotatedParameters); + } catch (SecurityException e) { + log.error("Exception occurred while trying to add annotated parameters", e); + } + } + + private void getAnnotationsFromInterfaces(MethodInvocation pjp, + Method mostSpecificMethod, + List annotatedParameters) { + Class[] implementedInterfaces = pjp.getThis().getClass().getInterfaces(); + if (implementedInterfaces.length > 0) { + for (Class implementedInterface : implementedInterfaces) { + for (Method methodFromInterface : implementedInterface.getMethods()) { + if (methodsAreTheSame(mostSpecificMethod, methodFromInterface)) { + List annotatedParametersForActualMethod = + SleuthAnnotationUtils.findAnnotatedParameters(methodFromInterface, pjp.getArguments()); + mergeAnnotatedParameters(annotatedParameters, annotatedParametersForActualMethod); + } + } + } + } + } + + private boolean methodsAreTheSame(Method mostSpecificMethod, Method method1) { + return method1.getName().equals(mostSpecificMethod.getName()) && + Arrays.equals(method1.getParameterTypes(), mostSpecificMethod.getParameterTypes()); + } + + private void mergeAnnotatedMethodsIfNecessary(MethodInvocation pjp, Method method, + Method mostSpecificMethod, List annotatedParameters) { + // that can happen if we have an abstraction and a concrete class that is + // annotated with @NewSpan annotation + if (!method.equals(mostSpecificMethod)) { + List annotatedParametersForActualMethod = SleuthAnnotationUtils.findAnnotatedParameters( + method, pjp.getArguments()); + mergeAnnotatedParameters(annotatedParameters, annotatedParametersForActualMethod); + } + } + + private void mergeAnnotatedParameters(List annotatedParametersIndices, + List annotatedParametersIndicesForActualMethod) { + for (SleuthAnnotatedParameter container : annotatedParametersIndicesForActualMethod) { + final int index = container.parameterIndex; + boolean parameterContained = false; + for (SleuthAnnotatedParameter parameterContainer : annotatedParametersIndices) { + if (parameterContainer.parameterIndex == index) { + parameterContained = true; + break; + } + } + if (!parameterContained) { + annotatedParametersIndices.add(container); + } + } + } + + private void addAnnotatedArguments(List toBeAdded) { + for (SleuthAnnotatedParameter container : toBeAdded) { + String tagValue = resolveTagValue(container.annotation, container.argument); + tracer().addTag(container.annotation.value(), tagValue); + } + } + + String resolveTagValue(SpanTag annotation, Object argument) { + if (argument == null) { + return ""; + } + if (annotation.resolver() != NoOpTagValueResolver.class) { + TagValueResolver tagValueResolver = this.beanFactory.getBean(annotation.resolver()); + return tagValueResolver.resolve(argument); + } else if (StringUtils.hasText(annotation.expression())) { + return this.beanFactory.getBean(TagValueExpressionResolver.class) + .resolve(annotation.expression(), argument); + } + return argument.toString(); + } + + private Tracer tracer() { + if (this.tracer == null) { + this.tracer = this.beanFactory.getBean(Tracer.class); + } + return this.tracer; + } + +} diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SpelTagValueExpressionResolver.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SpelTagValueExpressionResolver.java new file mode 100644 index 000000000..711e5bd62 --- /dev/null +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/SpelTagValueExpressionResolver.java @@ -0,0 +1,48 @@ +/* + * Copyright 2013-2016 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 java.lang.invoke.MethodHandles; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +/** + * Uses SPEL to evaluate the expression. If an exception is thrown will return + * the {@code toString()} of the parameter. + * + * @author Marcin Grzejszczak + * @since 1.2.0 + */ +class SpelTagValueExpressionResolver implements TagValueExpressionResolver { + private static final Log log = LogFactory.getLog(MethodHandles.lookup().lookupClass()); + + @Override + public String resolve(String expression, Object parameter) { + try { + ExpressionParser expressionParser = new SpelExpressionParser(); + Expression expressionToEvaluate = expressionParser.parseExpression(expression); + return expressionToEvaluate.getValue(parameter, String.class); + } catch (Exception e) { + log.error("Exception occurred while tying to evaluate the SPEL expression [" + expression + "]", e); + } + return parameter.toString(); + } +} diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/TagValueExpressionResolver.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/TagValueExpressionResolver.java new file mode 100644 index 000000000..ccc81d122 --- /dev/null +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/TagValueExpressionResolver.java @@ -0,0 +1,36 @@ +/* + * Copyright 2013-2016 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; + +/** + * Resolves the tag value for the given parameter and the provided expression. + * + * @author Marcin Grzejszczak + * @since 1.2.0 + */ +public interface TagValueExpressionResolver { + + /** + * Returns the tag value for the given parameter and the provided expression + * + * @param expression - the expression coming from {@link SpanTag#expression()} + * @param parameter - parameter annotated with {@link SpanTag} + * @return the value of the tag + */ + String resolve(String expression, Object parameter); + +} diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/TagValueResolver.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/TagValueResolver.java new file mode 100644 index 000000000..d52392ce0 --- /dev/null +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/annotation/TagValueResolver.java @@ -0,0 +1,19 @@ +package org.springframework.cloud.sleuth.annotation; + +/** + * Resolves the tag value for the given parameter. + * + * @author Christian Schwerdtfeger + * @since 1.2.0 + */ +public interface TagValueResolver { + + /** + * Returns the tag value for the given parameter + * + * @param parameter - parameter annotated with {@link SpanTag} + * @return the value of the tag + */ + String resolve(Object parameter); + +} 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 0759e9413..1c93d4d72 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,7 +16,8 @@ org.springframework.cloud.sleuth.instrument.web.client.TraceWebClientAutoConfigu org.springframework.cloud.sleuth.instrument.web.client.TraceWebAsyncClientAutoConfiguration,\ org.springframework.cloud.sleuth.instrument.web.client.feign.TraceFeignClientAutoConfiguration,\ org.springframework.cloud.sleuth.instrument.zuul.TraceZuulAutoConfiguration,\ -org.springframework.cloud.sleuth.instrument.rxjava.RxJavaAutoConfiguration +org.springframework.cloud.sleuth.instrument.rxjava.RxJavaAutoConfiguration,\ +org.springframework.cloud.sleuth.annotation.SleuthAnnotationAutoConfiguration # Environment Post Processor org.springframework.boot.env.EnvironmentPostProcessor=\ diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/NoOpTagValueResolverTests.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/NoOpTagValueResolverTests.java new file mode 100644 index 000000000..c3d91556c --- /dev/null +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/NoOpTagValueResolverTests.java @@ -0,0 +1,31 @@ +/* + * Copyright 2013-2016 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 org.junit.Test; + +import static org.assertj.core.api.BDDAssertions.then; + +/** + * @author Marcin Grzejszczak + */ +public class NoOpTagValueResolverTests { + @Test public void should_return_null() throws Exception { + then(new NoOpTagValueResolver().resolve("")).isNull(); + } + +} \ No newline at end of file diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SleuthSpanCreatorAnnotationDisableTests.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SleuthSpanCreatorAnnotationDisableTests.java new file mode 100644 index 000000000..4c3ba2c2f --- /dev/null +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SleuthSpanCreatorAnnotationDisableTests.java @@ -0,0 +1,38 @@ +/* + * Copyright 2013-2016 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 org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = SleuthAnnotationAutoConfiguration.class, + properties = "spring.sleuth.annotation.enabled=false") +public class SleuthSpanCreatorAnnotationDisableTests { + + @Autowired(required = false) SpanCreator spanCreator; + + @Test + public void shouldNotAutowireBecauseConfigIsDisabled() { + assertThat(this.spanCreator).isNull(); + } +} diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SleuthSpanCreatorAnnotationNoSleuthTests.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SleuthSpanCreatorAnnotationNoSleuthTests.java new file mode 100644 index 000000000..59a8e7482 --- /dev/null +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SleuthSpanCreatorAnnotationNoSleuthTests.java @@ -0,0 +1,41 @@ +/* + * Copyright 2013-2016 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 org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = SleuthAnnotationAutoConfiguration.class, + properties = "spring.sleuth.enabled=false") +public class SleuthSpanCreatorAnnotationNoSleuthTests { + + @Autowired(required = false) SpanCreator spanCreator; + @Autowired(required = false) Tracer tracer; + + @Test + public void shouldNotAutowireBecauseConfigIsDisabled() { + assertThat(this.spanCreator).isNull(); + assertThat(this.tracer).isNull(); + } +} diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SleuthSpanCreatorAspectNegativeTests.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SleuthSpanCreatorAspectNegativeTests.java new file mode 100644 index 000000000..e99fb29f2 --- /dev/null +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SleuthSpanCreatorAspectNegativeTests.java @@ -0,0 +1,156 @@ +/* + * Copyright 2013-2016 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 java.util.ArrayList; +import java.util.List; + +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.sleuth.Span; +import org.springframework.cloud.sleuth.SpanReporter; +import org.springframework.cloud.sleuth.annotation.SleuthSpanCreatorAspectNegativeTests.TestConfiguration; +import org.springframework.cloud.sleuth.assertions.ListOfSpans; +import org.springframework.cloud.sleuth.util.ArrayListSpanAccumulator; +import org.springframework.cloud.sleuth.util.ExceptionUtils; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.springframework.cloud.sleuth.assertions.SleuthAssertions.then; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = TestConfiguration.class) +public class SleuthSpanCreatorAspectNegativeTests { + + @Autowired NotAnnotatedTestBeanInterface testBean; + @Autowired TestBeanInterface annotatedTestBean; + @Autowired ArrayListSpanAccumulator accumulator; + + @Before + public void setup() { + ExceptionUtils.setFail(true); + this.accumulator.clear(); + } + + @Test + public void shouldNotCallAdviceForNotAnnotatedBean() { + this.testBean.testMethod(); + + then(this.accumulator.getSpans()).isEmpty(); + then(ExceptionUtils.getLastException()).isNull(); + } + + @Test + public void shouldCallAdviceForAnnotatedBean() throws Throwable { + this.annotatedTestBean.testMethod(); + + List spans = new ArrayList<>(this.accumulator.getSpans()); + then(new ListOfSpans(spans)).hasSize(1).hasASpanWithName("test-method"); + then(ExceptionUtils.getLastException()).isNull(); + } + + protected interface NotAnnotatedTestBeanInterface { + + void testMethod(); + } + + protected static class NotAnnotatedTestBean implements NotAnnotatedTestBeanInterface { + + @Override + public void testMethod() { + } + + } + + protected interface TestBeanInterface { + + @NewSpan + void testMethod(); + + void testMethod2(); + + void testMethod3(); + + @NewSpan(name = "testMethod4") + void testMethod4(); + + @NewSpan(name = "testMethod5") + void testMethod5(@SpanTag("testTag") String test); + + void testMethod6(String test); + + void testMethod7(); + } + + protected static class TestBean implements TestBeanInterface { + + @Override + public void testMethod() { + } + + @NewSpan + @Override + public void testMethod2() { + } + + @NewSpan(name = "testMethod3") + @Override + public void testMethod3() { + } + + @Override + public void testMethod4() { + } + + @Override + public void testMethod5(String test) { + } + + @NewSpan(name = "testMethod6") + @Override + public void testMethod6(@SpanTag("testTag6") String test) { + + } + + @Override + public void testMethod7() { + } + } + + @Configuration + @EnableAutoConfiguration + protected static class TestConfiguration { + @Bean SpanReporter spanReporter() { + return new ArrayListSpanAccumulator(); + } + + @Bean + public NotAnnotatedTestBeanInterface testBean() { + return new NotAnnotatedTestBean(); + } + + @Bean + public TestBeanInterface annotatedTestBean() { + return new TestBean(); + } + } +} diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SleuthSpanCreatorAspectTests.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SleuthSpanCreatorAspectTests.java new file mode 100644 index 000000000..a5a76c4ad --- /dev/null +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SleuthSpanCreatorAspectTests.java @@ -0,0 +1,347 @@ +/* + * Copyright 2013-2016 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 java.util.ArrayList; +import java.util.List; + +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.sleuth.Span; +import org.springframework.cloud.sleuth.SpanReporter; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.annotation.SleuthSpanCreatorAspectTests.TestConfiguration; +import org.springframework.cloud.sleuth.assertions.ListOfSpans; +import org.springframework.cloud.sleuth.sampler.AlwaysSampler; +import org.springframework.cloud.sleuth.util.ArrayListSpanAccumulator; +import org.springframework.cloud.sleuth.util.ExceptionUtils; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import static org.springframework.cloud.sleuth.assertions.SleuthAssertions.then; + +@SpringBootTest(classes = TestConfiguration.class) +@RunWith(SpringJUnit4ClassRunner.class) +public class SleuthSpanCreatorAspectTests { + + @Autowired TestBeanInterface testBean; + @Autowired Tracer tracer; + @Autowired ArrayListSpanAccumulator accumulator; + + @Before + public void setup() { + ExceptionUtils.setFail(true); + this.accumulator.clear(); + } + + @Test + public void shouldCreateSpanWhenAnnotationOnInterfaceMethod() { + this.testBean.testMethod(); + + List spans = new ArrayList<>(this.accumulator.getSpans()); + then(new ListOfSpans(spans)).hasSize(1).hasASpanWithName("test-method"); + then(ExceptionUtils.getLastException()).isNull(); + } + + @Test + public void shouldCreateSpanWhenAnnotationOnClassMethod() { + this.testBean.testMethod2(); + + List spans = new ArrayList<>(this.accumulator.getSpans()); + then(new ListOfSpans(spans)).hasSize(1).hasASpanWithName("test-method2"); + then(ExceptionUtils.getLastException()).isNull(); + } + + @Test + public void shouldCreateSpanWithCustomNameWhenAnnotationOnClassMethod() { + this.testBean.testMethod3(); + + List spans = new ArrayList<>(this.accumulator.getSpans()); + then(new ListOfSpans(spans)).hasSize(1).hasASpanWithName("custom-name-on-test-method3"); + then(ExceptionUtils.getLastException()).isNull(); + } + + @Test + public void shouldCreateSpanWithCustomNameWhenAnnotationOnInterfaceMethod() { + this.testBean.testMethod4(); + + List spans = new ArrayList<>(this.accumulator.getSpans()); + then(new ListOfSpans(spans)).hasSize(1).hasASpanWithName("custom-name-on-test-method4"); + then(ExceptionUtils.getLastException()).isNull(); + } + + @Test + public void shouldCreateSpanWithTagWhenAnnotationOnInterfaceMethod() { + // tag::execution[] + this.testBean.testMethod5("test"); + // end::execution[] + + List spans = new ArrayList<>(this.accumulator.getSpans()); + then(new ListOfSpans(spans)).hasSize(1) + .hasASpanWithName("custom-name-on-test-method5") + .hasASpanWithTagEqualTo("testTag", "test"); + then(ExceptionUtils.getLastException()).isNull(); + } + + @Test + public void shouldCreateSpanWithTagWhenAnnotationOnClassMethod() { + this.testBean.testMethod6("test"); + + List spans = new ArrayList<>(this.accumulator.getSpans()); + then(new ListOfSpans(spans)).hasSize(1) + .hasASpanWithName("custom-name-on-test-method6") + .hasASpanWithTagEqualTo("testTag6", "test"); + then(ExceptionUtils.getLastException()).isNull(); + } + + @Test + public void shouldCreateSpanWithLogWhenAnnotationOnInterfaceMethod() { + this.testBean.testMethod8("test"); + + List spans = new ArrayList<>(this.accumulator.getSpans()); + then(new ListOfSpans(spans)).hasSize(1) + .hasASpanWithName("custom-name-on-test-method8"); + then(ExceptionUtils.getLastException()).isNull(); + } + + @Test + public void shouldCreateSpanWithLogWhenAnnotationOnClassMethod() { + this.testBean.testMethod9("test"); + + List spans = new ArrayList<>(this.accumulator.getSpans()); + then(new ListOfSpans(spans)).hasSize(1) + .hasASpanWithName("custom-name-on-test-method9"); + then(ExceptionUtils.getLastException()).isNull(); + } + + @Test + public void shouldContinueSpanWithLogWhenAnnotationOnInterfaceMethod() { + Span span = this.tracer.createSpan("foo"); + + this.testBean.testMethod10("test"); + + this.tracer.close(span); + List spans = new ArrayList<>(this.accumulator.getSpans()); + then(new ListOfSpans(spans)).hasSize(1) + .hasASpanWithName("foo") + .hasASpanWithTagEqualTo("customTestTag10", "test") + .hasASpanWithLogEqualTo("customTest.before") + .hasASpanWithLogEqualTo("customTest.after"); + then(ExceptionUtils.getLastException()).isNull(); + } + + @Test + public void shouldContinueSpanWithLogWhenAnnotationOnClassMethod() { + Span span = this.tracer.createSpan("foo"); + + // tag::continue_span_execution[] + this.testBean.testMethod11("test"); + // end::continue_span_execution[] + + this.tracer.close(span); + List spans = new ArrayList<>(this.accumulator.getSpans()); + then(new ListOfSpans(spans)).hasSize(1) + .hasASpanWithName("foo") + .hasASpanWithTagEqualTo("customTestTag11", "test") + .hasASpanWithLogEqualTo("customTest.before") + .hasASpanWithLogEqualTo("customTest.after"); + then(ExceptionUtils.getLastException()).isNull(); + } + + @Test + public void shouldAddErrorTagWhenExceptionOccurredInNewSpan() { + try { + this.testBean.testMethod12("test"); + } catch (RuntimeException ignored) { + } + + List spans = new ArrayList<>(this.accumulator.getSpans()); + then(new ListOfSpans(spans)).hasSize(1) + .hasASpanWithName("test-method12") + .hasASpanWithTagEqualTo("testTag12", "test") + .hasASpanWithTagEqualTo("error", "test exception 12"); + then(ExceptionUtils.getLastException()).isNull(); + } + + @Test + public void shouldAddErrorTagWhenExceptionOccurredInContinueSpan() { + Span span = this.tracer.createSpan("foo"); + try { + this.testBean.testMethod13(); + } catch (RuntimeException ignored) { + } + finally { + this.tracer.close(span); + } + + List spans = new ArrayList<>(this.accumulator.getSpans()); + then(new ListOfSpans(spans)).hasSize(1) + .hasASpanWithName("foo") + .hasASpanWithTagEqualTo("error", "test exception 13") + .hasASpanWithLogEqualTo("testMethod13.before") + .hasASpanWithLogEqualTo("testMethod13.afterFailure") + .hasASpanWithLogEqualTo("testMethod13.after"); + then(ExceptionUtils.getLastException()).isNull(); + } + + @Test + public void shouldNotCreateSpanWhenNotAnnotated() { + this.testBean.testMethod7(); + + List spans = new ArrayList<>(this.accumulator.getSpans()); + then(spans).isEmpty(); + } + + protected interface TestBeanInterface { + + // tag::annotated_method[] + @NewSpan + void testMethod(); + // end::annotated_method[] + + void testMethod2(); + + @NewSpan(name = "interfaceCustomNameOnTestMethod3") + void testMethod3(); + + // tag::custom_name_on_annotated_method[] + @NewSpan("customNameOnTestMethod4") + void testMethod4(); + // end::custom_name_on_annotated_method[] + + // tag::custom_name_and_tag_on_annotated_method[] + @NewSpan(name = "customNameOnTestMethod5") + void testMethod5(@SpanTag("testTag") String param); + // end::custom_name_and_tag_on_annotated_method[] + + void testMethod6(String test); + + void testMethod7(); + + @NewSpan(name = "customNameOnTestMethod8") + void testMethod8(String param); + + @NewSpan(name = "testMethod9") + void testMethod9(String param); + + @ContinueSpan(log = "customTest") + void testMethod10(@SpanTag("testTag10") String param); + + // tag::continue_span[] + @ContinueSpan(log = "testMethod11") + void testMethod11(@SpanTag("testTag11") String param); + // end::continue_span[] + + @NewSpan + void testMethod12(@SpanTag("testTag12") String param); + + @ContinueSpan(log = "testMethod13") + void testMethod13(); + } + + protected static class TestBean implements TestBeanInterface { + + @Override + public void testMethod() { + } + + @NewSpan + @Override + public void testMethod2() { + } + + // tag::name_on_implementation[] + @NewSpan(name = "customNameOnTestMethod3") + @Override + public void testMethod3() { + } + // end::name_on_implementation[] + + @Override + public void testMethod4() { + } + + @Override + public void testMethod5(String test) { + } + + @NewSpan(name = "customNameOnTestMethod6") + @Override + public void testMethod6(@SpanTag("testTag6") String test) { + + } + + @Override + public void testMethod7() { + } + + @Override + public void testMethod8(String param) { + + } + + @NewSpan(name = "customNameOnTestMethod9") + @Override + public void testMethod9(String param) { + + } + + @Override + public void testMethod10(@SpanTag("customTestTag10") String param) { + + } + + @ContinueSpan(log = "customTest") + @Override + public void testMethod11(@SpanTag("customTestTag11") String param) { + + } + + @Override + public void testMethod12(String param) { + throw new RuntimeException("test exception 12"); + } + + @Override + public void testMethod13() { + throw new RuntimeException("test exception 13"); + } + } + + @Configuration + @EnableAutoConfiguration + protected static class TestConfiguration { + + @Bean + public TestBeanInterface testBean() { + return new TestBean(); + } + + @Bean SpanReporter spanReporter() { + return new ArrayListSpanAccumulator(); + } + + @Bean AlwaysSampler alwaysSampler() { + return new AlwaysSampler(); + } + } +} diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SleuthSpanCreatorCircularDependencyTests.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SleuthSpanCreatorCircularDependencyTests.java new file mode 100644 index 000000000..0c4f515d7 --- /dev/null +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SleuthSpanCreatorCircularDependencyTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2013-2016 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 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.SpanReporter; +import org.springframework.cloud.sleuth.annotation.SleuthSpanCreatorCircularDependencyTests.TestConfiguration; +import org.springframework.cloud.sleuth.util.ArrayListSpanAccumulator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit4.SpringRunner; + +@SpringBootTest(classes = TestConfiguration.class) +@RunWith(SpringRunner.class) +public class SleuthSpanCreatorCircularDependencyTests { + @Test public void contextLoads() throws Exception { + } + + private static class Service1 { + @Autowired private Service2 service2; + + @NewSpan public void foo() { + } + } + + private static class Service2 { + @Autowired private Service1 service1; + + @NewSpan public void bar() { + } + } + + @Configuration @EnableAutoConfiguration + protected static class TestConfiguration { + @Bean SpanReporter spanReporter() { + return new ArrayListSpanAccumulator(); + } + + @Bean public Service1 service1() { + return new Service1(); + } + + @Bean public Service2 service2() { + return new Service2(); + } + } +} \ No newline at end of file diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SpanTagAnnotationHandlerTests.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SpanTagAnnotationHandlerTests.java new file mode 100644 index 000000000..20a64e1e5 --- /dev/null +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SpanTagAnnotationHandlerTests.java @@ -0,0 +1,127 @@ +/* + * Copyright 2013-2016 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 java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.BeanFactory; +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.annotation.SpanTagAnnotationHandlerTests.TestConfiguration; +import org.springframework.cloud.sleuth.sampler.AlwaysSampler; +import org.springframework.cloud.sleuth.util.ExceptionUtils; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +@SpringBootTest(classes = TestConfiguration.class) +@RunWith(SpringJUnit4ClassRunner.class) +public class SpanTagAnnotationHandlerTests { + + @Autowired BeanFactory beanFactory; + @Autowired TagValueResolver tagValueResolver; + SpanTagAnnotationHandler handler; + + @Before + public void setup() { + ExceptionUtils.setFail(true); + this.handler = new SpanTagAnnotationHandler(this.beanFactory); + } + + @Test + public void shouldUseCustomTagValueResolver() throws NoSuchMethodException, SecurityException { + Method method = AnnotationMockClass.class.getMethod("getAnnotationForTagValueResolver", String.class); + Annotation annotation = method.getParameterAnnotations()[0][0]; + if (annotation instanceof SpanTag) { + String resolvedValue = handler.resolveTagValue((SpanTag) annotation, "test"); + assertThat(resolvedValue).isEqualTo("Value from myCustomTagValueResolver"); + } else { + fail("Annotation was not SleuthSpanTag"); + } + } + + @Test + public void shouldUseTagValueExpression() throws NoSuchMethodException, SecurityException { + Method method = AnnotationMockClass.class.getMethod("getAnnotationForTagValueExpression", String.class); + Annotation annotation = method.getParameterAnnotations()[0][0]; + if (annotation instanceof SpanTag) { + String resolvedValue = handler.resolveTagValue((SpanTag) annotation, "test"); + + assertThat(resolvedValue).isEqualTo("4 characters"); + } else { + fail("Annotation was not SleuthSpanTag"); + } + } + + @Test + public void shouldReturnArgumentToString() throws NoSuchMethodException, SecurityException { + Method method = AnnotationMockClass.class.getMethod("getAnnotationForArgumentToString", Long.class); + Annotation annotation = method.getParameterAnnotations()[0][0]; + if (annotation instanceof SpanTag) { + String resolvedValue = handler.resolveTagValue((SpanTag) annotation, 15); + assertThat(resolvedValue).isEqualTo("15"); + } else { + fail("Annotation was not SleuthSpanTag"); + } + } + + protected class AnnotationMockClass { + + // tag::resolver_bean[] + @NewSpan + public void getAnnotationForTagValueResolver(@SpanTag(key = "test", resolver = TagValueResolver.class) String test) { + } + // end::resolver_bean[] + + // tag::spel[] + @NewSpan + public void getAnnotationForTagValueExpression(@SpanTag(key = "test", expression = "length() + ' characters'") String test) { + } + // end::spel[] + + // tag::toString[] + @NewSpan + public void getAnnotationForArgumentToString(@SpanTag("test") Long param) { + } + // end::toString[] + } + + @Configuration + @EnableAutoConfiguration + protected static class TestConfiguration { + + // tag::custom_resolver[] + @Bean(name = "myCustomTagValueResolver") + public TagValueResolver tagValueResolver() { + return parameter -> "Value from myCustomTagValueResolver"; + } + // end::custom_resolver[] + + @Bean AlwaysSampler alwaysSampler() { + return new AlwaysSampler(); + } + } + +} diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SpelTagValueExpressionResolverTests.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SpelTagValueExpressionResolverTests.java new file mode 100644 index 000000000..e178846e7 --- /dev/null +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/annotation/SpelTagValueExpressionResolverTests.java @@ -0,0 +1,50 @@ +/* + * Copyright 2013-2016 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 org.junit.Test; + +import static org.assertj.core.api.BDDAssertions.then; + +/** + * @author Marcin Grzejszczak + */ +public class SpelTagValueExpressionResolverTests { + @Test + public void should_use_spel_to_resolve_a_value() throws Exception { + SpelTagValueExpressionResolver resolver = new SpelTagValueExpressionResolver(); + + String resolved = resolver.resolve("length() + 1", "foo"); + + then(resolved).isEqualTo("4"); + } + + @Test + public void should_use_to_string_if_expression_is_not_analyzed_properly() throws Exception { + SpelTagValueExpressionResolver resolver = new SpelTagValueExpressionResolver(); + + String resolved = resolver.resolve("invalid() structure + 1", new Foo()); + + then(resolved).isEqualTo("BAR"); + } +} + +class Foo { + @Override public String toString() { + return "BAR"; + } +} \ No newline at end of file diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/assertions/ListOfSpansAssert.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/assertions/ListOfSpansAssert.java index b180ad4da..197d4f5b3 100644 --- a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/assertions/ListOfSpansAssert.java +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/assertions/ListOfSpansAssert.java @@ -196,6 +196,25 @@ public class ListOfSpansAssert extends AbstractAssert \nto contain at least one span with log name " + + "equal to <%s>.\n\n", spansToString(), logName); + } + return this; + } + private String spansToString() { return this.actual.spans.stream().map(span -> "\nSPAN: " + span.toString() + " with name [" + span.getName() + "] " + "\nwith tags " + span.tags() + "\nwith logs " + span.logs() +