跳转到内容
简体中文

prom-client adapter

此内容尚不支持你的语言。

If your app already uses prom-client (the de-facto Node / Prometheus library) for its non-actor metrics, promClientRegistry(...) lets the framework’s metrics live in the same prom-client registry — one /metrics endpoint, all metrics together.

import client from 'prom-client';
import {
ActorSystem,
} from 'actor-ts';
import {
MetricsExtensionId,
promClientRegistry,
PromClientAdapterOptions,
} from 'actor-ts/metrics';
// Your app's existing prom-client registry. `client.register` is
// the default global one; here we use a fresh Registry for clarity.
const registry = new client.Registry();
// ... register your existing prom-client metrics on `registry` ...
const system = ActorSystem.create('my-app');
// Point the metrics extension at a registry that writes straight
// into prom-client. Every framework counter / gauge / histogram
// from here on lands in `registry` alongside your app metrics.
const promAdapterOptions = PromClientAdapterOptions.create()
.withClient(client)
.withRegistry(registry)
.withNamePrefix('actor_ts_');
system.extension(MetricsExtensionId).useRegistry(
promClientRegistry(promAdapterOptions),
);
// Your existing /metrics endpoint now emits framework + app metrics:
get(async () => ({
status: 200,
body: await registry.metrics(),
contentType: registry.contentType,
}));

promClientRegistry(...) returns a MetricsRegistry that writes straight through to prom-client — every framework counter, gauge, and histogram mutation lands in your prom-client registry synchronously. There’s no background sync, no interval, and no second copy of the values: prom-client holds the canonical state, and your existing register.metrics() route emits everything.

Two main reasons:

  1. Existing prom-client usage — your code has been emitting metrics via prom-client; you don’t want to maintain two registries.
  2. One scrape endpoint — your operators expect a single /metrics URL combining all your metrics.

If you don’t already use prom-client, prefer the framework’s native Prometheus exporter — no extra dependency.

The bridge keeps no copy of what it writes. prom-client holds the values, so collect() on the returned registry is empty — permanently, by design — and the registry says so by declaring collectable: false.

That is fine for the path this adapter exists for: you read the metrics through register.metrics(), exactly as you read your own. It matters for anything in the framework that reads the other way, through collect(). There are two, and both refuse rather than report zero:

  • The management GET /metrics route answers 503 while this bridge is installed, instead of 200 with an empty body. An empty body is a valid Prometheus scrape — the target stays up=1 and the framework’s series simply stop existing, so alerts written over them never fire again.
  • The DevTools overview shows unavailable for the three figures it reads from the registry — processed messages, mailbox drops and handler latency. Its Throughput chart leaves the messages / s line out for the same reason and names it in the legend, rather than drawing a flat line at zero: a chart is more persuasive than a tile, and that line would be making exactly the claim the tile beside it refuses to make. The dead-letter line next to it is counted off the event stream and stays, as does everything else on the page — it comes from the actor tree and the event stream and stays correct.

So: don’t enable enableMetricsEndpoint alongside this bridge — scrape your own prom-client route instead, which has all the same series. And read framework metrics in DevTools only on a system using the framework’s own registry.

Nothing about the writes is affected. Every framework counter, gauge and histogram is on your prom-client route, live, including actor_mailbox_dropped_total — the drop counter the DevTools tile cannot show you.

type PromClientAdapterOptionsType = {
client: PromClientLike; // the prom-client namespace: import client from 'prom-client'
registry: PromClientRegistryLike; // the Registry to publish into — typically client.register
namePrefix?: string; // prefix applied to every metric name — default: ''
maxSeriesPerFamily?: number; // per-family cardinality cap — default: 10 000, 0 disables
};

client and registry are mandatory — the bridge has nothing to publish into without them. client is the prom-client namespace you already import; registry is the Registry it writes into (usually client.register, the default global registry). The plain object above works anywhere the builder does — the builder is the documented default.

namePrefix lets you namespace framework metrics so the bridge-sourced families are easy to spot in the exposition:

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

The bridge enforces the same per-family cap as the in-process registry (default 10 000 label tuples, 0 disables), and it needs to more urgently: prom-client mints a series inside its own Counter / Gauge / Histogram on every .labels(...) call and never expires one, so an unbounded label grows your process’s resident memory, not just the scrape body.

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

Past the cap the bridge rewrites the tuple to the family’s own label names with every value set to __overflow__, so prom-client sees at most maxSeriesPerFamily + 1 distinct tuples per family. Reusing the declared names is not cosmetic: prom-client fixes a metric’s label names at construction and throws on a .labels(...) carrying anything else, so a synthetic marker name would fail the very call meant to contain the damage.

See cardinality discipline for bucketize, the way to not reach the cap in the first place.

remove forwards to prom-client’s own Metric.remove(labels), so a tuple whose subject is gone leaves both sides at once — the bridge’s cap tally and the exposition prom-client renders. Dropping only one of the two would be worse than not removing at all: a freed slot with the series still being scraped, or the reverse.

That is the one place the adapter needs more of prom-client than the construct-and-mutate surface, so the client you pass in must expose remove on its Counter / Gauge / Histogram — every release since v11.2 does. A hand-rolled stand-in that omits it is a compile error rather than a silent divergence.

Terminal window
npm install prom-client
# or: bun add prom-client

prom-client is a peer — only required if you use this adapter.