Coroutines context propagation instrumentation - #1935
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1935 +/- ##
==========================================
+ Coverage 65.34% 65.50% +0.16%
==========================================
Files 169 177 +8
Lines 3872 3966 +94
Branches 420 446 +26
==========================================
+ Hits 2530 2598 +68
- Misses 1211 1238 +27
+ Partials 131 130 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This is because codecov doesn't currently cover Android instrumentation tests, but in reality the coverage is higher. |
There was a problem hiding this comment.
Pull request overview
Adds a new build-time auto-instrumentation for Kotlin coroutines that propagates the current OpenTelemetry Context into coroutines started via CoroutineScope.launch, using a ByteBuddy Gradle plugin + a small runtime toggle installed through the standard AndroidInstrumentation loader.
Changes:
- Introduces
coroutinesinstrumentation library with a launch “bridge” that enriches coroutine contexts with the current OTelContextwhen enabled. - Adds a ByteBuddy agent plugin that rewrites
kotlinx.coroutines.BuildersKt.launch/launch$defaultcall sites in LOCAL application classes to call the bridge instead. - Adds unit tests + an Android instrumentation test app to validate propagation after suspension and absence of context leakage.
PR Merge Tier (per repository guidelines): Tier 3 (new feature + new instrumentation module + build-time bytecode plugin)
Reviewed changes
Copilot reviewed 16 out of 17 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| instrumentation/coroutines/README.md | Documents scope, supported APIs, installation, and precedence/suppression behavior. |
| instrumentation/coroutines/library/build.gradle.kts | New publishable library module for the coroutines instrumentation runtime toggle + bridge code. |
| instrumentation/coroutines/library/src/main/kotlin/io/opentelemetry/instrumentation/library/coroutines/CoroutinesInstrumentation.kt | Registers the instrumentation and toggles context injection on install/uninstall. |
| instrumentation/coroutines/library/src/main/kotlin/io/opentelemetry/instrumentation/library/coroutines/internal/CoroutinesContextHelper.kt | Implements “inject current OTel context unless disabled/root/already-present non-root” logic. |
| instrumentation/coroutines/library/src/main/kotlin/io/opentelemetry/instrumentation/library/coroutines/internal/CoroutinesLaunchBridge.kt | Provides JVM-signature-matching bridge methods that call launch with enriched context. |
| instrumentation/coroutines/library/src/test/kotlin/io/opentelemetry/instrumentation/library/coroutines/CoroutinesContextHelperTest.kt | Unit tests for enable/disable behavior, precedence rules, and leak prevention. |
| instrumentation/coroutines/library/api/library.api | API surface snapshot for the new library module. |
| instrumentation/coroutines/agent/build.gradle.kts | New publishable agent module that hosts the ByteBuddy build-time plugin. |
| instrumentation/coroutines/agent/src/main/kotlin/io/opentelemetry/instrumentation/agent/coroutines/CoroutinesPlugin.kt | ByteBuddy MemberSubstitution that rewrites BuildersKt.launch* invocations to the bridge. |
| instrumentation/coroutines/agent/src/main/resources/META-INF/net.bytebuddy/build.plugins | Registers the ByteBuddy plugin for discovery by the Gradle ByteBuddy plugin. |
| instrumentation/coroutines/agent/src/test/kotlin/io/opentelemetry/instrumentation/agent/coroutines/CoroutinesPluginTest.kt | Validates LOCAL vs EXTERNAL transformation scoping. |
| instrumentation/coroutines/agent/api/agent.api | API snapshot placeholder for the new agent module. |
| instrumentation/coroutines/testing/build.gradle.kts | Adds an Android test app module applying the ByteBuddy Gradle plugin with the new agent. |
| instrumentation/coroutines/testing/src/main/AndroidManifest.xml | Minimal manifest for the Android test app module. |
| instrumentation/coroutines/testing/src/main/kotlin/io/opentelemetry/instrumentation/library/coroutines/CoroutinesTestUtil.kt | Provides launch call sites in app bytecode to ensure ByteBuddy rewrites occur. |
| instrumentation/coroutines/testing/src/androidTest/kotlin/io/opentelemetry/instrumentation/library/coroutines/testing/InstrumentationTest.kt | Android instrumentation tests validating context propagation after suspension and no leakage. |
| gradle/libs.versions.toml | Adds the opentelemetry-extension-kotlin version-catalog entry. |
| dependencies { | ||
| implementation(project(":instrumentation:coroutines:library")) | ||
| implementation(libs.byteBuddy) | ||
| } |
fractalwrench
left a comment
There was a problem hiding this comment.
Thanks (and good luck) for taking this on! I wasn't sure whether this fits inside AndroidInstrumentation as it doesn't actually capture telemetry. I also had questions around whether it makes sense to allow folks to opt-out (presumably not)? Additionally I'd be curious how it interacts with opentelemetry-java's thread-local approach if the two are used together.
| import net.bytebuddy.dynamic.DynamicType | ||
| import net.bytebuddy.matcher.ElementMatchers | ||
|
|
||
| internal class CoroutinesPlugin( |
There was a problem hiding this comment.
Will folks want this included in every scope.launch, i.e. is there a possibility folks might want to opt-out of certain coroutines or specify which ones they want manually via a helper/extension function instead?
There was a problem hiding this comment.
There's no way to choose which coroutines would get the OTel context or not when using this plugin. If people wanted to do so, I think they should not use this plugin and instead directly use the Kotlin extension whenever they need it.
The only thing that they could do, even when applying this plugin, is to set their own coroutine OTel context manually, in case they want to use a custom one (the plugin won't override it).
There was a problem hiding this comment.
Using the extension manually instead sounds reasonable 👍
| import io.opentelemetry.instrumentation.library.coroutines.internal.CoroutinesContextHelper | ||
|
|
||
| @AutoService(AndroidInstrumentation::class) | ||
| class CoroutinesInstrumentation : AndroidInstrumentation { |
There was a problem hiding this comment.
I don't know whether this belongs as instrumentation. It doesn't actually record telemetry & exists solely to manage context. My instinct is that it should be in a separate opt-in module like it is now, but I'm not convinced we need to implement AndroidInstrumentation
There was a problem hiding this comment.
I see what you mean; it is a strange use case for calling it an "AndroidInstrumentation". I did it like this mostly to keep some consistency around opt-in tools that do code on behalf of users. If we strictly define "instrumentations" as telemetry-generating code, then it doesn't fit, but if we instead define them as "automatic code generation" in general, it could fit.
That being said, I'm open to ideas to avoid making this an "AndroidInstrumentation" implementation. If we're fine with leaving it all as is and just removing the explicit implementation of AndroidInstrumentation, I think it's fine. However, if we wanted to create a new category of opt-in tools for when they do bytecode weaving but don't create telemetry, I think we should wait for at least a second tool that would fit in there, because doing so for a single module seems a bit overkill.
Thanks for the feedback, I added some responses. Regarding
I'm not sure I follow the question. This plugin actually relies on opentelemetry-java's thread-local storage to capture and then propagate an existing OTel Context in a coroutine, so they work together by design. Is there a use case that you have in mind where they could clash? Also, we're probably going to keep this plugin simple forever, so I hope it won't become a headache in the future in case people start asking to support other means to create coroutines. The reason is that it only works because of the way OTel Java propagates its Context within threads, which I know is an OTel Java-specific feature. So when we switch to OTel Kotlin, this plugin will become obsolete. The reason we still decided to create it is that someone brought it up in a recent SIG meeting, where we noticed that the issue for it is currently the most upvoted of all OTel Android, so we decided to address it, but I don't think it should become a maintenance burden because of our future migration plans. |
I guess I'm just asking whether there could be any edge cases where library users expect the default thread-local context storage approach, but end up receiving the coroutine context storage approach? And if so, whether we could set expectations through documenting that one approach wins over the other. As an example, I'm unclear what the current context would be for |
Based on your example, it seems like the use case you're talking about is the following:
I think we can see this as a "Coroutine to Regular thread" scenario, in which case, it's not covered by this plugin, so the fully Java-managed thread won't get the context that existed in the Kotlin coroutine-managed one, which is an inherited behavior from the Kotlin extension mechanism that the plugin uses. I think this can be described as a feature where "Kotlin is aware of Java, but not vice versa", which I think is the ideal approach, as this is a Kotlin-first project and the test scenario can be considered quite niche. However, I agree that some people can probably expect a two-way propagation mechanism, whereas it's only one (from a regular Java thread to a Kotlin coroutine backing one), so I'll add some clarifications to the readme file on this. |
|
I added a clarification here: 9492043 on what to expect in the scenario you mentioned, @fractalwrench. Let me know if it helps or if we could improve it a bit further. |
|
Thanks, that's helpful! |
Closes #239
Example of what this instrumentation helps with, as stated in its README.md file:
Given:
Without this instrumentation,
coroutine-workdoes not inherit the active context and is recordedas a separate root span:
With this instrumentation, the active context is carried into the coroutine automatically, and
coroutine-workis recorded as a child ofwork: