Skip to content

Commit

Permalink
Add auto configuration for spring scheduling instrumentation using aop
Browse files Browse the repository at this point in the history
  • Loading branch information
kyy1996 committed Oct 14, 2024
1 parent 8a10097 commit 77d7cd0
Show file tree
Hide file tree
Showing 8 changed files with 423 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.scheduling;

import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
import io.opentelemetry.semconv.incubating.ThreadIncubatingAttributes;
import javax.annotation.Nullable;
import org.springframework.lang.NonNull;

/** Expose current thread to attributes. */
class InternalThreadAttributesExtractor<REQUEST, RESPONSE>
implements AttributesExtractor<REQUEST, RESPONSE> {
@Override
public void onStart(
@NonNull AttributesBuilder attributes,
@NonNull Context parentContext,
@NonNull REQUEST classAndMethod) {
attributes
.put(ThreadIncubatingAttributes.THREAD_ID, Thread.currentThread().getId())
.put(ThreadIncubatingAttributes.THREAD_NAME, Thread.currentThread().getName());
}

@Override
public void onEnd(
@NonNull AttributesBuilder attributes,
@NonNull Context context,
@NonNull REQUEST classAndMethod,
@Nullable RESPONSE o,
@Nullable Throwable error) {
// ignored
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* 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))
.addAttributesExtractor(new InternalThreadAttributesExtractor<>());
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;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* 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.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.
*/
@ConditionalOnEnabledInstrumentation(module = "spring-scheduling")
@ConditionalOnClass({Scheduled.class, Aspect.class})
@Configuration
public class SpringSchedulingInstrumentationAutoConfiguration {
@Bean
SpringSchedulingInstrumentationAspect springSchedulingInstrumentationAspect(
OpenTelemetry openTelemetry, ConfigProperties configProperties) {
return new SpringSchedulingInstrumentationAspect(openTelemetry, configProperties);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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)
.hasAttributesSatisfying(
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)
.hasAttributesSatisfying(
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)
.hasAttributesSatisfying(
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)
.hasAttributesSatisfying(
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 @WithSpan AND an exception is thrown span should record the exception")
void withSpanError() {
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())
.hasAttributesSatisfying(
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");
}
}
}
Loading

0 comments on commit 77d7cd0

Please sign in to comment.