Add what-broke status signals

This commit is contained in:
juvdiaz 2026-06-29 16:53:44 -06:00
parent 5f8fdfef3d
commit 329a3d1abd
3 changed files with 122 additions and 2 deletions

View File

@ -274,7 +274,9 @@ Run a read-only health snapshot from the Debian server with:
It reports host memory/disk, systemd services, Docker Compose stacks, It reports host memory/disk, systemd services, Docker Compose stacks,
Kubernetes health when the API is reachable, Pimox worker VM status, RPi Kubernetes health when the API is reachable, Pimox worker VM status, RPi
services, Tailscale, and key local/public HTTP endpoints. services, Tailscale, key local/public HTTP endpoints, and "what broke" signals
such as deployment readiness, pod restarts, node pressure, disk pressure,
Traefik 5xx/404 log evidence, and recent Gitea errors.
Focused doctor commands run narrower read-only checks and print the most likely Focused doctor commands run narrower read-only checks and print the most likely
next step: next step:

View File

@ -48,7 +48,9 @@ cluster state.
`status` `status`
: Print a cascade health report for the configured homelab, from bootstrap : Print a cascade health report for the configured homelab, from bootstrap
readiness through public website health. Set `LAB_STATUS_DETAILS=true` to append readiness through public website health. Set `LAB_STATUS_DETAILS=true` to append
the older detailed tables. the older detailed tables. The cascade includes "what broke" signals for recent
deployment readiness, pod restarts, node pressure, disk pressure, Traefik
5xx/404 log evidence, and recent Gitea errors.
`nuke` `nuke`
: Destroy Kubernetes state and Pimox worker VMs. Requires : Destroy Kubernetes state and Pimox worker VMs. Requires
@ -214,6 +216,17 @@ model.
`HOMELAB_INVENTORY_FILE` `HOMELAB_INVENTORY_FILE`
: Path to the canonical non-secret inventory. Defaults to `./homelab.yml`. : Path to the canonical non-secret inventory. Defaults to `./homelab.yml`.
`LAB_STATUS_LOG_SINCE`
: Log window for status log checks. Defaults to `30m`.
`LAB_STATUS_RESTART_THRESHOLD`
: Pod restart count that turns the status restart-pressure check into a warning.
Defaults to `3`.
`LAB_STATUS_DISK_USE_THRESHOLD`
: Disk usage percentage that turns the status disk check into a warning.
Defaults to `85`.
`HOMELAB_STATE_DIR` `HOMELAB_STATE_DIR`
: Local runtime state directory. Defaults to : Local runtime state directory. Defaults to
`${XDG_DATA_HOME:-$HOME/.local/share}/homelab`. `${XDG_DATA_HOME:-$HOME/.local/share}/homelab`.

105
jeannie
View File

@ -4767,6 +4767,103 @@ status_no_problem_pods() {
fi fi
} }
status_recent_deployments() {
local rows
rows="$(
kubectl --kubeconfig "${KUBECONFIG_PATH}" get deployments -A \
-o custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,READY:.status.readyReplicas,DESIRED:.spec.replicas,UPDATED:.status.updatedReplicas,AVAILABLE:.status.availableReplicas,AGE:.metadata.creationTimestamp' \
--no-headers 2>/dev/null |
awk '$3 != $4 || $5 != $4 || $6 != $4 { print }'
)"
if [[ -n "${rows}" ]]; then
printf '%s\n' "${rows}" >&2
return 1
fi
}
status_pod_restart_pressure() {
local threshold="${LAB_STATUS_RESTART_THRESHOLD:-3}"
local rows
rows="$(
kubectl --kubeconfig "${KUBECONFIG_PATH}" get pods -A --no-headers 2>/dev/null |
awk -v threshold="${threshold}" '$5 + 0 >= threshold { print $1 "/" $2 ": restarts=" $5 " age=" $6 }'
)"
if [[ -n "${rows}" ]]; then
printf '%s\n' "${rows}" >&2
return 1
fi
}
status_node_pressure() {
local rows
rows="$(
kubectl --kubeconfig "${KUBECONFIG_PATH}" get nodes -o jsonpath='{range .items[*]}{.metadata.name}{" "}{range .status.conditions[*]}{.type}{"="}{.status}{" "}{end}{"\n"}{end}' 2>/dev/null |
awk '/MemoryPressure=True|DiskPressure=True|PIDPressure=True|NetworkUnavailable=True/ { print }'
)"
if [[ -n "${rows}" ]]; then
printf '%s\n' "${rows}" >&2
return 1
fi
}
status_disk_pressure() {
local threshold="${LAB_STATUS_DISK_USE_THRESHOLD:-85}"
local rows
rows="$(df -P / /data 2>/dev/null | awk -v threshold="${threshold}" 'NR > 1 { gsub(/%/, "", $5); if ($5 + 0 >= threshold) print $6 ": " $5 "% used" }')"
if [[ -n "${rows}" ]]; then
printf '%s\n' "${rows}" >&2
return 1
fi
}
status_traefik_error_rates() {
local since="${LAB_STATUS_LOG_SINCE:-30m}"
local sample
local count_502
local count_404
sample="$(kubectl --kubeconfig "${KUBECONFIG_PATH}" -n traefik logs -l app.kubernetes.io/name=traefik --since="${since}" --tail=2000 2>/dev/null || true)"
if [[ -z "${sample}" ]]; then
echo "no recent Traefik logs available" >&2
return 1
fi
count_502="$(grep -Eac '(^|[^0-9])50[234]([^0-9]|$)|level=error' <<<"${sample}" || true)"
count_404="$(grep -Eac '(^|[^0-9])404([^0-9]|$)' <<<"${sample}" || true)"
if ((count_502 > 0)); then
printf '5xx/error lines in last %s: %s\n' "${since}" "${count_502}" >&2
return 1
fi
printf '404 lines in last %s: %s\n' "${since}" "${count_404}" >&2
}
status_gitea_recent_errors() {
local since="${LAB_STATUS_LOG_SINCE:-30m}"
local container="${GITEA_CONTAINER_NAME:-homelab-gitea}"
local sample
local matches
if ! command -v docker >/dev/null 2>&1; then
echo "docker not installed" >&2
return 1
fi
if ! sudo docker inspect "${container}" >/dev/null 2>&1; then
echo "container ${container} not found" >&2
return 1
fi
sample="$(sudo docker logs --since "${since}" "${container}" 2>&1 || true)"
matches="$(grep -Eai 'error|panic|fatal|authentication failed|denied| 50[0-9] ' <<<"${sample}" | tail -10 || true)"
if [[ -n "${matches}" ]]; then
printf '%s\n' "${matches}" >&2
return 1
fi
}
status_registry_ok() { status_registry_ok() {
status_http_ok "http://${LAB_REGISTRY_ENDPOINT:-192.168.100.73:30500}/v2/" status_http_ok "http://${LAB_REGISTRY_ENDPOINT:-192.168.100.73:30500}/v2/"
} }
@ -4843,6 +4940,14 @@ status_cascade_report() {
status_cascade_check "Kubernetes nodes Ready" status_kubernetes_nodes_ready || failures=$((failures + 1)) status_cascade_check "Kubernetes nodes Ready" status_kubernetes_nodes_ready || failures=$((failures + 1))
status_cascade_warn "No problem pods" status_no_problem_pods status_cascade_warn "No problem pods" status_no_problem_pods
status_section "What Broke Signals"
status_cascade_warn "Recent deployments healthy" status_recent_deployments
status_cascade_warn "Pod restart pressure" status_pod_restart_pressure
status_cascade_warn "Node pressure" status_node_pressure
status_cascade_warn "Disk usage" status_disk_pressure
status_cascade_warn "Traefik 5xx/404 signals" status_traefik_error_rates
status_cascade_warn "Gitea recent errors" status_gitea_recent_errors
status_section "Platform And Apps" status_section "Platform And Apps"
status_cascade_check "Local registry" status_registry_ok || failures=$((failures + 1)) status_cascade_check "Local registry" status_registry_ok || failures=$((failures + 1))
status_cascade_check "Traefik deployment" status_kubernetes_deployment_ready traefik traefik || failures=$((failures + 1)) status_cascade_check "Traefik deployment" status_kubernetes_deployment_ready traefik traefik || failures=$((failures + 1))