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

OpenMetrics exposition

The OpenMetrics text exposition lives in its own crate, metered-om. The core metered crate only describes a MetricSchema and collects MetricValues through the MetricSink trait. A service depends on a sink crate and chooses it at the exposition site. Add both crates:

[dependencies]
metered = "0.10.0-rc.1"
metered-om = "0.10.0-rc.1"

Registry composes metric trees. The OpenMetricsRegistryExt trait adds the encode_to_string convenience:

#![allow(unused)]
fn main() {
use metered::entry::counter;
use metered::{Counter, Registry};
use metered_om::OpenMetricsRegistryExt;
use std::sync::atomic::AtomicU64;

let requests = AtomicU64::new(0);
requests.incr();

let mut registry = Registry::with_prefix("demo");
registry.label("service", "api");
registry.register(counter("requests").source(&requests).help("Total requests handled"));

let text = registry.encode_to_string().unwrap();
assert!(text.contains("# HELP demo_requests Total requests handled"));
assert!(text.contains("demo_requests_total{service=\"api\"} 1"));
}

For reusable buffers or streaming responses, drive the encoder (a MetricSink) over the schema/values directly:

#![allow(unused)]
fn main() {
use metered_om::OpenMetricsEncoder;

let schema = registry.schema();
let values = registry.values();

let mut text = String::new();
let mut encoder = OpenMetricsEncoder::new(&mut text);
encoder.encode_document(&schema, &values).unwrap();
encoder.finish().unwrap();
}

The encoder keys HELP and UNIT metadata to the exact metric family. Metadata registered for a composite tree does not leak onto its child families.

VictoriaMetrics vmrange buckets

The encoder receives each histogram whole, so the sink chooses the bucket rendering. The default is the classic cumulative le form. Switch the encoder to vmrange for VictoriaMetrics, an extension of the same OpenMetrics text format. Exponential histograms emit vmrange natively, as non-cumulative lo...hi ranges. Classic bucket histograms fall back to le:

#![allow(unused)]
fn main() {
use metered_om::{HistogramProfile, OpenMetricsEncoder};

let mut text = String::new();
let mut encoder =
    OpenMetricsEncoder::new(&mut text).histogram_profile(HistogramProfile::VmRange);
registry.encode(&mut encoder).unwrap();
encoder.finish().unwrap();
// latency_seconds_bucket{vmrange="5.000e-3...1.000e-2"} 3
}

Rendering large metric sets incrementally

encode_document renders a whole document in one synchronous call. For very large metric sets, OpenMetricsRender writes the same document in budget-bounded steps so you can yield between chunks. An item is a family declaration or a sample line. Each step emits at most budget items:

#![allow(unused)]
fn main() {
use metered_om::{OpenMetricsRender, RenderProgress};

let schema = registry.schema();
let values = registry.values();

let mut render = OpenMetricsRender::new(&schema, &values);
let mut text = String::new();
while render.step(&mut text, 256).unwrap() == RenderProgress::Pending {
    // hand control back to your loop/runtime between chunks
}
assert!(text.trim_end().ends_with("# EOF"));
}

The stepper is not async, so it works anywhere. Async callers can use RenderFuture, a dependency-free Future that writes one budget chunk per poll and yields back to the executor while more work remains:

#![allow(unused)]
fn main() {
async fn scrape(schema: &metered::MetricSchema, values: &metered::MetricValues) {
use metered_om::RenderFuture;

let text = RenderFuture::new(schema, values, 256).await.unwrap();
let _ = text;
}
}

For tests and tooling, parse text exposition back into a structural model:

#![allow(unused)]
fn main() {
use metered_om::OpenMetricsDocument;

let doc = OpenMetricsDocument::parse(&text).unwrap();
let requests = doc.family("demo_requests").unwrap();
assert_eq!(requests.help.as_deref(), Some("Total requests handled"));
}

When metrics already live inside an app context, use MetricTreeView<C>. It stores selector closures rather than metric references:

#![allow(unused)]
fn main() {
use metered::entry::counter;
use metered::MetricTreeView;
use metered_om::OpenMetricsViewExt;
use std::sync::atomic::AtomicU64;

struct App {
    requests: AtomicU64,
}

let app = App { requests: AtomicU64::new(0) };
let mut view = MetricTreeView::with_prefix("demo");
view.register(counter("requests").select(|app: &App| &app.requests).help("Total requests"));

let text = view.encode_to_string(&app).unwrap();
}

This keeps registry composition free of shared ownership: the app/context owns the metrics, and the view only describes how to borrow them.

For existing non-metric state, register a direct reader:

#![allow(unused)]
fn main() {
use metered::entry::gauge_value;
use metered::MetricTreeView;
struct App { enabled: bool }
let app = App { enabled: true };
let mut view = MetricTreeView::with_prefix("demo");
view.register(
    gauge_value("enabled")
        .read(|app: &App| app.enabled as i64)
        .help("Whether the app is enabled"),
);
}

Adapting plain state with Registry

On the borrowed Registry path, the adapter metrics expose state that is not itself a metric: an AtomicBool, a queue length, a running total. The adapter reads the state at encode time, so the exported value is always live:

#![allow(unused)]
fn main() {
use std::sync::atomic::{AtomicBool, Ordering};
use metered::adapter::{flag, CounterFn};
use metered::entry::metric;
use metered::Registry;
use metered_om::OpenMetricsRegistryExt;

let enabled = AtomicBool::new(true);
let processed = std::sync::atomic::AtomicU64::new(7);

let flag_metric = flag(|| enabled.load(Ordering::Relaxed));     // gauge 0/1
let processed_metric = CounterFn(|| processed.load(Ordering::Relaxed));

let mut registry = Registry::new();
registry.register(metric("enabled").source(&flag_metric).help("Enabled"));
registry.register(metric("processed").source(&processed_metric).help("Processed"));
let text = registry.encode_to_string().unwrap();
}

When you own the type, prefer to implement Metric for it, so the type and its value live in one place. The adapter metrics are for state you only want to read.

Serving foreign Prometheus text

Migrations are rarely all-or-nothing. Part of a process often still produces classic Prometheus text: an older metrics stack, a sidecar, a library you do not own. TextSourceTree keeps those metrics on the same scrape endpoint as your native metered trees. It is a MetricTree that parses its source’s Prometheus text and re-emits the samples. It changes no name, label, or value, so existing dashboards keep working unmodified while you migrate one subsystem at a time.

The parser is lenient by design: it accepts the dialect that serde_prometheus and similar producers emit. That dialect is looser than OpenMetrics: metadata lines are optional, the parser tolerates spaces around = inside label braces, and a counter can lack the _total suffix. Mount one TextSourceTree beside your native trees:

#![allow(unused)]
fn main() {
use metered::entry::{counter, metric};
use metered::{Counter, Registry};
use metered_om::prom_text::TextSourceTree;
use metered_om::OpenMetricsRegistryExt;
use std::sync::atomic::AtomicU64;

// A native metered counter...
let requests = AtomicU64::new(0);
requests.incr();

// ...beside a foreign producer that already emits classic Prometheus text.
let legacy = TextSourceTree::new(|| {
    "legacy_hit_count{method=\"GetOrder\"} 42\n\
     legacy_response_seconds{method=\"GetOrder\",quantile=\"0.95\"} 0.250\n"
        .to_owned()
});

let mut registry = Registry::with_prefix("demo");
registry.register(counter("requests").source(&requests).help("Native requests"));
registry.register(metric("legacy").source(&legacy));

let text = registry.encode_to_string().unwrap();
// Native metrics carry the registry prefix...
assert!(text.contains("demo_requests_total 1"));
// ...while foreign samples are re-emitted exactly as parsed: no `demo_` prefix,
// no `_total` normalization, no invented `# TYPE` line.
assert!(text.contains("legacy_hit_count{method=\"GetOrder\"} 42"));
}

The tree calls the closure passed to TextSourceTree::new on every scrape, so the exported values are always live. The tree emits each sample with the same name, the same labels, and the same integral or float rendering as the source. Only the label form changes: the output uses the canonical OpenMetrics form, k="v" with no spaces. The bytes can differ, but the samples stay the same. Foreign samples are deliberately untyped: classic Prometheus text carries no # TYPE metadata, and an invented one would change the exposition. For that reason the tree’s mount name does not prefix foreign samples, and the encoder writes no type line for them.

A scrape must never fail because a foreign source glitched. The parser drops each line that it cannot parse. The good lines survive, and the endpoint still returns 200.

If the foreign source needs its own per-scrape maintenance, for example a swap of an interval histogram, attach that work with with_housekeep. The tree’s housekeep drives the hook once per scrape cycle:

#![allow(unused)]
fn main() {
use metered_om::prom_text::TextSourceTree;

let legacy = TextSourceTree::new(|| produce_legacy_text())
    .with_housekeep(|| swap_interval_histograms());
let _ = legacy;
fn produce_legacy_text() -> String { String::new() }
fn swap_interval_histograms() {}
}

Use TextSourceTree only during a migration. When a subsystem moves to native metered families, drop the TextSourceTree. Expose the families directly so they carry full schema metadata. If you only need the parsed samples, for a test or a one-off transform, parse_prometheus_text returns them as RawSamples without the MetricTree wrapper.

Parsing exposition back

For tests and tooling, parse OpenMetrics text into a structural model rather than matching strings:

#![allow(unused)]
fn main() {
let text = "# TYPE demo_requests counter\ndemo_requests_total 1\n# EOF\n";
use metered_om::OpenMetricsDocument;

let doc = OpenMetricsDocument::parse(text).unwrap();
assert_eq!(doc.families.len(), 1);
assert_eq!(doc.sample("demo_requests_total").map(|s| s.value.as_str()), Some("1"));
}