Add auto configuration for spring scheduling instrumentation using aop (#12438)
This commit is contained in:
parent
802e8789bd
commit
4497fbf968
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue