otel4s

Telemetry meets higher-kinded types

otel4s is an OpenTelemetry implementation for Scala. The design goal is to fully and faithfully implement the OpenTelemetry Specification atop Cats Effect.

Features

Status

The API is still highly experimental, but we are actively instrumenting various libraries and applications to check for fit. Don't put it in a binary-stable library yet, but we invite you to try it out and let us know what you think.

Modules availability

Module / Platform JVM Scala Native Scala.js
otel4s-core
otel4s-sdk (tracing only)
otel4s-oteljava

Getting started

If you develop an application and want to export the telemetry, use otel4s-oteljava module. If you develop a library, check out this recommendation.

Add settings to the build.sbt:

libraryDependencies ++= Seq(
  "org.typelevel" %% "otel4s-oteljava" % "0.6.0", // <1>
  "io.opentelemetry" % "opentelemetry-exporter-otlp" % "1.37.0" % Runtime, // <2>
  "io.opentelemetry" % "opentelemetry-sdk-extension-autoconfigure" % "1.37.0" % Runtime // <3>
)
javaOptions += "-Dotel.java.global-autoconfigure.enabled=true" // <4>

Add directives to the *.scala file:

//> using lib "org.typelevel::otel4s-oteljava:0.6.0" // <1>
//> using lib "io.opentelemetry:opentelemetry-exporter-otlp:1.37.0" // <2>
//> using lib "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:1.37.0" // <3>
//> using `java-opt` "-Dotel.java.global-autoconfigure.enabled=true" // <4>
  1. Add the otel4s-oteljava library
  2. Add an OpenTelemetry exporter. Without the exporter, the application will crash
  3. Add an OpenTelemetry autoconfigure extension
  4. Enable OpenTelemetry SDK autoconfigure mode

Examples

The Noop Tracer

If you use a library that supports otel4s (eg Skunk) but do not want to use Open Telemetry, then you can place the No-op Tracer into implicit scope.

The no-op Tracer can be provided in the following ways:

By using the import Tracer.Implicits.noop:

import cats.effect.IO
import org.typelevel.otel4s.trace.Tracer

def program[F[_]: Tracer]: F[Unit] = ???

import Tracer.Implicits.noop
val io: IO[Unit] = program[IO]

By defining an implicit val:

import cats.effect.IO
import org.typelevel.otel4s.trace.Tracer

def program[F[_]: Tracer]: F[Unit] = ???

implicit val tracer: Tracer[IO] = Tracer.noop
val io: IO[Unit] = program[IO]

By using the import Tracer.Implicits.noop:

import cats.effect.IO
import org.typelevel.otel4s.trace.Tracer

def program[F[_]](using Tracer[F]): F[Unit] = ???

import Tracer.Implicits.noop
val io: IO[Unit] = program[IO]

By defining a given:

import cats.effect.IO
import org.typelevel.otel4s.trace.Tracer

def program[F[_]](using Tracer[F]): F[Unit] = ???

given Tracer[IO] = Tracer.noop
val io: IO[Unit] = program[IO]