Metric catalog
Every metric below is emitted through the global OpenTelemetry meter named forze,
by an instrument_* call you make once at assembly. Nothing appears until you make that
call — the instrumentation surface is opt-in per plane. Point an SDK at a collector with
bootstrap_telemetry and see
them in Grafana with the stack recipe.
Names are stable. This table is checked against the constants in src/ by a unit
test, in both directions: a metric that exists but is not documented fails, and so does a
documented metric that no longer exists.
Conventions that hold throughout:
- Unit
1means a plain count.msis milliseconds,sseconds. - Observable instruments are sampled by the SDK at collection time from a live object
(pool, keyring, cache, mailbox). They are cumulative per process, so a restart resets
them —
rate()handles that correctly only if each process carries a distinctservice.instance.id, which is whybootstrap_telemetrymints one by default. tenant_idis never a metric label, anywhere, by design. Per-tenant questions are answered from traces and logs, which both carry it. See the cardinality doctrine.
Operations¶
instrument_operations(registry) — the outermost middleware on every operation.
| Metric | Instrument | Unit | Labels | Notes |
|---|---|---|---|---|
forze.operations |
counter | 1 | forze.operation, forze.operation.kind, forze.outcome |
kind is command / query; outcome is success / failed / error |
forze.operation.duration |
histogram | ms | same as above | ms-ladder buckets installed by bootstrap_telemetry |
The distinction that matters: outcome="failed" is a client-class domain failure
(validation, not-found, conflict, precondition — a 4xx the caller may well handle) and
leaves the span clean. outcome="error" is a genuine fault. Alert on error, chart both.
Resilience¶
instrument_resilience(executor) — independent of the tracing gate; a production process
with tracing off still reports these.
| Metric | Instrument | Unit | Labels | Notes |
|---|---|---|---|---|
forze.resilience.events |
counter | 1 | forze.event, forze.policy, forze.route |
retries, timeouts, rate-limit/bulkhead rejections, budget exhaustion, breaker transitions |
forze.resilience.breaker.state |
gauge | 1 | forze.policy, forze.route |
0 closed, 1 half-open, 2 open. A breaker that never tripped reports nothing — closed by absence |
forze.resilience.bulkhead.queue_depth |
observable gauge | 1 | forze.policy, forze.route |
calls queued behind the semaphore |
forze.resilience.bulkhead.limit |
observable gauge | 1 | forze.policy, forze.route |
current AIMD concurrency limit |
forze.resilience.hedge.delay |
observable gauge | s | forze.policy, forze.route |
effective adaptive hedge delay (windowed P² estimate) |
breaker_open in the events counter covers both the open transition and every admission
rejected while open, so its rate tracks shed load rather than flap count.
Tenant pools¶
instrument_tenant_pools({"postgres": client, ...}) — one entry per routed client.
| Metric | Instrument | Unit | Labels | Notes |
|---|---|---|---|---|
forze.tenancy.pool.size |
observable gauge | 1 | forze.client |
live tenant pools |
forze.tenancy.pool.capacity |
observable gauge | 1 | forze.client |
max_cached_tenants |
forze.tenancy.pool.created |
observable counter | 1 | forze.client |
cumulative pool creations |
forze.tenancy.pool.disposed |
observable counter | 1 | forze.client |
cumulative pool disposals |
forze.tenancy.pool.evicted_explicit |
observable counter | 1 | forze.client |
explicit evictions (rotation signals) |
The alert that matters: a sustained created rate while size == capacity is LRU
thrash — hot tenants' pools evicted by cold one-off traffic, each rebuild paying full
connection establishment.
Crypto and KMS¶
instrument_crypto({"default": keyring, ...}).
| Metric | Instrument | Unit | Labels | Notes |
|---|---|---|---|---|
forze.crypto.data_keys.generated |
observable counter | 1 | forze.keyring |
KMS round-trips on the encrypt path |
forze.crypto.data_keys.unwrapped |
observable counter | 1 | forze.keyring |
KMS round-trips on the decrypt path |
forze.crypto.cache.hits |
observable counter | 1 | forze.keyring, forze.crypto.path |
path is encrypt / decrypt; data keys reused without a KMS call |
forze.crypto.cold_miss |
observable counter | 1 | forze.keyring |
synchronous crypt that hit a cold cache and raised cipher_not_warm |
Hit ratio is hits / (hits + the matching generated|unwrapped) — composed at query time,
never precomputed, so it aggregates correctly across processes.
The alert that matters: cold_miss should sit at ~0. A sustained rate means a
read/write path is skipping warm / ensure_unwrapped.
Document L1 cache¶
instrument_document_l1() — reads a process-wide registry of live L1 stores.
| Metric | Instrument | Unit | Labels | Notes |
|---|---|---|---|---|
forze.cache.l1.size |
observable gauge | 1 | forze.document |
live entries |
forze.cache.l1.capacity |
observable gauge | 1 | forze.document |
configured capacity |
forze.cache.l1.hits |
observable counter | 1 | forze.document |
cumulative hits |
forze.cache.l1.misses |
observable counter | 1 | forze.document |
cumulative misses |
forze.cache.l1.evictions |
observable counter | 1 | forze.document |
includes rejected admissions |
Sustained evictions while size == capacity is the scan-pollution signature that
justifies W-TinyLFU (L1Spec(store_factory=tiny_lfu_l1_store)) or a bigger capacity.
Durable execution¶
DurableTelemetry.create(), wired into the durable runner.
| Metric | Instrument | Unit | Labels | Notes |
|---|---|---|---|---|
forze.durable.runs |
counter | 1 | forze.durable.name, forze.durable.outcome |
outcome is completed / failed / retrying / forward_incomplete / cancelled / timed_out / reclaimed / interrupted / unrecorded — alert on failed, not on the total: cancelled means somebody pressed Stop, and retrying is an attempt that failed and sent its run back for another under the runner's retry strategy. unrecorded describes the attempt, not the run: the body finished and its terminal write went unacknowledged, so the row is either already terminal (the write committed, the ack was lost) or still RUNNING for recovery — the worker cannot tell. A sustained rate of it means the run store is unreachable. interrupted is the neighbouring case where the body never finished at all (drain, shutdown) and no terminal state was written — expected during a deploy |
forze.durable.run.duration |
histogram | ms | forze.durable.name, forze.durable.outcome |
ms-ladder buckets installed by bootstrap_telemetry |
forze.durable.recovered |
counter | 1 | — | runs a recovery sweep claimed: reclaimed from a process that died holding them, or simply due (enqueued, back for a retry, requeued) |
forze.durable.schedule.fires |
counter | 1 | forze.durable.name |
one per schedule fire |
A recovered spike means runs are being reclaimed from processes that died holding them —
normal in small numbers after a deploy, a signal when it is sustained.
The counter cannot tell those from runs that were simply due, so enqueued runs, retries
under a retry strategy and requeues set its baseline: alert above it.
Leadership¶
instrument_leadership() — reads every leadership_lifecycle_step in the process.
| Metric | Instrument | Unit | Labels | Notes |
|---|---|---|---|---|
forze.leadership.held |
observable gauge | 1 | forze.leadership.key |
1 while this process holds the lease, through its term's grace and until the release |
forze.leadership.terms_ended |
observable counter | 1 | forze.leadership.key, forze.leadership.reason |
reason is lost / expiring / stopped / yielded / returned / raised / unclaimed |
Summed across processes, held should be 1 per key. A sustained rate of lost or
expiring means the lock backend is slow or unreachable from the leader; raised is the
body's own failure.
Job progress¶
instrument_job_staleness(monitor) — see Operation
progress.
| Metric | Instrument | Unit | Labels | Notes |
|---|---|---|---|---|
forze.jobs.stalled |
observable gauge | 1 | forze.job.kind |
started, unfinished, silent past the window |
forze.jobs.stalled.oldest_silence |
observable gauge | s | forze.job.kind |
seconds since the quietest stuck job reported; computed at scrape |
forze.jobs.staleness.scan_age |
observable gauge | s | — | seconds since the last successful sweep; -1 before the first |
Undeclared kinds land in the __other__ bucket rather than vanishing, so a kind added
later is still counted.
Alarm on scan_age too, not only on stalled. The counts are a cache: if the sweep
loop dies they freeze at their last value — almost always zero — and a rule watching only
the count goes green at the exact moment it stops knowing anything.
Realtime gateway¶
instrument_realtime_gateway(stats) — the live-emit path. All labelled
forze.realtime.channel.
| Metric | Instrument | Unit | Notes |
|---|---|---|---|
forze.realtime.gateway.emitted |
observable counter | 1 | frames delivered to Socket.IO |
forze.realtime.gateway.emit_failed |
observable counter | 1 | sio.emit raised, including emit-timeout expiries |
forze.realtime.gateway.presence_skipped |
observable counter | 1 | empty principal room; recoverable via the mailbox |
forze.realtime.gateway.dedup_skipped |
observable counter | 1 | durable signals already seen |
forze.realtime.gateway.admission_rejected |
observable counter | 1 | rejected at the catalog admission gate |
forze.realtime.gateway.untenanted_dropped |
observable counter | 1 | no tenant resolved on a require_tenant gateway |
forze.realtime.gateway.mailboxed |
observable counter | 1 | stored for offline replay |
forze.realtime.gateway.bridge_failed |
observable counter | 1 | redelivered if durable, dropped if ephemeral |
forze.realtime.gateway.poisoned |
observable counter | 1 | dropped at the delivery ceiling — bounded loss |
The two to alarm on: poisoned (every increment is a dropped durable delivery) and
emit_failed climbing while emitted is flat (Socket.IO, or its Redis backplane, stopped
taking frames).
Realtime backplane¶
instrument_realtime_backplane(health), fed by the heartbeat lifecycle step. Labelled
forze.realtime.channel.
| Metric | Instrument | Unit | Notes |
|---|---|---|---|
forze.realtime.backplane.seconds_since_ok |
observable gauge | s | seconds since the backplane last accepted a probe; -1 means never |
forze.realtime.backplane.consecutive_failures |
observable gauge | 1 | failed probes since the last success |
A dead AsyncRedisManager listener silently stops every cross-node emit and nothing in
python-socketio surfaces it. Alarm on seconds_since_ok exceeding a few heartbeat
intervals; -1 is a wiring problem, not an outage.
Realtime connections¶
instrument_realtime_connections(stats), with the same RealtimeConnectionStats handed to
attach_realtime_connection and connection_drain_lifecycle_step. Labelled
forze.realtime.channel.
| Metric | Instrument | Unit | Labels | Notes |
|---|---|---|---|---|
forze.realtime.connections |
observable up-down counter | 1 | forze.realtime.namespace |
connections open on this node |
forze.realtime.connect_refused |
observable counter | 1 | forze.realtime.namespace, forze.realtime.reason |
protocol, auth, draining, admission, internal; a draining connect is turned away by closing its transport |
forze.realtime.drain_closed |
observable counter | 1 | forze.realtime.outcome |
closes the drain started: closed, failed, timed_out |
What to watch: connect_refused{reason="draining"} that keeps climbing long after a
deploy means a balancer still routes to a replica it should have dropped, and any
drain_closed{outcome="timed_out"} means the drain's tail is too short for this node's
closes.
Realtime mailbox¶
instrument_realtime_mailbox(mailbox, cursors). Labelled forze.realtime.channel.
| Metric | Instrument | Unit | Notes |
|---|---|---|---|
forze.realtime.mailbox.stored |
observable counter | 1 | durable principal signals stored for replay |
forze.realtime.mailbox.replayed |
observable counter | 1 | entries fetched on connect-time replay |
forze.realtime.mailbox.trimmed |
observable counter | 1 | dropped by retention/ack trimming |
forze.realtime.mailbox.acked |
observable counter | 1 | per-device cursor advances |
forze.realtime.mailbox.overflowed |
observable counter | 1 | replays that lost their oldest backlog to the cap |
The alert that matters: every overflowed increment is a device that fell more than
cap entries behind and lost signals it will never see.
Access-token signing¶
instrument_signing({"default": service, ...}) — the identity plane. Labelled
forze.signer, forze.signer.algorithm, and forze.signer.kid when set.
| Metric | Instrument | Unit | Notes |
|---|---|---|---|
forze.authn.tokens.signed |
observable counter | 1 | access tokens issued; for a KMS-held key this tracks sign round-trips |
forze.authn.tokens.verified |
observable counter | 1 | verified successfully |
forze.authn.tokens.verify_failed |
observable counter | 1 | rejected as expired/invalid |
A rising verify_failed rate is the signal that matters: key-rotation gaps, clock skew,
or forgeries.
Revocation states¶
instrument_revocation_states({"default": revocation_states}) — the identity plane's fast
path for sessions and API keys. Labelled forze.revocation_states and forze.credential
(session or api_key).
| Metric | Instrument | Unit | Notes |
|---|---|---|---|
forze.authn.revocation_states.hits |
observable counter | 1 | verifications answered from Redis alone, refusals included |
forze.authn.revocation_states.misses |
observable counter | 1 | verifications that read the database for a state Redis lacked or could not trust |
forze.authn.revocation_states.fallbacks |
observable counter | 1 | verifications whose Redis read failed, timed out or met an open breaker |
forze.authn.revocation_states.write_failures |
observable counter | 1 | revocations whose write-through failed every attempt |
forze.authn.revocation_states.clock_skew |
observable counter | 1 | API-key states refused because the application's clock runs two ttl or more ahead of Redis's |
The alert that matters: every write_failures increment is a revoked credential that
stays admitted for up to the states' ttl. See
Authentication for every cause of that window.