diff --git a/instrumentation-core/spring/spring-boot-autoconfigure/spring-boot-autoconfigure.gradle b/instrumentation-core/spring/spring-boot-autoconfigure/spring-boot-autoconfigure.gradle index 919cd3c3e6..7e1566011f 100644 --- a/instrumentation-core/spring/spring-boot-autoconfigure/spring-boot-autoconfigure.gradle +++ b/instrumentation-core/spring/spring-boot-autoconfigure/spring-boot-autoconfigure.gradle @@ -13,19 +13,22 @@ dependencies { implementation group: 'org.springframework.boot', name: 'spring-boot-autoconfigure', version: versions.springboot 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 diff --git a/instrumentation-core/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/TraceAspectAutoConfiguration.java b/instrumentation-core/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/TraceAspectAutoConfiguration.java new file mode 100644 index 0000000000..34ca63c55c --- /dev/null +++ b/instrumentation-core/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/TraceAspectAutoConfiguration.java @@ -0,0 +1,42 @@ +/* + * 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); + } +} diff --git a/instrumentation-core/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/TraceAspectProperties.java b/instrumentation-core/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/TraceAspectProperties.java new file mode 100644 index 0000000000..1b643cc244 --- /dev/null +++ b/instrumentation-core/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/TraceAspectProperties.java @@ -0,0 +1,33 @@ +/* + * 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; + } +} diff --git a/instrumentation-core/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspect.java b/instrumentation-core/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspect.java new file mode 100644 index 0000000000..6f29f9482a --- /dev/null +++ b/instrumentation-core/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspect.java @@ -0,0 +1,75 @@ +/* + * 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}. + * + *

Ensure methods annotated with {@link WithSpan} are implemented on beans managed by the Spring + * container. + * + *

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; + } +} diff --git a/instrumentation-core/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/instrumentation-core/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index e23acb8459..4c63aebcfa 100644 --- a/instrumentation-core/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/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 diff --git a/instrumentation-core/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/TraceAspectAutoConfigurationTest.java b/instrumentation-core/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/TraceAspectAutoConfigurationTest.java new file mode 100644 index 0000000000..2525e0a093 --- /dev/null +++ b/instrumentation-core/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/TraceAspectAutoConfigurationTest.java @@ -0,0 +1,66 @@ +/* + * 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(); + }); + } +} diff --git a/instrumentation-core/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspectTest.java b/instrumentation-core/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspectTest.java new file mode 100644 index 0000000000..2d065a8732 --- /dev/null +++ b/instrumentation-core/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/aspects/WithSpanAspectTest.java @@ -0,0 +1,150 @@ +/* + * 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(); + } +} diff --git a/instrumentation-core/spring/starters/spring-starter/spring-starter.gradle b/instrumentation-core/spring/starters/spring-starter/spring-starter.gradle index 903a097a33..72b21a1bde 100644 --- a/instrumentation-core/spring/starters/spring-starter/spring-starter.gradle +++ b/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