Aller au contenu
Français

Core metrics

Ce contenu n’est pas encore disponible dans votre langue.

The metrics extension exposes three classic primitives:

TypeDirectionWhen
CounterMonotonically increasesTotal events, totals over time.
GaugeSettable / inc / decPoint-in-time values that go up and down.
HistogramDistribution of observationsLatency, payload size.
import { ActorSystem, MetricsExtensionId } from 'actor-ts';
const metrics = system.extension(MetricsExtensionId).enable();
const requests = metrics.counter('http_requests_total', { route: '/orders' });
const active = metrics.gauge('sessions_active');
const latency = metrics.histogram('http_request_duration_ms', { route: '/orders' });
requests.inc();
active.set(123);
latency.observe(42);

Until enable() is called the registry is a noop, so the framework’s own instrumentation records nothing. One thing switches it on for you: DevTools enables the registry while it is attached — it needs those counters for the overview — and calls disable() on detach. A registry you enabled yourself is never disabled behind your back.

const c = metrics.counter('events_total', { source: 'web' });
c.inc(); // → +1
c.inc(3); // → +3
c.value; // → 4

Monotonic — only goes up. Negative increments throw. Reset on process restart.

For “things you count”:

  • Total requests received.
  • Total errors emitted.
  • Total cache hits / misses.

For things that go down (active sessions decreasing), use a gauge, not a counter.

const g = metrics.gauge('sessions_active');
g.set(100); // → 100
g.inc(); // → 101
g.dec(5); // → 96
g.value; // → 96

Settable + bidirectional. Represents a point-in-time value.

For “things you measure right now”:

  • Active sessions / connections.
  • Mailbox depth.
  • Queue size.
  • Available memory.
const h = metrics.histogram('http_request_duration_ms', { route: '/orders' }, {
buckets: [10, 25, 50, 100, 250, 500, 1000, 2500, 5000],
});
h.observe(42);
h.observe(118);
h.observe(7);

A histogram counts how many observations fell into each bucket. At export time, you see:

http_request_duration_ms_bucket{route="/orders", le="10"} 1
http_request_duration_ms_bucket{route="/orders", le="25"} 1
http_request_duration_ms_bucket{route="/orders", le="50"} 2
http_request_duration_ms_bucket{route="/orders", le="100"} 2
http_request_duration_ms_bucket{route="/orders", le="250"} 3
http_request_duration_ms_bucket{route="/orders", le="+Inf"} 3
http_request_duration_ms_count{route="/orders"} 3
http_request_duration_ms_sum{route="/orders"} 167

Prometheus computes percentiles (p50, p95, p99) at query time from these buckets.

Picking buckets:

  • Pick buckets that capture your SLO. For an HTTP latency histogram with a 200ms p95 target, include 100, 200, 500.
  • Powers of 2 or 10 are common defaults — bias toward fewer buckets in the noise floor, more around your target.
  • Default buckets (used if you don’t specify): [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10] — seconds-scale. Override for ms-scale.

There is no dedicated timer primitive — time an operation by observing its elapsed duration into a histogram:

const timer = metrics.histogram('db_query_duration_ms', { table: 'users' });
const start = performance.now();
await runQuery();
timer.observe(performance.now() - start);

performance.now() is available on Bun, Node, and Deno. Pick buckets that match the unit you observe — milliseconds, here.

metrics.counter('events_total', { source: 'web', env: 'prod' });

Labels turn one metric into many time-series. At export time, each unique label combination is a separate series:

events_total{source="web", env="prod"} 1234
events_total{source="web", env="staging"} 56
events_total{source="batch", env="prod"} 89

Read in Prometheus / Grafana as filters or group-by axes.

// ✗ HIGH-CARDINALITY — DON'T
metrics.counter('events_total', {
requestId: req.id, // unique per request
userId: req.user.id, // unique per user
});

Every unique label combination creates a series. Unbounded labels (request id, user id, timestamps) produce unbounded series — your monitoring system runs out of memory.

Bounded labels only:

  • Route names (/orders, /users/:id).
  • Environment / region.
  • Status codes / kinds (a few dozen values).
  • Pod names if the pod count is bounded.

Aim for < 100 series per metric. Above that, alarm.

When a label value comes from outside — a URL path, a header, an id — map it onto a fixed allow-list before it reaches the registry:

import { bucketize } from 'actor-ts';
const ALLOWED_ROUTES = ['/orders', '/users/:id', '/health'] as const;
metrics.counter('http_requests_total', {
route: bucketize(routeTemplateOf(request), ALLOWED_ROUTES),
}).inc();

bucketize(value, allowed) returns value when it is one of allowed, and 'other' otherwise — so the family can never hold more series than allowed.length + 1, no matter what an attacker sends.

bucketize is the fix; the registry’s cap is the backstop for the labels nobody bounded. A family stops minting new series once it holds maxSeriesPerFamily of them (default 10 000) and folds everything past that into one overflow series:

http_requests_total{route="/orders"} 1234
http_requests_total{route="/health"} 56
http_requests_total{route="__overflow__"} 8_912_004

The first family to hit the cap logs a warning naming the family and the tuple that overflowed — one warning per family, not per call. The marker lives in the label value, not in a label name: Prometheus reserves __-prefixed label names and strips them at ingestion, which would silently merge the overflow series into a real one.

Raise or disable the cap when the registry is installed:

import { MetricsExtensionId, MetricsRegistryOptions } from 'actor-ts';
const metricsOptions = MetricsRegistryOptions.create().withMaxSeriesPerFamily(50_000);
const metrics = system.extension(MetricsExtensionId).enable(metricsOptions);
// or as a plain object: .enable({ maxSeriesPerFamily: 50_000 })

0 disables the cap entirely — only do that when every label value provably comes from a bounded set. (0, not Infinity: the cap is an integer count, and Infinity is not an integer.)

const counter = metrics.counter('events_total');
counter.inc();
counter.value; // → 1

value is the current counter / gauge value — a readonly property, not a method. For histograms, read count, sum, buckets, and counts directly:

const h = metrics.histogram('latency');
h.observe(10);
h.observe(20);
h.count; // → 2
h.sum; // → 30
h.buckets; // → readonly bucket bounds, ending in Infinity
h.counts; // → cumulative observation count per bucket

Useful for tests and custom exporters.

The MetricsExtension API reference covers the full surface.