Supportability
What this is
When a probectl deployment misbehaves, you want to hand a diagnostician a single file that says what's wrong — without that file leaking any secrets. That's what this layer provides:
- a one-command support bundle (triage-grade diagnostics, packaged — a flight data recorder for the deployment: it captures the instruments, never the passengers' conversations),
- deep health checks (per-component status, plus one "is it healthy?" answer),
- actionable readiness findings (a stable local task for every unhealthy component, with bounded redacted evidence and a safe in-product next step),
- self-monitoring (probectl emits metrics about itself — the monitoring platform is also a service someone has to operate).
All three are core (free, every deployment gets them) — better bug reports help everyone. The paid part is the support organization and SLA (the Enterprise entitlement); that's a contract, not code.
The non-negotiable property: a support bundle never contains secrets, credentials, or PII (it falls under the project's secrets-handling non-negotiables). Everything below is built to keep that true even if someone slips up.
Support bundle
A .tar.gz of JSON files. The code lives in internal/support/bundle.go.
| File | Contents |
|---|---|
manifest.json |
format version, when it was generated, probectl version, the file list |
version.json |
build version / commit / Go version / OS / arch |
config-redacted.json |
operational config — an allowlist (no secrets) |
health.json |
the deep-health report (each component + the aggregate) |
self-metrics.json |
goroutines, memory, uptime, GC, GOMAXPROCS |
topology-summary.json |
tenant-scoped, anonymized shape (the authenticated tenant, its agents, its isolation model, region); partial collection exposes only allowlisted error classes — no tenant identifiers, free-form dependency errors, telemetry, or other-tenant counts |
device-collection.json |
bounded LLDP/CDP readiness receipts with bundle-local agent-NNNN / target-NNNN references; no raw tenant, agent, target, credential, varbind, or error text |
flow-ingest-quality.json |
bounded flow-ingest receipts with bundle-local agent-NNNN / exporter-NNNN references; counters and allowlisted health only—no tenant, address, raw datagram, flow field, credential, or free-form error |
runtime.json |
a runtime snapshot of the process |
How it stays secret-free (defense in depth)
Three independent layers, so no single mistake leaks a secret:
- Allowlist config, not blocklist. An allowlist names what may enter —
a guest list; a blocklist names what may not — a bouncer's memory of past
troublemakers, which fails precisely when a stranger walks up. Secrets
added in next year's release are strangers. So
config.Redacted()builds the config snapshot from a fixed list of known-non-secret keys. Database URLs have their passwords stripped; the envelope encryption key shows up only as the booleanenvelope_key_configured(true/false), never the key itself. The safety is structural: a secret field someone adds later can't leak, because it simply isn't on the allowlist. - Tenant-scoped, anonymized operational identity. The topology-shape file is reduced at the database layer to the authenticated tenant before it is counted: one tenant, that tenant's agents, and that tenant's isolation model. Device collection and flow-ingest receipts preserve stable, allowlisted state/reason/counter evidence but replace agent, configured-target, and exporter identities with bundle-local ordinal references. None carries a tenant ID, hostname, IP, credential, raw datagram/varbind, decoded flow field, discovered neighbor, free-form dependency error, or another tenant's counts.
- A final scrub. Before the bundle is written, it's swept once more for the
specific sensitive values this deployment actually holds — the envelope
key, the OIDC / CMDB / SIEM / AI-model secrets, the provider-bootstrap and
OTLP tokens, and the database password. Any of those found anywhere in the
bundle bytes is replaced with
***REDACTED***. A test asserts these values never appear in the output. So even an accidental inclusion is caught.
Each file is bounded (4 MiB max) and the whole bundle is gzip'd.
Getting a bundle
| Method | Use |
|---|---|
GET /v1/diagnostics/bundle |
the live bundle (topology, deep health, self-metrics). Admin-only — requires the diagnostics.read permission. The Admin → Support & diagnostics page has the download button. |
GET /v1/diagnostics |
the live native diagnostics contract: deep health, deployment-local process metrics, and build identity. Admin-only — requires diagnostics.read; it contains no tenant telemetry. |
probectl diagnostics status |
the same live JSON diagnostics contract, including self-metrics and build identity, for local scripts and terminals. |
probectl-control support-bundle [-o file] |
an offline bundle straight from the binary (version, redacted config, a database health check, runtime) — no running server needed. |
Deep health checks
GET /v1/diagnostics (admin diagnostics.read) returns each component's status
— ok / degraded / down — plus an aggregate that equals the worst
component, so one field tells you whether the deployment is healthy. Worst,
not average, because a deployment is only as healthy as its sickest dependency
— one amber bulb makes the whole panel amber. The
checks are wired up in internal/control/diagnostics.go:
| Check | Degraded / down when |
|---|---|
database |
the writer connection-pool ping fails → down |
alert_evaluator |
stored alert rules are not being evaluated → degraded |
secrets_resolver |
a configured secret backend is failing → degraded |
cluster |
writes are fenced during a multi-region failover → degraded |
license |
expired into the grace period or read-only state → degraded |
bus |
the result bus runs in memory mode → degraded; a configured NATS/Kafka bus is unreachable → down |
tsdb |
the metrics store runs in memory mode → degraded; a configured Prometheus/VictoriaMetrics backend is unreachable → down |
event_store |
a configured ClickHouse event store is unreachable → down |
object_store |
a configured S3/MinIO object store is unreachable → down |
bus, tsdb, event_store and object_store (PLAT-09) make each configured
external backend observable: an outage of the result bus, TSDB, ClickHouse event
store or object store used to be invisible (diagnostics reported ok). A
memory-backed bus/TSDB is a named degraded warning (dev/test only — data is
lost on restart), and /readyz surfaces volatile planes while staying 200.
Reachability is probed on this deep-health surface, not on the hot /readyz
path, because these backends are shared infrastructure — draining a replica over
a shared-backend blip removes serving capacity without routing around the fault.
Every degraded or down check carries one finding; an ok check never
fabricates a task. A finding contains:
| Field | Meaning |
|---|---|
id |
stable machine key such as readiness.cluster |
component, scope |
the affected component and the redacted deployment scope |
severity |
warning for degraded, critical for down |
observed_at |
exactly the report's local checked_at time |
summary, evidence |
bounded operator text; raw dependency errors and secret values are never copied |
next_action |
a relative local navigate or download link; it never runs remediation |
The native Admin → Support & diagnostics card shows the local process snapshot and build identity alongside findings and the underlying component table. The same contract is available to the CLI and generated Go/TypeScript SDKs. During a rolling upgrade an older replica may omit self-metrics, build identity, or finding details. Each missing section is displayed as unavailable/incomplete; the UI does not invent a value or pretend that state is healthy.
This is separate from the liveness/readiness probes (/healthz, /readyz) —
liveness asks "is the process alive at all?" and readiness asks "should
the load balancer send it traffic right now?". Those answer a blunt up/down for
machines making routing decisions. The deep report is richer — it's for a human
doing triage (deciding what is broken and what to look at first) and for
the support bundle.
The endpoint is a tenant-authenticated, diagnostics.read-authorized sensitive
read and is audited by the central route policy. Health findings are
deployment-local and redacted; topology metadata is reduced to the caller's
tenant at the storage layer before counting. No tenant identifier, hostname, IP
address, credential, telemetry, or other-tenant count is returned. There is no
remote advisor, cloud model, plugin catalog, phone-home, or outbound request in
this path. Any future tenant-data section must preserve that storage-first
scope before RBAC and add a cross-tenant isolation test.
Self-monitoring (probectl observes probectl)
The control plane collects and emits probectl_self_* metrics every 30 seconds —
goroutines, mem_alloc_bytes, mem_sys_bytes, num_gc, uptime_seconds,
max_procs — plus probectl_build_info{version,commit,go} (value 1, the
standard Prometheus build-info trick: the labels carry the info, the value is
just a constant). The authoritative, dependency-free native view is
Admin → Support & diagnostics, backed by GET /v1/diagnostics; it shows
goroutines, memory, garbage collections, uptime, process capacity, and build
identity. It is intentionally administrator-only because these are
deployment-global process facts. The response contains no tenant identity,
labels, or telemetry. Agent scrape targets add tenant-agnostic
probectl_agent_* RED/USE
series: collection and publish rates, errors, bounded-buffer depth, and latest
publish latency.
The product does not bundle or require an external dashboard runtime. The
/metrics output and any example dashboard JSON are optional,
operator-supplied protocol interoperability only; they are not the
authoritative probectl surface, a runtime dependency, or part of the shipped
application path.
Configuration
No new config keys. The diagnostics endpoints and the offline CLI read the
existing config. The diagnostics.read permission that gates the endpoints is
seeded for admins by migration 0034_diagnostics.sql.
Completeness gate
The bundle's section set is pinned by a CI test
(internal/support/completeness_test.go, EXC-ORG-03): version, redacted config,
health, self-metrics, topology summary, device and flow readiness receipts,
runtime, and the manifest. Dropping a
section — the bundle silently shrinking so it no longer carries what an F500
support contract needs to triage — reds the build, as does the manifest index
drifting from the actual contents or a secret slipping through (the companion
TestBundleHasNoSecrets).
Out of scope
The support organization and SLAs (the Enterprise/acquirer-provided
contract — not code). In MSP mode, tier-1 support to end customers is the MSP's
job. The DR drill on real multi-region infrastructure is likewise an operator
action: the runbook + the CI failover drill exist (docs/ops/dr.md), but the
live cross-region exercise needs real DR infra and is run by the operator, not in
CI.