Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Choosing metric types

Picking the right type is most of what makes metrics good. This page is a decision guide plus a reference for each type Metered offers.

The decision in one paragraph

For an event you count, use a counter. For a current value, use a gauge, but if another component already owns that value, expose that value instead of a copy. For a distribution, latency in particular, use a histogram. For one-of-N state, use a StateSet. For a static fact, use info. For an extra bounded dimension, add a Family.

The same decision as a flowchart:

flowchart TD
    start{"What are you<br/>measuring?"}
    start -->|"an event happened"| counter["Counter"]
    start -->|"a current value"| owned{"who owns<br/>the value?"}
    owned -->|"this metric"| gauge["Gauge"]
    owned -->|"something else"| expose["expose that state directly<br/>(view reader / adapter)"]
    start -->|"a distribution (latency)"| hist["Histogram"]
    start -->|"one-of-N state"| stateset["StateSet"]
    start -->|"a static fact"| info["Info"]
    counter --> dim{"need a bounded<br/>extra dimension?"}
    gauge --> dim
    hist --> dim
    dim -->|"yes"| family["wrap in Family⟨L, M⟩"]
    dim -->|"no"| done["done"]

All metric types at a glance

OpenMetrics typeInstrumentNotes
counterCounter / AtomicU64Monotonic; suffix _total.
gaugeGauge view or owned Gauge/AtomicI64 fieldLive pull or cached accumulator. See Gauge below.
histogramBucketHistogram, DynamicExponentialHistogramAggregates across replicas; prefer for latencies.
summarySummary<S> over a QuantileSourcePer-instance quantile values; does not aggregate.
gaugehistogramGaugeHistogram<S> over a GaugeHistogramSource, for example GaugeBucketsCurrent-value buckets; _gcount/_gsum.
statesetStateSetMutually exclusive boolean states.
infoInfoMetricStatic metadata; value always 1.
unknownpassthrough samplesForeign metrics of unknown semantics.

Counter

For values that only increase: requests, errors, retries, bytes, dropped messages. Counter is the trait. AtomicU64 is the usual concrete storage.

#![allow(unused)]
fn main() {
use metered::Counter;
use std::sync::atomic::AtomicU64;

let processed = AtomicU64::new(0);
processed.incr();
processed.incr_by(10);
assert_eq!(processed.get(), 11);
}

Make intent obvious through the field name – attempts, failures, cache_misses – or through a domain newtype over the counter. The storage is the same AtomicU64 either way.

Do not name a counter field *_total: the encoding adds _total, so requests becomes requests_total on the wire (a requests_total field becomes requests_total_total).

Gauge

For a current value that moves up and down: in-flight work, queue depth, pool size, a flag. Gauge is the trait. Standard atomics are the usual concrete storage.

#![allow(unused)]
fn main() {
use std::sync::atomic::{AtomicI64, Ordering};

let in_flight = AtomicI64::new(0);
in_flight.fetch_add(1, Ordering::Relaxed); // a request started
in_flight.fetch_sub(1, Ordering::Relaxed); // it finished
let flag = AtomicI64::new(0);
flag.store(1, Ordering::Relaxed);          // 1 / 0
}

Your code updates plain atomics. The exposition side reads them through the Gauge trait, which the standard atomics implement. The trait also has incr/decr/set helpers if you prefer them, but nothing requires a metrics call on the update path.

Gauge vs counter is the most common mistake. “Total requests,” “failed publishes,” “retries,” and “dropped messages” are counters, not gauges – you want their rate, not their instantaneous value. “In-flight requests,” “queue depth,” “cache entries,” and “enabled?” are gauges.

Prefer existing state. If another component already owns a value, expose that value. Do not maintain a parallel gauge that can drift. See Adding Metrics and the queue module in the demo.

Live vs cached. Both forms encode as a plain gauge because OpenMetrics has no UpDownCounter type. A live gauge is a view reader like gauge_value("queue_depth").read(|state: &State| state.queue.len() as i64). Each scrape reads it fresh from your domain state, with no cache, so it is always truthful, but the read must be cheap.

A cached accumulator is an owned Gauge/AtomicI64 you bump with incr/decr. It reads O(1) at scrape. Use it when the value is cheap to maintain incrementally but expensive or impossible to observe live. There is no per-metric value cache: metered-om’s SnapshotCache bounds live-read cost at the document level.

Histogram

For distributions – almost always latency. BucketHistogram records raw values with observe. It records durations with observe_duration, which converts them to seconds, the base unit. Histogram is the trait that abstracts over the bucket and exponential backends. BucketHistogram is the classic le-bucket implementation.

#![allow(unused)]
fn main() {
use metered::{BucketHistogram, Buckets};

// Choose buckets that bracket your expected range.
let sizes = BucketHistogram::new(Buckets::exponential(64.0, 2.0, 10));
sizes.observe(512.0);
}

Bucket presets, all in seconds for durations:

  • Buckets::seconds_default() – general request latencies, 5 ms to 10 s.
  • Buckets::fast_seconds() – services below 5 ms, down to 25 µs.
  • Buckets::slow_seconds() – DB-heavy / batch work, out to a minute.
  • Buckets::wide_seconds() – fine near a microsecond, coarse near multi-second timeouts: 1 µs to about 30 s, at most 30% relative error. For fast paths whose latency spans a very wide range.
  • Buckets::relative(min, max, max_relative_error) – exponential buckets sized to a target relative resolution. You give the range and the error you tolerate, and the builder solves for the count.
  • Buckets::exponential(start, factor, n) / exponential_range(min, max, n) – the explicit-count exponential builders.

Wide dynamic range: fine low, coarse high

When you care about microsecond-scale fast paths but only need rough numbers near timeouts, use relative resolution. A bucket near value v is about v * max_relative_error wide. The absolute resolution is then automatically fine at the bottom and coarse at the top.

#![allow(unused)]
fn main() {
use metered::{BucketHistogram, Buckets};

// <=10% relative from 1µs to 30s: ~0.1µs near the floor, ~1s near a 10s timeout.
let h = BucketHistogram::new(Buckets::relative(0.000_001, 30.0, 0.10));
}

Tighter error or a wider range means more buckets. Each boundary is a stored le series per label set, so this is a dial between resolution and cardinality. About 10% error over 1 µs to 30 s is about 180 buckets, and about 30%, from wide_seconds, is about 70. There is no cheap way to get fine absolute resolution across the whole range. Linear 10 µs buckets up to 10 s would be a million series. That is exactly why exponential relative-error histogram designs exist.

The quantile values come from histogram_quantile(...) at query time, so they aggregate across replicas. That is the whole reason to prefer a histogram over a summary.

Exponential histograms

For wide dynamic ranges, Metered also provides log-linear exponential histogram backends: FixedExponentialHistogram and DynamicExponentialHistogram. They record sparse exponential bucket snapshots as ExponentialSnapshot values. metered-om can encode them either as classic cumulative le buckets or as VictoriaMetrics vmrange buckets. The full comparison – who chooses the buckets, memory, observe cost, saturation behavior – is in Histograms in Depth.

This is separate from Prometheus native histogram protobuf exposition. If a protobuf sink is worth the effort later, it can encode from the same MetricValues model. Metric ownership and observation code do not change. A BucketHistogram can also attach exemplars to buckets via observe_with_exemplar.

Summary

A Summary<S> renders each pre-computed quantile, for example name{quantile="0.99"}, plus name_sum and name_count. It reads them from any QuantileSource. Its quantile values are per-instance and do not aggregate across replicas. Prefer a histogram unless you specifically need exact per-instance quantile values or legacy dashboard parity.

#![allow(unused)]
fn main() {
use metered::summary::BucketQuantiles;
use metered::{BucketHistogram, Buckets, Summary};

let latency = BucketHistogram::new(Buckets::seconds_default());
latency.observe(0.012);
// Any `Histogram` is a `QuantileSource` via `BucketQuantiles`.
let summary = Summary::new(BucketQuantiles::new(&latency));
}

BucketQuantiles adapts any Histogram. The DynamicExponentialHistogram is the recommended source: lock-free observe, bounded memory, bounded relative error. A bespoke streaming sketch, for example CKMS, is intentionally not provided. It cannot be lock-free, and it does not beat the exponential histogram on memory. It only offers a different accuracy model, based on rank error. If a project ever needs one, it drops in as a QuantileSource implementation behind the same seam.

Gauge histogram

A GaugeHistogram<S> renders a current value distribution, buckets that can decrease, as name_bucket{le} + name_gcount + name_gsum. Use it for a live population’s size distribution, not a cumulative count of events. Implement GaugeHistogramSource for your own state, or use the provided GaugeBuckets:

#![allow(unused)]
fn main() {
use metered::{GaugeBuckets, GaugeHistogram};

let sizes = GaugeBuckets::new([1.0, 10.0, 100.0]);
sizes.enter(5.0); // an item of size 5 is now held
sizes.leave(5.0); // it was released
let gh = GaugeHistogram::new(sizes);
}

StateSet

For mutually exclusive state – a lifecycle, a mode – where exactly one member is active:

#![allow(unused)]
fn main() {
use metered::StateSet;

let lifecycle = StateSet::new(["starting", "running", "draining"]);
lifecycle.set("running");
}

It emits one series per state, the active one 1 and the rest 0, with the state carried in a label named after the metric.

Info

For static facts about the process – version, commit, region – as a constant 1 carrying labels:

#![allow(unused)]
fn main() {
use metered::InfoMetric;

let build = InfoMetric::new([("version", "0.10.0"), ("commit", "abc123")]);
}

Use it to attach build context to dashboards by joining on *_info.

Family: a bounded extra dimension

When one metric needs a label dimension – per route, per method, per upstream – wrap it in a Family. See Labels and Families for the cardinality discipline that keeps this safe.

#![allow(unused)]
fn main() {
use metered::{Counter, Family};
use std::sync::atomic::AtomicU64;

let by_route: Family<Vec<(String, String)>, AtomicU64> =
    Family::with_label_names(["route"]);
by_route.with(&vec![("route".to_owned(), "/health".to_owned())], |c| c.incr());
}

Unknown: passthrough

MetricType::Unknown exists for passthrough or foreign metrics with unknown semantics. It renders a plain sample with no suffix. You don’t construct it directly. The passthrough sources that re-emit metrics from another system use it.

Serving a foreign metrics endpoint, including Metered 0.9

metered_om::TextSourceTree re-emits any classic Prometheus/OpenMetrics text as a MetricTree. This lets you serve a foreign producer on a 0.10 /metrics endpoint during migration. The producer can be a sidecar, another exporter, or a Metered 0.9 registry’s serde_prometheus output. Nothing about it is 0.9-specific, and Metered 0.9 is just one such producer. To run 0.9 in-process, depend on the published 0.9 crate (see Migrating From Older Versions).

use metered_om::TextSourceTree;
let legacy = TextSourceTree::new(|| old_0_9_registry.to_prometheus_text());
// mount `legacy` alongside your native 0.10 trees

The passthrough is a normalizing re-encode, not a byte copy. Names, labels, and shapes survive, so a 0.9 HDR summary stays a summary and existing dashboards keep working. Value tokens re-encode from their parsed form, and the re-encode drops sample timestamps. After you migrate a metric to a native 0.10 instrument, drop it from the passthrough source.

When none of these fit

Implement Metric for a custom leaf: its type plus how it reads its value. Implement MetricTree for a custom composite. This is rare. Reach for it only when you genuinely have a new OpenMetrics shape or an unusual source for the value.