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
otel4s-oteljava

How to choose a backend

For most cases, otel4s-oteljava is the recommended backend, that utilizes OpenTelemetry Java library under the hood. You can benefit from various integrations and low memory overhead.

otel4s-sdk is an experimental implementation of the Open Telemetry specification in pure Scala and available for all platforms: JVM, Scala.js, and Scala Native. However, some features are missing, and the memory overhead may be noticeable.

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.11.2", // <1>
  "io.opentelemetry" % "opentelemetry-exporter-otlp" % "1.45.0" % Runtime, // <2>
  "io.opentelemetry" % "opentelemetry-sdk-extension-autoconfigure" % "1.45.0" % Runtime // <3>
)
javaOptions += "-Dotel.java.global-autoconfigure.enabled=true" // <4>

Add directives to the *.scala file:

//> using dep "org.typelevel::otel4s-oteljava:0.11.2" // <1>
//> using dep "io.opentelemetry:opentelemetry-exporter-otlp:1.45.0" // <2>
//> using dep "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:1.45.0" // <3>
//> using javaOpt "-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

Then, the instance can be materialized:

import cats.effect.{IO, IOApp}
import org.typelevel.otel4s.metrics.MeterProvider
import org.typelevel.otel4s.oteljava.OtelJava
import org.typelevel.otel4s.trace.TracerProvider

object Main extends IOApp.Simple {
  def run: IO[Unit] =
    OtelJava.autoConfigured[IO]().use { otel4s =>
      program(otel4s.meterProvider, otel4s.tracerProvider)
    }

  def program(
      meterProvider: MeterProvider[IO],
      tracerProvider: TracerProvider[IO]
  ): IO[Unit] =
    for {
      meter <- meterProvider.get("service")
      tracer <- tracerProvider.get("service")

      // create a counter and increment its value
      counter <- meter.counter[Long]("counter").create
      _ <- counter.inc()

      // create and materialize a span
      _ <- tracer.span("span").surround(IO.unit)
    } yield ()
}

OtelJava.autoConfigured creates an isolated non-global instance. If you create multiple instances, those instances won't interoperate (i.e. be able to see each others spans).

Examples

The noop Tracer and Meter

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 or Meter can be provided in the following ways:

By using the import Tracer.Implicits.noop and import Meter.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.metrics.Meter
import org.typelevel.otel4s.trace.Tracer

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

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

By using the import Tracer.Implicits.noop and import Meter.Implicits.noop:

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

def program[F[_]](using Meter[F], 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.metrics.Meter
import org.typelevel.otel4s.trace.Tracer

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

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