Customize histogram buckets

Use this page when the default histogram buckets do not match the values you record.

For example, if a latency histogram is recorded in seconds and most values are between 0.1 and 1.0, narrower buckets may be more useful.

Prerequisites

1. Choose the path that matches who owns the histogram

2. If you create the histogram yourself, use withExplicitBucketBoundaries

This is the simpler path when the histogram is under your control.

import cats.effect.{IO, IOApp}
import org.typelevel.otel4s.metrics.{BucketBoundaries, Histogram, Meter}
import org.typelevel.otel4s.oteljava.OtelJava

object Main extends IOApp.Simple {
  def run: IO[Unit] =
    OtelJava.autoConfigured[IO]().use { otel4s =>
      otel4s.meterProvider.get("auth-service").flatMap { implicit meter =>
        program
      }
    }

  def program(implicit meter: Meter[IO]): IO[Unit] =
    createLatencyHistogram.flatMap(_.record(0.12))

  def createLatencyHistogram(implicit meter: Meter[IO]): IO[Histogram[IO, Double]] =
    meter
      .histogram[Double]("http.server.duration")
      .withUnit("s")
      .withExplicitBucketBoundaries(
        BucketBoundaries(0.05, 0.1, 0.25, 0.5, 1.0)
      )
      .create
}

3. If the histogram is created elsewhere, register a view

Use a view when you cannot change the histogram builder directly.

This is the right path when the histogram is created by:

In this example, the histogram is still named http.server.duration and records values in seconds.

import java.{util => ju}

import cats.effect.{IO, IOApp}
import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdkBuilder
import io.opentelemetry.sdk.metrics.Aggregation
import io.opentelemetry.sdk.metrics.ExplicitBucketHistogramOptions
import io.opentelemetry.sdk.metrics.InstrumentSelector
import io.opentelemetry.sdk.metrics.InstrumentType
import io.opentelemetry.sdk.metrics.View
import org.typelevel.otel4s.metrics.Meter
import org.typelevel.otel4s.oteljava.OtelJava

object MainWithView extends IOApp.Simple {
  def run: IO[Unit] =
    OtelJava.autoConfigured[IO](configureBuilder).use { otel4s =>
      otel4s.meterProvider.get("auth-service").flatMap { implicit meter =>
        program
      }
    }

  def program(implicit meter: Meter[IO]): IO[Unit] =
    meter
      .histogram[Double]("http.server.duration")
      .withUnit("s")
      .create
      .flatMap(_.record(0.12))

  def configureBuilder(
      builder: AutoConfiguredOpenTelemetrySdkBuilder
  ): AutoConfiguredOpenTelemetrySdkBuilder =
    builder.addMeterProviderCustomizer { (meterProviderBuilder, _) =>
      meterProviderBuilder.registerView(
        InstrumentSelector
          .builder()
          .setName("http.server.duration")
          .setType(InstrumentType.HISTOGRAM)
          .build(),
        View
          .builder()
          .setAggregation(
            Aggregation.explicitBucketHistogram(
              ExplicitBucketHistogramOptions
                .builder()
                .setBucketBoundaries(ju.Arrays.asList(0.05, 0.1, 0.25, 0.5, 1.0))
                .build()
            )
          )
          .build()
      )
    }
}

4. Choose bucket boundaries in the same unit you record

The custom bucket boundaries must use the same unit as the histogram values.

In the examples above:

5. Keep the histogram name stable when you use a view

The view only applies to histograms that match the selector.

If you rename the histogram in application code, update the selector to match the new name.

What's next