Add @Withspan functionality using spring-aop (#902)
This commit is contained in:
parent
bbd63f7485
commit
1d80e19812
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue