http4s-otel4s-middleware

Getting started

Examples use Scala 3. The library also supports Scala 2.13.

Dependencies

To record metrics and traces for both clients and servers, add:

libraryDependencies ++= Seq(
  "org.http4s" %% "http4s-otel4s-middleware-client" % "0.19.0",
  "org.http4s" %% "http4s-otel4s-middleware-server" % "0.19.0"
)

Add only the client or server module if you do not need both. They include the required metrics, tracing and core dependencies.

For metrics alone, use metrics-client or metrics-server. For tracing alone, use trace-client or trace-server.

In an sbt cross-project, use %%% instead of %%. Choose an otel4s backend that supports your platform. Configure exporters and a collector separately.

Send a request on the JVM

This example uses the OtelJava backend and Ember. Add these alongside the client middleware:

libraryDependencies ++= Seq(
  "org.http4s" %% "http4s-ember-client" % "0.23.38",
  "org.typelevel" %% "otel4s-oteljava" % "1.2.0",
  "io.opentelemetry" % "opentelemetry-exporter-otlp" % "1.68.0" % Runtime,
  "io.opentelemetry" % "opentelemetry-sdk-extension-autoconfigure" % "1.68.0" % Runtime
)
import cats.effect.{IO, IOApp, Resource}
import org.http4s.ember.client.EmberClientBuilder
import org.http4s.implicits.*
import org.http4s.otel4s.middleware.client.{ClientInstrumentation, ClientTracingConfig}
import org.http4s.otel4s.middleware.metrics.client.ClientMetricsConfig
import org.http4s.otel4s.middleware.trace.client.{ClientSpanDataProvider, UriRedactor}
import org.http4s.otel4s.middleware.trace.redact.QueryRedactor
import org.typelevel.otel4s.context.ContextManager
import org.typelevel.otel4s.metrics.MeterProvider
import org.typelevel.otel4s.oteljava.OtelJava
import org.typelevel.otel4s.trace.TracerProvider

object FirstRequest extends IOApp.Simple {
  def run: IO[Unit] = {
    OtelJava.autoConfigured[IO]().use { otel =>
      given MeterProvider[IO] = otel.meterProvider
      given TracerProvider[IO] = otel.tracerProvider
      given ContextManager[IO] = otel.contextManager

      val redactor = new UriRedactor.OnlyRedactUserInfo with QueryRedactor.RedactSensitive {}
      val spanDataProvider = ClientSpanDataProvider.openTelemetry(redactor)
      val clientConfig = ClientMetricsConfig.default
      val tracingConfig = ClientTracingConfig(spanDataProvider)

      val instrumentedClient = for {
        client <- EmberClientBuilder.default[IO].build
        middleware <- Resource.eval(
          ClientInstrumentation.middleware[IO](clientConfig, tracingConfig)
        )
      } yield middleware(client)

      instrumentedClient.use { client =>
        client.expect[String](uri"http://localhost:8080/hello").flatMap(IO.println)
      }
    }
  }
}

Run a service at localhost:8080/hello and an OTLP collector before starting the example. For a collector accepting OTLP/HTTP on port 4318, configure the application environment:

export OTEL_SERVICE_NAME=hello-client
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_TRACES_EXPORTER=otlp
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=none

If using sbt run, fork the application JVM and add -Dotel.java.global-autoconfigure.enabled=true to its Java options, as in the repository's examples project. See the otel4s documentation for backend setup and exporter configuration.

The meter provider, tracer provider and ContextManager must come from the same telemetry instance. Keep that resource open while your application uses the middleware.

expect reads the response body and releases the response. If you use client.run instead, call use on the returned resource so the response is released.

Continue with Client instrumentation or Server instrumentation.