Histogram custom buckets
By default, OpenTelemetry use the following boundary values for histogram bucketing:[0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000].
In some cases, these boundaries don't represent the distribution of the values. For example, we expect that HTTP server latency should be somewhere between 100ms and 1s. Therefore, 2.5, 5, 7.5, and 10 seconds buckets are redundant.
In this example, we will customize the OpenTelemetry Autoconfigure extension with a View to configure custom buckets for a histogram.
Project setup
Configure the project using your favorite tool:
Add settings to the build.sbt
:
libraryDependencies ++= Seq(
"org.typelevel" %% "otel4s-oteljava" % "0.11.1", // <1>
"io.opentelemetry" % "opentelemetry-exporter-otlp" % "1.44.1" % Runtime, // <2>
"io.opentelemetry" % "opentelemetry-sdk-extension-autoconfigure" % "1.44.1" % Runtime // <3>
)
run / fork := true
javaOptions += "-Dotel.service.name=histogram-buckets-example" // <4>
Add directives to the histogram-buckets.scala
:
//> using dep "org.typelevel::otel4s-oteljava:0.11.1" // <1>
//> using dep "io.opentelemetry:opentelemetry-exporter-otlp:1.44.1" // <2>
//> using dep "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:1.44.1" // <3>
//> using javaOpt "-Dotel.service.name=histogram-buckets-example" // <4>
1) Add the otel4s
library
2) Add an OpenTelemetry exporter. Without the exporter, the application will crash
3) Add an OpenTelemetry autoconfigure extension
4) Add the name of the application to use in the traces and metrics
OpenTelemetry SDK configuration
As mentioned above, we use otel.service.name
system properties to configure the OpenTelemetry SDK.
The SDK can be configured via environment variables too. Check the full list
of environment variable configurations
for more options.
OpenTelemetry Autoconfigure extension customization
We can utilize AutoConfiguredOpenTelemetrySdk.builder
to customize the MeterProvider
and, as the result, a histogram
bucket. This approach leverages the automatic configuration functionality (e.g. configuring context propagators through
environment variables) and provides a handy way to re-configure some components.
In order to register a view, there are two essential components that must be provided: an instrument selector and a view definition.
Instrument selector
Here we establish the configuration for replacing existing instruments.
In the following example, we specifically target an instrument of type HISTOGRAM
with the
name service.work.duration
:
import io.opentelemetry.sdk.metrics.{InstrumentSelector, InstrumentType}
InstrumentSelector
.builder()
.setName("service.work.duration")
.setType(InstrumentType.HISTOGRAM)
.build()
To select multiple instruments, a wildcard pattern can be used: service.*.duration
.
View definition
The view determines how the selected instruments should be changed or aggregated.
In our particular case, we create a histogram view with custom buckets:[.005, .01, .025, .05, .075, .1, .25, .5].
import io.opentelemetry.sdk.metrics.{Aggregation, View}
View
.builder()
.setName("service.work.duration")
.setAggregation(
Aggregation.explicitBucketHistogram(
java.util.Arrays.asList(.005, .01, .025, .05, .075, .1, .25, .5)
)
)
.build()
Application example
By putting all the snippets together, we get the following:
import cats.Parallel
import cats.effect._
import cats.effect.kernel.Temporal
import cats.effect.std.Console
import cats.effect.std.Random
import cats.syntax.flatMap._
import cats.syntax.functor._
import cats.syntax.parallel._
import io.opentelemetry.sdk.metrics.Aggregation
import io.opentelemetry.sdk.metrics.InstrumentSelector
import io.opentelemetry.sdk.metrics.InstrumentType
import io.opentelemetry.sdk.metrics.View
import org.typelevel.otel4s.oteljava.context.LocalContextProvider
import org.typelevel.otel4s.oteljava.OtelJava
import org.typelevel.otel4s.metrics.Histogram
import java.util.concurrent.TimeUnit
import scala.concurrent.duration._
object HistogramBucketsExample extends IOApp.Simple {
def work[F[_]: Temporal: Console](
histogram: Histogram[F, Double],
random: Random[F]
): F[Unit] =
for {
sleepDuration <- random.nextIntBounded(5000)
_ <- histogram
.recordDuration(TimeUnit.SECONDS)
.surround(
Temporal[F].sleep(sleepDuration.millis) >>
Console[F].println(s"I'm working after [$sleepDuration ms]")
)
} yield ()
def program[F[_]: Async: LocalContextProvider: Parallel: Console]: F[Unit] =
configureSdk[F]
.evalMap(_.meterProvider.get("histogram-example"))
.use { meter =>
for {
random <- Random.scalaUtilRandom[F]
histogram <- meter.histogram[Double]("service.work.duration").create
_ <- work[F](histogram, random).parReplicateA_(50)
} yield ()
}
def run: IO[Unit] =
program[IO]
private def configureSdk[F[_]: Async: LocalContextProvider]: Resource[F, OtelJava[F]] =
OtelJava.autoConfigured { sdkBuilder =>
sdkBuilder
.addMeterProviderCustomizer { (meterProviderBuilder, _) =>
meterProviderBuilder
.registerView(
InstrumentSelector
.builder()
.setName("service.work.duration")
.setType(InstrumentType.HISTOGRAM)
.build(),
View
.builder()
.setName("service.work.duration")
.setAggregation(
Aggregation.explicitBucketHistogram(
java.util.Arrays.asList(.005, .01, .025, .05, .075, .1, .25, .5)
)
)
.build()
)
}
.setResultAsGlobal
}
}