diff options
Diffstat (limited to 'statuspage/render.sh')
| -rw-r--r-- | statuspage/render.sh | 155 |
1 files changed, 155 insertions, 0 deletions
diff --git a/statuspage/render.sh b/statuspage/render.sh new file mode 100644 index 0000000..22df5e7 --- /dev/null +++ b/statuspage/render.sh @@ -0,0 +1,155 @@ +#!/bin/sh +# statuspage/render.sh +# Orchestrator + renderer for the dandokmang status console. +# +# Runs every executable script in checks/, collects their JSONL rows, groups +# them by section, and writes a static HTML status page. A check script +# that fails or emits garbage is reported as a down/warn row instead of +# aborting the whole render. See README.md for the JSONL contract that +# check scripts must follow. +set -u + +SELF_DIR=$(cd "$(dirname "$0")" && pwd) +CHECKS_DIR="$SELF_DIR/checks" +OUT=${STATUSPAGE_OUT:-/usr/local/www/status/index.html} +HOST=$(hostname) +NOW=$(date "+%Y-%m-%d %H:%M:%S %Z") + +. "$SELF_DIR/lib/common.sh" + +JSONL=$(mktemp) +AWKFILE=$(mktemp) +trap 'rm -f "$JSONL" "$AWKFILE"' EXIT INT TERM + +# valid_jsonl OUTPUT +# Cheap structural check: every non-empty line must look like one of our +# flat {"section":.., "label":.., "value":.., "status":..} objects. This +# isn't a general JSON validator - it doesn't need to be, since the only +# producer of this format is json_line() in lib/common.sh. +valid_jsonl() { + bad=$(printf '%s\n' "$1" | grep -v '^$' | \ + grep -Evc '^\{.*"section"[[:space:]]*:.*"label"[[:space:]]*:.*"value"[[:space:]]*:.*"status"[[:space:]]*:.*\}$') + [ "$bad" -eq 0 ] +} + +for script in "$CHECKS_DIR"/*.sh; do + [ -e "$script" ] || continue + [ -x "$script" ] || continue + name=$(basename "$script") + + output=$("$script" 2>/dev/null) + rc=$? + + if [ "$rc" -ne 0 ]; then + json_line "checks" "$name" "check exited with status $rc" "down" + elif [ -z "$output" ]; then + json_line "checks" "$name" "no output" "warn" + elif ! valid_jsonl "$output"; then + json_line "checks" "$name" "malformed JSONL output" "warn" + else + printf '%s\n' "$output" + fi +done >"$JSONL" + +# Render with awk: group rows by section (fixed order hardware/host +# services/jails, then any other section in first-seen order - e.g. +# "checks" for broken-script rows), preserving row order within a section. +cat >"$AWKFILE" <<'AWKEOF' +function jsonval(line, key, pat, start, i, c, nc, res) { + pat = "\"" key "\"[ \t]*:[ \t]*\"" + if (!match(line, pat)) return "" + start = RSTART + RLENGTH + res = "" + for (i = start; i <= length(line); i++) { + c = substr(line, i, 1) + if (c == "\\") { + nc = substr(line, i + 1, 1) + if (nc == "n") res = res "\n" + else res = res nc + i++ + continue + } + if (c == "\"") break + res = res c + } + return res +} + +function htmlesc(s) { + gsub(/&/, "\\&", s) + gsub(/</, "\\<", s) + gsub(/>/, "\\>", s) + return s +} + +function sqclass(status) { + if (status == "ok") return "green" + if (status == "warn") return "yellow" + return "red" +} + +BEGIN { + order[1] = "hardware"; order[2] = "host services"; order[3] = "jails" + norder = 3 + for (i = 1; i <= norder; i++) known[order[i]] = 1 +} + +{ + if ($0 == "") next + section = jsonval($0, "section") + label = jsonval($0, "label") + value = jsonval($0, "value") + status = jsonval($0, "status") + if (section == "") next + + if (!(section in seen)) { + seen[section] = 1 + if (!(section in known)) { norder++; order[norder] = section } + } + n = ++count[section] + L[section, n] = label + V[section, n] = value + S[section, n] = status +} + +END { + print "<!DOCTYPE html>" + print "<html>" + print "<head>" + print "<title>" htmlesc(host) " status</title>" + print "<meta http-equiv=\"refresh\" content=\"30\">" + print "<style>" + print "body { background:#111; color:#ddd; font-family: monospace; padding: 2em; }" + print "h1 { color: #6ff; margin-bottom: 0; }" + print "h2 { color: #9cf; border-bottom: 1px solid #333; padding-bottom: 4px; }" + print "table { border-collapse: collapse; width: 100%; max-width: 600px; margin-bottom: 1.5em; }" + print "td { padding: 6px 10px; border-bottom: 1px solid #222; }" + print ".sq { display:inline-block; width:12px; height:12px; border-radius:2px; margin-right:6px; vertical-align:middle; }" + print ".green { background:#3c3; }" + print ".yellow { background:#cc3; }" + print ".red { background:#c33; }" + print ".dim { color:#888; font-size: 0.85em; }" + print "</style>" + print "</head>" + print "<body>" + print "<h1>" htmlesc(host) "</h1>" + print "<p class=\"dim\">updated " htmlesc(now) "</p>" + + for (i = 1; i <= norder; i++) { + sect = order[i] + if (!(sect in count)) continue + print "<h2>" htmlesc(sect) "</h2>" + print "<table>" + for (j = 1; j <= count[sect]; j++) { + valcell = (V[sect, j] == "") ? "" : "<td>" htmlesc(V[sect, j]) "</td>" + print "<tr><td><span class=\"sq " sqclass(S[sect, j]) "\"></span>" htmlesc(L[sect, j]) "</td>" valcell "</tr>" + } + print "</table>" + } + + print "</body>" + print "</html>" +} +AWKEOF + +awk -v host="$HOST" -v now="$NOW" -f "$AWKFILE" "$JSONL" >"$OUT" |
