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, 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);

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';
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';
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.)

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.