Skip to content

The sentinel

The sentinel is one process per cluster, and what it watches is the set of signal sources that are enabled. Out of the box that is --sources=auto: at startup the sentinel probes each portable source’s needs — RBAC grants, plus a metrics API for saturation, the Gateway API CRDs for gateway, and the ComputeClass CRD for compute-class — and enables everything your deployment supports, announcing each decision with one startup line. Three of the sixteen sources are never auto-enabled and stay explicit opt-ins: quota (a per-GCP-project deployment decision), notifications (needs an operator-created Pub/Sub subscription), and token-burn (a polling loop against the core-agent daemon’s cost stack).

Organized by what fails, not by how the code is arranged. Every “Example trigger” is either captured drill output or the source’s own shipped threshold.

Watches forExample triggerSource nameOn by default?Extra needs
Failures the control plane already reportedA pod enters CrashLoopBackOff; an image tag that doesn’t exist (ErrImagePull)k8s-eventsAuto (always on — a sentinel that cannot watch events refuses to start)none
Nodes going badA node’s Ready condition flips to NotReady, or flaps 3 times inside 10 minutesobject-stateAutonone
Nodes running out of room, and the evictions that followA node’s MemoryPressure/DiskPressure/PIDPressure condition goes True and stays True for 5 minutes; 3 pod evictions on one node inside 10 minutes, folded into one node-scoped signalobject-stateAutonone
Services going dark, drains about to stallA Service’s ready-endpoint count drops to zero; a PodDisruptionBudget’s allowed disruptions hit 0 with pods behind itobject-stateAutonone
Crash loops and stuck rollouts, before the eventsA pod’s restart count climbs 3 in 10 minutes — ahead of the kubelet’s BackOff events; a Deployment burns 80% of its progress deadline with unready replicasobject-stateAutonone
Bad deploys, while the old version still servesNew pods crash-looping while the old version still serves: zero ready-count progress for 3 minutes (--rollout-observe) with the old ReplicaSet healthyrolloutAutonone
Failed batch work and dead schedulesA Job’s Failed condition goes True (BackoffLimitExceeded, DeadlineExceeded); an unsuspended CronJob passes a scheduled activation without running — three consecutive misses escalate to criticalworkloadAutonone
Autoscalers out of headroom, or silently deadAn HPA sits at maxReplicas with its metric still over target for 10 minutes (critical past 30); an HPA’s ScalingActive goes False with a FailedGet* reason for 15 minutes — scaling has stopped and nothing says soautoscalingAutonone
Resources trending toward exhaustionA pod leaking ~1 MiB every 30 s, forecast to hit its 64 Mi memory limit in ~14 minutes; a PVC filling in ~3 hsaturationAutometrics-server (metrics.k8s.io) — absent, auto skips the source with one loud line
Service capacity eroding before the outageA backend’s ready endpoints declining 5/5 → 3/5 across the trend window; a readiness probe that keeps flapping below the reactive thresholddegradationAutonone
Certificates and tokens running outA TLS certificate 13 days from expiry; a cert-manager Certificate whose last renewal failedexpiryAutonone
The autoscaler failing to deliver nodesA pod Pending and unschedulable past 5 minutes; a nodegroup that asked the cloud for a node and didn’t get one for 3 minutescapacityAutoa running cluster-autoscaler; GCP provider (-gke image) for the structured whys — stockout vs quota vs IP exhaustion
Load balancers that never get programmed (Ingress)An ingress-gce Warning Sync (“Error syncing to GCP: …”) or Translate event on an Ingress; a NEG-controller AttachFailed/SyncNetworkEndpointGroupFailed on a Service — endpoints never reach the load balancer while the Ingress object looks fineingressAutonone (nothing fires on clusters without ingress-gce/NEG controllers)
Load balancers that never get programmed (Gateway API)A Gateway or listener holds Programmed=False past the 5-minute grace, with observedGeneration caught up and the reason not Pending; an HTTPRoute parent holds Accepted=False/ResolvedRefs=False — the route config never became routablegatewayAutothe Gateway API CRDs served — absent, auto skips the source with one loud line (RBAC alone can’t tell, so this is a discovery check)
Workload placement drifting across topology domainsA Deployment that declared a DoNotSchedule spread constraint is violating it, sustained past the 10-minute dwell; a workload’s replicas pile into one zone and stay there. Scored against a declared constraint, an intent inferred from what the workload does say, or — Tier C, metrics-only unless --topology-tier-c-signals — its own learned normaltopology-driftAutonone (pods, nodes and replicasets — grants the sentinel already holds)
A whole topology domain with nothing schedulable left in itEvery node in a zone goes NotReady, is deleted, or is cordoned — the subject is the domain, so this is one signal for the cluster and not one per workload that drifted because of it (those are suppressed while the zone is out). The three cases are told apart as the suspected cause: nodes gone is consolidation, nodes present and none Ready is domain_outage, nodes Ready and none schedulable is taint_exclusion. Judged on zone and region by default — --topology-domain-unavailable-keys, and an empty value turns it offtopology-driftAutonone (nodes — a grant the sentinel already holds)
Workloads quietly running on a compute class’s fallback hardwareA GKE custom compute class is an ordered list of machine shapes; when the first choice has no capacity GKE provisions the next one down, the pod runs, the Deployment stays at full replica count, and nothing anywhere says so. The source resolves each node’s preference RANK (which is not the raw ccc_priority_index — a class that sets priorityScore can rank its list in the opposite order), accumulates pod-seconds per rank, and fires when a class is wedged (Pending pods, DoNotScaleUp), running 90% of its time on its least-preferred rank, or not migrating back once preferred capacity returns. A whole priority nothing has occupied for 30 days is Tier C — metrics-only unless --compute-class-tier-c-signalscompute-classAutothe cloud.google.com/v1 ComputeClass CRD served — a GKE feature; absent, auto skips the source with one loud line (RBAC alone can’t tell, so this is a discovery check)
Cloud quota exhaustion, days outCPUS/us-east1 at 98% of limit, exhausted in ~16 h at the current slope — drafted increase request attachedquotaNo — explicitGCP provider (-gke image); project tier — exactly one sentinel per GCP project enables it
Agent token spend burning out of controlOne session’s token rate at 4× the cross-session median, sustained two polls; a session budget projected to exhaust inside 30 minutestoken-burnNo — explicitcore-agent daemon — its cost stack is the data source
The provider’s own announcements: upgrades and security bulletinsA control-plane or node-pool upgrade starts (recorded for incident-window correlation); a security bulletin affecting the cluster lands on the watchboardnotificationsNo — explicitGKE notificationConfig topic + a Pub/Sub subscription (--notifications-subscription)

“Auto” means the source is on whenever the startup probe finds its grants (the shipped deploy/ manifests carry all of them) — a miss skips the source with a startup line naming the missing grant and the fix, never silently.

Every kind these sources can emit — 57 in the frozen schema — is cataloged in the Signal kinds reference; every threshold above is a flag documented in the lookout watch reference.

A source emits a signal; the pipeline dedups it per object and reason, so a pod that crashes forty times inside the dedup window is one incident with a rising count — not forty pages. Severity then decides the route: a critical signal opens its own agent session on the daemon, warnings batch into the shared watchboard digest, and info signals are stored (with --store) rather than surfaced. A critical session arrives enriched: the initial inject carries a pre-warmed, size-capped bundle — sanitized spec, recent changes, dependency edges, blast radius, distilled log tails — so the agent’s first tool calls are already answered. And when the symptom clears and stays clear, the sentinel injects a kind=resolved record into the same session: the incident ends with verified proof, not silence. The full mechanics — recovery, storms, the watchboard, triage-status — are in The closed loop.

With no --sources flag at all, startup resolves the portable set against what your deployment can actually do and prints one line per decision — the summary block, enabled lines included:

sources: auto — probing the portable set (RBAC per source; metrics.k8s.io for saturation); misses are skipped loudly — pin --sources explicitly to make a miss fatal (§11)
source k8s-events: enabled (always on — a sentinel that cannot watch events is misdeployed)
source object-state: enabled
source rollout: enabled
source workload: enabled
source autoscaling: enabled
source saturation: disabled (metrics.k8s.io unavailable — install metrics-server)
source degradation: enabled
source expiry: enabled
source capacity: enabled
source ingress: enabled
source gateway: disabled (Gateway API CRDs not served — install a GKE Gateway class or the upstream gateway.networking.k8s.io CRDs, or name gateway in --sources to make this fatal)
sources: auto resolved → k8s-events,object-state,rollout,workload,autoscaling,degradation,expiry,capacity,ingress (quota, notifications, and token-burn stay explicit-only: project tier, the notification subscription, and the core-agent cost stack)

--storm defaults to auto the same way: the graph informer grants (pods/nodes/replicasets list+watch) present resolve storm correlation on, a miss resolves it off with a line naming the grant. Storm is what turns a dead node’s thirty pod incidents into one session naming the node. The one skip auto never makes is k8s-events: a sentinel that cannot watch events is misdeployed, and that is a fatal startup error, not a line in the block.

Add --store to complete the experience — the store is what makes info signals durable, scans aware of prior triage, and post-mortem queries possible; its path is deliberately always explicit (--store=/var/lib/lookout/lookout.db, on a volume — the shipped Deployment now wires one).

An explicit list is the strict mode, and its semantics are unchanged: every named source’s startup probe failure is a fatal error naming the exact grant (source "object-state" requires permission to "list nodes cluster-wide" …) — never a silently empty watch, and never a skip. Pin a list when you’d rather crash-loop than run with less than you asked for — the shipped deploy/51 manifest carries the strict list as a ready-to-uncomment alternative, since it ships alongside the full RBAC:

--sources=k8s-events,object-state,rollout,workload,autoscaling,saturation,degradation,expiry,capacity,ingress,gateway,token-burn --storm=on --store=/var/lib/lookout/lookout.db --enrich=critical

--sources=k8s-events reproduces the pre-auto default surface byte-for-byte. The three explicit-only sources have deployment-specific homes: quota is a per-GCP-project opt-in on the -gke image, notifications needs a Pub/Sub subscription on the project’s GKE notification topic (--notifications-subscription), and token-burn reads the core-agent daemon’s cost stack (it disables itself, loudly, under the webhook sink) — which is why the strict list above names token-burn explicitly and leaves the other two out. Naming gateway in an explicit list makes a cluster without the Gateway API CRDs a fatal startup error rather than a skip. The shipped manifests in deploy/ carry everything every portable source needs; see Troubleshooting for the source-by-source requirements and the summary-block anatomy.

  • Deploy the sentinel — the manifests, RBAC tiers, and the rest of the flag walkthrough.
  • Signal kinds — the exhaustive catalog of everything that can go on the wire.
  • lookout watch — every flag, generated from the live flag surface.