Skip to main content

Metrics DSL

The metrics DSL provides a fluent API for constructing primitive and derived metrics from a MetricFactory. It is designed to provide some compile time safety when recording metrics in terms of matching label values to label names. It does not check that a metric is unique - this is only checked at runtime, the uniqueness of a metric (name & labels combination) depends on the MetricFactory implementation.

The examples in this section assume you have imported the following and have created a MetricFactory:

import cats.effect._
import prometheus4cats._

val factory: MetricFactory.WithCallbacks[IO] = MetricFactory.builder.noop[IO]

Expected Behaviour

Every metric or callback created/registered using this DSL returns a Cats-Effect Resource which indicates the lifecycle of that metric. When the Resource is allocated the metric/callback is registered and when it is finalized it is de-registered.

It should be possible to request/register the same metric or callback multiple times without error, where you will be returned the currently registered instance rather than a new instance. This does depend on the implementation of MetricRegistry and CallbackRegistry however, the provided Java wrapper implementation implements this via reference counting and implementers of MetricRegistry and CallbackRegistry are advised to do the same in order to preserve this expected behaviour at runtime.

Refined Types

Value classes exist for metric and label names that are refined at compile time from string literals. It is also possible to refine at runtime, where the result is returned in an Either.

The value classes used by the DSL are as follows:

  • Counter.Name
  • Gauge.Name
  • Histogram.Name
  • Summary.Name
  • Info.Name
  • Label.Name
  • Metric.Help

When used in the DSL with string literals the value classes are implicitly resolved, so there is no need to wrap every value.

Choosing a Primitive Metric

factory.counter("counter_total")
factory.gauge("gauge")
factory.histogram("histogram")
factory.summary("summary")
factory.info("info_info")

Specifying the Underlying Number Format

factory.counter("counter_total").ofDouble
factory.counter("counter_total").ofLong

Defining the Help String

factory.counter("counter_total").ofDouble.help("Describe what this metric does")

Building a Simple Metric

Once you have specified all the parameters with which you want to create your metric you can call the build method. This will return a cats.effect.Resource of your desired metric, which will de-register the metric from the underlying MetricRegistry or CallbackRegistry upon finalization.

val simpleCounter = factory
.counter("counter_total")
.ofDouble
.help("Describe what this metric does")

simpleCounter.build

While not recommended, it is possible to build the metric without a cats.effect.Resource, which will not de-register from the underlying MetricRegistry:

simpleCounter.unsafeBuild

Adding Labels

Adding Individual Labels

case class MyClass(value: String)

val tupleLabelledCounter = factory
.counter("counter_total")
.ofDouble
.help("Describe what this metric does")
.label[String]("this_uses_show")
.label[MyClass]("this_doesnt_use_show", _.value)

tupleLabelledCounter.build.evalMap(_.inc(2.0, ("label_value", MyClass("label_value"))))

Compile-Time Checked Sequence of Labels

case class MyMultiClass(value1: String, value2: Int)

val classLabelledCounter = factory
.counter("counter_total")
.ofDouble
.help("Describe what this metric does")
.labels[MyMultiClass](
Label.Name("label1") -> (_.value1),
Label.Name("label2") -> (_.value2.toString)
)

classLabelledCounter.build.evalMap(_.inc(2.0, MyMultiClass("label_value", 42)))

Unchecked Sequence of Labels

val unsafeLabelledCounter = factory
.counter("counter_total")
.ofDouble
.help("Describe what this metric does")
.unsafeLabels(Label.Name("label1"), Label.Name("label2"))

val labels = Map(
Label.Name("label1") -> "label1_value",
Label.Name("label2") -> "label1_value"
)

unsafeLabelledCounter.build.evalMap(_.inc(3.0, labels))

Contramapping a Metric Type

Simple Metric

val intCounter: Resource[IO, Counter[IO, Int, Unit]] = factory
.counter("counter_total")
.ofLong
.help("Describe what this metric does")
.contramap[Int](_.toLong)
.build
val shortCounter: Resource[IO, Counter[IO, Short, Unit]] =
intCounter.map(_.contramap[Short](_.toInt))

Labelled Metric

val intLabelledCounter: Resource[IO, Counter[IO, Int, (String, Int)]] = factory
.counter("counter_total")
.ofLong
.help("Describe what this metric does")
.label[String]("string_label")
.label[Int]("int_label")
.contramap[Int](_.toLong)
.build
val shortLabelledCounter: Resource[IO, Counter[IO, Short, (String, Int)]] =
intLabelledCounter.map(_.contramap[Short](_.toInt))

Contramapping Metric Labels

This can work as a nice alternative to providing a compile-time checked sequence of labels

case class LabelsClass(string: String, int: Int)

val updatedLabelsCounter: Resource[IO, Counter[IO, Long, LabelsClass]] = factory
.counter("counter_total")
.ofLong
.help("Describe what this metric does")
.label[String]("string_label")
.label[Int]("int_label")
.contramapLabels[LabelsClass](c => (c.string, c.int))
.build

Metric Callbacks

The callback DSL is only available with the MetricFactory.WithCallbacks implementation of MetricFactory.

Callbacks are useful when you have some runtime source of a metric value, like a JMX MBean, which will be loaded when the current values for each metric is inspected for export to Prometheus.

Callbacks are both extremely powerful and dangerous, so should be used with care. Callbacks are assumed to be side-effecting in that each execution of the callback may yield a different underlying value, this also means that the operation could take a long time to complete if there is I/O involved (this is strongly discouraged). Therefore, implementations of CallbackRegistry may include a timeout.

ℹ️ Some general guidance on callbacks:

  • Do not perform any complex calculations as part of the callback, such as an I/O operation
  • Make callback calculations CPU bound, such as accessing a concurrent value

All primitive metric types, with exception to Info can be implemented as callbacks, like so for Counter and Gauge:

factory
.counter("counter_total")
.ofDouble
.help("Describe what this metric does")
.callback(IO(1.0))

factory
.gauge("gauge")
.ofDouble
.help("Describe what this metric does")
.callback(IO(1.0))

Histogram and Summary metrics are slightly different as they need a special value to contain the calculated components of each metric type:

import cats.data.NonEmptySeq

factory
.histogram("histogram")
.ofDouble
.help("Describe what this metric does")
.buckets(0.1, 0.5)
.callback(
IO(Histogram.Value(sum = 2.0, bucketValues = NonEmptySeq.of(0.0, 1.0, 1.0)))
)

⚠️️ Note that with a histogram value there must always be one more bucket value than defined when creating the metric, this is to provide a value for +Inf.

factory
.summary("summary")
.ofDouble
.help("Describe what this metric does")
.callback(
IO(Summary.Value(count = 1.0, sum = 1.0, quantiles = Map(0.5 -> 1.0)))
)

⚠️️ Note that is you specify quantiles, max age or age buckets for the summary, you cannot register a callback. This is because these parameters are used when configuring a summary metric type which would be returned you, whereas the summary implementation may be configured differently.

Metric Collection

It is possible to submit multiple metrics in a single callback, this may be useful where the metrics available in some collection may not be known at compile time. As with callbacks in general, this should be used carefully to ensure that collisions at runtime aren't encountered, it is suggested that you use a custom prefix for all metrics in a given collection to avoid this.

val metricCollection: IO[MetricCollection] = IO(MetricCollection.empty)

factory.metricCollectionCallback(metricCollection)