Core metrics
Esta página aún no está disponible en tu idioma.
The metrics extension exposes three classic primitives:
| Type | Direction | When |
|---|---|---|
| Counter | Monotonically increases | Total events, totals over time. |
| Gauge | Settable / inc / dec | Point-in-time values that go up and down. |
| Histogram | Distribution of observations | Latency, payload size. |
import { ActorSystem } from 'actor-ts';import { MetricsExtensionId } from 'actor-ts/metrics';
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.
Counters
Section titled “Counters”const c = metrics.counter('events_total', { source: 'web' });
c.inc(); // → +1c.inc(3); // → +3c.value; // → 4Monotonic — 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.
Gauges
Section titled “Gauges”const g = metrics.gauge('sessions_active');
g.set(100); // → 100g.inc(); // → 101g.dec(5); // → 96g.value; // → 96Settable + bidirectional. Represents a point-in-time value.
For “things you measure right now”:
- Active sessions / connections.
- Mailbox depth.
- Queue size.
- Available memory.
Histograms
Section titled “Histograms”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"} 1http_request_duration_ms_bucket{route="/orders", le="25"} 1http_request_duration_ms_bucket{route="/orders", le="50"} 2http_request_duration_ms_bucket{route="/orders", le="100"} 2http_request_duration_ms_bucket{route="/orders", le="250"} 3http_request_duration_ms_bucket{route="/orders", le="+Inf"} 3http_request_duration_ms_count{route="/orders"} 3http_request_duration_ms_sum{route="/orders"} 167Prometheus 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.
Timing operations
Section titled “Timing operations”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.
Labels
Section titled “Labels”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"} 1234events_total{source="web", env="staging"} 56events_total{source="batch", env="prod"} 89Read in Prometheus / Grafana as filters or group-by axes.
Cardinality discipline
Section titled “Cardinality discipline”// ✗ HIGH-CARDINALITY — DON'Tmetrics.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.
bucketize — bound a value at the source
Section titled “bucketize — bound a value at the source”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/metrics';
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.
The cardinality cap
Section titled “The cardinality cap”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"} 1234http_requests_total{route="/health"} 56http_requests_total{route="__overflow__"} 8_912_004The 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/metrics';
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.)
Removing a series
Section titled “Removing a series”The cap bounds how many series a family holds; remove is what
makes that a bound on how many are live:
metrics.gauge('queue_depth', { queue: 'orders' }).set(4_200);
// The queue is gone — so is its series, and so is the slot it held.metrics.remove('queue_depth', { queue: 'orders' }); // → trueIt answers true when a series was actually dropped, and false for a
family that does not exist, a tuple that was never minted, and the
overflow series — which stays, because it is the standing record that
tuples were discarded and its counter holds how many. The family
itself survives its last series, help text and kind included.
Removed, not zeroed. A gauge child holds its last value, so a tuple
that comes back is minted fresh rather than resuming at the reading it
had before. That is the difference the framework’s own
actor_mailbox_size
wanted: an actor that drains stops being reported instead of asserting
a 0 backlog for ever.
A call, not a TTL. The registry cannot tell a finished entity from a counter for something rare — a series that has not moved in an hour looks identical either way — while the code that owns the instrument knows exactly when the subject is gone. An age-based sweep would also need a clock and a timer in a primitive that has neither, and it would be actively wrong for counters: one that ages out and is minted again reads downstream as a reset that never happened. For that reason, remove a counter’s series only when what it counted is gone for good, not to reclaim cardinality; gauges carry no such history and are the intended caller.
Reading values in-process
Section titled “Reading values in-process”const counter = metrics.counter('events_total');counter.inc();counter.value; // → 1value 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; // → 2h.sum; // → 30h.buckets; // → readonly bucket bounds, ending in Infinityh.counts; // → cumulative observation count per bucketUseful for tests and custom exporters.
Where to next
Section titled “Where to next”- Observability overview — the bigger picture.
- Prometheus exporter —
expose
/metricsfor Prometheus to scrape. - Stock metrics — the framework’s auto-recorded actor/mailbox/cluster metrics.
- prom-client adapter —
for projects already using
prom-client.
The MetricsExtension
API reference covers the full surface.
