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
-
Simple and idiomatic metrics and tracing API
Provides a user-friendly and idiomatic API for telemetry, designed with Typelevel ecosystem best practices. Intuitive interfaces for metrics, tracing, and context propagation.
-
Minimal overhead
The library utilizes metaprogramming techniques to reduce runtime costs and allocations. Near-zero overhead when telemetry is disabled, ensuring production performance is unaffected when tracing or metrics collection is not required.
-
Modularity
A modular architecture allows to include only the required components:
- Core modules: designed for library instrumentation, offering a lightweight dependency footprint
- Selective integration: use only the metrics or tracing functionality without requiring the other
-
Cross-platform
All modules are available for Scala 2.13 and Scala 3. Core modules are available on all platforms: JVM, Scala.js, and Scala Native.
-
OpenTelemetry Java SDK backend
The backend utilizes OpenTelemetry Java SDK under the hood, offering production-ready telemetry:
- Low memory overhead
- Extensive instrumentation ecosystem
- Well-tested implementation
-
SDK backend
SDK backend is implemented in Scala from scratch. Available for JVM, Scala.js, and Scala Native. While the implementation is compliant with the OpenTelemetry specification, it remains experimental and some functionality may be lacking.
-
Testkit
A testkit simplifies the validation of telemetry behavior in the applications and libraries:
- Framework-agnostic, works with any test framework, for example weaver, munit, scalatest
- Ideal for testing of the instrumented applications in end-to-end or unit tests
- Available for OpenTelemetry Java and SDK backends
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>
- Add the
otel4s-oteljava
library - Add an OpenTelemetry exporter. Without the exporter, the application will crash
- Add an OpenTelemetry autoconfigure extension
- 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
- Start tracing your application with Jaeger and Docker
- Implement tracing and metrics with Honeycomb
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]