Observability
Prometheus metrics with bounded labels, a bearer-protected /metrics route, ComputeSpan observers, trace context, health endpoints and doctor.
@better-compute/observability holds the pieces a host uses to watch a deployment: a Prometheus registry that
refuses tenant labels and caps its series, the standard better_compute_* metric set of PLAN.md section 14, a
GET /metrics handler behind a bearer token, wrappers that meter IAM calls, gateway services, attestors and
providers, ComputeSpan observers and W3C traceparent helpers. It is browser-safe (no Node.js imports), so every
package can depend on it. The CLI host mounts it for you when the configuration's host export has a registry.
Wire metrics into a deployment
One registry per process. The configuration creates it, registers the standard series, passes it to what it meters,
and hands it to the host through host.metrics:
import { defineHost } from 'better-compute/cli';
import { createMetrics, eventCounter, meteredIamPort, standardMetrics } from 'better-compute/observability';
import { eventsModule } from 'better-compute/events';
import { defineConfig } from 'better-compute/server';
export const metrics = createMetrics(); // maxSeries 2000 per metric
const standard = standardMetrics(metrics);
export default defineConfig(({ env = process.env } = {}) => ({
// …database, secret, baseURL
iam: meteredIamPort(bridge.embedded(iam, { rootTenantId }), standard), // iam_calls_total, iam_call_duration_seconds
modules: [eventsModule({ secret: env.BETTER_COMPUTE_SECRET!, countEvent: eventCounter(metrics) })],
}));
export const host = defineHost(async ({ env }) => ({
metrics: { registry: metrics, bearerToken: env.BETTER_COMPUTE_METRICS_TOKEN }, // GET /metrics
}));better-compute serve then:
- mounts
GET /metrics(orhost.metrics.path) on the API listener whenbearerTokenis set; without a token there is no route; - when it runs the gateway role, meters
compute.gatewayServiceswithmeteredGatewayServicesand samples the gateway at every scrape withgatewayCollector; - when it runs any worker role, starts
databaseCollectorinto the same registry: a first reading at start, then every 15 s, stopped with the server. It takes each command kind's lane from the registry and watches the host's own certificates ascert_expiry_seconds{kind}:node-ca,command-signingandserving(the gateway's TLS certificate).
better-compute dev mounts the configuration's host.metrics too. The examples read the token from
BETTER_COMPUTE_METRICS_TOKEN.
Scrape it with the token as a bearer credential:
scrape_configs:
- job_name: better-compute
authorization:
type: Bearer
credentials_file: /etc/prometheus/better-compute.token
static_configs:
- targets: ['compute-api-1:3000', 'compute-api-2:3000']The registry
createMetrics({ prefix?, maxSeries?, forbiddenLabels?, now? }) returns a registry of counters, gauges and
histograms.
- Prefix. Every name gets
better_compute_unless it already has it. Names and labels must match[a-z_][a-z0-9_]*;leis reserved. - No tenant labels. A label named
tenant,tenant_id,tenantid,org,organization,identityoruseris refused withINVALID_CONFIGwhen the metric is defined. Per-tenant numbers belong in usage and audit, not in your TSDB. - Bounded series. Each metric holds at most
maxSerieslabel sets (default 2000). New label sets past the cap fold into one series whose labels all read(other), andbetter_compute_metrics_collapsed_totalcounts every folded recording. - Caller-controlled values. A label value that is not a string or is longer than 256 characters reads
(invalid); an empty one reads(unknown). - Collectors.
registry.collect(fn)runsfnat the start of everyexpose(), for gauges sampled at scrape time. A collector that throws is skipped for that scrape. - Exposition.
expose()renders text format 0.0.4.value(name, labels)andstats()(series,dropped) serve tests and health checks.
metricsHandler({ registry, bearerToken, timingSafeEqual }) answers GET only (405 otherwise), requires a token of
at least 16 characters, compares the SHA-256 digests of the presented and expected tokens with the constant-time
function you pass (crypto.timingSafeEqual in Node), answers 401 with WWW-Authenticate: Bearer on a mismatch,
and serves the text with Cache-Control: no-store.
The standard series
standardMetrics(registry) defines the section 14 series: sessions and handshakes, command latency and sweeps, the
work queue and reconcile durations, placements and booking conflicts, pool GPUs, node connectivity, backlogs, IAM
calls and resolver lookups, certificate expiry, clock offset, and security events. None carries a tenant label.
The metrics reference lists every family with its type, labels, help text and buckets,
generated from metricCatalog(), so it always matches the code. This page covers who records them.
Two IAM series have hooks in the IAM bridge you can point at them: bridge.plugin({ onSpill }) is called for every
event the plugin spills, and bridge.resolveResource(fallback, { onLookup }) reports each lookup as hit, miss,
timeout, unavailable, scope or fallback. Hook failures never change a decision.
const plugin = bridge.plugin({ onSpill: () => standard.iamInboxSpill.inc() });
const resolveResource = bridge.resolveResource(undefined, {
onLookup: (result) => standard.resolverLookups.inc({ result }),
});The database collector
Some section 14 series describe the whole deployment, not one process. databaseCollector samples them from
compute's own tables with read-only, bounded statements:
import { databaseCollector, standardMetrics } from 'better-compute/observability';
const collector = databaseCollector(standardMetrics(registry), {
sql: compute.context.sql,
laneOf: (kind) => compute.context.registry.commandKind(kind)?.lane, // default label (unknown)
certificates: () => [{ kind: 'serving', notAfter }], // host certificates besides node certificates
});
collector.start(); // every intervalMs (default 15 s); refresh() polls once; stop() ends it| Series | Sampled from |
|---|---|
work_queue_depth{kind} | Due, unclaimed work_queue rows by kind |
backlog{queue} | Pending commands, unprocessed iam_inbox rows, unrecorded usage_windows, pending webhook_deliveries, events a sink gave up on (outbox_dead_letters), and each outbox_cursors reader's lag as outbox:<name>; each capped at 100,000 |
node_connectivity{state} | Nodes by connectivity |
generation_lag_nodes | Nodes whose observed generation is behind |
pool_gpus{state} | pool_capacity_rollup GPUs: total, allocatable, allocated, reserved, provisioning, unhealthy, maintenance, unreachable |
cert_expiry_seconds{kind} | Each node's newest live certificate, soonest per role as node:<role>, plus the certificates you pass |
clock_offset_seconds | This process's clock minus compute_now() |
command_latency_seconds{kind,lane} | Commands settled since the last poll, updated_at - created_at (needs commands.created_at, core/0020) |
A table an optional module did not create is skipped, and a label set that disappears reads 0 instead of going
stale. Latency starts at the first poll, so a restart does not observe history twice, and at most 5,000 settled
commands are observed per poll. Poll failures go to onError and never throw.
Any number of processes may run the collector, and serve starts it in every process with a worker role. Each one
reports the same deployment-wide gauges, so query them with max rather than sum across instances. The latency
histogram counts each command once. Only the holder of the leases row metrics:command-latency observes
command_latency_seconds: the row is taken or renewed on each poll with a TTL of three polls, and released on
stop(). A stopped collector never takes it again. Pass latencyLease to name another row, or false to observe in
every process.
Series the instrumentation adds
Beyond the standard set, these come from the collectors and wrappers that record them:
| Series | From | Labels |
|---|---|---|
better_compute_gateway_open_sessions | gatewayCollector | gateway, role |
better_compute_gateway_stream_headroom, _draining, _lease_held, _commands_in_flight, _sessions_upgrade_required | gatewayCollector | gateway |
better_compute_gateway_loop_consecutive_failures, _loop_failures, _loop_last_ok_age_seconds | gatewayCollector | gateway, loop (dispatch, lease, liveness, specs, status) |
better_compute_gateway_service_calls_total, _service_seconds | meteredGatewayServices | method (join, renew, secret), outcome |
better_compute_attestor_verifications_total, _verification_seconds | meteredAttestor | attestor, outcome (admitted, deferred-<kind>, refused or an error code) |
better_compute_provider_calls_total, provider_call_seconds | meteredProvider | provider, method, outcome |
better_compute_revocation_feed_consecutive_failures | revocationFeedCollector | feed |
better_compute_events_total | eventCounter via eventsModule({ countEvent }) | family, module |
better_compute_placements_total, booking_conflicts_total, placement_seconds | placementObserver via capacityModule({ onPlacement }) | outcome; none; placer (inline, pool-lease, batch) |
better_compute_spans_total, span_duration_seconds | createObserver({ metrics }) | kind, name, outcome, code; kind |
gatewayCollector and meteredGatewayServices are wired by serve for the gateway role; the others are yours to
wrap around what you register. Placements, for example, are observed by passing capacity's hook the standard
series (booking_conflicts_total counts replans forced by a booking conflict):
capacityModule({ onPlacement: placementObserver(standardMetrics(registry), registry) });Outcome labels are bounded: ids, method names, error codes and catalog families. Help text and buckets for each of these are in the metrics reference.
Spans and traces
createObserver({ onSpan?, metrics?, now? }) times units of work as a ComputeSpan, shaped like better-iam's
IamSpan:
interface ComputeSpan {
kind: 'operation' | 'decision' | 'http' | 'command' | 'reconcile' | 'placement' | 'iam' | 'sink';
name: string; // method, route, command kind or reconciler
tenantId?: string; // for hooks only, never a label
outcome: 'ok' | 'denied' | 'error';
code?: string;
status?: number;
requestId?: string;
durationMs: number;
}observer.span(kind, name, fn, extra?) runs fn, classifies the outcome (401, 403 and 429, or the codes
UNAUTHENTICATED, ACCESS_DENIED, CONFIRMATION_REQUIRED, RATE_LIMITED, CSRF_REJECTED and
UNTRUSTED_ORIGIN, are denied; anything else that throws is error), fans the span out to onSpan hooks and feeds
the span metrics. Hooks and metrics can never affect the work: their failures are swallowed. A name the caller
controls (an unknown route, NOT_FOUND, INVALID_INPUT, METHOD_NOT_ALLOWED) is recorded as (invalid) or
(unknown), so it cannot grow the series. The server does not create an observer itself; hosts use it to export
spans to their tracing system.
Trace context. parseTraceparent, formatTraceparent and childTraceparent handle W3C Trace Context version
00 (00-<32 hex trace id>-<16 hex parent id>-<2 hex flags>), refusing all-zero ids and version ff. The protocol
carries traceparent on every CommandBody and DesiredStateBody, and the driver SDK passes it to drivers, so a
daemon's work can join the request's trace. A valid traceparent header on an API request (version 00, non-zero
trace and parent ids) reaches the method as MethodContext.traceparent, and ctx.commands.enqueue in that method
defaults CommandBody.traceparent to it, so the commands a call enqueues carry the caller's trace. An invalid header
is dropped, not refused. Commands enqueued by reconcilers, and NodeSpecs, go out with it empty.
Request ids. The API takes the caller's X-Request-Id when it is 1 to 128 characters of letters, digits and
._:-, else it mints a req_ id, and returns it in the response's X-Request-Id. IAM receives the same id in
the forwarded headers, and the admission context of a mutation's audit row records it, so a support ticket with the
header finds the call.
Logs. betterCompute({ log }) takes a Logger; the default writes warnings and errors as JSON to standard error.
GPU telemetry does not go through the control plane (principle P8); the nodes send it straight to your collector.
Node metrics from bcd
bcd exports its own metrics over OTLP/HTTP, in the JSON encoding, to the collector its NodeSpec's telemetry
section names (27c7ee2). Nothing reaches the gateway, the session or PostgreSQL.
Where to. nodeTelemetryModule(options) (53aa356) gives every node that section: options are the platform's
collector, and a pool's policy.telemetry wins field by field, with resourceAttributes merged by key. A node whose
result has no endpoint gets no section, and exports nothing. The module is not registered by default; add it to your
modules:
import { nodeTelemetryModule } from 'better-compute/observability';
betterCompute({
// …
modules: [
nodeTelemetryModule({
otlpEndpoint: 'https://otel.example.internal:4318',
intervalSeconds: 30,
headersRef: 'secret://root/sec_01k…', // the collector's auth headers
resourceAttributes: { 'deployment.environment': 'prod' },
}),
],
});| Setting | Rules |
|---|---|
otlpEndpoint | https://, or http:// to localhost, 127.0.0.1 or [::1]; no credentials. bcd appends /v1/metrics unless the path already ends with it |
intervalSeconds | 5 to 3600; bcd's default is 60 seconds and its minimum 5 |
headersRef | A secret reference to the headers: a JSON object, or Name: value lines |
resourceAttributes | At most 32, single-line values of at most 256 characters; keys under bcd. and service. are bcd's own |
roles | Module option only: the node roles that export (default all, site agents included) |
The module checks its options when it is built (INVALID_INPUT). A pool whose policy.telemetry fails the same
checks is skipped, and its nodes keep the default; better-compute doctor (and the support bundle) reports such
pools as pool-telemetry-invalid, from nodeTelemetryFindings(sql).
What. Every interval, bcd sends:
| Metric | Unit | From |
|---|---|---|
bcd.gpu.memory.used, bcd.gpu.memory.total, bcd.gpu.memory.reserved | bytes | nvidia-smi, per GPU |
bcd.gpu.utilization, bcd.gpu.memory.utilization | ratio (0 to 1) | nvidia-smi |
bcd.gpu.temperature | Celsius | nvidia-smi |
bcd.gpu.power.draw, bcd.gpu.power.limit | watts | nvidia-smi |
bcd.gpu.clock.sm, bcd.gpu.clock.memory | MHz | nvidia-smi |
bcd.gpu.processes | processes | nvidia-smi |
bcd.gpu.mig.memory.used, bcd.gpu.mig.memory.total | bytes | nvidia-smi, per MIG instance (mig.id) |
bcd.gpu.ecc.errors | errors, cumulative sum | the inventory's aggregate ECC counters, type correctable or uncorrectable |
bcd.gpu.rows.remapped | rows | the inventory's row remapping, by type |
bcd.gpu.xid.active | conditions | bcd's active XID conditions per GPU |
bcd.host.pressure.cpu, bcd.host.pressure.memory, bcd.host.pressure.io | percent | PSI some avg10 |
bcd.host.cpu.steal | percent | CPU steal |
bcd.host.memory.total, bcd.host.memory.available | bytes (By) | /proc/meminfo's MemTotal and MemAvailable (3111284) |
bcd.host.hugepages.total, bcd.host.hugepages.free | pages ({page}) | /sys/kernel/mm/hugepages, each pool's nr_hugepages and free_hugepages, by page.size (the page size in bytes) |
bcd.instances | instances ({instance}) | every instance runtime's list of domains, managed or not, counted by runtime and state (running, stopped, paused, error) |
bcd.instance.vcpu | vCPUs ({cpu}) | each domain's configured vCPUs, by instance.id, runtime and managed |
bcd.instance.memory | bytes (By) | each domain's configured memory, by the same attributes and hugepages (the backing page size in bytes, 0 for normal pages) |
bcd.instance.memory.balloon.reclaimed | bytes (By) | the memory a ballooned guest lends the host now: its configured memory less what its balloon leaves it, by instance.id and runtime (9bdd43a; bcd.instance.memory.balloon before). Only for running, managed guests whose spec gives them a balloon (placement.balloon); a guest without a balloon reading counts as fully deflated (0). See memory ballooning |
bcd.instance.cpu.time | seconds (s), cumulative sum | live usage (0b21b4b): CPU time since the domain started, by instance.id and runtime |
bcd.instance.memory.used | bytes (By) | live usage: the memory the domain uses now |
bcd.instance.io.read, bcd.instance.io.write | bytes (By), cumulative sum | live usage: disk bytes read and written |
bcd.instance.network.receive, bcd.instance.network.transmit | bytes (By), cumulative sum | live usage: bytes the guest received and sent |
Instances. The host and instance series come every interval from a node that has them: the memory and huge page
series from any Linux host, the bcd.instance* series only from a host that runs an instance runtime (VPS and
sandbox hosts). The configured series (bcd.instances, bcd.instance.vcpu, bcd.instance.memory) cover every
runtime, plugin runtimes included. The live usage series come only from runtimes that can read it
(runtime.UsageReader), for their running domains, and count from the guest's side:
| Runtime | Where the usage comes from |
|---|---|
cloud-hypervisor | The VMM unit's cgroup v2, /sys/fs/cgroup/bcd-instances.slice/bcd-ch-<id>.service: cpu.stat usage_usec, memory.current, and io.stat's rbytes and wbytes over every device; plus the counters of the instance's taps, where a tap's transmitted bytes are what the guest received. Managed domains that are running or paused only |
libvirt-kvm | virsh domstats --raw --state-running --cpu-total --balloon --interface --block: cpu.time, balloon.rss (the QEMU process's resident memory) and the summed net.* and block.* byte counters |
A domain whose usage cannot be read is left out of the live series. Plugin runtimes report none, and a fakehw
build's file-backed runtime reports synthetic usage (half the configured memory, a second of CPU per boot). Under
bcd helper libvirt-kvm reads domstats on libvirt's read-only socket
(since 12e9ced; before it the read was refused there), and cloud-hypervisor, offered there since 7fe74a1, reads
the same cgroup and tap counters as the node's own user, with no ch.* operation: since 1c9684a
TestCloudHypervisorUnderTheHelper (internal/runtime) reads a helper-started domain's usage from a fake cgroup tree
and tap statistics and checks that root was asked nothing. The series are tested against a fake /proc, sysfs,
cgroup tree and captured domstats output (internal/agent telemetry_host_test.go, internal/runtime
usage_test.go). On the control-plane side, tests/e2e/helper-vps.test.ts runs sandboxes on FakeDaemon VPS hosts
under the helper and checks that their bcd.instance.* usage reaches the collector nodeTelemetryModule names, and
leaves the next export once they are deleted (see testing).
GPU points carry gpu.uuid, gpu.index and gpu.model. The resource carries service.name=bcd,
service.version, host.name, bcd.node.id and bcd.site.id, then the section's own attributes (which cannot
replace those). bcd fetches the headers with a SecretRequest that names workflow host:telemetry and step
headersRef; the secret broker releases the reference only while the node's current spec names it there (binding
rule 3; see sealed secrets).
Failures. An endpoint that is not an http(s) URL, or a malformed resource attribute, fails the telemetry
section terminally; an empty endpoint stops the export. A collector's 4xx answer other than 429 drops that batch;
after anything else the export runs again on the next interval, with that interval's fresh readings (a failed batch
is never re-sent). bcd logs a failure once, not every interval, and logs when the
export resumes. The exporter restarts whenever the section changes. bcd status prints the endpoint, the last
export and the last error (ad8791f). It is tested against a fake collector, and since efdff1d end to end: the real
fakehw bcd in tests/e2e/bcd.test.ts posts its GPU metrics to a local OTLP receiver the test runs, named in
NodeSpec.telemetry (see testing). No collector product has received it yet.
Health and readiness
The CLI host answers two routes beside the API on every api listener:
| Route | Answers |
|---|---|
GET /healthz | 200 { "ok": true } while the process serves requests |
GET /readyz | 200 { "ok": true } when SELECT 1 succeeds on the store, else 503 { "ok": false } |
Point the load balancer's health check at /readyz. A gateway-only or worker-only process has no HTTP listener;
watch it through the gateway series and leases instead.
doctor
better-compute doctor runs compute.selfCheck() and adds the CLI's own findings. It only reads. --strict exits
non-zero with DOCTOR_FINDINGS when any error or warning is found; --pki PATH names the PKI bundle to check.
better-compute doctor --strict --query "findings[].check"| Check | Severity | Finds |
|---|---|---|
migrations | error | A database never migrated, or migrations of this release and its modules not applied |
btree-gist | error | The extension the booking exclusion constraints need is missing |
clock | error or info | compute_now() is not the database clock (info when it reads the sim clock) |
clock.skew | warning | This process's clock differs from compute_now() by more than 1 s |
catalog.* | error or warning | Methods whose actions are uncatalogued, mismatched or undeclared |
database | error | The check itself could not run |
workers.queue | warning | Work items more than 5 minutes overdue and unclaimed, noting when no worker has heartbeated in the last minute |
workers.sweeper | warning | Pending commands more than 2 minutes past not_after and not yet expired: no worker holds the CommandSweeper's lease |
usage.dead-letters | warning | Unresolved rows in usage_dead_letters |
usage.backlog | warning | Closed usage windows unrecorded for more than 2 hours |
fleet.gateway-headroom | warning | Live, non-draining gateways holding at least 80% of their max_streams |
fleet.heartbeats | warning | Connected nodes whose session sent nothing for more than 2 minutes |
fleet.daemon-skew | warning | Connected daemons whose newest protocol is below the control plane's minimum |
fleet.cert-expiry | warning | Connected nodes with no unrevoked certificate valid for another hour: renewals are failing |
iam.not-configured | warning | No IAM is configured, so only public methods answer |
iam.projection | error | The authz projection is not loaded, so every decision is denied |
iam.inbox-spill | warning or error | IAM events spilled in memory, or dropped |
iam.inbox-backlog | warning | More than 10,000 IAM events wait in iam_inbox |
iam.usage-dead-letters | warning | Usage events that could not be recorded in IAM billing |
iam.usage-backlog | warning or error | Usage windows over 2 hours unrecorded (warning), or windows of a past month that IAM's closePeriod will refuse (error) |
events.webhook-dead-letters | warning | Webhook deliveries that failed every attempt |
events.webhook-backlog | warning | More than 1,000 deliveries overdue by 5 minutes |
capacity.reservations.stale | warning | Reservations more than 10 minutes past their end and not released |
capacity.mig.stuck | warning | Nodes that have waited more than 24 hours for a MIG layout change |
capacity.dedicated.unoffered | warning | Nodes dedicated to a tenant their pool is not offered to, so nobody can use them |
capacity.device_counts.drift | error | The trigger-kept per-node device counts the placement shortlist ranks on disagree with devices (a TRUNCATE bypasses the trigger) |
vps.pools.runtime | error | A vps-host pool runs a runtime this deployment does not list in vps({ runtimes }) |
vps.pools.single-tenant | error | A container-runtime pool without policy.singleTenant: true, which cannot run instances |
vps.pools.ipam | warning | A vps-host pool without an IP range, so its instances get no address |
vps.hosts.capacity | warning | Adopted VPS hosts without vCPU, memory and disk capacity rows: no inventory yet, or no worker runs vps.hosts |
vps.sandboxes.expiry | error | Sandboxes more than two reconciler passes (30 s) past expiresAt and not deleting: no worker runs the vps.sandboxes reconciler, or it is failing |
vps.sandboxes.deleting | warning | Sandboxes deleting for more than 10 minutes: their instance is stuck (its host must report it gone, or be declared lost) |
vps.sandboxes.warm | warning | Templates below warm.min settled warm instances for over 10 minutes with no refill in that time (quota, capacity, a vanished flavor or image), and warm instances not settled within 10 minutes |
vps.sandboxes.network | error | Open sandboxes with a private or none network on a host without the bc.dev/cap.instance.network-policy label (placed before the capability constraint, or its bcd since downgraded): their guests have full egress. Upgrade bcd there, or delete the sandboxes |
workflows-stuck | warning | provision: active workflows with an in-flight step more than 5 minutes past its deadline. The engine fails an overdue step whenever it looks, so nobody runs their workflow reconciler |
workflows-failed | info | provision: workflows that failed in the last day; inspect with workflows.get, retry with workflows.retry |
metal-failed | warning | metal: machines failed with maintenance on, waiting for metal.recover or metal.retire, listed with their failure codes |
metal-stalled | warning | metal: machines more than 10 minutes in a workflow-driven state without an active workflow, so no worker runs the metal reconciler (a rescued machine resting after its rescue workflow is not stalled) |
pool-telemetry-invalid | warning | Pools whose policy.telemetry fails nodeTelemetryModule's checks; their nodes export to the default collector (or none) until it is fixed |
audit.checkpoints | warning | Audit rows older than 3 hours not sealed by a checkpoint |
history-partitions | warning | retentionModule(): this or next month's partition of a history table is missing, so the retention pass is not running |
history-default-partition | warning | retentionModule(): rows of the last 13 months sit in a history table's default partition |
retention-cursors | warning | retentionModule(): a durable outbox_cursors row has not moved for a day, so its stream is not pruned behind it |
pki.* | error or warning | The PKI bundle unreadable or not unsealable, or a CA expired (error) or expiring (warning) |
host.* | error or warning | The host export: routes, tunnels, IAM, and whether the gateway role can authenticate daemons, reach the API's pokes, tunnels and revocations |
system.selfCheck serves the server's part of the report over the API
(action compute:fleet:admin on compute-fleet); the pki.*, host.* and iam.not-configured findings come from
the CLI only.
The workers.*, usage.* and fleet.* checks are one read-only statement the server runs once the schema is fully
migrated (they are skipped while a migrations finding stands). Each is a warning with a count and a fix, and
each maps to an operations check of PLAN.md section 14: queues, the sweeper, usage, gateway headroom, heartbeats,
daemon version skew and certificate expiry.
What to alert on
These follow from the series and findings above, and from PLAN.md section 8.6 on what breaks first.
- The gateway is fenced or stuck.
better_compute_gateway_lease_heldat 0, orbetter_compute_gateway_loop_consecutive_failuresrising for any loop. A gateway that loses its lease drops its streams; nodes re-home to the others. - Stream headroom.
better_compute_gateway_stream_headroomnear 0 (defaultmaxStreams5,000 per gateway): add a gateway before handshakes are refused. - The IAM lane.
better_compute_iam_call_duration_secondsp99 rising, oriam_calls_total{outcome="error"}. Every IAM call takes at least one global lock acquisition in embedded mode. - Backlogs.
workers.queue,workers.sweeper,iam.inbox-backlog,usage.backlog,events.webhook-backlogandaudit.checkpointsfindings: a worker role is not running or cannot keep up. Rundoctor --stricton a schedule and alert on a non-zero exit. - Clock.
clock.skew: run NTP on every host. Business time stays anchored to the database, but ids and logs use the host clock. - Stale reservations.
capacity.reservations.stale: thecontrollersrole is not running reconcilers. - Revocations.
better_compute_revocation_feed_consecutive_failuresabove 0: revoked certificates keep their sessions while the feed fails. - Series cap.
better_compute_metrics_collapsed_totalrising means some label is not bounded.
Better Compute is created by Sean Filimon
Last updated
PKI
The local CAs, JoinService keys and signing keys a deployment owns, the pki CLI and bundle, SPIFFE IDs, node certificate lifetimes, revocation and rotation.
Retention
What the retention passes prune from PostgreSQL and when, how cursors hold them back, how history partitions work, and what is kept on purpose.