lookout bundle
The first call of every incident: one correlated snapshot of a workload — sanitized spec, everything abnormal, broken dependency edges, blast radius, distilled logs — sectioned into a single payload instead of 4–5 separate reads.
MCP tool: k8s_triage_workload (MCP profile: triage)
lookout bundle [flags]| Flag | Type | Default | Meaning |
|---|---|---|---|
--incident | string | — | inject payload JSON (the message a lookout-watch incident session starts with); its object reference resolves to the target workload via the owner chain, or for a Service via its selector — alternative to —workload |
--depth | int | 2 | blast-radius traversal depth: graph edges followed per direction in the radius section |
--max-templates | int | 15 | cap distilled log template clusters in the logs section (triage logs defaults to 40; the bundle keeps the tighter budget) |
--cert-warn | duration | 720h | report TLS certificates expiring within this window (edges section) |
--store | string | — | path to a sentinel’s SQLite store (its —store file); merges open triage-status records so the bundle’s findings carry triage_* fields and severity reflects the agent’s override |
--store-cluster | string | — | read/write the store for THIS cluster, treating —store as the multi-cluster stem the sentinel was given: —store=/var/lib/lookout/lookout.db —store-cluster=prod-us opens /var/lib/lookout/lookout-prod-us.db (issue #410). Set it only against a sentinel running —clusters/—clusters-from; a single-cluster sentinel writes the literal —store path |
--lists | string | all | which cluster resources the List pass reads: ‘all’ (default), a comma-separated allowlist (pods,deployments), or subtractions (all,-secrets) for a least-privilege posture. Denied or deselected lists degrade to a partial bundle with a skipped= note on the head, never an error. |
--lists-preflight | bool | — | before listing, SelfSubjectAccessReview each selected resource and drop the denied ones proactively (fewer 403s); falls back to reactive Forbidden-skip if SSAR is not permitted |
Common flags (every lookout command)
Section titled “Common flags (every lookout command)”| Flag | Type | Default | Meaning |
|---|---|---|---|
--namespace | string | — | limit the scan to one namespace |
-A | bool | — | scan all namespaces |
--workload | string | — | target one workload as <Kind>/<namespace>/<name>, e.g. Deployment/prod/api |
--since | duration | — | how far back to look (0 = command default) |
--format | string | logfmt | output format: logfmt|json (one record per line either way) |
--timeout | duration | 10s | abort the invocation after this long (exit 1) |
--kubeconfig | string | — | path to a kubeconfig file, instead of $KUBECONFIG / ~/.kube/config |
--context | string | — | kubeconfig context to read, instead of its current-context. Selects a cluster for THIS invocation only — nothing is written back — so concurrent invocations can target different clusters. Reported as context=<name> in the summary line |
--exemptions | string | — | path to a git-reviewed exemption file (YAML); covered findings are ANNOTATED with their reason and expiry and counted as exempt=<n> in the summary, never dropped |
Finding kinds
Section titled “Finding kinds”Every kind= this command can emit, and the severities it carries them at. Nothing else appears in its output; a kind absent from a run means the check looked and found nothing. See the finding-kind glossary for the whole vocabulary.
| Kind | Severity | Claim |
|---|---|---|
bundle.target | info | the head record: which workload the bundle is about and which sections follow |
radius.neighbor | info | one object in the target’s neighborhood, with its direction, relation, and hop distance — an enumeration of impact, not a defect |
radius.missing | warning | a neighbor the graph references but never observed, in a kind the snapshot does watch: the reference is dangling |
spec.resource | info | the object itself: metadata, owner, and the kind-specific highlights (one per target) |
spec.container | info | one container of the target: image, resources, ports, probes, env (one per container) |
spec.condition | warning | a status condition of the target that is not in its nominal state |
pod.crashloop | critical | a container is crash looping |
pod.imagepull | critical | a container cannot pull its image |
pod.waiting | warning | a container is stuck in an error waiting state (CreateContainerConfigError, InvalidImageName, …) |
pod.oomkilled | warning | a container’s last termination was an OOM kill |
pod.restarts | warning | a container has restarted at least —restarts times |
pod.notready | warning | a container in a Running pod has been not-ready past the —pending-age grace |
pod.failed | warning | the pod reached phase Failed |
pod.pending | critical, warning | the pod has been Pending longer than —pending-age with no container-level diagnosis; critical when the scheduler has declared it Unschedulable, which is a capacity or constraint problem rather than latency |
workload.replicafailure | critical | the controller cannot create pods at all (quota, PodSecurity, admission) — no pod exists to diagnose |
workload.stalled | critical | a Deployment’s Progressing condition is False: the rollout has given up |
workload.rollout | critical, warning | replicas are short of desired; critical when nothing is serving at all |
job.failed | warning | a Job’s Failed condition is set |
cron.missed | critical, warning | an unsuspended CronJob’s schedule said to run more than —cron-grace ago and status says it did not; critical once several activations in a row are gone |
cron.unparseable | warning | a CronJob’s spec.schedule could not be parsed, so its activations cannot be judged at all |
node.notready | critical | the node’s Ready condition is not True |
node.pressure | critical | the node reports Memory/Disk/PID pressure |
node.condition | critical, warning | a non-standard node condition is True — NPD and its cousins publish problems that way |
node.cordoned | warning | the node is unschedulable but still holds pods: a stuck drain or a forgotten maintenance step |
node.preempt | critical, warning, info | a reclaim taint marks the node for termination; severity tracks how imminent |
pdb.gridlocked | critical, warning | the budget permits no disruptions; critical when healthy pods are already below the required minimum |
addon.degraded | critical, warning | a kube-system add-on (dns, proxy, cni, csi, metrics, connectivity) is short of replicas; critical when none are available |
quota.near | warning | a ResourceQuota resource is at or past —quota-warn percent of its hard limit |
quota.exhausted | critical | a ResourceQuota resource is at its hard limit: the next create is rejected |
log.template | critical, warning, info | one distilled template and how many lines collapsed into it; severity is the guessed level — critical at fatal, warning for error-ish, info otherwise |
log.stacktrace | critical, warning, info | a template that is a Go panic, Java exception, or Python traceback, with its innermost frames |
log.overflow | info | the low-count tail —max-templates dropped, counted rather than discarded silently (no coverage lies) |
log.probe_noise | info | health/readiness probe request lines stripped before distillation, counted so the removal is visible |
log.fetch_error | warning | a container’s log stream could not be read, so its lines are missing from the distillation |
edge.missing_ref | critical | a referenced ConfigMap, Secret, ServiceAccount, TLS secret, IngressClass, StorageClass, or governing Service does not exist |
edge.missing_key | critical | the referenced key is absent from an existing ConfigMap/Secret |
edge.invalid_ref | warning | the referenced object exists but is the wrong type to serve the reference |
edge.unclassed | warning | the Ingress names no class and no IngressClass declares itself the cluster default — no controller will claim it |
edge.selector_empty | critical | a Service selector selects zero pods, so the service routes nowhere |
edge.selector_unready | critical, warning | the Service selects pods but some are not Ready; critical when none are |
edge.endpoints_missing | critical | a selecting Service has no EndpointSlices at all |
edge.endpoints_orphaned | warning | an endpoint targetRef names a pod that no longer exists |
edge.endpoints_unready | critical, warning | the endpoint ready-count disagrees with the selected pods (stale or lagging slices); critical at zero ready |
edge.backend_missing | critical | an Ingress backend service, or the port it names, does not exist |
edge.cert_expired | critical | a TLS certificate’s NotAfter is in the past |
edge.cert_expiring | warning | a TLS certificate expires within —cert-warn |
edge.cert_invalid | warning | tls.crt is missing or unparseable, or the secret is not kubernetes.io/tls |
edge.rbac_dangling | warning | a (Cluster)RoleBinding for the workload’s ServiceAccount points at a missing (Cluster)Role |
Output fields
Section titled “Output fields”Beyond the shared envelope fields (kind, severity, namespace, kind_of_object, name, reason, message, fingerprint, exempt_reason, exempt_expires):
| Field | Meaning |
|---|---|
section | which bundle section the finding belongs to: spec|delta|edges|radius|logs |
sections | on the bundle.target head finding: the sections that follow |
skipped | on the bundle.target head finding: comma-separated resources the List pass could not read (denied) or was told to omit (—lists) — the bundle is a documented partial, secret-free by default under a least-privilege role |
relation | radius neighbor’s relation to the target: upstream (routes/owns/governs it), downstream (it points at), lateral (shares a node/volume/config) |
hop | radius neighbor’s BFS depth from the target (1 = direct edge) |
triage_status | triage state from the matched record (investigating|triaged|actioned|escalated) — present only with —store on merged findings |
triage_root_cause | the incident agent’s root-cause hypothesis, from the matched triage-status record |
triage_action | the incident agent’s paper trail (PRs opened, escalations), from the matched triage-status record |
triage_session | incident session that wrote the matched triage-status record |
triage_age | how long ago the matched triage-status record was last updated |
labels | resource labels as sorted k=v pairs |
owner | controlling owner as Kind/name |
phase | status.phase, only when abnormal for the kind (zero nominal state) |
node | node the pod is scheduled on |
service_account | pod’s service account |
volumes | pod volumes as name:source (source names its referent, never its payload) |
container | container name (one spec.container finding per container) |
init | “true” when the container is an init container |
image | container image reference |
requests | resource requests as sorted k=v pairs |
limits | resource limits as sorted k=v pairs |
ports | container or service ports, compact ([name:]port[->target][/proto]) |
liveness | liveness probe one-liner (kind, target, non-default timings) |
readiness | readiness probe one-liner |
env | env vars; literal credential values are [REDACTED], valueFrom entries render as named references |
env_from | envFrom sources as kind:name |
replicas | desired replica count |
strategy | rollout strategy summary (type + non-default knobs) |
selector | workload/service selector as sorted k=v pairs |
type | Service or Secret type, only when non-default |
external_name | ExternalName service target |
session_affinity | service session affinity, only when not None |
keys | ConfigMap/Secret data KEYS with byte sizes — values are never rendered |
condition | abnormal status condition as Type=Status |
since | the condition’s lastTransitionTime |
spec | kinds without a dedicated renderer: sanitized spec flattened to path=value pairs |
restarts | container restart count |
exit_code | exit code of the container’s last termination |
last_state | reason of the container’s last termination (e.g. OOMKilled) |
age | how long the abnormal state has persisted |
desired | desired replica/scheduled count from spec |
ready | ready count from status |
updated | updated-to-current-revision count from status |
available | available count from status |
failed | failed pod count of a Job |
schedule | a CronJob’s spec.schedule |
expected | the activation a CronJob should have run and did not |
missed_runs | activations missed since the anchor; ≥N when the walk was capped |
anchor | what the missed count was measured from: last_schedule or creation |
time_zone | a CronJob’s spec.timeZone, when set |
last_schedule | a CronJob’s status.lastScheduleTime, or never |
active_jobs | Jobs a CronJob still has running |
taint | taint key indicating reclaim/drain |
pods | pods affected (behind a cordoned node or a PDB) |
healthy | currently healthy pods behind a PDB |
required | pods the PDB requires healthy |
addon | system add-on role: dns, proxy, cni, csi, metrics, connectivity |
resource | ResourceQuota resource name at or near its limit |
used | quota usage from status |
hard | quota hard limit from status |
pct | quota usage as percent of the hard limit |
template | log template; <*> marks positions that varied across merged lines |
count | lines merged into this cluster (on log.probe_noise: probe lines stripped) |
level | guessed log level (fatal|error|warn|info|debug) from token/field match |
first_seen | RFC3339 timestamp of the oldest merged line (from log timestamps when parseable) |
last_seen | RFC3339 timestamp of the newest merged line |
lang | stack-trace runtime on log.stacktrace findings: go|java|python |
frames | top stack frames on log.stacktrace findings, innermost first, ’ < ’ separated |
sample | one representative raw line, truncated and sanitized |
omitted_templates | clusters dropped by —max-templates (log.overflow only) |
omitted_lines | lines inside the dropped clusters (log.overflow only) |
workload | the target the edges were traced from as <Kind>/<namespace>/<name>, stamped on every finding — a workload, or the Service itself when entered from the service side |
likely_workload | on a Service-entry edge.selector_empty: the workload in that namespace whose pod labels best fit the broken selector, i.e. the one it was probably meant to select. Absent when two workloads fit equally well, because then naming one would be a guess |
volume | pod volume, or StatefulSet volumeClaimTemplate, whose reference is broken |
key | the referenced key that is missing from the ConfigMap/Secret |
selected | pods the Service selector currently selects |
endpoints | total endpoints across the Service’s EndpointSlices |
slices | how many EndpointSlices back the Service |
service | the Service a slice, Ingress backend, or StatefulSet serviceName refers to |
pod | pod named by an orphaned endpoint targetRef |
subject | TLS certificate subject (CN when set); never key material |
not_after | TLS certificate NotAfter, RFC 3339 |
days_left | whole days until NotAfter (negative = expired) |
via | how the broken reference is reached from the workload: mount, ingress, or imagePullSecret |
ingress | Ingress referencing the TLS secret, or the unserved Ingress itself |
host | Ingress rule host of the broken backend (empty for the default backend) |
path | Ingress rule path of the broken backend |
port | Service port (name or number) the Ingress backend asks for |
role_ref | dangling roleRef as <Kind>/<name> |
Output contract
Section titled “Output contract”Output: one finding per line (logfmt; —format=json for one JSON object
per line), keys in fixed order; healthy resources emit nothing. The final
line is always the summary: scanned=
Examples
Section titled “Examples”lookout bundle --workload=Deployment/prod/apilookout bundle --workload=StatefulSet/db/postgres --since=30m --format=jsonlookout bundle --incident='{"namespace":"prod","kind_of_object":"Pod","name":"api-6d5f8c-x2v9k"}'