DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Testcontainers With Kotlin and Spring Data R2DBC
  • Unraveling Lombok's Code Design Pitfalls: Exploring Encapsulation Issues
  • Java, Spring Boot, and MongoDB: Performance Analysis and Improvements
  • Circuit Breaker Pattern With Netflix-Hystrix: Java

Trending

  • Visualizing Matrix Multiplication as a Linear Combination
  • AWS Managed Database Observability: Monitoring DynamoDB, ElastiCache, and Redshift Beyond CloudWatch
  • Detecting Advanced Persistent Threats Using Behavioral Analytics and Log Correlation
  • Introduction to Retrieval Augmented Generation (RAG)
  1. DZone
  2. Testing, Deployment, and Maintenance
  3. Monitoring and Observability
  4. Kotlin Coroutines and OpenTelemetry Tracing

Kotlin Coroutines and OpenTelemetry Tracing

Explore the underlying workings of OpenTelemetry and analyze the workings of @WithSpan in general as well as within the context of Kotlin Coroutines.

By 
Nicolas Fränkel user avatar
Nicolas Fränkel
·
Aug. 22, 24 · Tutorial
Likes (5)
Comment
Save
Tweet
Share
7.7K Views

Join the DZone community and get the full member experience.

Join For Free

I recently compared three OpenTelemetry approaches on the JVM: Java Agent v1, v2, and Micrometer. I used Kotlin and coroutines without overthinking. I received interesting feedback on the usage of @WithSpan with coroutines:

Tweet regarding opentelemetry-extension-kotlin

Indeed, the @WithSpan annotation has worked flawlessly in conjunction with coroutines for some time already. However, it made me think about the underlying workings of OpenTelemetry. Here are my findings.

The @WithSpan Annotation Processor

@WithSpan is a simple annotation. To be of any use, one needs an annotation processor. If you need a refresher on annotation processors, please check this not-so-new but still-relevant post.

A quick search on the OpenTelemetry repository reveals that the processor involved is WithSpanInstrumentation.

Here's an abridged summary of the classes involved:

Opentelemetry core class diagram

WithSpanInstrumentation does the annotation processing part; it delegates to WithSpanSingleton. In turn, the latter bridges the call to the Instrumenter class. Instrumenter contains the core of creating spans and interacting with the OpenTelemetry collector.

Instrumenter and Context

The Instrumenter encapsulates the entire logic for gathering telemetry, from collecting the data, to starting and ending spans, to recording values using metrics instruments.

An Instrumenter is called at the start and the end of a request/response lifecycle.
When instrumenting a library, there will generally be four steps.

  1. Create an Instrumenter using InstrumenterBuilder. Use the builder to
    configure any library-specific customizations, and also expose useful knobs to your user.
  2. Call Instrumenter#shouldStart(Context, Object) and do not proceed if it returns
    false.
  3. Call Instrumenter#start(Context, Object) at the beginning of a request.
  4. Call Instrumenter#end(Context, Object, Object, Throwable) at the end of a request.

For more detailed information about using the Instrumenter see the Using the Instrumenter API page.

- Instrumenter class

Instrumenter works in conjunction with Context. OpenTelemetry API users should be familiar with it, specifically the call to Context.current(). Let's describe it in more detail.

Instrumenter working in conjunction with Context

Context stores data in a ContextStorage instance, whose default is ThreadLocal. The ThreadLocal class has been the old-age way to pass data around without interfering with method signatures. It stores data in the current thread.

Kotlin's OpenTelemetry Extension

ThreadLocal works perfectly — until you spawn other threads. In this case, you must explicitly pass data around. So-called Reactive Programming frameworks, such as Spring WebFlux, do spawn other threads; most, if not all, provide utilities to handle the passing automatically.

Coroutines implement Reactive Programming. Not only do they spawn threads, but they also decouple coroutine from threads. A coroutine may "jump" across several threads in its lifetime. Thus, storing the OpenTelemetry context in a ThreadLocal doesn't work.

Yet, coroutines provide a dedicated storage mechanism, the coroutine context. We need a way to move the OpenTelemetry context from the ThreadLocal to the coroutine context and back again. The way exists in the opentelemetry-extension-kotlin jar:

opentelemetry-extension-kotlin jar

The only part that needs to be added is where these functions are called. Unsurprisingly, the magic happens in the Java Agent and all other instrumentation classes. You might remember the TypeInstrumentation interface on the first diagram, which the class WithSpanInstrumentation implemented. The Java Agent caters to many different frameworks and libraries, e.g., Spring WebFlux, and Kotlin Coroutines. Its developers designed it so each TypeInstrumentation concrete class focuses on the instrumentation of a specific aspect of the framework or library; coroutines are no exception.

Each TypeInstrumentation concrete class

Note that the code provides a more specific instrumentation of WithSpanInstrumentation, which is dedicated to coroutines.

It turns out that the KotlinCoroutinesInstrumentationHelper contains the magic to copy the context from the ThreadLocal to the coroutine context:

Java
 
package io.opentelemetry.javaagent.instrumentation.kotlinxcoroutines;

import io.opentelemetry.context.Context;
import io.opentelemetry.extension.kotlin.ContextExtensionsKt;
import kotlin.coroutines.CoroutineContext;

public final class KotlinCoroutinesInstrumentationHelper {

  public static CoroutineContext addOpenTelemetryContext(CoroutineContext coroutineContext) {
    Context current = Context.current();                                                      //1
    Context inCoroutine = ContextExtensionsKt.getOpenTelemetryContext(coroutineContext);
    if (current == inCoroutine || inCoroutine != Context.root()) {
      return coroutineContext;
    }
    return coroutineContext.plus(ContextExtensionsKt.asContextElement(current));              //2
  }

  private KotlinCoroutinesInstrumentationHelper() {}
}


  1. Get the OpenTelemetry context - from the ThreadLocal. 
  2. Add the context to the coroutine context.

And that's a wrap.

Summary

In this post, I've analyzed the workings of @WithSpan in general and in the context of Kotlin Coroutines. The Java Agent provides many different instrumenting classes, each dedicated to a unique facet of a framework or library. The WithSpanInstrumentation in the io.opentelemetry.javaagent.instrumentation.extensionannotations manages "regular" code; the one in io.opentelemetry.javaagent.instrumentation.kotlinxcoroutines manages coroutines.

The biggest challenge is that OpenTelemetry stores data in a ThreadLocal by default. The coroutine library doesn't guarantee the same thread will be used. On the contrary, a coroutine will likely bounce across different threads during its lifetime.

The Java Agent provides the mechanism to cope with it. One part focuses on moving OpenTelemetry data from the ThreadLocal to the coroutine context; the other provides a dedicated instrumentation to call the above code when it enters the latter.

To Go Further

  • OpenTelemetry auto-instrumentation and instrumentation libraries for Java
Annotation Data (computing) Java (programming language) Kotlin (programming language) Telemetry

Published at DZone with permission of Nicolas Fränkel. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Testcontainers With Kotlin and Spring Data R2DBC
  • Unraveling Lombok's Code Design Pitfalls: Exploring Encapsulation Issues
  • Java, Spring Boot, and MongoDB: Performance Analysis and Improvements
  • Circuit Breaker Pattern With Netflix-Hystrix: Java

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook