Add support for new annotation
MODIFY
agent-bootstrap/agent-bootstrap.gradle
agent-bootstrap/agent-bootstrap.gradle
@@ -11,6 +11,7 @@ minimumInstructionCoverage = 0.0
dependencies {
compile deps.opentelemetryApi
+ compile deps.opentelemetryApiAutoAnnotations
compile project(':utils:thread-utils')
compile deps.slf4j
compile group: 'org.slf4j', name: 'slf4j-simple', version: versions.slf4j

MODIFY
agent-bootstrap/src/main/java/io/opentelemetry/auto/bootstrap/instrumentation/decorator/BaseDecorator.java
agent-bootstrap/src/main/java/io/opentelemetry/auto/bootstrap/instrumentation/decorator/BaseDecorator.java
@@ -87,9 +87,6 @@ public abstract class BaseDecorator {
/**
* This method is used to generate an acceptable span (operation) name based on a given method
* reference. Anonymous classes are named based on their parent.
- *
- * @param method
- * @return
*/
public String spanNameForMethod(final Method method) {
return spanNameForClass(method.getDeclaringClass()) + "." + method.getName();
@@ -98,9 +95,6 @@ public abstract class BaseDecorator {
/**
* This method is used to generate an acceptable span (operation) name based on a given class
* reference. Anonymous classes are named based on their parent.
- *
- * @param clazz
- * @return
*/
public String spanNameForClass(final Class clazz) {
if (!clazz.isAnonymousClass()) {

ADD
instrumentation/trace-annotation/src/main/java/io/opentelemetry/auto/instrumentation/traceannotation/AbstractTraceAnnotationInstrumentation.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.auto.instrumentation.traceannotation;
+
+import static net.bytebuddy.matcher.ElementMatchers.isDeclaredBy;
+import static net.bytebuddy.matcher.ElementMatchers.none;
+
+import io.opentelemetry.auto.config.Config;
+import io.opentelemetry.auto.tooling.Instrumenter;
+import java.util.Map;
+import java.util.Set;
+import net.bytebuddy.description.ByteCodeElement;
+import net.bytebuddy.description.method.MethodDescription;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+import net.bytebuddy.matcher.ElementMatchers;
+
+public abstract class AbstractTraceAnnotationInstrumentation extends Instrumenter.Default {
+ public AbstractTraceAnnotationInstrumentation(
+ String instrumentationName, String... additionalNames) {
+ super(instrumentationName, additionalNames);
+ }
+
+ /*
+ Returns a matcher for all methods that should be excluded from auto-instrumentation by
+ annotation-based advices.
+ */
+ ElementMatcher.Junction<MethodDescription> configureExcludedMethods() {
+ ElementMatcher.Junction<MethodDescription> result = none();
+
+ Map<String, Set<String>> excludedMethods =
+ MethodsConfigurationParser.parse(Config.get().getTraceMethodsExclude());
+ for (Map.Entry<String, Set<String>> entry : excludedMethods.entrySet()) {
+ String className = entry.getKey();
+ ElementMatcher.Junction<ByteCodeElement> classMather =
+ isDeclaredBy(ElementMatchers.<TypeDescription>named(className));
+
+ ElementMatcher.Junction<MethodDescription> excludedMethodsMatcher = none();
+ for (String methodName : entry.getValue()) {
+ excludedMethodsMatcher = excludedMethodsMatcher.or(ElementMatchers.named(methodName));
+ }
+
+ result = result.or(classMather.and(excludedMethodsMatcher));
+ }
+
+ return result;
+ }
+}

MODIFY
instrumentation/trace-annotation/src/main/java/io/opentelemetry/auto/instrumentation/traceannotation/MethodsConfigurationParser.java
instrumentation/trace-annotation/src/main/java/io/opentelemetry/auto/instrumentation/traceannotation/MethodsConfigurationParser.java
@@ -44,7 +44,7 @@ public class MethodsConfigurationParser {
* keys are class names and corresponding value is a set of methods for that class.
*
* <p>Strings of such format are used e.g. to configure {@link Config#getTraceMethods()} and
- * {@link Config#getExcludedMethods()}
+ * {@link Config#getTraceMethodsExclude()}
*/
public static Map<String, Set<String>> parse(String configString) {
if (configString == null || configString.trim().isEmpty()) {

MODIFY
instrumentation/trace-annotation/src/main/java/io/opentelemetry/auto/instrumentation/traceannotation/TraceAnnotationsInstrumentation.java
instrumentation/trace-annotation/src/main/java/io/opentelemetry/auto/instrumentation/traceannotation/TraceAnnotationsInstrumentation.java
@@ -20,7 +20,6 @@ import static io.opentelemetry.auto.tooling.bytebuddy.matcher.AgentElementMatche
import static java.util.Collections.singletonMap;
import static net.bytebuddy.matcher.ElementMatchers.declaresMethod;
import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith;
-import static net.bytebuddy.matcher.ElementMatchers.isDeclaredBy;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.none;
import static net.bytebuddy.matcher.ElementMatchers.not;
@@ -33,16 +32,14 @@ import java.util.Collections;
import java.util.Map;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
-import net.bytebuddy.description.ByteCodeElement;
import net.bytebuddy.description.NamedElement;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
-import net.bytebuddy.matcher.ElementMatchers;
@Slf4j
@AutoService(Instrumenter.class)
-public final class TraceAnnotationsInstrumentation extends Instrumenter.Default {
+public final class TraceAnnotationsInstrumentation extends AbstractTraceAnnotationInstrumentation {
private static final String PACKAGE_CLASS_NAME_REGEX = "[\\w.$]+";
@@ -67,7 +64,7 @@ public final class TraceAnnotationsInstrumentation extends Instrumenter.Default
};
private final Set<String> additionalTraceAnnotations;
- private final ElementMatcher.Junction<NamedElement> annotationMatcher;
+ private final ElementMatcher.Junction<NamedElement> traceAnnotationMatcher;
/*
This matcher matches all methods that should be excluded from transformation
*/
@@ -99,7 +96,7 @@ public final class TraceAnnotationsInstrumentation extends Instrumenter.Default
}
if (additionalTraceAnnotations.isEmpty()) {
- annotationMatcher = none();
+ traceAnnotationMatcher = none();
} else {
ElementMatcher.Junction<NamedElement> methodTraceMatcher = null;
for (final String annotationName : additionalTraceAnnotations) {
@@ -109,33 +106,12 @@ public final class TraceAnnotationsInstrumentation extends Instrumenter.Default
methodTraceMatcher = methodTraceMatcher.or(named(annotationName));
}
}
- this.annotationMatcher = methodTraceMatcher;
+ this.traceAnnotationMatcher = methodTraceMatcher;
}
excludedMethodsMatcher = configureExcludedMethods();
}
- private ElementMatcher.Junction<MethodDescription> configureExcludedMethods() {
- ElementMatcher.Junction<MethodDescription> result = none();
-
- Map<String, Set<String>> excludedMethods =
- MethodsConfigurationParser.parse(Config.get().getTraceMethodsExclude());
- for (Map.Entry<String, Set<String>> entry : excludedMethods.entrySet()) {
- String className = entry.getKey();
- ElementMatcher.Junction<ByteCodeElement> classMather =
- isDeclaredBy(ElementMatchers.<TypeDescription>named(className));
-
- ElementMatcher.Junction<MethodDescription> excludedMethodsMatcher = none();
- for (String methodName : entry.getValue()) {
- excludedMethodsMatcher = excludedMethodsMatcher.or(ElementMatchers.named(methodName));
- }
-
- result = result.or(classMather.and(excludedMethodsMatcher));
- }
-
- return result;
- }
-
@Override
public ElementMatcher<ClassLoader> classLoaderMatcher() {
// Optimization for expensive typeMatcher.
@@ -155,7 +131,7 @@ public final class TraceAnnotationsInstrumentation extends Instrumenter.Default
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
- return safeHasSuperType(declaresMethod(isAnnotatedWith(annotationMatcher)));
+ return safeHasSuperType(declaresMethod(isAnnotatedWith(traceAnnotationMatcher)));
}
@Override
@@ -168,7 +144,7 @@ public final class TraceAnnotationsInstrumentation extends Instrumenter.Default
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(
- isAnnotatedWith(annotationMatcher).and(not(excludedMethodsMatcher)),
+ isAnnotatedWith(traceAnnotationMatcher).and(not(excludedMethodsMatcher)),
packageName + ".TraceAdvice");
}
}

MODIFY
instrumentation/trace-annotation/src/main/java/io/opentelemetry/auto/instrumentation/traceannotation/TraceDecorator.java
instrumentation/trace-annotation/src/main/java/io/opentelemetry/auto/instrumentation/traceannotation/TraceDecorator.java
@@ -17,11 +17,28 @@ package io.opentelemetry.auto.instrumentation.traceannotation;
import io.opentelemetry.OpenTelemetry;
import io.opentelemetry.auto.bootstrap.instrumentation.decorator.BaseDecorator;
+import io.opentelemetry.contrib.auto.annotations.WithSpan;
import io.opentelemetry.trace.Tracer;
+import java.lang.reflect.Method;
public class TraceDecorator extends BaseDecorator {
public static final TraceDecorator DECORATE = new TraceDecorator();
public static final Tracer TRACER =
OpenTelemetry.getTracerProvider().get("io.opentelemetry.auto.trace-annotation");
+
+ /**
+ * This method is used to generate an acceptable span (operation) name based on a given method
+ * reference. It first checks for existence of {@link WithSpan} annotation. If it is present, then
+ * tries to derive name from its {@code value} attribute. Otherwise delegates to {@link
+ * #spanNameForMethod(Method)}.
+ */
+ public String spanNameForMethodWithAnnotation(final Method method) {
+ WithSpan annotation = method.getAnnotation(WithSpan.class);
+ if (annotation != null && !annotation.value().isEmpty()) {
+ return annotation.value();
+ }
+
+ return spanNameForMethod(method);
+ }
}

ADD
instrumentation/trace-annotation/src/main/java/io/opentelemetry/auto/instrumentation/traceannotation/WithSpanAdvice.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.auto.instrumentation.traceannotation;
+
+import static io.opentelemetry.auto.instrumentation.traceannotation.TraceDecorator.DECORATE;
+import static io.opentelemetry.auto.instrumentation.traceannotation.TraceDecorator.TRACER;
+import static io.opentelemetry.trace.TracingContextUtils.currentContextWith;
+
+import io.opentelemetry.auto.instrumentation.api.SpanWithScope;
+import io.opentelemetry.trace.Span;
+import java.lang.reflect.Method;
+import net.bytebuddy.asm.Advice;
+
+/**
+ * Instrumentation for methods annotated with {@link
+ * io.opentelemetry.contrib.auto.annotations.WithSpan} annotation.
+ *
+ * @see WithSpanAnnotationInstrumentation
+ */
+public class WithSpanAdvice {
+
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static SpanWithScope onEnter(@Advice.Origin final Method method) {
+ final Span span =
+ TRACER.spanBuilder(DECORATE.spanNameForMethodWithAnnotation(method)).startSpan();
+ DECORATE.afterStart(span);
+ return new SpanWithScope(span, currentContextWith(span));
+ }
+
+ @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+ public static void stopSpan(
+ @Advice.Enter final SpanWithScope spanWithScope, @Advice.Thrown final Throwable throwable) {
+ final Span span = spanWithScope.getSpan();
+ DECORATE.onError(span, throwable);
+ DECORATE.beforeFinish(span);
+ span.end();
+ spanWithScope.closeScope();
+ }
+}

ADD
instrumentation/trace-annotation/src/main/java/io/opentelemetry/auto/instrumentation/traceannotation/WithSpanAnnotationInstrumentation.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.auto.instrumentation.traceannotation;
+
+import static java.util.Collections.singletonMap;
+import static net.bytebuddy.matcher.ElementMatchers.declaresMethod;
+import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith;
+import static net.bytebuddy.matcher.ElementMatchers.not;
+
+import com.google.auto.service.AutoService;
+import io.opentelemetry.auto.tooling.Instrumenter;
+import io.opentelemetry.contrib.auto.annotations.WithSpan;
+import java.util.Map;
+import net.bytebuddy.description.annotation.AnnotationSource;
+import net.bytebuddy.description.method.MethodDescription;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+
+/**
+ * Instrumentation for methods annotated with {@link
+ * io.opentelemetry.contrib.auto.annotations.WithSpan} annotation. As that is Otel annotation, we
+ * provide full support for all its attributes, as opposed to bare minimum functionality of {@link
+ * TraceAnnotationsInstrumentation} for third party annotations.
+ */
+@AutoService(Instrumenter.class)
+public final class WithSpanAnnotationInstrumentation
+ extends AbstractTraceAnnotationInstrumentation {
+
+ private final ElementMatcher.Junction<AnnotationSource> annotatedMethodMatcher;
+ /*
+ This matcher matches all methods that should be excluded from transformation
+ */
+ private final ElementMatcher.Junction<MethodDescription> excludedMethodsMatcher;
+
+ public WithSpanAnnotationInstrumentation() {
+ super("trace", "with-span-annotation");
+ annotatedMethodMatcher = isAnnotatedWith(WithSpan.class);
+ excludedMethodsMatcher = configureExcludedMethods();
+ }
+
+ @Override
+ public ElementMatcher<TypeDescription> typeMatcher() {
+ return declaresMethod(annotatedMethodMatcher);
+ }
+
+ @Override
+ public String[] helperClassNames() {
+ return new String[] {
+ packageName + ".TraceDecorator",
+ };
+ }
+
+ @Override
+ public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
+ return singletonMap(
+ annotatedMethodMatcher.and(not(excludedMethodsMatcher)), packageName + ".WithSpanAdvice");
+ }
+}

ADD
instrumentation/trace-annotation/src/test/groovy/WithSpanInstrumentationTest.groovy
+/*
+ * 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.
+ */
+import io.opentelemetry.auto.test.AgentTestRunner
+import io.opentelemetry.auto.test.utils.ConfigUtils
+import io.opentelemetry.test.annotation.TracedWithSpan
+
+/**
+ * This test verifies that auto-instr supports {@link io.opentelemetry.contrib.auto.annotations.WithSpan} contrib annotation.
+ */
+class WithSpanInstrumentationTest extends AgentTestRunner {
+
+ static {
+ ConfigUtils.updateConfig {
+ System.setProperty("ota.trace.classes.exclude", WithSpanInstrumentationTest.name + "*")
+ System.setProperty("ota.trace.methods.exclude", "${TracedWithSpan.name}[ignored]")
+ }
+ }
+
+ def specCleanup() {
+ ConfigUtils.updateConfig {
+ System.clearProperty("ota.trace.classes.exclude")
+ System.clearProperty("ota.trace.methods.exclude")
+ }
+ }
+
+ def "should derive automatic name"() {
+ setup:
+ new TracedWithSpan().otel()
+
+ expect:
+ assertTraces(1) {
+ trace(0, 1) {
+ span(0) {
+ operationName "TracedWithSpan.otel"
+ parent()
+ errored false
+ tags {
+ "providerAttr" "Otel"
+ }
+ }
+ }
+ }
+ }
+
+ def "should take span name from annotation"() {
+ setup:
+ new TracedWithSpan().namedOtel()
+
+ expect:
+ assertTraces(1) {
+ trace(0, 1) {
+ span(0) {
+ operationName "manualName"
+ parent()
+ errored false
+ tags {
+ "providerAttr" "Otel"
+ }
+ }
+ }
+ }
+ }
+
+ def "should ignore method excluded by trace.methods.exclude configuration"() {
+ setup:
+ new TracedWithSpan().ignored()
+
+ expect:
+ Thread.sleep(500) // sleep a bit just to make sure no span is captured
+ assertTraces(0) {}
+ }
+
+}

ADD
instrumentation/trace-annotation/src/test/java/io/opentelemetry/test/annotation/TracedWithSpan.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.test.annotation;
+
+import io.opentelemetry.OpenTelemetry;
+import io.opentelemetry.contrib.auto.annotations.WithSpan;
+import io.opentelemetry.trace.Tracer;
+
+public class TracedWithSpan {
+
+ private static final Tracer TRACER =
+ OpenTelemetry.getTracerProvider().get("io.opentelemetry.auto");
+
+ @WithSpan
+ public String otel() {
+ TRACER.getCurrentSpan().setAttribute("providerAttr", "Otel");
+ return "hello!";
+ }
+
+ @WithSpan("manualName")
+ public String namedOtel() {
+ TRACER.getCurrentSpan().setAttribute("providerAttr", "Otel");
+ return "hello!";
+ }
+
+ @WithSpan
+ public String ignored() {
+ TRACER.getCurrentSpan().setAttribute("providerAttr", "Otel");
+ return "hello!";
+ }
+}

MODIFY
java-agent/java-agent.gradle
java-agent/java-agent.gradle
@@ -62,6 +62,7 @@ shadowJar {
relocate "io.opentelemetry.internal", "io.opentelemetry.auto.shaded.io.opentelemetry.internal"
relocate "io.opentelemetry.metrics", "io.opentelemetry.auto.shaded.io.opentelemetry.metrics"
relocate "io.opentelemetry.trace", "io.opentelemetry.auto.shaded.io.opentelemetry.trace"
+ relocate "io.opentelemetry.contrib.auto.annotations", "io.opentelemetry.auto.shaded.io.opentelemetry.contrib.auto.annotations"
// relocate OpenTelemetry API dependency
relocate "io.grpc", "io.opentelemetry.auto.shaded.io.grpc"

MODIFY
testing/src/main/groovy/io/opentelemetry/auto/test/SpockRunner.java
testing/src/main/groovy/io/opentelemetry/auto/test/SpockRunner.java
@@ -66,6 +66,7 @@ public class SpockRunner extends Sputnik {
"io.opentelemetry.internal", // OpenTelemetry API
"io.opentelemetry.metrics", // OpenTelemetry API
"io.opentelemetry.trace", // OpenTelemetry API
+ "io.opentelemetry.contrib.auto.annotations", // OpenTelemetry API Contrib
"io.grpc.Context", // OpenTelemetry API dependency
"io.grpc.Deadline", // OpenTelemetry API dependency
"io.grpc.PersistentHashArrayMappedTrie", // OpenTelemetry API dependency

Add support for new @WithSpan annotation (#379)