Skip to main content
The otel4s-oteljava modules provide implementation of the otel4s API interfaces using OpenTelemetry Java under the hood.

Why OTel Java?

There are several advantages of using the otel4s-oteljava backend:
  • Low memory overhead - Leverages the battle-tested Java SDK implementation
  • Extensive instrumentation ecosystem - Access to the rich Java instrumentation libraries
  • Easy integration with Java libraries - Seamless interoperability with Java-based systems
  • Well-tested implementation - Benefits from the maturity of the upstream project
This is the recommended backend to use on the JVM.

Installation

libraryDependencies ++= Seq(
  "org.typelevel" %% "otel4s-oteljava" % "0.15.0", // <1>
  "io.opentelemetry" % "opentelemetry-exporter-otlp" % "1.59.0" % Runtime, // <2>
  "io.opentelemetry" % "opentelemetry-sdk-extension-autoconfigure" % "1.59.0" % Runtime // <3>
)
javaOptions += "-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

Creating an Autoconfigured Instance

You can use OtelJava.autoConfigured to autoconfigure the SDK:
import cats.effect.{IO, IOApp}
import org.typelevel.otel4s.oteljava.OtelJava
import org.typelevel.otel4s.metrics.MeterProvider
import org.typelevel.otel4s.trace.TracerProvider

object TelemetryApp 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] =
    ???
}
The OtelJava.autoConfigured(...) relies on environment variables and system properties to configure the SDK. For example, use export OTEL_SERVICE_NAME=auth-service to configure the name of the service. See the full set of supported configuration options.
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 other’s spans).

Accessing the Global Instance

There are several reasons to use the global instance:
  • You are using the Java Agent
  • You must reuse the global instance to interoperate with Java libraries
  • You have no control over the entry point
In such cases, you can use OtelJava.global to use the global OpenTelemetry instance:
import cats.effect.{IO, IOApp}
import org.typelevel.otel4s.oteljava.OtelJava
import org.typelevel.otel4s.metrics.MeterProvider
import org.typelevel.otel4s.trace.TracerProvider

object TelemetryApp extends IOApp.Simple {

  def run: IO[Unit] =
    OtelJava.global[IO].flatMap { otel4s =>
      program(otel4s.meterProvider, otel4s.tracerProvider)
    }

  def program(
      meterProvider: MeterProvider[IO], 
      tracerProvider: TracerProvider[IO]
  ): IO[Unit] =
    ???
}

API Reference

The OtelJava class provides access to all telemetry providers:
final class OtelJava[F[_]] private (
    val underlying: JOpenTelemetry,
    val propagators: ContextPropagators[Context],
    val meterProvider: MeterProvider[F],
    val tracerProvider: TracerProvider[F],
    val loggerProvider: LoggerProvider[F, Context],
)(implicit val localContext: LocalContext[F])

Factory Methods

MethodDescription
autoConfigured[F[_]]()Creates an autoconfigured instance from environment variables
global[F[_]]Accesses the global OpenTelemetry instance
fromJOpenTelemetry[F[_]](JOpenTelemetry)Creates an instance from a Java OpenTelemetry instance
noop[F[_]]Creates a no-op implementation

Next Steps

Configuration

Learn how to configure the SDK using environment variables

Context Propagation

Understand how context propagation works with Cats Effect

Java Interop

Integrate with Java-instrumented libraries

Java Agent

Use zero-code instrumentation with the Java Agent

Build docs developers (and LLMs) love