diff options
author | savaki <matt.ho@gmail.com> | 2018-07-13 21:23:51 -0700 |
---|---|---|
committer | savaki <matt.ho@gmail.com> | 2018-07-21 07:44:40 -0700 |
commit | 6e885b6a0b2756676ec6443dedaeccfb4e8176dc (patch) | |
tree | 8ef18723f8f1367ad77339ff37842a191321569b | |
parent | 82e2988b912f8ec73f391f3ad5e9fe64dee2b5fe (diff) | |
download | opencensus-java-6e885b6a0b2756676ec6443dedaeccfb4e8176dc.tar.gz |
added initial support for spring annotations
13 files changed, 468 insertions, 1 deletions
diff --git a/all/build.gradle b/all/build.gradle index 387efabb..f62ecab8 100644 --- a/all/build.gradle +++ b/all/build.gradle @@ -21,6 +21,7 @@ def subprojects = [ project(':opencensus-contrib-http-util'), project(':opencensus-contrib-log-correlation-stackdriver'), project(':opencensus-contrib-monitored-resource-util'), + project(':opencensus-contrib-spring'), project(':opencensus-contrib-zpages'), project(':opencensus-exporter-trace-logging'), project(':opencensus-exporter-trace-stackdriver'), @@ -43,6 +44,7 @@ def subprojects_javadoc = [ project(':opencensus-contrib-http-util'), project(':opencensus-contrib-log-correlation-stackdriver'), project(':opencensus-contrib-monitored-resource-util'), + project(':opencensus-contrib-spring'), project(':opencensus-contrib-zpages'), project(':opencensus-exporter-trace-logging'), project(':opencensus-exporter-trace-stackdriver'), diff --git a/build.gradle b/build.gradle index 572bbcaf..c306f137 100644 --- a/build.gradle +++ b/build.gradle @@ -144,6 +144,7 @@ subprojects { ext { appengineVersion = '1.9.64' + aspectjVersion = '1.8.11' autoValueVersion = '1.4' findBugsVersion = '3.0.1' errorProneVersion = '2.3.1' @@ -153,6 +154,7 @@ subprojects { googleCloudBetaVersion = '0.54.0-beta' googleCloudGaVersion = '1.36.0' signalfxVersion = '0.0.39' + springVersion = '4.3.12.RELEASE' prometheusVersion = '0.4.0' protobufVersion = '3.5.1' zipkinReporterVersion = '2.3.2' @@ -160,6 +162,7 @@ subprojects { libraries = [ appengine_api: "com.google.appengine:appengine-api-1.0-sdk:${appengineVersion}", + aspectj: "org.aspectj:aspectjrt:${aspectjVersion}", auto_value: "com.google.auto.value:auto-value:${autoValueVersion}", auto_service: 'com.google.auto.service:auto-service:1.0-rc3', byte_buddy: 'net.bytebuddy:byte-buddy:1.7.11', @@ -179,6 +182,10 @@ subprojects { guava: "com.google.guava:guava:${guavaVersion}", jsr305: "com.google.code.findbugs:jsr305:${findBugsVersion}", signalfx_java: "com.signalfx.public:signalfx-java:${signalfxVersion}", + spring_aspects: "org.springframework:spring-aspects:${springVersion}", + spring_context: "org.springframework:spring-context:${springVersion}", + spring_context_support: "org.springframework:spring-context-support:${springVersion}", + spring_test: "org.springframework:spring-test:${springVersion}", prometheus_simpleclient: "io.prometheus:simpleclient:${prometheusVersion}", protobuf: "com.google.protobuf:protobuf-java:${protobufVersion}", @@ -365,6 +372,7 @@ subprojects { 'opencensus-contrib-http-util', 'opencensus-contrib-log-correlation-stackdriver', 'opencensus-contrib-monitored-resource-util', + 'opencensus-contrib-spring', 'opencensus-contrib-zpages', 'opencensus-exporter-stats-prometheus', 'opencensus-exporter-stats-signalfx', diff --git a/contrib/spring/README.md b/contrib/spring/README.md new file mode 100644 index 00000000..1394049e --- /dev/null +++ b/contrib/spring/README.md @@ -0,0 +1,94 @@ +# spring +[![Build Status][travis-image]][travis-url] +[![Windows Build Status][appveyor-image]][appveyor-url] +[![Maven Central][maven-image]][maven-url] + +Provides annotation support for projects that use Spring. + +## Quickstart + +### Add the dependencies to your project. + +For Maven add to your `pom.xml`: +```xml +<dependencies> + <!-- census --> + <dependency> + <groupId>io.opencensus</groupId> + <artifactId>opencensus-api</artifactId> + <version>0.15.0</version> + </dependency> + <dependency> + <groupId>io.opencensus</groupId> + <artifactId>opencensus-contrib-spring</artifactId> + <version>0.15.0</version> + </dependency> + <dependency> + <groupId>io.opencensus</groupId> + <artifactId>opencensus-impl</artifactId> + <version>0.15.0</version> + <scope>runtime</scope> + </dependency> + + <!-- spring aspects --> + <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-aspects</artifactId> + <version>SPRING_VERSION</version> + <scope>runtime</scope> + </dependency> + +</dependencies> +``` + +For Gradle add to your dependencies: +```gradle +compile 'io.opencensus:opencensus-api:0.15.0' +compile 'io.opencensus:opencensus-contrib-spring:0.15.0' +runtime 'io.opencensus:opencensus-impl:0.15.0' +runtime 'org.springframework:spring-aspects:SPRING_VERSION' +``` + +### Configure Spring + +To configure annotation support within Spring, include the following with your +spring xml configuration. + +```xml + <!-- Enable @AspectJ annotation support --> + <aop:aspectj-autoproxy/> + + <!-- traces explicit calls to @Trace --> + <bean id="censusAspect" class="io.opencensus.contrib.spring.aop.CensusSpringAspect"/> + + <!-- traces all SQL calls e.g. New Relic --> + <bean id="censusSQLAspect" class="io.opencensus.contrib.spring.aop.CensusSpringSQLAspect"/> +``` + +### Usage + +Once configured, you can use the `@Trace` annotation to indicate that a method should be traces. + +```java + @Trace() + void example1() { + // do work + } + + // a custom span name can also be provided to @Trace + @Trace(name = "custom-label") + void example2() { + // do moar work + } +``` + +#### Notes + +Spring support only enables annotations. You'll still need to configure opencensus and register exporters / views. + +[travis-image]: https://travis-ci.org/census-instrumentation/opencensus-java.svg?branch=master +[travis-url]: https://travis-ci.org/census-instrumentation/opencensus-java +[appveyor-image]: https://ci.appveyor.com/api/projects/status/hxthmpkxar4jq4be/branch/master?svg=true +[appveyor-url]: https://ci.appveyor.com/project/opencensusjavateam/opencensus-java/branch/master +[maven-image]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-contrib-spring/badge.svg +[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-contrib-spring diff --git a/contrib/spring/build.gradle b/contrib/spring/build.gradle new file mode 100644 index 00000000..a01ccf24 --- /dev/null +++ b/contrib/spring/build.gradle @@ -0,0 +1,21 @@ +description = 'OpenCensus Spring' + +apply plugin: 'java' + +[compileJava, compileTestJava].each() { + it.sourceCompatibility = 1.8 + it.targetCompatibility = 1.8 +} + +dependencies { + compile project(':opencensus-api'), + project(':opencensus-impl'), + project(':opencensus-testing'), + libraries.aspectj, + libraries.spring_aspects, + libraries.spring_context, + libraries.spring_context_support, + libraries.spring_test + + signature "org.codehaus.mojo.signature:java18:+@signature" +} diff --git a/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/CensusSpringAspect.java b/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/CensusSpringAspect.java new file mode 100644 index 00000000..7baaa590 --- /dev/null +++ b/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/CensusSpringAspect.java @@ -0,0 +1,34 @@ +package io.opencensus.contrib.spring.aop; + +import io.opencensus.trace.SpanBuilder; +import io.opencensus.trace.Tracing; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.beans.factory.annotation.Configurable; + +import java.lang.reflect.Method; + +/** + * CensusSpringAspect handles logic for the @Trace annotation + */ +@Aspect +@Configurable +public class CensusSpringAspect { + @Around("@annotation(Trace)") + public Object trace(ProceedingJoinPoint call) throws Throwable { + MethodSignature signature = (MethodSignature) call.getSignature(); + Method method = signature.getMethod(); + + Trace annotation = method.getAnnotation(Trace.class); + String spanName = annotation.name(); + if ("".equals(spanName)) { + spanName = method.getName(); + } + + SpanBuilder builder = Tracing.getTracer().spanBuilder(spanName); + + return Handler.proceed(call, builder); + } +} diff --git a/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/CensusSpringSQLAspect.java b/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/CensusSpringSQLAspect.java new file mode 100644 index 00000000..d13172d7 --- /dev/null +++ b/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/CensusSpringSQLAspect.java @@ -0,0 +1,60 @@ +package io.opencensus.contrib.spring.aop; + +import io.opencensus.trace.SpanBuilder; +import io.opencensus.trace.Tracer; +import io.opencensus.trace.Tracing; +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.springframework.beans.factory.annotation.Configurable; + +/** + */ +@Aspect +@Configurable +public class CensusSpringSQLAspect { + private static final Tracer tracer = Tracing.getTracer(); + + @Around("execute() || testing()") + public Object trace(ProceedingJoinPoint call) throws Throwable { + if (call.getArgs().length == 0 || call.getArgs()[0] == null) { + return call.proceed(); + } + + String sql = (String) call.getArgs()[0]; + String spanName = makeSpanName(call, sql); + SpanBuilder builder = tracer.spanBuilder(spanName); + + return Handler.proceed(call, builder, sql); + } + + /** + * execute creates spans around all invocations of Statement.execute*. The raw SQL + * will be stored in an annotation associated with the Span + */ + @Pointcut("execution(public !void java.sql.Statement.execute*(java.lang.String))") + protected void execute() { + } + + @Pointcut("execution(public void io.opencensus.contrib.spring.aop.Sample.execute*(java.lang.String))") + protected void testing() { + } + + private static String makeSpanName(ProceedingJoinPoint call, String sql) { + String hash = Integer.toHexString(hashCode(sql.toCharArray())); + return call.getSignature().getName() + "-" + hash; + } + + private static int hashCode(char[] seq) { + if (seq == null) { + return 0; + } + + int hash = 0; + for (char c : seq) { + hash = 31 * hash + c; + } + return hash; + } +} diff --git a/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/Handler.java b/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/Handler.java new file mode 100644 index 00000000..2bb622f3 --- /dev/null +++ b/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/Handler.java @@ -0,0 +1,35 @@ +package io.opencensus.contrib.spring.aop; + +import io.opencensus.common.Scope; +import io.opencensus.trace.SpanBuilder; +import io.opencensus.trace.Status; +import io.opencensus.trace.Tracer; +import io.opencensus.trace.Tracing; +import org.aspectj.lang.ProceedingJoinPoint; + +/** + * Handler defines common logic for wrapping a span around the specified JoinPoint. + */ +final class Handler { + private static final Tracer tracer = Tracing.getTracer(); + + private Handler() { + } + + static Object proceed(ProceedingJoinPoint call, SpanBuilder builder, String... annotations) throws Throwable { + try (Scope scope = builder.startScopedSpan()) { + + for (String annotation : annotations) { + tracer.getCurrentSpan().addAnnotation(annotation); + } + + return call.proceed(); + + } catch (Throwable t) { + io.opencensus.trace.Span span = tracer.getCurrentSpan(); + span.addAnnotation(t.getMessage()); + span.setStatus(Status.UNKNOWN); + throw t; + } + } +} diff --git a/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/Trace.java b/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/Trace.java new file mode 100644 index 00000000..e022c576 --- /dev/null +++ b/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/Trace.java @@ -0,0 +1,24 @@ +package io.opencensus.contrib.spring.aop; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Trace specifies the annotated method should be included in the Trace. + * + * <p> + * By default, the name of the method will be used for the span name. However, the + * span name can be explicitly set via the name interface. + * </p> + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Trace { + /** + * @return the optional custom span name; if not specified the method name will be + * used as the span name + */ + String name() default ""; +} diff --git a/contrib/spring/src/test/java/io/opencensus/contrib/spring/aop/CensusSpringInterceptorTest.java b/contrib/spring/src/test/java/io/opencensus/contrib/spring/aop/CensusSpringInterceptorTest.java new file mode 100644 index 00000000..0513c942 --- /dev/null +++ b/contrib/spring/src/test/java/io/opencensus/contrib/spring/aop/CensusSpringInterceptorTest.java @@ -0,0 +1,131 @@ +package io.opencensus.contrib.spring.aop; + +import io.opencensus.testing.export.TestHandler; +import io.opencensus.trace.Annotation; +import io.opencensus.trace.Tracing; +import io.opencensus.trace.config.TraceParams; +import io.opencensus.trace.export.SpanData; +import io.opencensus.trace.export.SpanExporter; +import io.opencensus.trace.samplers.Samplers; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import java.util.List; + +import static com.google.common.truth.Truth.assertThat; + +/** + */ +@RunWith(JUnit4.class) +public class CensusSpringInterceptorTest { + ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml"); + + private TestHandler handler; + + @Before + public void setup() { + handler = new TestHandler(); + + SpanExporter exporter = Tracing.getExportComponent().getSpanExporter(); + exporter.registerHandler("testing", handler); + + TraceParams params = Tracing + .getTraceConfig() + .getActiveTraceParams() + .toBuilder() + .setSampler(Samplers.alwaysSample()) + .build(); + Tracing.getTraceConfig().updateActiveTraceParams(params); + } + + @After + public void teardown() { + SpanExporter exporter = Tracing.getExportComponent().getSpanExporter(); + exporter.unregisterHandler("testing"); + } + + @Test + public void testTraceUsesMethodAsSpanName() throws Exception { + // When + Sample sample = (Sample) context.getBean("sample"); + sample.call(100); + + // Then + List<SpanData> data = handler.waitForExport(1); + assertThat(data).isNotNull(); + assertThat(data.size()).isEqualTo(1); + assertThat(data.get(0).getName()).isEqualTo("call"); + } + + @Test + public void testTraceAcceptsCustomSpanName() throws Exception { + // When + Sample sample = (Sample) context.getBean("sample"); + sample.custom(100); + + // Then + List<SpanData> data = handler.waitForExport(1); + assertThat(data).isNotNull(); + assertThat(data.size()).isEqualTo(1); + assertThat(data.get(0).getName()).isEqualTo("blah"); + } + + @Test + public void testSQLExecute() throws Exception { + // When + String sql = "select 1"; + Sample sample = (Sample) context.getBean("sample"); + sample.execute(sql); + + // Then + List<SpanData> data = handler.waitForExport(1); + assertThat(data).isNotNull(); + assertThat(data.size()).isEqualTo(1); + assertThat(data.get(0).getName()).isEqualTo("execute-4705ea0d"); // sql-{hash of sql statement} + + List<SpanData.TimedEvent<Annotation>> events = data.get(0).getAnnotations().getEvents(); + assertThat(events.size()).isEqualTo(1); + assertThat(events.get(0).getEvent().getDescription()).isEqualTo(sql); + } + + @Test + public void testSQLQuery() throws Exception { + // When + String sql = "select 2"; + Sample sample = (Sample) context.getBean("sample"); + sample.executeQuery(sql); + + // Then + List<SpanData> data = handler.waitForExport(1); + assertThat(data).isNotNull(); + assertThat(data.size()).isEqualTo(1); + assertThat(data.get(0).getName()).isEqualTo("executeQuery-4705ea0e"); // sql-{hash of sql statement} + + List<SpanData.TimedEvent<Annotation>> events = data.get(0).getAnnotations().getEvents(); + assertThat(events.size()).isEqualTo(1); + assertThat(events.get(0).getEvent().getDescription()).isEqualTo(sql); + } + + @Test + public void testSQLUpdate() throws Exception { + // When + String sql = "update content set value = 1"; + Sample sample = (Sample) context.getBean("sample"); + sample.executeUpdate(sql); + + // Then + List<SpanData> data = handler.waitForExport(1); + assertThat(data).isNotNull(); + assertThat(data.size()).isEqualTo(1); + assertThat(data.get(0).getName()).isEqualTo("executeUpdate-acaeb423"); // sql-{hash of sql statement} + + List<SpanData.TimedEvent<Annotation>> events = data.get(0).getAnnotations().getEvents(); + assertThat(events.size()).isEqualTo(1); + assertThat(events.get(0).getEvent().getDescription()).isEqualTo(sql); + } +}
\ No newline at end of file diff --git a/contrib/spring/src/test/java/io/opencensus/contrib/spring/aop/Sample.java b/contrib/spring/src/test/java/io/opencensus/contrib/spring/aop/Sample.java new file mode 100644 index 00000000..7c1e1594 --- /dev/null +++ b/contrib/spring/src/test/java/io/opencensus/contrib/spring/aop/Sample.java @@ -0,0 +1,39 @@ +package io.opencensus.contrib.spring.aop; + +import java.sql.SQLException; + +/** + */ +public class Sample { + @Trace() + void example1() { + // do work + } + + @Trace(name = "custom-label") + void example2() { + // do moar work + } + + @Trace() + void call(long delay) throws Exception { + Thread.sleep(delay); + } + + @Trace(name = "blah") + void custom(long delay) throws Exception { + Thread.sleep(delay); + } + + public void execute(String sql) throws SQLException { + } + + public void executeQuery(String sql) throws SQLException { + } + + public void executeUpdate(String sql) throws SQLException { + } + + public void executeLargeUpdate(String sql) throws SQLException { + } +} diff --git a/contrib/spring/src/test/resources/spring.xml b/contrib/spring/src/test/resources/spring.xml new file mode 100644 index 00000000..4185a919 --- /dev/null +++ b/contrib/spring/src/test/resources/spring.xml @@ -0,0 +1,16 @@ +<beans xmlns="http://www.springframework.org/schema/beans" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:aop="http://www.springframework.org/schema/aop" + xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd"> + + <bean id="sample" class="io.opencensus.contrib.spring.aop.Sample"/> + + <!-- Enable @AspectJ annotation support --> + <aop:aspectj-autoproxy/> + + <!-- traces explicit calls to @Trace --> + <bean id="censusAspect" class="io.opencensus.contrib.spring.aop.CensusSpringAspect"/> + + <!-- traces all SQL calls e.g. New Relic --> + <bean id="censusSQLAspect" class="io.opencensus.contrib.spring.aop.CensusSpringSQLAspect"/> +</beans>
\ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 16d28051..046d2e4f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Wed Jul 11 18:13:40 PDT 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.7-all.zip diff --git a/settings.gradle b/settings.gradle index 036df52d..509f7e78 100644 --- a/settings.gradle +++ b/settings.gradle @@ -57,9 +57,11 @@ project(':opencensus-metrics').projectDir = "$rootDir/metrics" as File if (JavaVersion.current().isJava8Compatible()) { include ":opencensus-all" include ":opencensus-benchmarks" + include ":opencensus-contrib-spring" include ":opencensus-contrib-zpages" project(':opencensus-all').projectDir = "$rootDir/all" as File project(':opencensus-benchmarks').projectDir = "$rootDir/benchmarks" as File + project(':opencensus-contrib-spring').projectDir = "$rootDir/contrib/spring" as File project(':opencensus-contrib-zpages').projectDir = "$rootDir/contrib/zpages" as File } |