Create spans around effectful code
Use this page when you want to trace application code with Tracer.
Prerequisites
1. Get a Tracer
Create OtelJava, then get a Tracer from its TracerProvider.
import cats.effect.{IO, IOApp}
import org.typelevel.otel4s.oteljava.OtelJava
import org.typelevel.otel4s.trace.Tracer
object Main extends IOApp.Simple {
def run: IO[Unit] =
OtelJava.autoConfigured[IO]().use { otel4s =>
otel4s.tracerProvider.get("auth-service").flatMap { implicit tracer =>
handleRequest(userId = 42L)
}
}
def handleRequest(userId: Long)(implicit tracer: Tracer[IO]): IO[Unit] =
Tracer[IO].currentSpanContext.flatMap(_ => IO.println(s"handling user $userId"))
}
get("auth-service") names the instrumentation scope for the tracer. Use a stable name that identifies the code
emitting telemetry, such as your application or module name.
2. Wrap work in a span
Use span(...).surround(...) for the common case where the span should cover one effect.
Inside that scope, otel4s keeps the span current for nested effects. You only need explicit propagation when crossing process or context boundaries.
import cats.effect.IO
import org.typelevel.otel4s.Attribute
import org.typelevel.otel4s.trace.Tracer
def handleRequest(userId: Long)(implicit tracer: Tracer[IO]): IO[Unit] =
Tracer[IO].span("http.request", Attribute("user.id", userId)).surround {
Tracer[IO].currentSpanContext.flatMap(_ => IO.println(s"handling user $userId"))
}
If you need to add events or attributes while the span is open, use use.
import cats.effect.IO
import org.typelevel.otel4s.Attribute
import org.typelevel.otel4s.trace.Tracer
def handleRequest(userId: Long)(implicit tracer: Tracer[IO]): IO[Unit] =
Tracer[IO].span("http.request", Attribute("user.id", userId)).use { span =>
span.addEvent("loading-user").flatMap { _ =>
span.addAttribute(Attribute("user.found", true))
}
}
3. Add child spans for nested work
When you create spans inside another span, otel4s attaches them as children of the current span.
import cats.effect.IO
import org.typelevel.otel4s.Attribute
import org.typelevel.otel4s.trace.Tracer
def handleRequest(userId: Long)(implicit tracer: Tracer[IO]): IO[Unit] =
Tracer[IO].span("http.request", Attribute("user.id", userId)).surround {
validateUser(userId).flatMap(_ => fetchUser(userId))
}
def validateUser(userId: Long)(implicit tracer: Tracer[IO]): IO[Unit] =
Tracer[IO].span("user.validate").surround(IO.println(s"validating user $userId"))
def fetchUser(userId: Long)(implicit tracer: Tracer[IO]): IO[Unit] =
Tracer[IO].span("user.fetch").use { span =>
span.addEvent("loading-user").flatMap { _ =>
span.addAttribute(Attribute("user.found", true)).flatMap { _ =>
IO.println(s"fetching user $userId")
}
}
}
4. Start a root span when the work should begin a new trace
Use rootSpan when the current work should not inherit the active trace context.
import cats.effect.IO
import org.typelevel.otel4s.trace.Tracer
def runIndependentTask(implicit tracer: Tracer[IO]): IO[Unit] =
Tracer[IO].rootSpan("maintenance.run").surround {
Tracer[IO].span("cleanup").surround(IO.println("cleanup"))
}
What's next
- Add generated OpenTelemetry semantic attributes to spans: Use semantic attributes
- Keep a span open until later code finishes the work: Use unmanaged spans when a span must end outside its scope
- Trace
Resourceandfs2.Streamcode: Trace Resource and fs2.Stream code - Continue incoming traces and propagate them downstream: Propagate trace context across service boundaries
- Work with Java libraries that depend on OpenTelemetry context: Use otel4s with Java-instrumented libraries