Prometheus Exporter
The exporter exports metrics in a prometheus-compatible format, so Prometheus can scrape metrics from the HTTP server. You can either allow exporter to launch its own server or add Prometheus routes to the existing one.
An example of output (e.g. curl -H "Accept:text/plain" http://localhost:9464/metrics
):
# TYPE counter_total counter
counter_total{otel_scope_name="meter"} 1
# HELP target_info Target metadata
# TYPE target_info gauge
target_info{host_arch="amd64",host_name="fv-az532-915",os_description="Linux 6.5.0-1025-azure",os_type="linux",process_command_line="/usr/lib/jvm/temurin-11-jdk-amd64/bin/java -Xms1G -Xmx4G -XX:+UseG1GC -Dsbt.script=/opt/hostedtoolcache/sbt/1.10.5/sbt/bin/sbt -Dscala.ext.dirs=/home/runner/.sbt/1.0/java9-rt-ext-eclipse_adoptium_11_0_25 /opt/hostedtoolcache/sbt/1.10.5/sbt/bin/sbt-launch.jar docs/tlSite",process_executable_path="/usr/lib/jvm/temurin-11-jdk-amd64/bin/java",process_pid="1782",process_runtime_description="Eclipse Adoptium OpenJDK 64-Bit Server VM 11.0.25+9",process_runtime_name="OpenJDK Runtime Environment",process_runtime_version="11.0.25+9",service_name="unknown_service:scala",telemetry_sdk_language="scala",telemetry_sdk_name="otel4s",telemetry_sdk_version="0.11.1-24-e2c4ee7-SNAPSHOT"} 1
Getting Started
Add settings to the build.sbt
:
libraryDependencies ++= Seq(
"org.typelevel" %%% "otel4s-sdk" % "0.11.1", // <1>
"org.typelevel" %%% "otel4s-sdk-exporter-prometheus" % "0.11.1", // <2>
)
Add directives to the *.scala
file:
//> using dep "org.typelevel::otel4s-sdk::0.11.1" // <1>
//> using dep "org.typelevel::otel4s-sdk-exporter-prometheus::0.11.1" // <2>
- Add the
otel4s-sdk
library - Add the
otel4s-sdk-exporter-prometheus
library
Configuration
The OpenTelemetrySdk.autoConfigured(...)
and SdkMetrics.autoConfigured(...)
rely on the environment variables
and system properties to configure the SDK.
Check out the configuration details.
Autoconfigured (built-in server)
By default, Prometheus metrics exporter will launch its own HTTP server.
To make autoconfiguration work, we must configure the otel.metrics.exporter
property:
Add settings to the build.sbt
:
javaOptions += "-Dotel.metrics.exporter=prometheus"
javaOptions += "-Dotel.traces.exporter=none"
envVars ++= Map("OTEL_METRICS_EXPORTER" -> "prometheus", "OTEL_TRACES_EXPORTER" -> "none")
Add directives to the *.scala
file:
//> using javaOpt -Dotel.metrics.exporter=prometheus
//> using javaOpt -Dotel.traces.exporter=none
$ export OTEL_METRICS_EXPORTER=prometheus
$ export OTEL_TRACES_EXPORTER=none
Then autoconfigure the SDK:
OpenTelemetrySdk.autoConfigured
configures both MeterProvider
and TracerProvider
:
import cats.effect.{IO, IOApp}
import org.typelevel.otel4s.metrics.MeterProvider
import org.typelevel.otel4s.sdk.OpenTelemetrySdk
import org.typelevel.otel4s.sdk.exporter.prometheus.autoconfigure.PrometheusMetricExporterAutoConfigure
import org.typelevel.otel4s.trace.TracerProvider
object TelemetryApp extends IOApp.Simple {
def run: IO[Unit] =
OpenTelemetrySdk
.autoConfigured[IO](
// register Prometheus exporter configurer
_.addMetricExporterConfigurer(PrometheusMetricExporterAutoConfigure[IO])
)
.use { autoConfigured =>
val sdk = autoConfigured.sdk
program(sdk.meterProvider, sdk.tracerProvider) >> IO.never
}
def program(
meterProvider: MeterProvider[IO],
tracerProvider: TracerProvider[IO]
): IO[Unit] = {
val _ = tracerProvider
for {
meter <- meterProvider.meter("meter").get
counter <- meter.counter[Long]("counter").create
_ <- counter.inc()
} yield ()
}
}
SdkMetrics
configures only MeterProvider
:
import cats.effect.{IO, IOApp}
import org.typelevel.otel4s.metrics.MeterProvider
import org.typelevel.otel4s.sdk.exporter.prometheus.autoconfigure.PrometheusMetricExporterAutoConfigure
import org.typelevel.otel4s.sdk.metrics.SdkMetrics
object TelemetryApp extends IOApp.Simple {
def run: IO[Unit] =
SdkMetrics
.autoConfigured[IO](
// register Prometheus exporters configurer
_.addExporterConfigurer(PrometheusMetricExporterAutoConfigure[IO])
)
.use { autoConfigured =>
program(autoConfigured.meterProvider)
}
def program(
meterProvider: MeterProvider[IO]
): IO[Unit] =
for {
meter <- meterProvider.meter("meter").get
counter <- meter.counter[Long]("counter").create
_ <- counter.inc()
} yield ()
}
The SDK will launch an HTTP server, and you can scrape metrics from the http://localhost:9464/metrics
endpoint.
Manual (use Prometheus routes with existing server)
If you already run an HTTP server, you can attach Prometheus routes to it.
For example, you can expose Prometheus metrics at /prometheus/metrics
alongside your app routes.
Note: since we configure the exporter manually, the exporter autoconfiguration must be disabled.
Add settings to the build.sbt
:
javaOptions += "-Dotel.metrics.exporter=none"
javaOptions += "-Dotel.traces.exporter=none"
envVars ++= Map("OTEL_METRICS_EXPORTER" -> "none", "OTEL_TRACES_EXPORTER" -> "none")
Add directives to the *.scala
file:
//> using javaOpt -Dotel.metrics.exporter=none
//> using javaOpt -Dotel.traces.exporter=none
$ export OTEL_METRICS_EXPORTER=none
$ export OTEL_TRACES_EXPORTER=none
Then autoconfigure the SDK and attach Prometheus routes to your HTTP server:
OpenTelemetrySdk.autoConfigured
configures both MeterProvider
and TracerProvider
:
import cats.effect.{IO, IOApp}
import cats.syntax.semigroupk._
import org.http4s._
import org.http4s.ember.server.EmberServerBuilder
import org.http4s.server.Router
import org.typelevel.otel4s.metrics.MeterProvider
import org.typelevel.otel4s.sdk.OpenTelemetrySdk
import org.typelevel.otel4s.sdk.exporter.prometheus._
import org.typelevel.otel4s.trace.TracerProvider
object TelemetryApp extends IOApp.Simple {
def run: IO[Unit] =
PrometheusMetricExporter.builder[IO].build.flatMap { exporter =>
OpenTelemetrySdk
.autoConfigured[IO](
// disable exporter autoconfiguration
// can be skipped if you use system properties or env variables
_.addPropertiesCustomizer(_ => Map("otlp.metrics.exporter" -> "none"))
// register Prometheus exporter
.addMeterProviderCustomizer((b, _) =>
b.registerMetricReader(exporter.metricReader)
)
)
.use { autoConfigured =>
val sdk = autoConfigured.sdk
val appRoutes: HttpRoutes[IO] = HttpRoutes.empty // your app routes
val writerConfig = PrometheusWriter.Config.default
val prometheusRoutes = PrometheusHttpRoutes.routes[IO](exporter, writerConfig)
val routes = appRoutes <+> Router("prometheus/metrics" -> prometheusRoutes)
EmberServerBuilder.default[IO].withHttpApp(routes.orNotFound).build.use { _ =>
program(sdk.meterProvider, sdk.tracerProvider) >> IO.never
}
}
}
def program(
meterProvider: MeterProvider[IO],
tracerProvider: TracerProvider[IO]
): IO[Unit] = {
val _ = tracerProvider
for {
meter <- meterProvider.meter("meter").get
counter <- meter.counter[Long]("counter").create
_ <- counter.inc()
} yield ()
}
}
SdkMetrics
configures only MeterProvider
:
import cats.effect.{IO, IOApp}
import cats.syntax.semigroupk._
import org.http4s._
import org.http4s.ember.server.EmberServerBuilder
import org.http4s.server.Router
import org.typelevel.otel4s.metrics.MeterProvider
import org.typelevel.otel4s.sdk.exporter.prometheus._
import org.typelevel.otel4s.sdk.metrics.SdkMetrics
object TelemetryApp extends IOApp.Simple {
def run: IO[Unit] =
PrometheusMetricExporter.builder[IO].build.flatMap { exporter =>
SdkMetrics
.autoConfigured[IO](
// disable exporter autoconfiguration
// can be skipped if you use system properties or env variables
_.addPropertiesCustomizer(_ => Map("otlp.metrics.exporter" -> "none"))
// register Prometheus exporter
.addMeterProviderCustomizer((b, _) =>
b.registerMetricReader(exporter.metricReader)
)
)
.use { autoConfigured =>
val appRoutes: HttpRoutes[IO] = HttpRoutes.empty // your app routes
val writerConfig = PrometheusWriter.Config.default
val prometheusRoutes = PrometheusHttpRoutes.routes[IO](exporter, writerConfig)
val routes = appRoutes <+> Router("prometheus/metrics" -> prometheusRoutes)
EmberServerBuilder.default[IO].withHttpApp(routes.orNotFound).build.use { _ =>
program(autoConfigured.meterProvider) >> IO.never
}
}
}
def program(
meterProvider: MeterProvider[IO]
): IO[Unit] =
for {
meter <- meterProvider.meter("meter").get
counter <- meter.counter[Long]("counter").create
_ <- counter.inc()
} yield ()
}
That way you attach Prometheus routes to the existing HTTP server, and you can scrape metrics from the http://localhost:8080/prometheus/metrics endpoint.