summaryrefslogtreecommitdiff
path: root/scripts/health-check
blob: b1c34d03af397772e2c834814299042671142f22 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#!/bin/bash
#
# Periodic health check for doot.terst.org and its sub-services, with recovery.
#
# The failure this exists for: doot itself stays green while a sub-service
# behind the gateway is dead, so /scout/ returns 502 and nothing anywhere says
# so. Scout and hawaii were down for three and a half weeks, and claudomator
# crash-looped 426,000 times, before anyone opened the page and noticed.
#
# There is no mail/ntfy/apprise on this host, so "alerting" here means:
#   * a loud, greppable line in the journal
#   * a machine-readable status file the dashboard can surface later
#   * a non-zero exit, which makes the systemd unit go failed and show up in
#     `systemctl --failed`
#
# It also attempts recovery, because the common case (a unit that hit its
# restart limit and gave up) is fixed by exactly two commands and waiting weeks
# for a human to run them is the actual problem.
#
# Usage:
#   scripts/health-check [--no-recover]

set -uo pipefail

cd "$(dirname "$0")/.."
# shellcheck source=scripts/lib/site-checks.sh
. "$(dirname "$0")/lib/site-checks.sh"

STATUS_FILE="${STATUS_FILE:-${SITE_DIR}/data/health-status.json}"
recover=1
[ "${1:-}" = "--no-recover" ] && recover=0

log() { printf '%s %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$*"; }

rc=0
recovered=()
still_broken=()

# ── binaries ──────────────────────────────────────────────────────────────────
check_binaries; bin_rc=$?
if [ $bin_rc -ne 0 ]; then
    rc=1
    if [ $recover -eq 1 ]; then
        # A lost execute bit is recoverable in place and is the known failure
        # mode here, so fix it rather than just reporting it.
        for f in "${SITE_BINARIES[@]}"; do
            if [ -f "$f" ] && [ ! -x "$f" ]; then
                log "HEALTH recovering: chmod +x $f"
                chmod +x "$f" && recovered+=("chmod +x $f")
            fi
        done
    fi
fi

# ── services ──────────────────────────────────────────────────────────────────
# Called directly, NOT in a command substitution: check_services sets
# FAILED_SERVICES, and a subshell would populate the array then throw it away,
# leaving the recovery loop below with nothing to do.
check_services; svc_rc=$?
if [ $svc_rc -ne 0 ]; then
    rc=1
    if [ $recover -eq 1 ]; then
        for s in "${FAILED_SERVICES[@]}"; do
            log "HEALTH recovering: restarting $s"
            systemctl reset-failed "$s" 2>/dev/null || true
            if systemctl start "$s" 2>/dev/null; then
                sleep 3
                if [ "$(systemctl is-active "$s" 2>/dev/null || true)" = "active" ]; then
                    log "HEALTH recovered: $s is active"
                    recovered+=("restarted $s")
                else
                    log "HEALTH ALERT: $s still not active after restart"
                    still_broken+=("$s")
                fi
            else
                log "HEALTH ALERT: failed to start $s"
                still_broken+=("$s")
            fi
        done
    fi
fi

# ── routes, re-checked after any recovery ─────────────────────────────────────
check_routes; route_rc=$?
[ $route_rc -ne 0 ] && rc=1

# A recovered site is healthy now even though it was not a moment ago. Re-run
# the checks so the final verdict describes the current state, not the state we
# walked in on.
if [ $rc -ne 0 ] && [ ${#recovered[@]} -gt 0 ]; then
    echo "==> Re-checking after recovery..."
    rc=0
    check_binaries >/dev/null || rc=1
    check_services >/dev/null || rc=1
    check_routes   >/dev/null || rc=1
fi

# ── status file ───────────────────────────────────────────────────────────────
mkdir -p "$(dirname "$STATUS_FILE")" 2>/dev/null || true
{
    printf '{\n'
    printf '  "checked_at": "%s",\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
    printf '  "healthy": %s,\n' "$([ $rc -eq 0 ] && echo true || echo false)"
    printf '  "recovered": ['
    for i in "${!recovered[@]}"; do
        [ "$i" -gt 0 ] && printf ', '
        printf '"%s"' "${recovered[$i]}"
    done
    printf '],\n'
    printf '  "still_broken": ['
    for i in "${!still_broken[@]}"; do
        [ "$i" -gt 0 ] && printf ', '
        printf '"%s"' "${still_broken[$i]}"
    done
    printf ']\n}\n'
} > "$STATUS_FILE" 2>/dev/null || log "HEALTH warn: could not write $STATUS_FILE"

if [ $rc -eq 0 ]; then
    if [ ${#recovered[@]} -gt 0 ]; then
        log "HEALTH ALERT recovered: ${recovered[*]}"
        # Deliberately non-zero is NOT used here: the site is healthy now. The
        # ALERT line in the journal is the durable record that something had
        # broken and was silently fixed.
    fi
    exit 0
fi

log "HEALTH ALERT: doot.terst.org is unhealthy; still broken: ${still_broken[*]:-see checks above}"
exit 1