Add functionality using spring-aop
MODIFY
instrumentation-core/spring/spring-boot-autoconfigure/spring-boot-autoconfigure.gradle
instrumentation-core/spring/spring-boot-autoconfigure/spring-boot-autoconfigure.gradle
@@ -14,18 +14,21 @@ dependencies {
annotationProcessor group: 'org.springframework.boot', name: 'spring-boot-autoconfigure-processor', version: versions.springboot
implementation group: 'javax.validation', name: 'validation-api', version: '2.0.1.Final'
+ compileOnly group: 'org.springframework.boot', name: 'spring-boot-starter-aop', version: versions.springboot
compileOnly group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: versions.springboot
compileOnly group: 'org.springframework.boot', name: 'spring-boot-starter-webflux', version: versions.springboot
implementation project(':instrumentation-core:spring:spring-webmvc-3.1')
implementation project(':instrumentation-core:spring:spring-web-3.1')
implementation project(':instrumentation-core:spring:spring-webflux-5.0')
+ compileOnly deps.opentelemetryApiAutoAnnotations
compileOnly group: 'io.grpc', name: 'grpc-api', version: '1.30.2'
compileOnly deps.opentelemetryLogging
compileOnly deps.opentelemetryJaeger
compileOnly deps.opentelemetryOtlp
compileOnly deps.opentelemetryZipkin
+ testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-aop', version: versions.springboot
testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-webflux', version: versions.springboot
testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: versions.springboot
testImplementation(group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: versions.springboot) {
@@ -33,6 +36,7 @@ dependencies {
}
testImplementation deps.opentelemetrySdk
+ testImplementation deps.opentelemetryApiAutoAnnotations
testImplementation group: 'io.grpc', name: 'grpc-api', version: '1.30.2'
testImplementation group: 'io.grpc', name: 'grpc-netty-shaded', version: '1.30.2'
testImplementation deps.opentelemetryLogging

ADD
instrumentation-core/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/TraceAspectAutoConfiguration.java
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opentelemetry.instrumentation.spring.autoconfigure.aspects;
+
+import io.opentelemetry.extensions.auto.annotations.WithSpan;
+import io.opentelemetry.trace.Tracer;
+import org.aspectj.lang.annotation.Aspect;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/** Configures {@link WithSpanAspect} to trace bean methods annotated with {@link WithSpan}. */
+@Configuration
+@EnableConfigurationProperties(TraceAspectProperties.class)
+@ConditionalOnProperty(
+ prefix = "opentelemetry.trace.aspects",
+ name = "enabled",
+ matchIfMissing = true)
+@ConditionalOnClass({Aspect.class, WithSpan.class})
+public class TraceAspectAutoConfiguration {
+
+ @Bean
+ public WithSpanAspect withSpanAspect(Tracer tracer) {
+ return new WithSpanAspect(tracer);
+ }
+}

ADD
instrumentation-core/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/TraceAspectProperties.java
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opentelemetry.instrumentation.spring.autoconfigure.aspects;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/** Configuration for enabling tracing aspects. */
+@ConfigurationProperties(prefix = "opentelemetry.trace.aspects")
+public final class TraceAspectProperties {
+ private boolean enabled;
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+}

ADD
instrumentation-core/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspect.java
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opentelemetry.instrumentation.spring.autoconfigure.aspects;
+
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.extensions.auto.annotations.WithSpan;
+import io.opentelemetry.trace.Span;
+import io.opentelemetry.trace.Status;
+import io.opentelemetry.trace.Tracer;
+import java.lang.reflect.Method;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.reflect.MethodSignature;
+
+/**
+ * Uses Spring-AOP to wrap methods marked by {@link WithSpan} in a {@link
+ * io.opentelemetry.trace.Span}.
+ *
+ * <p>Ensure methods annotated with {@link WithSpan} are implemented on beans managed by the Spring
+ * container.
+ *
+ * <p>Note: This Aspect uses spring-aop to proxy beans. Therefore the {@link WithSpan} annotation
+ * can not be applied to constructors.
+ */
+@Aspect
+public class WithSpanAspect {
+
+ private final Tracer tracer;
+
+ public WithSpanAspect(Tracer tracer) {
+ this.tracer = tracer;
+ }
+
+ @Around("@annotation(io.opentelemetry.extensions.auto.annotations.WithSpan)")
+ public Object traceMethod(ProceedingJoinPoint pjp) throws Throwable {
+ MethodSignature signature = (MethodSignature) pjp.getSignature();
+ Method method = signature.getMethod();
+ WithSpan withSpan = method.getAnnotation(WithSpan.class);
+
+ Span span =
+ tracer.spanBuilder(getSpanName(withSpan, method)).setSpanKind(withSpan.kind()).startSpan();
+ try (Scope scope = tracer.withSpan(span)) {
+ return pjp.proceed();
+ } catch (Throwable t) {
+ span.setStatus(Status.INTERNAL);
+ span.recordException(t);
+ throw t;
+ } finally {
+ span.end();
+ }
+ }
+
+ private String getSpanName(WithSpan withSpan, Method method) {
+ String spanName = withSpan.value();
+ if (spanName.isEmpty()) {
+ return method.getDeclaringClass().getSimpleName() + "." + method.getName();
+ }
+ return spanName;
+ }
+}

MODIFY
instrumentation-core/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories
instrumentation-core/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories
@@ -6,4 +6,5 @@ io.opentelemetry.instrumentation.spring.autoconfigure.exporters.logging.LoggingS
io.opentelemetry.instrumentation.spring.autoconfigure.TracerAutoConfiguration,\
io.opentelemetry.instrumentation.spring.autoconfigure.httpclients.resttemplate.RestTemplateAutoConfiguration,\
io.opentelemetry.instrumentation.spring.autoconfigure.httpclients.webclient.WebClientAutoConfiguration,\
-io.opentelemetry.instrumentation.spring.autoconfigure.webmvc.WebMVCFilterAutoConfiguration
+io.opentelemetry.instrumentation.spring.autoconfigure.webmvc.WebMVCFilterAutoConfiguration,\
+io.opentelemetry.instrumentation.spring.autoconfigure.aspects.TraceAspectAutoConfiguration

ADD
instrumentation-core/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/TraceAspectAutoConfigurationTest.java
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opentelemetry.instrumentation.spring.autoconfigure.aspects;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.opentelemetry.instrumentation.spring.autoconfigure.TracerAutoConfiguration;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+/** Spring Boot auto configuration test for {@link TraceAspectAutoConfiguration} */
+public class TraceAspectAutoConfigurationTest {
+
+ private final ApplicationContextRunner contextRunner =
+ new ApplicationContextRunner()
+ .withConfiguration(
+ AutoConfigurations.of(
+ TracerAutoConfiguration.class, TraceAspectAutoConfiguration.class));
+
+ @Test
+ @DisplayName("when aspects are ENABLED should initialize WithSpanAspect bean")
+ void aspectsEnabled() {
+ this.contextRunner
+ .withPropertyValues("opentelemetry.trace.aspects.enabled=true")
+ .run(
+ (context) -> {
+ assertThat(context.getBean("withSpanAspect", WithSpanAspect.class)).isNotNull();
+ });
+ }
+
+ @Test
+ @DisplayName("when aspects are DISABLED should NOT initialize WithSpanAspect bean")
+ void disabledProperty() {
+ this.contextRunner
+ .withPropertyValues("opentelemetry.trace.aspects.enabled=false")
+ .run(
+ (context) -> {
+ assertThat(context.containsBean("withSpanAspect")).isFalse();
+ });
+ }
+
+ @Test
+ @DisplayName("when aspects enabled property is MISSING should initialize WithSpanAspect bean")
+ void noProperty() {
+ this.contextRunner.run(
+ (context) -> {
+ assertThat(context.getBean("withSpanAspect", WithSpanAspect.class)).isNotNull();
+ });
+ }
+}

ADD
instrumentation-core/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspectTest.java
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opentelemetry.instrumentation.spring.autoconfigure.aspects;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.extensions.auto.annotations.WithSpan;
+import io.opentelemetry.trace.Span;
+import io.opentelemetry.trace.Span.Kind;
+import io.opentelemetry.trace.Tracer;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.aop.aspectj.annotation.AspectJProxyFactory;
+
+/** Spring AOP Test for {@link WithSpanAspect} */
+@ExtendWith(MockitoExtension.class)
+public class WithSpanAspectTest {
+ static class WithSpanTester {
+ @WithSpan
+ public String testWithSpan() {
+ return "Span with name testWithSpan was created";
+ }
+
+ @WithSpan("greatestSpanEver")
+ public String testWithSpanWithValue() {
+ return "Span with name greatestSpanEver was created";
+ }
+
+ @WithSpan(kind = Kind.CLIENT)
+ public String testWithSpanWithKind() {
+ return "Span with name testWithSpanWithKind and Kind.CLIENT was created";
+ }
+
+ @WithSpan
+ public String testWithSpanWithException() throws Exception {
+ throw new Exception("Test @WithSpan With Exception");
+ }
+ }
+
+ @Mock private Tracer tracer;
+ @Mock private Span span;
+ @Mock private Span.Builder spanBuilder;
+ @Mock private Scope scope;
+ @Mock private ProceedingJoinPoint pjp;
+ @Mock private MethodSignature signature;
+
+ private WithSpanTester withSpanTester;
+
+ @BeforeEach
+ void setup() {
+ when(tracer.spanBuilder(any())).thenReturn(spanBuilder);
+ when(spanBuilder.setSpanKind(any())).thenReturn(spanBuilder);
+ when(spanBuilder.startSpan()).thenReturn(span);
+ when(tracer.withSpan(span)).thenReturn(scope);
+
+ AspectJProxyFactory factory = new AspectJProxyFactory(new WithSpanTester());
+ WithSpanAspect aspect = new WithSpanAspect(tracer);
+ factory.addAspect(aspect);
+
+ withSpanTester = factory.getProxy();
+ }
+
+ @Test
+ @DisplayName("when method is annotated with @WithSpan should wrap method execution in a Span")
+ void withSpan() throws Throwable {
+
+ withSpanTester.testWithSpan();
+
+ verify(tracer, times(1)).spanBuilder("WithSpanTester.testWithSpan");
+ verify(spanBuilder, times(1)).startSpan();
+ verify(span, times(1)).end();
+ }
+
+ @Test
+ @DisplayName(
+ "when @WithSpan value is set should wrap method execution in a Span with custom name")
+ void withSpanName() throws Throwable {
+
+ withSpanTester.testWithSpanWithValue();
+
+ verify(tracer, times(1)).spanBuilder("greatestSpanEver");
+ verify(spanBuilder, times(1)).startSpan();
+ verify(span, times(1)).end();
+ }
+
+ @Test
+ @DisplayName(
+ "when method is annotated with @WithSpan AND an exception is thrown span should record the exception")
+ void withSpanError() throws Throwable {
+
+ assertThatThrownBy(
+ () -> {
+ withSpanTester.testWithSpanWithException();
+ })
+ .isInstanceOf(Exception.class);
+
+ verify(spanBuilder, times(1)).startSpan();
+ verify(span, times(1)).recordException(any(Exception.class));
+ verify(span, times(1)).end();
+ }
+
+ @Test
+ @DisplayName(
+ "when method is annotated with @WithSpan AND Span.Kind is missing should set default Kind")
+ void withSpanDefaultKind() throws Throwable {
+
+ withSpanTester.testWithSpan();
+
+ verify(spanBuilder, times(1)).setSpanKind(Kind.INTERNAL);
+ verify(spanBuilder, times(1)).startSpan();
+ verify(span, times(1)).end();
+ }
+
+ @Test
+ @DisplayName(
+ "when method is annotated with @WithSpan AND WithSpan.kind is set should build span with the declared Kind")
+ void withSpanClientKind() throws Throwable {
+
+ withSpanTester.testWithSpanWithKind();
+
+ verify(spanBuilder, times(1)).setSpanKind(Kind.CLIENT);
+ verify(spanBuilder, times(1)).startSpan();
+ verify(span, times(1)).end();
+ }
+}

MODIFY
instrumentation-core/spring/starters/spring-starter/spring-starter.gradle
instrumentation-core/spring/starters/spring-starter/spring-starter.gradle
@@ -11,7 +11,9 @@ sourceCompatibility = '8'
dependencies {
api group: "org.springframework.boot", name: "spring-boot-starter", version: versions.springboot
+ api group: 'org.springframework.boot', name: 'spring-boot-starter-aop', version: versions.springboot
api project(':instrumentation-core:spring:spring-boot-autoconfigure')
+ api deps.opentelemetryApiAutoAnnotations
api deps.opentelemetryApi
api deps.opentelemetryLogging
api deps.opentelemetrySdk

Add @Withspan functionality using spring-aop (#902)