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() +