Перейти к содержимому
Русский

Core metrics

Это содержимое пока не доступно на вашем языке.

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

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

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/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.)

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' }); // → true

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

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.