diff --git a/docs/src/main/asciidoc/spring-cloud-sleuth.adoc b/docs/src/main/asciidoc/spring-cloud-sleuth.adoc index da967c847..ab722bd7e 100644 --- a/docs/src/main/asciidoc/spring-cloud-sleuth.adoc +++ b/docs/src/main/asciidoc/spring-cloud-sleuth.adoc @@ -5,6 +5,7 @@ :toc: left :toclevels: 8 :nofooter: +:sectlinks: true Spring Cloud Sleuth ==================== @@ -768,7 +769,8 @@ You can disable this behaviour by setting the value of `spring.sleuth.async.enab If you annotate your method with `@Async` then we'll automatically create a new Span with the following characteristics: - - the Span name will be the annotated method name + - if the method is annotated with `@SpanName` then the value of the annotation will be the Span's name + - if the method is *not* annotated with `@SpanName` the Span name will be the annotated method name - the Span will be tagged with that method's class name and the method name too ==== @Scheduled annotated methods diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/DefaultSpanNamer.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/DefaultSpanNamer.java index 2f077cfa9..c8fd215f3 100644 --- a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/DefaultSpanNamer.java +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/DefaultSpanNamer.java @@ -16,13 +16,16 @@ package org.springframework.cloud.sleuth; +import java.lang.reflect.Method; + import org.springframework.core.annotation.AnnotationUtils; /** * Default implementation of SpanNamer that tries to get the span name as follows: * *
  • - * + * + * * * @@ -37,8 +40,7 @@ public class DefaultSpanNamer implements SpanNamer { @Override public String name(Object object, String defaultValue) { - SpanName annotation = AnnotationUtils - .findAnnotation(object.getClass(), SpanName.class); + SpanName annotation = annotation(object); String spanName = annotation != null ? annotation.value() : object.toString(); // If there is no overridden toString method we'll put a constant value if (isDefaultToString(object, spanName)) { @@ -47,7 +49,18 @@ public class DefaultSpanNamer implements SpanNamer { return spanName; } + private SpanName annotation(Object o) { + if (o instanceof Method) { + return AnnotationUtils.findAnnotation((Method) o, SpanName.class); + } + return AnnotationUtils + .findAnnotation(o.getClass(), SpanName.class); + } + private static boolean isDefaultToString(Object delegate, String spanName) { + if (delegate instanceof Method) { + return delegate.toString().equals(spanName); + } return (delegate.getClass().getName() + "@" + Integer.toHexString(delegate.hashCode())).equals(spanName); } diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/SpanName.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/SpanName.java index f676fc0d8..729294111 100644 --- a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/SpanName.java +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/SpanName.java @@ -55,10 +55,13 @@ import java.lang.annotation.Target; * }; * } * + * Starting with version {@code 1.3.0} you can also put the annotation on an {@link org.springframework.scheduling.annotation.Async} + * annotated method and the value of that annotation will be used as the span name. + * * @author Marcin Grzejszczak * @since 1.0.0 */ -@Target(ElementType.TYPE) +@Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface SpanName { diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/async/TraceAsyncAspect.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/async/TraceAsyncAspect.java index 65a82ce86..2d92887f5 100644 --- a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/async/TraceAsyncAspect.java +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/async/TraceAsyncAspect.java @@ -22,6 +22,7 @@ import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.beans.factory.BeanFactory; import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.SpanNamer; import org.springframework.cloud.sleuth.TraceKeys; import org.springframework.cloud.sleuth.Tracer; import org.springframework.cloud.sleuth.util.SpanNameUtil; @@ -46,6 +47,7 @@ public class TraceAsyncAspect { private final Tracer tracer; private final TraceKeys traceKeys; private final BeanFactory beanFactory; + private SpanNamer spanNamer; public TraceAsyncAspect(Tracer tracer, TraceKeys traceKeys, BeanFactory beanFactory) { this.tracer = tracer; @@ -55,8 +57,9 @@ public class TraceAsyncAspect { @Around("execution (@org.springframework.scheduling.annotation.Async * *.*(..))") public Object traceBackgroundThread(final ProceedingJoinPoint pjp) throws Throwable { - Span span = this.tracer.createSpan( + String spanName = spanNamer().name(getMethod(pjp, pjp.getTarget()), SpanNameUtil.toLowerHyphen(pjp.getSignature().getName())); + Span span = this.tracer.createSpan(spanName); this.tracer.addTag(Span.SPAN_LOCAL_COMPONENT_TAG_NAME, ASYNC_COMPONENT); this.tracer.addTag(this.traceKeys.getAsync().getPrefix() + this.traceKeys.getAsync().getClassNameKey(), pjp.getTarget().getClass().getSimpleName()); @@ -76,4 +79,11 @@ public class TraceAsyncAspect { .findMethod(object.getClass(), method.getName(), method.getParameterTypes()); } + SpanNamer spanNamer() { + if (this.spanNamer == null) { + this.spanNamer = this.beanFactory.getBean(SpanNamer.class); + } + return this.spanNamer; + } + } diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/DefaultSpanNamerTests.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/DefaultSpanNamerTests.java index 18f27dd42..cc865e797 100644 --- a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/DefaultSpanNamerTests.java +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/DefaultSpanNamerTests.java @@ -16,7 +16,10 @@ package org.springframework.cloud.sleuth; +import java.lang.reflect.Method; + import org.junit.Test; +import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.BDDAssertions.then; @@ -42,6 +45,18 @@ public class DefaultSpanNamerTests { then(this.defaultSpanNamer.name(new ClassWithoutToString(), "default")).isEqualTo("default"); } + @Test + public void should_return_value_of_span_name_from_annotation_on_method() throws Exception { + Method method = ReflectionUtils.findMethod(ClassWithAnnotatedMethod.class, "method"); + then(this.defaultSpanNamer.name(method, "default")).isEqualTo("foo"); + } + + @Test + public void should_return_default_value_of_span_name_from_annotation_on_method() throws Exception { + Method method = ReflectionUtils.findMethod(ClassWithNonAnnotatedMethod.class, "method"); + then(this.defaultSpanNamer.name(method, "default")).isEqualTo("default"); + } + @SpanName("somevalue") static class ClassWithAnnotation {} @@ -60,4 +75,13 @@ public class DefaultSpanNamerTests { } static class ClassWithoutToString {} + + static class ClassWithAnnotatedMethod { + @SpanName("foo") + void method() {} + } + + static class ClassWithNonAnnotatedMethod { + void method() {} + } } \ No newline at end of file diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/SpringDataInstrumentationTests.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/SpringDataInstrumentationTests.java index f51b979b3..94a776b88 100644 --- a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/SpringDataInstrumentationTests.java +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/SpringDataInstrumentationTests.java @@ -38,6 +38,7 @@ import org.springframework.data.rest.core.annotation.RepositoryRestResource; import org.springframework.hateoas.PagedResources; import org.springframework.http.RequestEntity; import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.web.client.RestTemplate; @@ -58,6 +59,7 @@ import static org.springframework.cloud.sleuth.assertions.SleuthAssertions.then; @RunWith(SpringRunner.class) @SpringBootTest(classes = ReservationServiceApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @DirtiesContext +@ActiveProfiles("data") public class SpringDataInstrumentationTests { @Autowired diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/TraceAsyncIntegrationTests.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/TraceAsyncIntegrationTests.java index 29645c716..a4928643e 100644 --- a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/TraceAsyncIntegrationTests.java +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/web/TraceAsyncIntegrationTests.java @@ -2,17 +2,21 @@ package org.springframework.cloud.sleuth.instrument.web; import static java.util.concurrent.TimeUnit.SECONDS; +import static org.assertj.core.api.BDDAssertions.then; import static org.springframework.cloud.sleuth.assertions.SleuthAssertions.then; import java.util.concurrent.atomic.AtomicReference; +import org.assertj.core.api.BDDAssertions; import org.junit.After; +import org.junit.Before; 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.Sampler; import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.SpanName; import org.springframework.cloud.sleuth.Tracer; import org.springframework.cloud.sleuth.instrument.DefaultTestAutoConfiguration; import org.springframework.cloud.sleuth.sampler.AlwaysSampler; @@ -35,6 +39,11 @@ public class TraceAsyncIntegrationTests { @Autowired Tracer tracer; + @Before + public void cleanup() { + this.classPerformingAsyncLogic.clear(); + } + @Test public void should_set_span_on_an_async_annotated_method() { Span span = givenASpanInCurrentThread(); @@ -42,6 +51,17 @@ public class TraceAsyncIntegrationTests { whenAsyncProcessingTakesPlace(); thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOne(span); + this.tracer.close(span); + } + + @Test + public void should_set_span_with_custom_method_on_an_async_annotated_method() { + Span span = givenASpanInCurrentThread(); + + whenAsyncProcessingTakesPlaceWithCustomSpanName(); + + thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOneAndSpanHasCustomName(span); + this.tracer.close(span); } private Span givenASpanInCurrentThread() { @@ -52,6 +72,10 @@ public class TraceAsyncIntegrationTests { this.classPerformingAsyncLogic.invokeAsynchronousLogic(); } + private void whenAsyncProcessingTakesPlaceWithCustomSpanName() { + this.classPerformingAsyncLogic.customNameInvokeAsynchronousLogic(); + } + private void thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOne(final Span span) { Awaitility.await().atMost(5, SECONDS).untilAsserted( () -> then(TraceAsyncIntegrationTests.this.classPerformingAsyncLogic.getSpan()) @@ -62,6 +86,16 @@ public class TraceAsyncIntegrationTests { .hasATag("method", "invokeAsynchronousLogic")); } + private void thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOneAndSpanHasCustomName(final Span span) { + Awaitility.await().atMost(5, SECONDS).untilAsserted( + () -> then(TraceAsyncIntegrationTests.this.classPerformingAsyncLogic.getSpan()) + .hasTraceIdEqualTo(span.getTraceId()) + .hasNameEqualTo("foo") + .isALocalComponentSpan() + .hasATag("class", "ClassPerformingAsyncLogic") + .hasATag("method", "customNameInvokeAsynchronousLogic")); + } + @After public void cleanTrace() { TestSpanContextHolder.removeCurrentSpan(); @@ -93,8 +127,18 @@ public class TraceAsyncIntegrationTests { this.span.set(TestSpanContextHolder.getCurrentSpan()); } + @Async + @SpanName("foo") + public void customNameInvokeAsynchronousLogic() { + this.span.set(TestSpanContextHolder.getCurrentSpan()); + } + public Span getSpan() { return this.span.get(); } + + public void clear() { + this.span.set(null); + } } } diff --git a/spring-cloud-sleuth-core/src/test/resources/application-data.yml b/spring-cloud-sleuth-core/src/test/resources/application-data.yml new file mode 100644 index 000000000..de733c518 --- /dev/null +++ b/spring-cloud-sleuth-core/src/test/resources/application-data.yml @@ -0,0 +1 @@ +spring.autoconfigure.exclude: \ No newline at end of file diff --git a/spring-cloud-sleuth-core/src/test/resources/application.yml b/spring-cloud-sleuth-core/src/test/resources/application.yml index 293cf2e33..2de958e10 100644 --- a/spring-cloud-sleuth-core/src/test/resources/application.yml +++ b/spring-cloud-sleuth-core/src/test/resources/application.yml @@ -15,4 +15,7 @@ ribbon.eureka.enabled: false spring.sleuth.scheduled.skipPattern: "^org.*TestBeanWithScheduledMethodToBeIgnored$" # comma separated list of matchers spring.sleuth.rxjava.schedulers.ignoredthreads: HystixMetricPoller,^MyCustomThread.*$,^RxComputation.*$ -security.ignored: /** \ No newline at end of file +security.ignored: /** + +#disable hibernate by default +spring.autoconfigure.exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration \ No newline at end of file