Zum Inhalt springen
Deutsch

prom-client-Adapter

Wenn deine App prom-client (die De-facto-Node-/Prometheus- Bibliothek) schon für ihre Nicht-Actor-Metriken nutzt, lässt promClientRegistry(...) die Framework-Metriken in derselben prom-client-Registry leben — ein /metrics-Endpunkt, alle Metriken zusammen.

import client from 'prom-client';
import {
ActorSystem,
} from 'actor-ts';
import {
MetricsExtensionId,
promClientRegistry,
PromClientAdapterOptions,
} from 'actor-ts/metrics';
// Die bestehende prom-client-Registry deiner App. `client.register`
// ist die globale Standard-Registry; hier nutzen wir der Klarheit
// halber eine frische Registry.
const registry = new client.Registry();
// ... registriere deine bestehenden prom-client-Metriken auf `registry` ...
const system = ActorSystem.create('my-app');
// Richte die Metrics-Extension auf eine Registry aus, die direkt in
// prom-client schreibt. Jeder Framework-Counter / jede -Gauge / jedes
// -Histogram landet ab hier in `registry` neben deinen App-Metriken.
const promAdapterOptions = PromClientAdapterOptions.create()
.withClient(client)
.withRegistry(registry)
.withNamePrefix('actor_ts_');
system.extension(MetricsExtensionId).useRegistry(
promClientRegistry(promAdapterOptions),
);
// Dein bestehender /metrics-Endpunkt liefert jetzt Framework- + App-Metriken:
get(async () => ({
status: 200,
body: await registry.metrics(),
contentType: registry.contentType,
}));

promClientRegistry(...) gibt eine MetricsRegistry zurück, die direkt durchschreibt zu prom-client — jede Mutation eines Framework-Counters, einer -Gauge oder eines -Histograms landet synchron in deiner prom-client-Registry. Es gibt keinen Hintergrund-Sync, kein Intervall und keine zweite Kopie der Werte: prom-client hält den kanonischen Zustand, und deine bestehende register.metrics()-Route liefert alles aus.

Zwei Hauptgründe:

  1. Bestehende prom-client-Nutzung — dein Code emittiert Metriken schon via prom-client; du willst keine zwei Registries pflegen.
  2. Ein Scrape-Endpunkt — deine Operatoren erwarten eine einzelne /metrics-URL, die alle deine Metriken kombiniert.

Wenn du prom-client noch nicht nutzt, bevorzuge den nativen Prometheus-Exporter des Frameworks — keine zusätzliche Dependency.

Die Bridge behält keine Kopie dessen, was sie schreibt. prom-client hält die Werte, also ist collect() auf der zurückgegebenen Registry leer — dauerhaft und absichtlich — und die Registry sagt das, indem sie collectable: false deklariert.

Für den Weg, für den es diesen Adapter gibt, ist das in Ordnung: du liest die Metriken über register.metrics(), genau wie deine eigenen. Relevant wird es für alles im Framework, das in die andere Richtung liest, über collect(). Davon gibt es zwei, und beide verweigern die Antwort, statt Null zu melden:

  • Die Management-Route GET /metrics antwortet mit 503, solange diese Bridge installiert ist, statt mit 200 und leerem Body. Ein leerer Body ist ein gültiger Prometheus-Scrape — das Target bleibt up=1, und die Serien des Frameworks hören schlicht auf zu existieren, sodass darauf geschriebene Alerts nie wieder feuern.
  • Die DevTools-Übersicht zeigt unavailable für die drei Werte, die sie aus der Registry liest: verarbeitete Nachrichten, Mailbox-Drops und Handler-Latenz. Ihr Throughput-Diagramm lässt die Linie messages / s aus demselben Grund weg und nennt sie in der Legende, statt eine flache Null zu zeichnen: Ein Diagramm überzeugt stärker als eine Kachel, und diese Linie würde genau die Behauptung aufstellen, die die Kachel daneben verweigert. Die Dead-Letter-Linie daneben wird aus dem Event-Stream gezählt und bleibt — ebenso alles andere auf dieser Seite: Es stammt aus dem Aktorenbaum und dem Event-Stream und bleibt korrekt.

Also: aktiviere enableMetricsEndpoint nicht zusammen mit dieser Bridge — scrape stattdessen deine eigene prom-client-Route, die dieselben Serien enthält. Und lies Framework-Metriken in den DevTools nur auf einem System, das die eigene Registry des Frameworks nutzt.

An den Schreibvorgängen ändert sich nichts. Jeder Counter, jede Gauge und jedes Histogram des Frameworks liegt live auf deiner prom-client-Route, einschließlich actor_mailbox_dropped_total — dem Drop-Counter, den die DevTools-Kachel dir nicht zeigen kann.

type PromClientAdapterOptionsType = {
client: PromClientLike; // der prom-client-Namespace: import client from 'prom-client'
registry: PromClientRegistryLike; // die Registry, in die publiziert wird — typischerweise client.register
namePrefix?: string; // Präfix für jeden Metriknamen — Default: ''
maxSeriesPerFamily?: number; // Kardinalitätsgrenze pro Familie — Default: 10 000, 0 schaltet ab
};

client und registry sind Pflicht — ohne sie hat die Brücke nichts, wohin sie publizieren kann. client ist der prom-client-Namespace, den du ohnehin importierst; registry ist die Registry, in die er schreibt (üblicherweise client.register, die globale Standard-Registry). Das obige Plain-Object funktioniert überall dort, wo auch der Builder funktioniert — der Builder ist der dokumentierte Standard.

namePrefix lässt dich Framework-Metriken namespacen, damit die von der Brücke stammenden Familien in der Exposition leicht zu erkennen sind:

const prefixedOptions = PromClientAdapterOptions.create()
.withClient(client)
.withRegistry(registry)
.withNamePrefix('actorts_');
promClientRegistry(prefixedOptions);
// → actorts_messages_delivered_total, actorts_members_up, ...

Die Brücke erzwingt dieselbe Grenze pro Familie wie die In-Process-Registry (Default 10 000 Label-Tupel, 0 schaltet ab) — und braucht sie dringender: prom-client legt bei jedem .labels(...)-Aufruf eine Serie in seinem eigenen Counter / Gauge / Histogram an und lässt sie nie verfallen. Ein unbeschränktes Label lässt also den Resident-Speicher deines Prozesses wachsen, nicht nur den Scrape-Body.

const cappedOptions = PromClientAdapterOptions.create()
.withClient(client)
.withRegistry(registry)
.withMaxSeriesPerFamily(50_000);
promClientRegistry(cappedOptions);

Jenseits der Grenze schreibt die Brücke das Tupel auf die Label-Namen der Familie um, alle Werte auf __overflow__ gesetzt — prom-client sieht damit höchstens maxSeriesPerFamily + 1 verschiedene Tupel pro Familie. Die deklarierten Namen wiederzuverwenden ist nicht kosmetisch: prom-client legt die Label-Namen einer Metrik bei der Konstruktion fest und wirft bei einem .labels(...) mit irgendetwas anderem — ein synthetischer Marker-Name würde also genau den Aufruf sprengen, der den Schaden begrenzen soll.

Siehe Kardinalitätsdisziplin für bucketize, den Weg, die Grenze gar nicht erst zu erreichen.

remove reicht an prom-clients eigenes Metric.remove(labels) weiter — ein Tupel, dessen Gegenstand weg ist, verlässt damit beide Seiten gleichzeitig: die Serien-Zählung der Bridge und die Exposition, die prom-client rendert. Nur eine von beiden zu treffen wäre schlimmer als gar nicht zu entfernen: ein freigegebener Platz, während die Serie weiter gescrapt wird, oder umgekehrt.

Das ist die eine Stelle, an der der Adapter mehr von prom-client braucht als die Konstruieren-und-Mutieren-Oberfläche. Der client, den du übergibst, muss also remove auf seinen Counter / Gauge / Histogram anbieten — jedes Release seit v11.2 tut das. Ein selbstgebauter Ersatz, der es weglässt, ist ein Compile-Fehler statt eines stillen Auseinanderlaufens.

Terminal-Fenster
npm install prom-client
# oder: bun add prom-client

prom-client ist Peer — nur benötigt, wenn du diesen Adapter nutzt.