Skip to content

Coroutines context propagation instrumentation - #1935

Open
LikeTheSalad wants to merge 4 commits into
open-telemetry:mainfrom
LikeTheSalad:feature/coroutines-instrumentation
Open

Coroutines context propagation instrumentation#1935
LikeTheSalad wants to merge 4 commits into
open-telemetry:mainfrom
LikeTheSalad:feature/coroutines-instrumentation

Conversation

@LikeTheSalad

Copy link
Copy Markdown
Contributor

Closes #239

Example of what this instrumentation helps with, as stated in its README.md file:

Given:

val span = tracer.spanBuilder("work").startSpan()
span.makeCurrent().use {
    scope.launch {
        val inner = tracer.spanBuilder("coroutine-work").startSpan()
        // ...
        inner.end()
    }
}
span.end()

Without this instrumentation, coroutine-work does not inherit the active context and is recorded
as a separate root span:

work
coroutine-work

With this instrumentation, the active context is carried into the coroutine automatically, and
coroutine-work is recorded as a child of work:

work
└── coroutine-work

@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 52.72727% with 26 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.50%. Comparing base (d04c80d) to head (9492043).
⚠️ Report is 18 commits behind head on main.

Files with missing lines Patch % Lines
...strumentation/agent/coroutines/CoroutinesPlugin.kt 55.55% 12 Missing ⚠️
...rary/coroutines/internal/CoroutinesLaunchBridge.kt 0.00% 12 Missing ⚠️
...mentation/library/coroutines/CoroutinesTestUtil.kt 0.00% 2 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@LikeTheSalad
LikeTheSalad marked this pull request as ready for review July 29, 2026 08:57
@LikeTheSalad
LikeTheSalad requested a review from a team as a code owner July 29, 2026 08:57
Copilot AI review requested due to automatic review settings July 29, 2026 08:57
@LikeTheSalad

Copy link
Copy Markdown
Contributor Author

❌ Patch coverage is 52.72727% with 26 lines in your changes missing coverage. Please review.

This is because codecov doesn't currently cover Android instrumentation tests, but in reality the coverage is higher.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 coroutines instrumentation library with a launch “bridge” that enriches coroutine contexts with the current OTel Context when enabled.
  • Adds a ByteBuddy agent plugin that rewrites kotlinx.coroutines.BuildersKt.launch / launch$default call 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.

Comment on lines +17 to +20
dependencies {
implementation(project(":instrumentation:coroutines:library"))
implementation(libs.byteBuddy)
}

@fractalwrench fractalwrench left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Using the extension manually instead sounds reasonable 👍

import io.opentelemetry.instrumentation.library.coroutines.internal.CoroutinesContextHelper

@AutoService(AndroidInstrumentation::class)
class CoroutinesInstrumentation : AndroidInstrumentation {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

@LikeTheSalad

Copy link
Copy Markdown
Contributor Author

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.

Thanks for the feedback, I added some responses.

Regarding

I'd be curious how it interacts with opentelemetry-java's thread-local approach if the two are used together

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.

@fractalwrench

Copy link
Copy Markdown
Member

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?

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 ctx and ctx2 in the following scenario:

val executor = Executors.newSingleThreadExecutor()
val scope = CoroutineScope(Dispatchers.Default)

scope.launch {
    tracer.createSpan("a").makeCurrent()
    val ctx = Context.current()
    
    executor.submit { 
        tracer.createSpan("b").makeCurrent()
        val ctx2 = Context.current()
    }
}

@LikeTheSalad

LikeTheSalad commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

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.

Based on your example, it seems like the use case you're talking about is the following:

  1. User launches coroutine
  2. Creates span and makes it current in the coroutine backing thread
  3. Launches a non-coroutine thread executor (using Java's executors)
  4. Creates span within the Java-managed thread

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.

@LikeTheSalad

Copy link
Copy Markdown
Contributor Author

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.

@fractalwrench

Copy link
Copy Markdown
Member

Thanks, that's helpful!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Automatic coroutine OTel Context propagation

3 participants