Metrics

Track custom metrics in your Rust application

Encore.rs lets you declare custom metrics in your code. They're exported along with Encore's built-in metrics, displayed in the Encore Cloud dashboard, and can be sent on to third-party observability services.

See the Platform metrics documentation for integrations with services like Grafana Cloud and Datadog.

Defining metrics

Declare a metric as a module-level static, and record to it wherever the thing it measures happens. Its doc comment is its description:

orders/src/metrics.rs
use encore::metrics::{Counter, Gauge, Histogram, UpDownCounter}; /// Orders placed. static ORDERS: Counter = Counter::new("orders_placed").unit("{order}"); /// Jobs waiting for a worker. static QUEUED: UpDownCounter = UpDownCounter::new("jobs_queued").unit("{job}"); /// The temperature of the oven. static OVEN: Gauge = Gauge::new("oven_temperature").unit("Cel"); /// How long an order takes to place. static LATENCY: Histogram = Histogram::new("order_latency").unit("s"); async fn place_order(order: &Order) -> Result<(), encore::Error> { let started = std::time::Instant::now(); // ... ORDERS.increment(); LATENCY.record(started.elapsed().as_secs_f64()); Ok(()) }

Encore reads metric declarations from your source code, so a few rules apply:

  • A metric must be declared as a module-level static.
  • The name must be a string literal: a letter, then letters, digits, _, ., -, and /.
  • The options (.unit(…), .description(…), and for histograms .buckets(…)) are chained directly onto new, each at most once, with literal arguments.

The unit is exported as given. UCUM is the convention: "s", "By", "{request}". If a metric has no doc comment, give it a description with .description("…").

Metric types

TypeWhat it isRecordingValue types
CounterA total that only goes up: how many times something happened, or how much of something there has been.increment(), add(n)u64 (default), f64
UpDownCounterA total that goes up and down: requests in flight, jobs queued.increment(), decrement(), add(n)i64 (default), f64
GaugeWhat something is right now.set(v)f64 (default), i64, u64
HistogramHow recorded values are distributed: the median latency, the 99th percentile.record(v)f64

Pick a value type other than the default with the type parameter:

/// Bytes received. static RECEIVED: Counter<f64> = Counter::new("bytes_received").unit("By"); /// Connections open right now. static CONNECTIONS: Gauge<i64> = Gauge::new("connections_open");

A counter doesn't go down: add ignores an amount below zero.

Histogram buckets

A histogram counts what it records in buckets. Set their upper bounds, in increasing order, with .buckets(…). A value is counted in the first bucket whose bound it's no greater than, or in one past the last. Without .buckets, the OpenTelemetry SDK's default buckets are used.

/// How long adding a user takes. static ADD_DURATION: Histogram = Histogram::new("add_user_duration") .unit("s") .buckets(&[0.001, 0.01, 0.1, 1.0]);

Labels

A metric group records one number for each combination of its labels. Define the labels as a struct that derives Labels; each field is a label, named after the field:

use encore::metrics::{CounterGroup, Labels}; #[derive(Labels)] struct Outcome { method: &'static str, status: u16, } /// Requests handled, by method and status. static REQUESTS: CounterGroup<Outcome> = CounterGroup::new("requests_handled"); fn record(method: &'static str, status: u16) { REQUESTS.with(&Outcome { method, status }).increment(); }

Every metric type has a group: CounterGroup, UpDownCounterGroup, GaugeGroup, and HistogramGroup. with(&labels) returns the metric for those labels. Making it converts the labels to attributes, so keep the result around where you record to it often.

The label type must be named in the static's type, as in CounterGroup<Outcome>, and a group takes a value type as its second parameter: GaugeGroup<Region, i64>.

Label values can be strings (&'static str, String, Cow<str>), integers, bool, and char. To use your own type, such as an enum, implement LabelValue:

use encore::metrics::LabelValue; use encore::opentelemetry::Value; enum Plan { Free, Paid, } impl LabelValue for Plan { fn label_value(&self) -> Value { match self { Plan::Free => "free", Plan::Paid => "paid", } .into() } }
Take care

Each distinct combination of label values is a separate time series. Use labels for a small set of values, like a status or a region, never for something like a user ID. For attributes of any other shape, record with the OpenTelemetry API.

Scopes

As with secrets, the type you declare a metric with decides who records to it:

TypesScopeExported as
Counter, Gauge, …Belongs to the service that declares it, which alone records to it. Two services can each declare a metric with the same name. It must be declared inside a service.<service>.<name>, e.g. shop.orders_placed
SharedCounter, SharedGauge, …Shared between services under one name. Each service that records to it is recorded as using it.<name>
GlobalCounter, GlobalGauge, …The whole app's. Any code can record to it, including a library outside every service, and who does isn't recorded.<name>

Groups come in the same three scopes: SharedCounterGroup, GlobalHistogramGroup, and so on.

use encore::metrics::{GlobalHistogram, SharedCounterGroup}; /// Attempts to add a user, by how they went. static ADD_ATTEMPTS: SharedCounterGroup<Attempt> = SharedCounterGroup::new("add_user_attempts"); /// How long adding a user takes. static ADD_DURATION: GlobalHistogram = GlobalHistogram::new("add_user_duration").unit("s");

Recording to a metric declared elsewhere

A shared or global metric is declared once in the app. Other code in the same crate records to it by importing the declaration. From another service's crate, or a service written in another language, bind it by name with named:

use encore::metrics::CounterGroup; // Declared as a SharedCounterGroup<Attempt> in another service. static ADD_ATTEMPTS: CounterGroup<Attempt> = CounterGroup::named("add_user_attempts");

named is available on the service-scoped types (Counter::named, CounterGroup::named, and so on). The binding must be of the same kind and labels as the declaration, and it takes the declaration's scope, unit, description, and buckets, so it has no options of its own. It's exported under the name as written, never prefixed with a service name.

Passing a metric to other code

Encore sees which service records to a metric from where it's used by name. To hand a metric to a function that records to whichever metric it's given, pass .reference(), which counts as recording to it:

fn time<F: FnOnce()>(histogram: &Histogram, f: F) { let started = std::time::Instant::now(); f(); histogram.record(started.elapsed().as_secs_f64()); } time(LATENCY.reference(), || place_order_sync());

Using the OpenTelemetry API

Each Encore metric is an instrument of the OpenTelemetry Metrics API, from the meter encore. You can also create instruments with the OpenTelemetry API directly, for attributes of any value or for observable instruments. Encore's meter provider, encore::metrics::meter_provider(), exports them alongside Encore's metrics. The OpenTelemetry crate is re-exported as encore::opentelemetry.

Encore doesn't install a global meter provider. Install its provider at startup to use global::meter:

use std::sync::LazyLock; use encore::opentelemetry::{global, metrics, KeyValue}; /// Page views, by page. static PAGE_VIEWS: LazyLock<metrics::Counter<u64>> = LazyLock::new(|| { global::meter("my-app") .u64_counter("page_views") .with_unit("{view}") .build() }); #[tokio::main] async fn main() { encore::initialize(); global::set_meter_provider(encore::metrics::meter_provider()); // ... } fn viewed(page: &str) { PAGE_VIEWS.add(1, &[KeyValue::new("page", page.to_string())]); }

Or get a meter from the provider directly, and pass the provider to instrumentation libraries that take one:

use encore::opentelemetry::metrics::MeterProvider as _; let meter = encore::metrics::meter_provider().meter("my-app");

Instruments made this way aren't declared to Encore: they have no scope and aren't prefixed with a service name.

Viewing metrics

Locally, encore metrics list shows the metrics reported in the last hour, with their stats over a window, and encore metrics get <name> shows one metric's series. Add --watch to keep the output updated as new data is exported:

$ encore metrics list
$ encore metrics get shop.orders_placed --since 15m
$ encore metrics get requests_handled --attr status=500 --watch

In deployed environments, metrics are shown in the Encore Cloud dashboard. See the Platform metrics documentation.

Testing

Under encore test, as anywhere the runtime exports no metrics, recording to a metric does nothing. Calls like increment() and record() are safe to leave in code under test.