In this example, we’ll use Jaeger to collect and visualize traces produced by an otel4s application. We’ll cover the installation and configuration of Jaeger, as well as the instrumentation of the application.
Project Setup
Configure your project with the required dependencies:
// Add to build.sbt
libraryDependencies ++= Seq(
"org.typelevel" %% "otel4s-oteljava" % "0.15.0",
"io.opentelemetry" % "opentelemetry-exporter-otlp" % "1.59.0" % Runtime,
"io.opentelemetry" % "opentelemetry-sdk-extension-autoconfigure" % "1.59.0" % Runtime
)
run / fork := true
javaOptions += "-Dotel.java.global-autoconfigure.enabled=true"
javaOptions += "-Dotel.service.name=jaeger-example"
javaOptions += "-Dotel.metrics.exporter=none"
The otel.metrics.exporter=none setting is required because Jaeger is compatible only with traces, not metrics.
OpenTelemetry SDK Configuration
The OpenTelemetry SDK can be configured via system properties or environment variables. See the full list of environment variable configurations for more options.
Jaeger Setup with Docker
Run Jaeger using Docker with OpenTelemetry support enabled:
docker run --name jaeger \
-e COLLECTOR_OTLP_ENABLED=true \
-p 16686:16686 \
-p 4317:4317 \
-p 4318:4318 \
jaegertracing/all-in-one:1.35
Port mappings:
16686 - Jaeger UI
4317 - OpenTelemetry gRPC receiver
4318 - OpenTelemetry HTTP receiver
Application Example
Here’s a complete example that creates nested spans with events and attributes:
import cats.effect.{Async, IO, IOApp, Resource}
import cats.effect.std.Console
import cats.effect.std.Random
import cats.syntax.all._
import org.typelevel.otel4s.Attribute
import org.typelevel.otel4s.oteljava.OtelJava
import org.typelevel.otel4s.trace.Tracer
import scala.concurrent.duration._
trait Work[F[_]] {
def doWork: F[Unit]
}
object Work {
def apply[F[_]: Async: Tracer: Console]: Work[F] =
new Work[F] {
def doWork: F[Unit] =
Tracer[F].span("Work.DoWork").use { span =>
span.addEvent("Starting the work.") *>
doWorkInternal(steps = 10) *>
span.addEvent("Finished working.")
}
def doWorkInternal(steps: Int): F[Unit] = {
val step = Tracer[F]
.span("internal", Attribute("steps", steps.toLong))
.surround {
for {
random <- Random.scalaUtilRandom
delay <- random.nextIntBounded(1000)
_ <- Async[F].sleep(delay.millis)
_ <- Console[F].println("Doin' work")
} yield ()
}
if (steps > 0) step *> doWorkInternal(steps - 1) else step
}
}
}
object TracingExample extends IOApp.Simple {
def tracer: Resource[IO, Tracer[IO]] =
OtelJava.autoConfigured[IO]().evalMap(_.tracerProvider.get("Example"))
def run: IO[Unit] =
tracer
.evalMap { implicit tracer: Tracer[IO] =>
Work[IO].doWork
}
.use_
}
Running the Application
Viewing Traces in Jaeger
Choose “jaeger-example” from the service dropdown.
Click “Find Traces” to see the collected traces.
Click on a trace to see the span hierarchy, timing information, events, and attributes.
You should see a trace with a parent span “Work.DoWork” containing 10 nested “internal” spans, each with a “steps” attribute showing the iteration count.