Tracing | Interop with Java-instrumented libraries
Glossary
Name | Description |
---|---|
Context | otel4s context that carries tracing information (spans, etc) |
Local[F, Context] | The context carrier tool within the effect environment |
Java SDK | The OpenTelemetry library for Java |
JContext | Alias for io.opentelemetry.context.Context |
JSpan | Alias for io.opentelemetry.api.trace.Span |
The problem
OpenTelemetry Java SDK and otel4s rely on different context manipulation approaches, which aren't interoperable out of the box. Java SDK utilizes ThreadLocal variables to share tracing information, otel4s, on the other hand, uses Local.
Let's take a look at example below:
import cats.effect.IO
import org.typelevel.otel4s.trace.Tracer
import io.opentelemetry.api.trace.{Span => JSpan}
def test(implicit tracer: Tracer[IO]): IO[Unit] =
tracer.span("test").use { span => // start 'test' span using otel4s
val jSpanContext = JSpan.current().getSpanContext // get a span from a ThreadLocal var
IO.println(s"Java ctx: $jSpanContext") >> IO.println(s"Otel4s ctx: ${span.context}")
}
The output will be:
Java ctx: {traceId=00000000000000000000000000000000, spanId=0000000000000000, ...}
Otel4s ctx: {traceId=318854a5bd6ac0dd7b0a926f89c97ecb, spanId=925ad3a126cec272, ...}
Here we try to get the current JSpan
within the effect.
Unfortunately, due to different context manipulation approaches,
the context operated by otel4s isn't visible to the Java SDK.
To mitigate this limitation, the context must be shared manually.
Before we start
Since we need to manually modify the context we need direct access to Local[F, Context]
.
It can be constructed in the following way:
import cats.effect._
import cats.mtl.Local
import cats.syntax.functor._
import org.typelevel.otel4s.instances.local._ // brings Local derived from IOLocal
import org.typelevel.otel4s.oteljava.context.Context
import org.typelevel.otel4s.oteljava.OtelJava
import io.opentelemetry.api.GlobalOpenTelemetry
def createOtel4s[F[_]: Async](implicit L: Local[F, Context]): F[OtelJava[F]] =
Async[F].delay(GlobalOpenTelemetry.get).map(OtelJava.local[F])
def program[F[_]: Async](otel4s: OtelJava[F])(implicit L: Local[F, Context]): F[Unit] = {
val _ = (otel4s, L) // both OtelJava and Local[F, Context] are available here
Async[F].unit
}
val run: IO[Unit] =
IOLocal(Context.root).flatMap { implicit ioLocal: IOLocal[Context] =>
createOtel4s[IO].flatMap(otel4s => program(otel4s))
}
How to use Java SDK context with otel4s
There are several scenarios when you want to run an effect with an explicit Java SDK context. For example, when you need to materialize an effect inside Pekko HTTP request handler.
To make it work, we can define a utility method:
import cats.mtl.Local
import org.typelevel.otel4s.oteljava.context.Context
import io.opentelemetry.context.{Context => JContext}
def withJContext[F[_], A](ctx: JContext)(fa: F[A])(implicit L: Local[F, Context]): F[A] =
Local[F, Context].scope(fa)(Context.wrap(ctx))
1) Context.wrap(ctx)
- creates otel4s context from the JContext
2) Local[F, Context].scope
- sets the given context as an active environment for the effect fa
Let's say you use Pekko HTTP and want to materialize an IO
using the current tracing context:
import cats.effect.{Async, IO}
import cats.effect.std.Random
import cats.effect.syntax.temporal._
import cats.effect.unsafe.implicits.global
import cats.mtl.Local
import cats.syntax.all._
import org.apache.pekko.http.scaladsl.model.StatusCodes.OK
import org.apache.pekko.http.scaladsl.server.Directives._
import org.apache.pekko.http.scaladsl.server.Route
import org.typelevel.otel4s.Attribute
import org.typelevel.otel4s.trace.Tracer
import org.typelevel.otel4s.oteljava.context.Context
import io.opentelemetry.instrumentation.annotations.WithSpan
import io.opentelemetry.context.{Context => JContext}
import scala.concurrent.duration._
def route(implicit T: Tracer[IO], L: Local[IO, Context]): Route =
path("gen-random-name") {
get {
complete {
OK -> generateRandomName(length = 10)
}
}
}
@WithSpan("generate-random-name")
def generateRandomName(length: Int)(implicit T: Tracer[IO], L: Local[IO, Context]): String =
withJContext(JContext.current())(generate[IO](length)).unsafeRunSync()
def generate[F[_]: Async: Tracer](length: Int): F[String] =
Tracer[F].span("generate", Attribute("length", length.toLong)).surround {
for {
random <- Random.scalaUtilRandom[F]
delay <- random.betweenInt(100, 2000)
chars <- random.nextAlphaNumeric.replicateA(length).delayBy(delay.millis)
} yield chars.mkString
}
def withJContext[F[_], A](ctx: JContext)(fa: F[A])(implicit L: Local[F, Context]): F[A] =
Local[F, Context].scope(fa)(Context.wrap(ctx))
When you invoke the gen-random-name
endpoint, the spans will be structured in the following way:
> GET { http.method = GET, http.target = /gen-random-name, ... }
> generate-random-name
> generate { length = 10 }
How to use otel4s context with Java SDK
To interoperate with Java libraries that rely on the Java SDK context, you need to activate the context manually. The following utility method allows you to extract the current otel4s context and set it into the ThreadLocal variable:
import cats.effect.Sync
import cats.mtl.Local
import cats.syntax.flatMap._
import org.typelevel.otel4s.oteljava.context.Context
import io.opentelemetry.context.{Context => JContext}
def useJContext[F[_]: Sync, A](use: JContext => A)(implicit L: Local[F, Context]): F[A] =
Local[F, Context].ask.flatMap { ctx => // <1>
Sync[F].delay {
val jContext: JContext = ctx.underlying // <2>
val scope = jContext.makeCurrent() // <3>
try {
use(jContext)
} finally {
scope.close()
}
}
}
1) Local[F, Context].ask
- get the current otel4s context
2) ctx.underlying
- unwrap otel4s context and get JContext
3) jContext.makeCurrent()
- activate JContext
within the current thread
Note: we use Sync[F].delay
to handle the side effects.
Depending on your use case, you may prefer Sync[F].interruptible
or Sync[F].blocking
.
Now we can run a slightly modified original 'problematic' example:
tracer.span("test").use { span => // start 'test' span using otel4s
IO.println(s"Otel4s ctx: ${span.context}") >> useJContext[IO, Unit] { _ =>
val jSpanContext = JSpan.current().getSpanContext // get a span from the ThreadLocal variable
println(s"Java ctx: $jSpanContext")
}
}
The output will be:
Java ctx: {traceId=06f5d9112efbe711947ebbded1287a30, spanId=26ed80c398cc039f, ...}
Otel4s ctx: {traceId=06f5d9112efbe711947ebbded1287a30, spanId=26ed80c398cc039f, ...}
As we can see, the tracing information is in sync now,
and you can use Java-instrumented libraries within the useJContext
block.
Pekko HTTP example
PekkoHttpExample is a complete example that shows how to use otel4s with OpenTelemetry Java SDK instrumented libraries.