Zum Inhalt springen
Deutsch

Core-Metriken

Die Metrics-Extension stellt drei klassische Primitive bereit:

TypRichtungWann
CounterMonoton steigendGesamtevents, Gesamtwerte über die Zeit.
GaugeSetzbar / inc / decWerte zu einem Zeitpunkt, die rauf und runter gehen.
HistogramVerteilung von BeobachtungenLatenz, Payload-Größe.
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);

Bis enable() gerufen wird, ist die Registry eine Noop-Implementierung — die Instrumentierung des Frameworks zeichnet also nichts auf. Genau eine Sache schaltet sie für dich ein: DevTools aktiviert die Registry, solange es attached ist — die Übersicht braucht diese Zähler — und ruft beim Detach disable(). Eine Registry, die du selbst eingeschaltet hast, wird nie hinter deinem Rücken abgeschaltet.

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

Monoton — geht nur nach oben. Negative Inkremente werfen einen Fehler. Reset beim Prozess-Neustart.

Für „Dinge, die du zählst”:

  • Empfangene Requests insgesamt.
  • Ausgelöste Fehler insgesamt.
  • Cache-Hits / -Misses insgesamt.

Für Dinge, die runtergehen (aktive Sessions sinken), nutze einen Gauge, keinen Counter.

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

Setzbar + bidirektional. Repräsentiert einen Wert zu einem Zeitpunkt.

Für „Dinge, die du gerade misst”:

  • Aktive Sessions / Verbindungen.
  • Mailbox-Tiefe.
  • Queue-Größe.
  • Verfügbarer Speicher.
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);

Ein Histogram zählt, wie viele Beobachtungen in jeden Bucket fielen. Zur Export-Zeit siehst du:

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 berechnet Perzentile (p50, p95, p99) zur Query-Zeit aus diesen Buckets.

Buckets wählen:

  • Wähle Buckets, die dein SLO abbilden. Für ein HTTP-Latenz-Histogram mit 200 ms p95-Ziel inkludiere 100, 200, 500.
  • Zweier- oder Zehnerpotenzen sind übliche Defaults — neige zu weniger Buckets im Noise-Floor und mehr rund um dein Ziel.
  • Default-Buckets (genutzt, wenn du keine spezifizierst): [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10] — Sekundenskala. Überschreibe für ms-Skala.

Es gibt kein eigenes Timer-Primitiv — miss eine Operation, indem du ihre verstrichene Dauer in ein Histogram beobachtest:

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

performance.now() ist auf Bun, Node und Deno verfügbar. Wähle Buckets, die zur beobachteten Einheit passen — hier Millisekunden.

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

Labels machen aus einer Metrik viele Zeitreihen. Zur Export-Zeit ist jede eindeutige Label-Kombination eine separate Serie:

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

In Prometheus / Grafana lesbar als Filter oder Group-by-Achsen.

// ✗ HOCH-KARDINAL — TU DAS NICHT
metrics.counter('events_total', {
requestId: req.id, // unique pro Request
userId: req.user.id, // unique pro User
});

Jede eindeutige Label-Kombination erzeugt eine Serie. Unbeschränkte Labels (Request-ID, User-ID, Zeitstempel) produzieren unbeschränkte Serien — dein Monitoring-System geht der Speicher aus.

Nur beschränkte Labels:

  • Route-Namen (/orders, /users/:id).
  • Environment / Region.
  • Status-Codes / Arten (ein paar Dutzend Werte).
  • Pod-Namen, wenn die Pod-Anzahl beschränkt ist.

Ziel: < 100 Serien pro Metrik. Darüber Alarm schlagen.

Wenn ein Label-Wert von außen kommt — ein URL-Pfad, ein Header, eine ID — bilde ihn auf eine feste Allow-List ab, bevor er die Registry erreicht:

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) gibt value zurück, wenn es in allowed steht, sonst 'other' — die Familie kann also nie mehr als allowed.length + 1 Serien halten, egal was ein Angreifer schickt.

bucketize ist die eigentliche Lösung; die Grenze in der Registry ist der Auffangnetz für die Labels, die niemand beschränkt hat. Eine Familie hört auf, neue Serien anzulegen, sobald sie maxSeriesPerFamily davon hält (Default 10 000), und faltet alles darüber in eine einzige Overflow-Serie:

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

Die erste Familie, die die Grenze erreicht, loggt eine Warnung mit Familienname und dem übergelaufenen Tupel — eine Warnung pro Familie, nicht pro Aufruf. Der Marker steckt im Label-Wert, nicht in einem Label-Namen: Prometheus reserviert Label-Namen mit __-Präfix und entfernt sie beim Ingest, was die Overflow-Serie still in eine echte Serie mischen würde.

Grenze beim Installieren der Registry anheben oder abschalten:

import { MetricsExtensionId, MetricsRegistryOptions } from 'actor-ts/metrics';
const metricsOptions = MetricsRegistryOptions.create().withMaxSeriesPerFamily(50_000);
const metrics = system.extension(MetricsExtensionId).enable(metricsOptions);
// oder als einfaches Objekt: .enable({ maxSeriesPerFamily: 50_000 })

0 schaltet die Grenze komplett ab — tu das nur, wenn jeder Label-Wert nachweislich aus einer beschränkten Menge stammt. (0, nicht Infinity: die Grenze ist eine Ganzzahl-Anzahl, und Infinity ist keine Ganzzahl.)

Die Grenze beschränkt, wie viele Serien eine Familie hält; remove macht daraus eine Schranke dafür, wie viele lebendig sind:

metrics.gauge('queue_depth', { queue: 'orders' }).set(4_200);
// Die Queue ist weg — ihre Serie auch, und der Platz, den sie belegte.
metrics.remove('queue_depth', { queue: 'orders' }); // → true

Es antwortet true, wenn tatsächlich eine Serie entfernt wurde, und false für eine Familie, die es nicht gibt, für ein Tupel, das nie erzeugt wurde, und für die Overflow-Serie — die bleibt, weil sie der stehende Nachweis dafür ist, dass Tupel verworfen wurden, und ihr Counter hält, wie viele. Die Familie selbst überlebt ihre letzte Serie, samt Hilfetext und Art.

Entfernt, nicht genullt. Ein Gauge-Child hält seinen letzten Wert, ein zurückkehrendes Tupel wird also frisch erzeugt und setzt nicht bei dem Wert fort, den es vorher hatte. Genau das wollte das framework-eigene actor_mailbox_size: ein Actor, der abarbeitet, hört auf zu melden, statt für immer einen Rückstau von 0 zu behaupten.

Ein Aufruf, kein TTL. Die Registry kann eine fertige Entity nicht von einem Counter für etwas Seltenes unterscheiden — eine Serie, die sich eine Stunde nicht bewegt hat, sieht in beiden Fällen gleich aus —, während der Code, dem das Instrument gehört, genau weiß, wann sein Gegenstand weg ist. Ein altersbasierter Durchlauf bräuchte außerdem Uhr und Timer in einem Primitiv, das beides nicht hat, und wäre für Counter schlicht falsch: einer, der herausaltert und neu erzeugt wird, liest sich downstream als ein Reset, den es nie gab. Entferne die Serie eines Counters deshalb nur, wenn das Gezählte endgültig weg ist, nicht um Kardinalität zurückzugewinnen; Gauges haben keine solche Historie und sind der vorgesehene Aufrufer.

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

value ist der aktuelle Counter-/Gauge-Wert — eine readonly-Property, keine Methode. Für Histogramme lies count, sum, buckets und counts direkt:

const h = metrics.histogram('latency');
h.observe(10);
h.observe(20);
h.count; // → 2
h.sum; // → 30
h.buckets; // → readonly Bucket-Grenzen, endend mit Infinity
h.counts; // → kumulative Beobachtungsanzahl pro Bucket

Nützlich für Tests und eigene Exporter.

Die MetricsExtension API-Referenz deckt die volle Oberfläche ab.