-
Notifications
You must be signed in to change notification settings - Fork 81
Draft: Add instrumentation for otel span annotations #1348
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| placeholder |
| 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? | ||
| } | ||
| } |
| 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?>, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not faimilar with
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I found the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
||
| ) { | ||
| 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 |
| 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()) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.