Skip to content

Commit 8da1085

Browse files
committed
Add auto configuration for spring scheduling instrumentation using aop
1 parent 780cdf4 commit 8da1085

File tree

7 files changed

+387
-1
lines changed

7 files changed

+387
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.scheduling;
7+
8+
import io.opentelemetry.api.OpenTelemetry;
9+
import io.opentelemetry.api.common.AttributeKey;
10+
import io.opentelemetry.context.Context;
11+
import io.opentelemetry.context.Scope;
12+
import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesExtractor;
13+
import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter;
14+
import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeSpanNameExtractor;
15+
import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod;
16+
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
17+
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
18+
import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder;
19+
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
20+
import org.aspectj.lang.ProceedingJoinPoint;
21+
import org.aspectj.lang.annotation.Around;
22+
import org.aspectj.lang.annotation.Aspect;
23+
import org.aspectj.lang.annotation.Pointcut;
24+
import org.aspectj.lang.reflect.MethodSignature;
25+
import org.springframework.aop.framework.AopProxyUtils;
26+
27+
/**
28+
* Spring Scheduling instrumentation aop.
29+
*
30+
* <p>This aspect would intercept all methods annotated with {@link
31+
* org.springframework.scheduling.annotation.Scheduled} and {@link
32+
* org.springframework.scheduling.annotation.Schedules}.
33+
*
34+
* <p>Normally this would cover most of the Spring Scheduling use cases, but if you register jobs
35+
* programmatically such as {@link
36+
* org.springframework.scheduling.config.ScheduledTaskRegistrar#addCronTask}, this aspect would not
37+
* cover them. You may use {@link io.opentelemetry.instrumentation.annotations.WithSpan} to trace
38+
* these jobs manually.
39+
*/
40+
@Aspect
41+
final class SpringSchedulingInstrumentationAspect {
42+
public static final String INSTRUMENTATION_NAME = "io.opentelemetry.spring-scheduling-3.1";
43+
private final Instrumenter<ClassAndMethod, Object> instrumenter;
44+
45+
public SpringSchedulingInstrumentationAspect(
46+
OpenTelemetry openTelemetry, ConfigProperties configProperties) {
47+
CodeAttributesGetter<ClassAndMethod> codedAttributesGetter =
48+
ClassAndMethod.codeAttributesGetter();
49+
InstrumenterBuilder<ClassAndMethod, Object> builder =
50+
Instrumenter.builder(
51+
openTelemetry,
52+
INSTRUMENTATION_NAME,
53+
CodeSpanNameExtractor.create(codedAttributesGetter))
54+
.addAttributesExtractor(CodeAttributesExtractor.create(codedAttributesGetter));
55+
if (configProperties.getBoolean(
56+
"otel.instrumentation.spring-scheduling.experimental-span-attributes", false)) {
57+
builder.addAttributesExtractor(
58+
AttributesExtractor.constant(AttributeKey.stringKey("job.system"), "spring_scheduling"));
59+
}
60+
instrumenter = builder.buildInstrumenter();
61+
}
62+
63+
@Pointcut(
64+
"@annotation(org.springframework.scheduling.annotation.Scheduled)"
65+
+ "|| @annotation(org.springframework.scheduling.annotation.Schedules)")
66+
public void pointcut() {
67+
// ignored
68+
}
69+
70+
@Around("pointcut()")
71+
public Object execution(ProceedingJoinPoint joinPoint) throws Throwable {
72+
Context parent = Context.current();
73+
ClassAndMethod request =
74+
ClassAndMethod.create(
75+
AopProxyUtils.ultimateTargetClass(joinPoint.getTarget()),
76+
((MethodSignature) joinPoint.getSignature()).getMethod().getName());
77+
if (!instrumenter.shouldStart(parent, request)) {
78+
return joinPoint.proceed();
79+
}
80+
Context context = instrumenter.start(parent, request);
81+
try (Scope ignored = context.makeCurrent()) {
82+
Object object = joinPoint.proceed();
83+
instrumenter.end(context, request, object, null);
84+
return object;
85+
} catch (Throwable t) {
86+
instrumenter.end(context, request, null, t);
87+
throw t;
88+
}
89+
}
90+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.scheduling;
7+
8+
import io.opentelemetry.api.OpenTelemetry;
9+
import io.opentelemetry.instrumentation.spring.autoconfigure.internal.ConditionalOnEnabledInstrumentation;
10+
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
11+
import org.aspectj.lang.annotation.Aspect;
12+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
13+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
14+
import org.springframework.context.annotation.Bean;
15+
import org.springframework.context.annotation.Configuration;
16+
import org.springframework.scheduling.annotation.Scheduled;
17+
18+
/**
19+
* Configures an aspect for tracing.
20+
*
21+
* <p>This class is internal and is hence not for public use. Its APIs are unstable and can change
22+
* at any time.
23+
*/
24+
@ConditionalOnBean(OpenTelemetry.class)
25+
@ConditionalOnEnabledInstrumentation(module = "spring-scheduling")
26+
@ConditionalOnClass({Scheduled.class, Aspect.class})
27+
@Configuration
28+
class SpringSchedulingInstrumentationAutoConfiguration {
29+
@Bean
30+
SpringSchedulingInstrumentationAspect springSchedulingInstrumentationAspect(
31+
OpenTelemetry openTelemetry, ConfigProperties configProperties) {
32+
return new SpringSchedulingInstrumentationAspect(openTelemetry, configProperties);
33+
}
34+
}

instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring3/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/annotations/OpenTelemetryAnnotationsRuntimeHints.java

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
1919
.registerType(
2020
TypeReference.of(
2121
"io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.annotations.InstrumentationWithSpanAspect"),
22+
hint -> hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS))
23+
.registerType(
24+
TypeReference.of(
25+
"io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.scheduling.SpringSchedulingInstrumentationAspect"),
2226
hint -> hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS));
2327
}
2428
}

instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.m
99
io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.r2dbc.R2dbcInstrumentationAutoConfiguration,\
1010
io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.web.SpringWebInstrumentationAutoConfiguration,\
1111
io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webflux.SpringWebfluxInstrumentationAutoConfiguration,\
12-
io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webmvc.SpringWebMvc5InstrumentationAutoConfiguration
12+
io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webmvc.SpringWebMvc5InstrumentationAutoConfiguration,\
13+
io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.scheduling.SpringSchedulingInstrumentationAutoConfiguration
1314

1415
org.springframework.context.ApplicationListener=\
1516
io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.logging.LogbackAppenderApplicationListener

instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

+1
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.w
1010
io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webflux.SpringWebfluxInstrumentationAutoConfiguration
1111
io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.web.RestClientInstrumentationAutoConfiguration
1212
io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webmvc.SpringWebMvc6InstrumentationAutoConfiguration
13+
io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.scheduling.SpringSchedulingInstrumentationAutoConfiguration
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.scheduling;
7+
8+
import static io.opentelemetry.api.trace.SpanKind.INTERNAL;
9+
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
10+
import static io.opentelemetry.sdk.testing.assertj.TracesAssert.assertThat;
11+
import static io.opentelemetry.semconv.incubating.CodeIncubatingAttributes.CODE_FUNCTION;
12+
import static io.opentelemetry.semconv.incubating.CodeIncubatingAttributes.CODE_NAMESPACE;
13+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
14+
15+
import io.opentelemetry.api.OpenTelemetry;
16+
import io.opentelemetry.api.trace.Span;
17+
import io.opentelemetry.api.trace.Tracer;
18+
import io.opentelemetry.context.Context;
19+
import io.opentelemetry.context.Scope;
20+
import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension;
21+
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
22+
import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties;
23+
import io.opentelemetry.sdk.trace.data.SpanData;
24+
import io.opentelemetry.sdk.trace.data.StatusData;
25+
import java.util.Collections;
26+
import java.util.List;
27+
import org.junit.jupiter.api.BeforeEach;
28+
import org.junit.jupiter.api.DisplayName;
29+
import org.junit.jupiter.api.Test;
30+
import org.junit.jupiter.api.extension.RegisterExtension;
31+
import org.springframework.aop.aspectj.annotation.AspectJProxyFactory;
32+
import org.springframework.scheduling.annotation.Scheduled;
33+
import org.springframework.scheduling.annotation.Schedules;
34+
35+
class SchedulingInstrumentationAspectTest {
36+
37+
@RegisterExtension
38+
static final LibraryInstrumentationExtension testing = LibraryInstrumentationExtension.create();
39+
40+
private InstrumentationSchedulingTester schedulingTester;
41+
private String unproxiedTesterSimpleClassName;
42+
private String unproxiedTesterClassName;
43+
44+
SpringSchedulingInstrumentationAspect newAspect(
45+
OpenTelemetry openTelemetry, ConfigProperties configProperties) {
46+
return new SpringSchedulingInstrumentationAspect(openTelemetry, configProperties);
47+
}
48+
49+
@BeforeEach
50+
void setup() {
51+
InstrumentationSchedulingTester unproxiedTester =
52+
new InstrumentationSchedulingTester(testing.getOpenTelemetry());
53+
unproxiedTesterSimpleClassName = unproxiedTester.getClass().getSimpleName();
54+
unproxiedTesterClassName = unproxiedTester.getClass().getName();
55+
56+
AspectJProxyFactory factory = new AspectJProxyFactory();
57+
factory.setTarget(unproxiedTester);
58+
59+
SpringSchedulingInstrumentationAspect aspect =
60+
newAspect(
61+
testing.getOpenTelemetry(),
62+
DefaultConfigProperties.createFromMap(Collections.emptyMap()));
63+
factory.addAspect(aspect);
64+
65+
schedulingTester = factory.getProxy();
66+
}
67+
68+
@Test
69+
@DisplayName("when method is annotated with @Scheduled should start a new span.")
70+
void scheduled() {
71+
// when
72+
schedulingTester.testScheduled();
73+
74+
// then
75+
List<List<SpanData>> traces = testing.waitForTraces(1);
76+
assertThat(traces)
77+
.hasTracesSatisfyingExactly(
78+
trace ->
79+
trace.hasSpansSatisfyingExactly(
80+
span ->
81+
span.hasName(unproxiedTesterSimpleClassName + ".testScheduled")
82+
.hasKind(INTERNAL)
83+
.hasAttributesSatisfyingExactly(
84+
equalTo(CODE_NAMESPACE, unproxiedTesterClassName),
85+
equalTo(CODE_FUNCTION, "testScheduled"))));
86+
}
87+
88+
@Test
89+
@DisplayName("when method is annotated with multiple @Scheduled should start a new span.")
90+
void multiScheduled() {
91+
// when
92+
schedulingTester.testMultiScheduled();
93+
94+
// then
95+
List<List<SpanData>> traces = testing.waitForTraces(1);
96+
assertThat(traces)
97+
.hasTracesSatisfyingExactly(
98+
trace ->
99+
trace.hasSpansSatisfyingExactly(
100+
span ->
101+
span.hasName(unproxiedTesterSimpleClassName + ".testMultiScheduled")
102+
.hasKind(INTERNAL)
103+
.hasAttributesSatisfyingExactly(
104+
equalTo(CODE_NAMESPACE, unproxiedTesterClassName),
105+
equalTo(CODE_FUNCTION, "testMultiScheduled"))));
106+
}
107+
108+
@Test
109+
@DisplayName("when method is annotated with @Schedules should start a new span.")
110+
void schedules() {
111+
// when
112+
schedulingTester.testSchedules();
113+
114+
// then
115+
List<List<SpanData>> traces = testing.waitForTraces(1);
116+
assertThat(traces)
117+
.hasTracesSatisfyingExactly(
118+
trace ->
119+
trace.hasSpansSatisfyingExactly(
120+
span ->
121+
span.hasName(unproxiedTesterSimpleClassName + ".testSchedules")
122+
.hasKind(INTERNAL)
123+
.hasAttributesSatisfyingExactly(
124+
equalTo(CODE_NAMESPACE, unproxiedTesterClassName),
125+
equalTo(CODE_FUNCTION, "testSchedules"))));
126+
}
127+
128+
@Test
129+
@DisplayName(
130+
"when method is annotated with @Scheduled and it starts nested span, spans should be nested.")
131+
void nestedSpanInScheduled() {
132+
// when
133+
schedulingTester.testNestedSpan();
134+
135+
// then
136+
List<List<SpanData>> traces = testing.waitForTraces(1);
137+
assertThat(traces)
138+
.hasTracesSatisfyingExactly(
139+
trace ->
140+
trace.hasSpansSatisfyingExactly(
141+
span ->
142+
span.hasName(unproxiedTesterSimpleClassName + ".testNestedSpan")
143+
.hasKind(INTERNAL)
144+
.hasAttributesSatisfyingExactly(
145+
equalTo(CODE_NAMESPACE, unproxiedTesterClassName),
146+
equalTo(CODE_FUNCTION, "testNestedSpan")),
147+
nestedSpan ->
148+
nestedSpan.hasParent(trace.getSpan(0)).hasKind(INTERNAL).hasName("test")));
149+
}
150+
151+
@Test
152+
@DisplayName(
153+
"when method is annotated with @Scheduled AND an exception is thrown span should record the exception")
154+
void scheduledError() {
155+
assertThatThrownBy(() -> schedulingTester.testScheduledWithException())
156+
.isInstanceOf(Exception.class);
157+
158+
List<List<SpanData>> traces = testing.waitForTraces(1);
159+
assertThat(traces)
160+
.hasTracesSatisfyingExactly(
161+
trace ->
162+
trace.hasSpansSatisfyingExactly(
163+
span ->
164+
span.hasName(unproxiedTesterSimpleClassName + ".testScheduledWithException")
165+
.hasKind(INTERNAL)
166+
.hasStatus(StatusData.error())
167+
.hasAttributesSatisfyingExactly(
168+
equalTo(CODE_NAMESPACE, unproxiedTesterClassName),
169+
equalTo(CODE_FUNCTION, "testScheduledWithException"))));
170+
}
171+
172+
static class InstrumentationSchedulingTester {
173+
private final OpenTelemetry openTelemetry;
174+
175+
InstrumentationSchedulingTester(OpenTelemetry openTelemetry) {
176+
this.openTelemetry = openTelemetry;
177+
}
178+
179+
@Scheduled(fixedRate = 1L)
180+
public void testScheduled() {
181+
// no-op
182+
}
183+
184+
@Scheduled(fixedRate = 1L)
185+
@Scheduled(fixedRate = 2L)
186+
public void testMultiScheduled() {
187+
// no-op
188+
}
189+
190+
@Schedules({@Scheduled(fixedRate = 1L), @Scheduled(fixedRate = 2L)})
191+
public void testSchedules() {
192+
// no-op
193+
}
194+
195+
@Scheduled(fixedRate = 1L)
196+
public void testNestedSpan() {
197+
Context current = Context.current();
198+
Tracer tracer = openTelemetry.getTracer("test");
199+
try (Scope ignored = current.makeCurrent()) {
200+
Span span = tracer.spanBuilder("test").startSpan();
201+
span.end();
202+
}
203+
}
204+
205+
@Scheduled(fixedRate = 1L)
206+
public void testScheduledWithException() {
207+
throw new IllegalStateException("something went wrong");
208+
}
209+
}
210+
}

0 commit comments

Comments
 (0)