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)