Core metrics
このコンテンツはまだ日本語訳がありません。
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, 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.
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';
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';
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.)
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.
