Skip to main content
Version: 2.x

ZIO Metric Reference

Measuring​

All ZIO metrics are defined in the form of aspects that can be applied with @@ to measure an effect. Applying an aspect to an effect does not change the type of the effect. For example:

val metric = Metric.counter("effect_counter")
val effect: ZIO[Any, Throwable, Double] = ???
val effectWithMeasurement: ZIO[Any, Throwable, Double] = effect @@ metric

Name, description and labels​

Every metric has a name and optionally a description. The name may be augmented by zero or more Labels (sometimes called tags). Labels are useful, because some reporting platforms support them and provide aggregation mechanisms for them.

For example, think of a counter that counts how often a particular service has been invoked. If the application is deployed across several hosts, we can model our counter with the name my_service and an additional label (host, ${hostname}). With this definition we can see the number of executions for each host, but we can also create a query in Grafana or DatadogHQ to visualize the aggregated value over all hosts. Using more than one label allows us to create visualizations across any combination of the labels.

Name and description are always given as part of the metric constructor, labels can be added via one of the tagged methods. Here is an example:

val countRequests = Metric.counter("count_requests", "Description of the counter").tagged("hostname", hostname)
val effect: ZIO[Any, Throwable, Double] = ???
val effectWithTaggedMeasurement = effect @@ countRequests.tagged("path", path)

We recommend you follow the Prometheus best practices guide for the metric name and labels.

Supported type​

Every Metric has the type parameter In which indicates what types of values can be measured by the metric. When applying the metric as an aspect to an effect, the metric's In type must be compatible with the output type of the effect (the A in ZIO[R, E, A]). For example, a Metric.Counter[Any] can be applied to any effect while a Metric.Counter[Double] can only be applied to an effect that produces a Double.

In the underlying implementation, each metric type supports only one base type. Counters, Gauges, Histograms and Summaries all support Double values, while a Frequency supports String values. To support a different type, we can use the contramap method. For example, a Gauge that supports Longs is written as:

val longGauge: Metrics.Gauge[Long] =
Metric.Gauge("my_metric").contramap[Long](_.toDouble)

Method fromConst works like contramap but always returns the same value. For example a counter that always increases with the same value is written as:

val occurrenceCounter: Metric.Counter[Any] =
Metric.counter("my_metric").fromConst(1L)

Measuring durations​

To measure how long an effect takes, we can use methods trackDuration and trackDurationWith. See the histogram section below for an example.

Measuring failures​

Normally, we only measure effects that succeed. To measure an effect that failed, we can use methods like trackAll, trackDefectWith, trackErrorWith and trackSuccessWith. For example trackError lets you measure the error output of an effect:

val countAllErrors =
Metric.counter("all_errors").fromConst(1L).trackError

// counts the errors of `effect`
val effectWithMeasurement = effect @@ countAllErrors

Metric types

Counter​

A counter is a metric of which the value can only increase over time. A counter can be created with one of these methods:

val longCounter: Metric.Counter[Long] = Metric.counter(name = "my_metric")
val intCounter: Metric.Counter[Int] = Metric.counterInt(name = "my_metric")
val doubleCounter: Metric.Counter[Double] = Metric.counterDouble(name = "my_metric")

Besides using a counter as an aspect (with the @@ operator), counter metrics can also measure values with their increment and incrementBy methods. For example:

longCounter.incrementBy(10L)

Examples​

Create a counter named count_all which is incremented by 1 every time it is invoked. Since the metric type is Any the aspect can be applied to effect of any type.

val aspCountAll: Metric.Counter[Any] =
Metric.counter("count_all").fromConst(1L)

The same aspect can be applied to more than one effect. In the following example we count the sum of executions of both effects in the for comprehension:

val countAll = for {
_ <- ZIO.unit @@ aspCountAll
_ <- ZIO.unit @@ aspCountAll
} yield ()

Create a counter named count_bytes that can be applied to effects having the output type Double.

val aspCountBytes = Metric.counterDouble("count_bytes")

Now we can apply it to effects producing Double (in a real application the value might be the number of bytes read from a stream or something similar):

val countBytes = nextDoubleBetween(0.0d, 100.0d) @@ aspCountBytes

Gauges​

A gauge is a metric that can change over time. A gauge can be created as follows:

val doubleGauge: Metric.Gauge[Double] = Metric.gauge(name = "my_metric")

Besides using a gauge as an aspect (with the @@ operator), gauge metrics can also measure values with their set, increment, incrementBy, decrement and decrementBy methods. For example:

for {
_ <- doubleGauge.set(10.0) // sets the gauge to 10.0
_ <- doubleGauge.decrement // decreases the gauge with 1.0
} yield ()

Examples​

Create a gauge that can be applied to effects that produce a Double:

val aspGauge = Metric.gauge("set_gauge")

Now we can apply the metric to effects that produce Doubles. Note that we can instrument an effect with any number of aspects if the type constraints are satisfied.

val measuredEffect = nextDoubleBetween(0.0d, 100.0d) @@ aspGauge @@ aspCountAll

Histograms​

A histogram is a metric that counts the measured values in buckets. Each bucket is defined by an upper boundary. The count in a bucket with upper boundary b increases by 1 if an observed value v is less or equal to b. As a consequence, all buckets that have a boundary b1 with b1 > b will increase by 1 after observing v.

A histogram also keeps track of the overall count of observed values and the sum of all observed values.

The last bucket is always defined as Double.MaxValue, so that the count of observed values in the last bucket is always equal to the overall count of observed values within the histogram.

The used histogram model is inspired by Prometheus.

Boundaries can be created as follows:

// given boundaries: 100.0, 200.0, 300.0, 400.0, MaxValue
MetricKeyType.Histogram.Boundaries.fromChunk(Chunk(100.0, 200.0, 300.0, 400.0))
// linear boundaries: 100.0, 200.0, 300.0, 400.0, 500.0, 600.0, 700.0, 800.0, 900.0, 1000.0, MaxValue
MetricKeyType.Histogram.Boundaries.linear(100.0, 100.0, 10)
// exponential boundaries: 100.0, 200.0, 400.0, 800.0, 1600.0, 3200.0, 6400.0, 12800.0, 25600.0, 51200.0, MaxValue
MetricKeyType.Histogram.Boundaries.exponential(100.0, 2.0, 10)

A histogram can be created with the Metric.histogram(name, boundaries) method. For example:

val latencyHistogram: Metric.Histogram[Double] =
Metric.histogram("queue_size", boundaries)

Examples​

Create a histogram with 10 buckets: 0..100 in steps of 10 and Double.MaxValue. It can be applied to effects yielding a Double.

val aspHistogram =
Metric.histogram("my_histogram", Histogram.Boundaries.linear(0.0d, 10.0d, 10))

Now we can apply the histogram to effects producing Double:

val measuredEffect = nextDoubleBetween(0.0d, 120.0d) @@ aspHistogram 

Create a histogram that observes Durations in seconds. Use trackDuration to track the duration of an effect. Note that using seconds as unit is the recommended convention in many metric platforms such as Prometheus and OpenTelemetry.

val latencyHistogram: Metric.Histogram[Duration] =
Metric.histogram(
"my_latency_seconds",
"The latency in seconds.",
MetricKeyType.Histogram.Boundaries.exponential(0.01, 2.0, 10)
)
.contramap[Duration](_.toNanos.toDouble / 1e9) // convert to seconds

val effect: ZIO[Any, Throwable, Unit] = ???

effect @@ latencyHistogram.trackDuration

Summaries​

Similar to a histogram a summary also observes Double values. While a histogram directly modifies the bucket counters and does not keep the individual samples, the summary keeps the observed samples in its internal state. To avoid the set of samples grow uncontrolled, the summary need to be configured with a maximum age t and a maximum size n. To calculate the statistics, maximal n samples will be used, all of which are not older than t.

Essentially the set of samples is a sliding window over the last observed samples matching the conditions above.

A summary is used to calculate a set of quantiles over the current set of samples. A quantile is defined by a Double value q with 0 <= q <= 1 and resolves to a Double as well.

The value of a given quantile q is the maximum value v out of the current sample buffer with size n where at most q * n values out of the sample buffer are less or equal to v.

Typical quantiles for observation are 0.5 (the median) and the 0.95. Quantiles are very good for monitoring Service Level Agreements.

The ZIO Metrics API also allows summaries to be configured with an error margin e. The error margin is applied to the count of values, so that a quantile q for a set of size s resolves to value v if the number n of values less or equal to v is (1 -e)q * s <= n <= (1+e)q.

A summary can be created with the summary method:

Metric.summary(
name: String,
maxAge: Duration,
maxSize: Int,
error: Double,
quantiles: Chunk[Double]
): Metric.Summary[Double]

Examples​

Create a summary that can hold 100 samples, the max age of the samples is 1 day and the error margin is 3%. The summary should report the 10%, 50% and 90% quantiles. It can be applied to effects producing an Int.

val aspSummary =
Metric.summary("my_summary", 1.day, 100, 0.03d, Chunk(0.1, 0.5, 0.9)).contramap[Int](_.toDouble)

Now we can apply this aspect to an effect producing an Int:

val summary = nextIntBetween(100, 500) @@ aspSummary

Frequencies​

Frequencies are used to count the occurrences of distinct string values. For example an application that uses logical names for its services, the number of invocations for each service can be tracked.

Essentially, a Frequency is a set of related counters sharing the same name and tags. The counters are set apart from each other by an additional configurable tag. The values of the tag represent the observed distinct values.

To configure a frequency aspect, the name of the tag holding the distinct values must be configured.

A frequency can be created with the frequency method:

Metric.frequency(name: String): Metric.Frequency[String]

Examples​

Create a Frequency to observe the occurrences of unique Strings. It can be applied to effects producing a String.

val aspSet = Metric.frequency("my_set")

Now we can generate some keys within an effect and start counting the occurrences for each value.

val set = nextIntBetween(10, 20).map(v => s"my_key_$v") @@ aspSet