diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/scheduling/SpringSchedulingInstrumentationAspect.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/scheduling/SpringSchedulingInstrumentationAspect.java new file mode 100644 index 000000000000..00a7aaae704c --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/scheduling/SpringSchedulingInstrumentationAspect.java @@ -0,0 +1,90 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.scheduling; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeSpanNameExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.aop.framework.AopProxyUtils; + +/** + * Spring Scheduling instrumentation aop. + * + *
This aspect would intercept all methods annotated with {@link + * org.springframework.scheduling.annotation.Scheduled} and {@link + * org.springframework.scheduling.annotation.Schedules}. + * + *
Normally this would cover most of the Spring Scheduling use cases, but if you register jobs
+ * programmatically such as {@link
+ * org.springframework.scheduling.config.ScheduledTaskRegistrar#addCronTask}, this aspect would not
+ * cover them. You may use {@link io.opentelemetry.instrumentation.annotations.WithSpan} to trace
+ * these jobs manually.
+ */
+@Aspect
+final class SpringSchedulingInstrumentationAspect {
+ public static final String INSTRUMENTATION_NAME = "io.opentelemetry.spring-scheduling-3.1";
+ private final Instrumenter This class is internal and is hence not for public use. Its APIs are unstable and can change
+ * at any time.
+ */
+@ConditionalOnBean(OpenTelemetry.class)
+@ConditionalOnEnabledInstrumentation(module = "spring-scheduling")
+@ConditionalOnClass({Scheduled.class, Aspect.class})
+@Configuration
+class SpringSchedulingInstrumentationAutoConfiguration {
+ @Bean
+ SpringSchedulingInstrumentationAspect springSchedulingInstrumentationAspect(
+ OpenTelemetry openTelemetry, ConfigProperties configProperties) {
+ return new SpringSchedulingInstrumentationAspect(openTelemetry, configProperties);
+ }
+}
diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring3/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/OpenTelemetryAnnotationsRuntimeHints.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring3/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/OpenTelemetryAnnotationsRuntimeHints.java
index 79a9fad3c069..7bb5e2aa24bf 100644
--- a/instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring3/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/OpenTelemetryAnnotationsRuntimeHints.java
+++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring3/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/OpenTelemetryAnnotationsRuntimeHints.java
@@ -19,6 +19,10 @@ public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
.registerType(
TypeReference.of(
"io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.annotations.InstrumentationWithSpanAspect"),
+ hint -> hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS))
+ .registerType(
+ TypeReference.of(
+ "io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.scheduling.SpringSchedulingInstrumentationAspect"),
hint -> hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS));
}
}
diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories
index b389d6a1e381..049b3b068f1d 100644
--- a/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories
+++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories
@@ -9,7 +9,8 @@ io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.m
io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.r2dbc.R2dbcInstrumentationAutoConfiguration,\
io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.web.SpringWebInstrumentationAutoConfiguration,\
io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webflux.SpringWebfluxInstrumentationAutoConfiguration,\
-io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webmvc.SpringWebMvc5InstrumentationAutoConfiguration
+io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webmvc.SpringWebMvc5InstrumentationAutoConfiguration,\
+io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.scheduling.SpringSchedulingInstrumentationAutoConfiguration
org.springframework.context.ApplicationListener=\
io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.logging.LogbackAppenderApplicationListener
diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index 227a59072e48..73b6f4a6f840 100644
--- a/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -10,3 +10,4 @@ io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.w
io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webflux.SpringWebfluxInstrumentationAutoConfiguration
io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.web.RestClientInstrumentationAutoConfiguration
io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webmvc.SpringWebMvc6InstrumentationAutoConfiguration
+io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.scheduling.SpringSchedulingInstrumentationAutoConfiguration
diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/scheduling/SchedulingInstrumentationAspectTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/scheduling/SchedulingInstrumentationAspectTest.java
new file mode 100644
index 000000000000..79e15e180b9d
--- /dev/null
+++ b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/scheduling/SchedulingInstrumentationAspectTest.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.scheduling;
+
+import static io.opentelemetry.api.trace.SpanKind.INTERNAL;
+import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
+import static io.opentelemetry.sdk.testing.assertj.TracesAssert.assertThat;
+import static io.opentelemetry.semconv.incubating.CodeIncubatingAttributes.CODE_FUNCTION;
+import static io.opentelemetry.semconv.incubating.CodeIncubatingAttributes.CODE_NAMESPACE;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.Tracer;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension;
+import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
+import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties;
+import io.opentelemetry.sdk.trace.data.SpanData;
+import io.opentelemetry.sdk.trace.data.StatusData;
+import java.util.Collections;
+import java.util.List;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.springframework.aop.aspectj.annotation.AspectJProxyFactory;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.scheduling.annotation.Schedules;
+
+class SchedulingInstrumentationAspectTest {
+
+ @RegisterExtension
+ static final LibraryInstrumentationExtension testing = LibraryInstrumentationExtension.create();
+
+ private InstrumentationSchedulingTester schedulingTester;
+ private String unproxiedTesterSimpleClassName;
+ private String unproxiedTesterClassName;
+
+ SpringSchedulingInstrumentationAspect newAspect(
+ OpenTelemetry openTelemetry, ConfigProperties configProperties) {
+ return new SpringSchedulingInstrumentationAspect(openTelemetry, configProperties);
+ }
+
+ @BeforeEach
+ void setup() {
+ InstrumentationSchedulingTester unproxiedTester =
+ new InstrumentationSchedulingTester(testing.getOpenTelemetry());
+ unproxiedTesterSimpleClassName = unproxiedTester.getClass().getSimpleName();
+ unproxiedTesterClassName = unproxiedTester.getClass().getName();
+
+ AspectJProxyFactory factory = new AspectJProxyFactory();
+ factory.setTarget(unproxiedTester);
+
+ SpringSchedulingInstrumentationAspect aspect =
+ newAspect(
+ testing.getOpenTelemetry(),
+ DefaultConfigProperties.createFromMap(Collections.emptyMap()));
+ factory.addAspect(aspect);
+
+ schedulingTester = factory.getProxy();
+ }
+
+ @Test
+ @DisplayName("when method is annotated with @Scheduled should start a new span.")
+ void scheduled() {
+ // when
+ schedulingTester.testScheduled();
+
+ // then
+ List> traces = testing.waitForTraces(1);
+ assertThat(traces)
+ .hasTracesSatisfyingExactly(
+ trace ->
+ trace.hasSpansSatisfyingExactly(
+ span ->
+ span.hasName(unproxiedTesterSimpleClassName + ".testScheduled")
+ .hasKind(INTERNAL)
+ .hasAttributesSatisfyingExactly(
+ equalTo(CODE_NAMESPACE, unproxiedTesterClassName),
+ equalTo(CODE_FUNCTION, "testScheduled"))));
+ }
+
+ @Test
+ @DisplayName("when method is annotated with multiple @Scheduled should start a new span.")
+ void multiScheduled() {
+ // when
+ schedulingTester.testMultiScheduled();
+
+ // then
+ List
> traces = testing.waitForTraces(1);
+ assertThat(traces)
+ .hasTracesSatisfyingExactly(
+ trace ->
+ trace.hasSpansSatisfyingExactly(
+ span ->
+ span.hasName(unproxiedTesterSimpleClassName + ".testMultiScheduled")
+ .hasKind(INTERNAL)
+ .hasAttributesSatisfyingExactly(
+ equalTo(CODE_NAMESPACE, unproxiedTesterClassName),
+ equalTo(CODE_FUNCTION, "testMultiScheduled"))));
+ }
+
+ @Test
+ @DisplayName("when method is annotated with @Schedules should start a new span.")
+ void schedules() {
+ // when
+ schedulingTester.testSchedules();
+
+ // then
+ List
> traces = testing.waitForTraces(1);
+ assertThat(traces)
+ .hasTracesSatisfyingExactly(
+ trace ->
+ trace.hasSpansSatisfyingExactly(
+ span ->
+ span.hasName(unproxiedTesterSimpleClassName + ".testSchedules")
+ .hasKind(INTERNAL)
+ .hasAttributesSatisfyingExactly(
+ equalTo(CODE_NAMESPACE, unproxiedTesterClassName),
+ equalTo(CODE_FUNCTION, "testSchedules"))));
+ }
+
+ @Test
+ @DisplayName(
+ "when method is annotated with @Scheduled and it starts nested span, spans should be nested.")
+ void nestedSpanInScheduled() {
+ // when
+ schedulingTester.testNestedSpan();
+
+ // then
+ List
> traces = testing.waitForTraces(1);
+ assertThat(traces)
+ .hasTracesSatisfyingExactly(
+ trace ->
+ trace.hasSpansSatisfyingExactly(
+ span ->
+ span.hasName(unproxiedTesterSimpleClassName + ".testNestedSpan")
+ .hasKind(INTERNAL)
+ .hasAttributesSatisfyingExactly(
+ equalTo(CODE_NAMESPACE, unproxiedTesterClassName),
+ equalTo(CODE_FUNCTION, "testNestedSpan")),
+ nestedSpan ->
+ nestedSpan.hasParent(trace.getSpan(0)).hasKind(INTERNAL).hasName("test")));
+ }
+
+ @Test
+ @DisplayName(
+ "when method is annotated with @Scheduled AND an exception is thrown span should record the exception")
+ void scheduledError() {
+ assertThatThrownBy(() -> schedulingTester.testScheduledWithException())
+ .isInstanceOf(Exception.class);
+
+ List
> traces = testing.waitForTraces(1);
+ assertThat(traces)
+ .hasTracesSatisfyingExactly(
+ trace ->
+ trace.hasSpansSatisfyingExactly(
+ span ->
+ span.hasName(unproxiedTesterSimpleClassName + ".testScheduledWithException")
+ .hasKind(INTERNAL)
+ .hasStatus(StatusData.error())
+ .hasAttributesSatisfyingExactly(
+ equalTo(CODE_NAMESPACE, unproxiedTesterClassName),
+ equalTo(CODE_FUNCTION, "testScheduledWithException"))));
+ }
+
+ static class InstrumentationSchedulingTester {
+ private final OpenTelemetry openTelemetry;
+
+ InstrumentationSchedulingTester(OpenTelemetry openTelemetry) {
+ this.openTelemetry = openTelemetry;
+ }
+
+ @Scheduled(fixedRate = 1L)
+ public void testScheduled() {
+ // no-op
+ }
+
+ @Scheduled(fixedRate = 1L)
+ @Scheduled(fixedRate = 2L)
+ public void testMultiScheduled() {
+ // no-op
+ }
+
+ @Schedules({@Scheduled(fixedRate = 1L), @Scheduled(fixedRate = 2L)})
+ public void testSchedules() {
+ // no-op
+ }
+
+ @Scheduled(fixedRate = 1L)
+ public void testNestedSpan() {
+ Context current = Context.current();
+ Tracer tracer = openTelemetry.getTracer("test");
+ try (Scope ignored = current.makeCurrent()) {
+ Span span = tracer.spanBuilder("test").startSpan();
+ span.end();
+ }
+ }
+
+ @Scheduled(fixedRate = 1L)
+ public void testScheduledWithException() {
+ throw new IllegalStateException("something went wrong");
+ }
+ }
+}
diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/scheduling/SchedulingInstrumentationAutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/scheduling/SchedulingInstrumentationAutoConfigurationTest.java
new file mode 100644
index 000000000000..37d91b411d5a
--- /dev/null
+++ b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/scheduling/SchedulingInstrumentationAutoConfigurationTest.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.scheduling;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
+import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties;
+import java.util.Collections;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+class SchedulingInstrumentationAutoConfigurationTest {
+ private final ApplicationContextRunner runner =
+ new ApplicationContextRunner()
+ .withBean(OpenTelemetry.class, OpenTelemetry::noop)
+ .withBean(
+ ConfigProperties.class,
+ () -> DefaultConfigProperties.createFromMap(Collections.emptyMap()))
+ .withConfiguration(
+ AutoConfigurations.of(SpringSchedulingInstrumentationAutoConfiguration.class));
+
+ @Test
+ void instrumentationEnabled() {
+ runner
+ .withPropertyValues("otel.instrumentation.spring-scheduling.enabled=true")
+ .run(
+ context ->
+ assertThat(context.containsBean("springSchedulingInstrumentationAspect")).isTrue());
+ }
+
+ @Test
+ void instrumentationDisabled() {
+ runner
+ .withPropertyValues("otel.instrumentation.spring-scheduling.enabled=false")
+ .run(
+ context ->
+ assertThat(context.containsBean("springSchedulingInstrumentationAspect"))
+ .isFalse());
+ }
+}