Add auto configuration for spring scheduling instrumentation using aop (#12438)

This commit is contained in:
kyy1996 2024-10-16 23:01:57 +08:00 committed by GitHub
parent 802e8789bd
commit 4497fbf968
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 387 additions and 1 deletions

View File

@ -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.
*
* <p>This aspect would intercept all methods annotated with {@link
* org.springframework.scheduling.annotation.Scheduled} and {@link
* org.springframework.scheduling.annotation.Schedules}.
*
* <p>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<ClassAndMethod, Object> instrumenter;
public SpringSchedulingInstrumentationAspect(
OpenTelemetry openTelemetry, ConfigProperties configProperties) {
CodeAttributesGetter<ClassAndMethod> codedAttributesGetter =
ClassAndMethod.codeAttributesGetter();
InstrumenterBuilder<ClassAndMethod, Object> builder =
Instrumenter.builder(
openTelemetry,
INSTRUMENTATION_NAME,
CodeSpanNameExtractor.create(codedAttributesGetter))
.addAttributesExtractor(CodeAttributesExtractor.create(codedAttributesGetter));
if (configProperties.getBoolean(
"otel.instrumentation.spring-scheduling.experimental-span-attributes", false)) {
builder.addAttributesExtractor(
AttributesExtractor.constant(AttributeKey.stringKey("job.system"), "spring_scheduling"));
}
instrumenter = builder.buildInstrumenter();
}
@Pointcut(
"@annotation(org.springframework.scheduling.annotation.Scheduled)"
+ "|| @annotation(org.springframework.scheduling.annotation.Schedules)")
public void pointcut() {
// ignored
}
@Around("pointcut()")
public Object execution(ProceedingJoinPoint joinPoint) throws Throwable {
Context parent = Context.current();
ClassAndMethod request =
ClassAndMethod.create(
AopProxyUtils.ultimateTargetClass(joinPoint.getTarget()),
((MethodSignature) joinPoint.getSignature()).getMethod().getName());
if (!instrumenter.shouldStart(parent, request)) {
return joinPoint.proceed();
}
Context context = instrumenter.start(parent, request);
try (Scope ignored = context.makeCurrent()) {
Object object = joinPoint.proceed();
instrumenter.end(context, request, object, null);
return object;
} catch (Throwable t) {
instrumenter.end(context, request, null, t);
throw t;
}
}
}

View File

@ -0,0 +1,34 @@
/*
* 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.instrumentation.spring.autoconfigure.internal.ConditionalOnEnabledInstrumentation;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.Scheduled;
/**
* Configures an aspect for tracing.
*
* <p>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);
}
}

View File

@ -19,6 +19,10 @@ class OpenTelemetryAnnotationsRuntimeHints implements RuntimeHintsRegistrar {
.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));
}
}

View File

@ -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

View File

@ -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

View File

@ -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<List<SpanData>> 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<List<SpanData>> 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<List<SpanData>> 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<List<SpanData>> 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<List<SpanData>> 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");
}
}
}

View File

@ -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());
}
}