Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
opentelemetry-instrumentation-api = { module = "io.opentelemetry.instrumentation:opentelemetry-instrumentation-api" }
opentelemetry-instrumentation-apiSemconv = { module = "io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-incubator", version.ref = "opentelemetry-instrumentation-alpha" }
opentelemetry-instrumentation-okhttp = { module = "io.opentelemetry.instrumentation:opentelemetry-okhttp-3.0", version.ref = "opentelemetry-instrumentation-alpha" }
opentelemetry-instrumentation-annotations = { module = "io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations", version.ref = "opentelemetry-instrumentation-alpha" }
opentelemetry-semconv = { module = "io.opentelemetry.semconv:opentelemetry-semconv", version.ref = "opentelemetry-semconv" }
opentelemetry-semconv-incubating = { module = "io.opentelemetry.semconv:opentelemetry-semconv-incubating", version.ref = "opentelemetry-semconv-alpha" }
opentelemetry-api = { module = "io.opentelemetry:opentelemetry-api" }
Expand Down
1 change: 1 addition & 0 deletions instrumentation/span-annotation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
placeholder
18 changes: 18 additions & 0 deletions instrumentation/span-annotation/agent/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
plugins {
id("otel.android-library-conventions")
id("otel.publish-conventions")
}

description = "placeholder"

android {
namespace = "io.opentelemetry.android.spanannotation.agent"
}

dependencies {
implementation(libs.opentelemetry.api)
implementation(libs.opentelemetry.context)
implementation(libs.opentelemetry.instrumentation.annotations)
implementation(project(":instrumentation:span-annotation:library"))
implementation(libs.byteBuddy)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package io.opentelemetry.instrumentation.agent.spanannotation

import io.opentelemetry.instrumentation.agent.spanannotation.advice.constructor.AddingSpanAttributesConstructorAdvice
import io.opentelemetry.instrumentation.agent.spanannotation.advice.constructor.SpanAttributeConstructorAdvice
import io.opentelemetry.instrumentation.agent.spanannotation.advice.constructor.WithSpanConstructorAdvice
import io.opentelemetry.instrumentation.agent.spanannotation.advice.method.AddingSpanAttributesMethodAdvice
import io.opentelemetry.instrumentation.agent.spanannotation.advice.method.SpanAttributeMethodAdvice
import io.opentelemetry.instrumentation.agent.spanannotation.advice.method.WithSpanMethodAdvice
import net.bytebuddy.asm.Advice
import net.bytebuddy.build.Plugin
import net.bytebuddy.description.method.MethodDescription
import net.bytebuddy.description.type.TypeDescription
import net.bytebuddy.dynamic.ClassFileLocator
import net.bytebuddy.dynamic.DynamicType
import net.bytebuddy.matcher.ElementMatchers

const val WITH_SPAN_ANNOTATION = "io.opentelemetry.instrumentation.annotations.WithSpan"
const val SPAN_ATTRIBUTE_ANNOTATION = "io.opentelemetry.instrumentation.annotations.SpanAttribute"
const val ADDING_SPAN_ATTRIBUTES_ANNOTATION = "io.opentelemetry.instrumentation.annotations.AddingSpanAttributes"

class SpanAnnotationPlugin : Plugin {
override fun apply(
builder: DynamicType.Builder<*>,
typeDescription: TypeDescription,
classFileLocator: ClassFileLocator
): DynamicType.Builder<*> {
return builder
// Apply advice to methods annotated with @WithSpan
.visit(
Advice.to(WithSpanMethodAdvice::class.java)
.on(
ElementMatchers.not(ElementMatchers.isConstructor())
.and(ElementMatchers.isAnnotatedWith(
ElementMatchers.named(WITH_SPAN_ANNOTATION)
)
)
)
)
// Apply advice to constructors annotated with @WithSpan
.visit(
Advice.to(WithSpanConstructorAdvice::class.java)
.on(
ElementMatchers.isConstructor<MethodDescription>()
.and(ElementMatchers.isAnnotatedWith(
ElementMatchers.named(WITH_SPAN_ANNOTATION)
)
)
)
)
// Apply advice to methods annotated with @AddingSpanAttributes
.visit(
Advice.to(AddingSpanAttributesMethodAdvice::class.java)
.on(
ElementMatchers.not(ElementMatchers.isConstructor())
.and(ElementMatchers.isAnnotatedWith(
ElementMatchers.named(ADDING_SPAN_ATTRIBUTES_ANNOTATION)
)
)
)
)
// Apply advice to constructors annotated with @AddingSpanAttributes
.visit(
Advice.to(AddingSpanAttributesConstructorAdvice::class.java)
.on(
ElementMatchers.isConstructor<MethodDescription>()
.and(ElementMatchers.isAnnotatedWith(
ElementMatchers.named(ADDING_SPAN_ATTRIBUTES_ANNOTATION)
)
)
)
)
// Apply advice to methods with parameters annotated with @SpanAttribute
.visit(
Advice.to(SpanAttributeMethodAdvice::class.java)
.on(
ElementMatchers.not(ElementMatchers.isConstructor())
.and(ElementMatchers.hasParameters(
ElementMatchers.whereAny(
ElementMatchers.isAnnotatedWith(
ElementMatchers.named(SPAN_ATTRIBUTE_ANNOTATION)
)
)
)
)
)
)
// Apply advice to constructors with parameters annotated with @SpanAttribute
.visit(
Advice.to(SpanAttributeConstructorAdvice::class.java)
.on(
ElementMatchers.isConstructor<MethodDescription>()
.and(ElementMatchers.hasParameters(
ElementMatchers.whereAny(
ElementMatchers.isAnnotatedWith(
ElementMatchers.named(SPAN_ATTRIBUTE_ANNOTATION)
)
)
)
)
)
)
}

override fun matches(target: TypeDescription?): Boolean {
return target?.declaredMethods?.any { method ->
method.declaredAnnotations.any { annotation ->
annotation.annotationType.name == WITH_SPAN_ANNOTATION ||
annotation.annotationType.name == ADDING_SPAN_ATTRIBUTES_ANNOTATION
}
} == true
}

override fun close() {
// Nothing here yet?
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we aware of a use case that requires tracing a constructor? It sounds a bit of an edge case to me. If there's no foreseeable usage right now, I'd prefer to leave it for a future PR.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, i will remove the constructor implementation and focus on just the normal method case to simplify the PR.

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.opentelemetry.instrumentation.agent.spanannotation.advice.constructor

import io.opentelemetry.api.trace.Span
import io.opentelemetry.instrumentation.library.spanannotation.HelperFunctions
import net.bytebuddy.asm.Advice
import java.lang.reflect.Constructor

object AddingSpanAttributesConstructorAdvice {

@JvmStatic
@Advice.OnMethodEnter(suppress = Throwable::class)
fun onEnter(
@Advice.AllArguments args: Array<Any?>,
@Advice.Origin constructor: Constructor<*>
) {
HelperFunctions.argsAsAttributes(Span.current(), args, constructor.declaringClass.simpleName)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.opentelemetry.instrumentation.agent.spanannotation.advice.constructor

import io.opentelemetry.api.trace.Span
import io.opentelemetry.instrumentation.library.spanannotation.HelperFunctions
import net.bytebuddy.asm.Advice
import java.lang.reflect.Constructor

object SpanAttributeConstructorAdvice {

@JvmStatic
@Advice.OnMethodEnter(suppress = Throwable::class)
fun onEnter(
@Advice.AllArguments args: Array<Any?>,
@Advice.Origin constructor: Constructor<*>
) {
HelperFunctions.argAsAttribute(
Span.current(),
constructor.parameterAnnotations,
args,
constructor.declaringClass.simpleName
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.opentelemetry.instrumentation.agent.spanannotation.advice.constructor

import io.opentelemetry.api.trace.Span
import io.opentelemetry.context.Scope
import io.opentelemetry.instrumentation.library.spanannotation.HelperFunctions
import io.opentelemetry.instrumentation.annotations.WithSpan
import net.bytebuddy.asm.Advice
import java.lang.reflect.Constructor

object WithSpanConstructorAdvice {

@JvmStatic
@Advice.OnMethodEnter(suppress = Throwable::class)
fun onEnter(
@Advice.Origin constructor: Constructor<*>
) : Pair<Span, Scope> {
val withSpan = constructor.getAnnotation(WithSpan::class.java)
?: throw IllegalStateException("WithSpan annotation not found on constructor ${constructor.declaringClass.simpleName}")

return HelperFunctions.startSpan(withSpan, constructor.declaringClass.simpleName)
}

@JvmStatic
@Advice.OnMethodExit(suppress = Throwable::class)
fun onExit(
@Advice.Enter spanPair: Pair<Span, Scope>
) {
HelperFunctions.stopSpan(spanPair, null)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.opentelemetry.instrumentation.agent.spanannotation.advice.method

import io.opentelemetry.api.trace.Span
import io.opentelemetry.instrumentation.library.spanannotation.HelperFunctions
import net.bytebuddy.asm.Advice
import java.lang.reflect.Method

object AddingSpanAttributesMethodAdvice {

@JvmStatic
@Advice.OnMethodEnter(suppress = Throwable::class)
fun onEnter(
@Advice.AllArguments args: Array<Any?>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not faimilar with @WithSpan, though I'm curious why it might need all the method's arguments?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found the @AddingSpanAttributes annotation but it seems i might've misunderstood it's function.
It is not mentioned in the linked documentation so perhaps I should remove this as well and stick to @WithSpan and @SpanAttribute ? It covers all the most common use cases already.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I also don't see it in the docs so it should be fine to leave it out, at least for now. I'll cc @breedx-splk in case he's more familiar with it to make sure we're not missing something important.

@Advice.Origin method: Method
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to follow best practices from the upstream instrumentation, such as avoiding using @Advice.Origin Method. In this case it seems like we can just get the method name by using @Advice.Origin("#m") String methodName.

) {
HelperFunctions.argsAsAttributes(
Span.current(),
args,
method.name
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.opentelemetry.instrumentation.agent.spanannotation.advice.method

import io.opentelemetry.api.trace.Span
import io.opentelemetry.instrumentation.library.spanannotation.HelperFunctions
import net.bytebuddy.asm.Advice
import java.lang.reflect.Method

object SpanAttributeMethodAdvice {

@JvmStatic
@Advice.OnMethodEnter(suppress = Throwable::class)
fun onEnter(
@Advice.AllArguments args: Array<Any?>,
@Advice.Origin method: Method
) {
HelperFunctions.argAsAttribute(
Span.current(),
method.parameterAnnotations,
args,
method.name
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.opentelemetry.instrumentation.agent.spanannotation.advice.method

import io.opentelemetry.api.trace.Span
import io.opentelemetry.context.Scope
import io.opentelemetry.instrumentation.annotations.WithSpan
import io.opentelemetry.instrumentation.library.spanannotation.HelperFunctions
import net.bytebuddy.asm.Advice
import java.lang.reflect.Method

object WithSpanMethodAdvice {

@JvmStatic
@Advice.OnMethodEnter(suppress = Throwable::class)
fun onEnter(
@Advice.Origin method: Method
) : Pair<Span, Scope> {
val withSpan = method.getAnnotation(WithSpan::class.java)
?: throw IllegalStateException("WithSpan annotation not found on method ${method.name}")

return HelperFunctions.startSpan(
withSpan,
method.name
)
}

@JvmStatic
@Advice.OnMethodExit(suppress = Throwable::class, onThrowable = Throwable::class)
fun onExit(
@Advice.Enter spanPair: Pair<Span, Scope>,
@Advice.Thrown throwable: Throwable?
) {
HelperFunctions.stopSpan(
spanPair,
throwable
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
io.opentelemetry.instrumentation.agent.spanannotation.SpanAnnotationPlugin
17 changes: 17 additions & 0 deletions instrumentation/span-annotation/library/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
plugins {
id("otel.android-library-conventions")
id("otel.publish-conventions")
}

description = "placeholder"

android {
namespace = "io.opentelemetry.android.spanannotation.library"
}

dependencies {
implementation(libs.opentelemetry.api)
implementation(libs.opentelemetry.context)
implementation(libs.opentelemetry.instrumentation.annotations)
api(project(":instrumentation:android-instrumentation"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package io.opentelemetry.instrumentation.library.spanannotation

import io.opentelemetry.api.trace.Span
import io.opentelemetry.context.Scope
import io.opentelemetry.instrumentation.annotations.SpanAttribute
import io.opentelemetry.instrumentation.annotations.WithSpan
import kotlin.text.ifEmpty

object HelperFunctions {

@JvmStatic
fun startSpan(withSpan: WithSpan, name: String): Pair<Span, Scope> {
val spanBuilder = SpanAnnotationInstrumentation
.tracer
.spanBuilder(withSpan.value.ifEmpty { name })
.setSpanKind(withSpan.kind)

if (!withSpan.inheritContext) {
spanBuilder.setNoParent()
}

val span = spanBuilder.startSpan()
val scope = span.makeCurrent()

return Pair(span, scope)
}

@JvmStatic
fun stopSpan(spanPair: Pair<Span, Scope>, throwable: Throwable?) {
spanPair.let { (span, scope) ->
throwable?.let {
span.recordException(throwable)
span.setStatus(io.opentelemetry.api.trace.StatusCode.ERROR, it.message ?: "Exception thrown")
}
scope.close()
span.end()
}
}

@JvmStatic
fun argAsAttribute(span: Span, parameterAnnotations: Array<Array<Annotation>>, args: Array<Any?>, name: String) {
args.forEachIndexed { index, arg ->
parameterAnnotations[index]
.filterIsInstance<SpanAttribute>()
.firstOrNull()?.let { spanAttribute ->
val attributeKey = spanAttribute.value.takeIf { it.isNotEmpty() } ?: "arg${index}_$name"
span.setAttribute(attributeKey, arg.toString())
}
}
}

@JvmStatic
fun argsAsAttributes(span: Span, args: Array<Any?>, name: String) {
args.forEachIndexed { index, arg ->
val attributeKey = "arg${index}_$name"
span.setAttribute(attributeKey, arg.toString())
}
}
}
Loading
Loading