# 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](/docs/platform/observability/metrics) 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:

```rust
-- 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](https://ucum.org/ucum) is the convention: `"s"`, `"By"`, `"{request}"`. If a metric has no doc comment, give it a description with `.description("…")`.

## Metric types

| Type | What it is | Recording | Value types |
|---|---|---|---|
| `Counter` | A total that only goes up: how many times something happened, or how much of something there has been. | `increment()`, `add(n)` | `u64` (default), `f64` |
| `UpDownCounter` | A total that goes up and down: requests in flight, jobs queued. | `increment()`, `decrement()`, `add(n)` | `i64` (default), `f64` |
| `Gauge` | What something is right now. | `set(v)` | `f64` (default), `i64`, `u64` |
| `Histogram` | How 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:

```rust
/// 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.

```rust
/// 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:

```rust
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`:

```rust
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()
    }
}
```

> **Important**
>
> 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](#using-the-opentelemetry-api).

## Scopes

As with [secrets](/docs/rs/primitives/secrets#scopes), the type you declare a metric with decides who records to it:

| Types | Scope | Exported 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.

```rust
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`:

```rust
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:

```rust
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](https://docs.rs/opentelemetry/0.29/opentelemetry/metrics/), 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`:

```rust
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:

```rust
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:

```shell
$ 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](/docs/platform/observability/metrics).

## 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.
