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

This commit is contained in:
Munir Abdinur 2020-08-08 03:22:30 +00:00 committed by GitHub
parent bbd63f7485
commit 1d80e19812
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 376 additions and 3 deletions

View File

@ -13,19 +13,22 @@ dependencies {
implementation group: 'org.springframework.boot', name: 'spring-boot-autoconfigure', version: versions.springboot 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 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' 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-web', version: versions.springboot
compileOnly group: 'org.springframework.boot', name: 'spring-boot-starter-webflux', 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-webmvc-3.1')
implementation project(':instrumentation-core:spring:spring-web-3.1') implementation project(':instrumentation-core:spring:spring-web-3.1')
implementation project(':instrumentation-core:spring:spring-webflux-5.0') implementation project(':instrumentation-core:spring:spring-webflux-5.0')
compileOnly deps.opentelemetryApiAutoAnnotations
compileOnly group: 'io.grpc', name: 'grpc-api', version: '1.30.2' compileOnly group: 'io.grpc', name: 'grpc-api', version: '1.30.2'
compileOnly deps.opentelemetryLogging compileOnly deps.opentelemetryLogging
compileOnly deps.opentelemetryJaeger compileOnly deps.opentelemetryJaeger
compileOnly deps.opentelemetryOtlp compileOnly deps.opentelemetryOtlp
compileOnly deps.opentelemetryZipkin 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-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-web', version: versions.springboot
testImplementation(group: 'org.springframework.boot', name: 'spring-boot-starter-test', 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.opentelemetrySdk
testImplementation deps.opentelemetryApiAutoAnnotations
testImplementation group: 'io.grpc', name: 'grpc-api', version: '1.30.2' testImplementation group: 'io.grpc', name: 'grpc-api', version: '1.30.2'
testImplementation group: 'io.grpc', name: 'grpc-netty-shaded', version: '1.30.2' testImplementation group: 'io.grpc', name: 'grpc-netty-shaded', version: '1.30.2'
testImplementation deps.opentelemetryLogging testImplementation deps.opentelemetryLogging

View File

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

View File

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

View File

@ -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}.
*
* <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;
}
}

View File

@ -6,4 +6,5 @@ io.opentelemetry.instrumentation.spring.autoconfigure.exporters.logging.LoggingS
io.opentelemetry.instrumentation.spring.autoconfigure.TracerAutoConfiguration,\ io.opentelemetry.instrumentation.spring.autoconfigure.TracerAutoConfiguration,\
io.opentelemetry.instrumentation.spring.autoconfigure.httpclients.resttemplate.RestTemplateAutoConfiguration,\ io.opentelemetry.instrumentation.spring.autoconfigure.httpclients.resttemplate.RestTemplateAutoConfiguration,\
io.opentelemetry.instrumentation.spring.autoconfigure.httpclients.webclient.WebClientAutoConfiguration,\ 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

View File

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

View File

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

View File

@ -11,7 +11,9 @@ sourceCompatibility = '8'
dependencies { dependencies {
api group: "org.springframework.boot", name: "spring-boot-starter", version: versions.springboot 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 project(':instrumentation-core:spring:spring-boot-autoconfigure')
api deps.opentelemetryApiAutoAnnotations
api deps.opentelemetryApi api deps.opentelemetryApi
api deps.opentelemetryLogging api deps.opentelemetryLogging
api deps.opentelemetrySdk api deps.opentelemetrySdk