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.14.0", // <1>
  "io.opentelemetry" % "opentelemetry-exporter-otlp" % "1.55.0" % Runtime, // <2>
  "io.opentelemetry" % "opentelemetry-sdk-extension-autoconfigure" % "1.55.0" % Runtime // <3>
)
javaOptions += "-Dotel.java.global-autoconfigure.enabled=true" // <4>Add directives to the *.scala file:
//> using dep "org.typelevel::otel4s-oteljava:0.14.0" // <1>
//> using dep "io.opentelemetry:opentelemetry-exporter-otlp:1.55.0" // <2>
//> using dep "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:1.55.0" // <3>
//> using javaOpt "-Dotel.java.global-autoconfigure.enabled=true" // <4>- Add the otel4s-oteljavalibrary
- 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]