Files
stack/infra/images/zotero/zotero-healthcheck.sh
kert 225ce01d6b fix(zotero): heal kasmxproxy display-bridge race + add real healthcheck/observability
zotero.fhirworx.io rendered a blank desktop: KDE+Zotero draw on Xvfb :20,
but kasmvnc serves :21, and the kasmxproxy mirror that bridges them never
started. The stock kasmvnc-entrypoint.sh blocks on `until [ -S
/tmp/.X11-unix/X21 ]` (a filesystem socket path) while entrypoint.sh's
concurrent `rm -rf /tmp/.X*` unlinks it — Xvnc keeps its fd + abstract
socket open and serves a blank :21 forever. A startup race, hence the
intermittent "doesn't open" with every process reporting healthy.

- kasmxproxy-guard.sh: supervised watchdog that probes :20/:21 via xdpyinfo
  (not the racy socket file) and (re)launches kasmxproxy, self-healing if it
  dies. Emits structured `zotero_event=` logs to container stdout + a
  heartbeat file.
- zotero-healthcheck.sh + compose healthcheck: detect the real failure mode
  (bridge down / :21 unreachable / KDE dead / stale heartbeat) that
  container/nginx/Xvnc liveness all miss.
- promtail: extract zotero_event into a Loki label.
- Grafana: Zotero Desktop dashboard + alert on sustained bridge failure.
- otel-cli baked in; OTLP tracing dormant by default (enabling it requires
  attaching zotero to the observability network — documented opt-in, left
  off to preserve gateway-only isolation).

Verified live: guard self-heals on kasmxproxy kill; healthcheck passes
healthy / fails on stale heartbeat.
2026-06-17 19:49:25 -04:00

26 lines
1.1 KiB
Bash
Executable File

#!/bin/bash
# Zotero container healthcheck.
#
# The container is only "healthy" if the desktop the browser actually sees
# (KasmVNC display :21) is being driven. This probes the real failure mode the
# stock checks miss: a blank :21 because the :20->:21 kasmxproxy bridge died
# (see kasmxproxy-guard.sh). Container-up / nginx-up / Xvnc-up are all true in
# that failure, which is exactly why it went unnoticed.
set -u
DST_DISPLAY="${KASMVNC_DISPLAY:-:21}"
HEARTBEAT="${ZOTERO_HEARTBEAT:-/tmp/kasmxproxy-guard.heartbeat}"
MAX_AGE="${ZOTERO_HEARTBEAT_MAX_AGE:-30}"
fail() { echo "unhealthy: $1" >&2; exit 1; }
pgrep -x kasmxproxy >/dev/null 2>&1 || fail "kasmxproxy (desktop bridge) not running"
DISPLAY="$DST_DISPLAY" xdpyinfo >/dev/null 2>&1 || fail "KasmVNC display ${DST_DISPLAY} unreachable"
pgrep -x plasmashell >/dev/null 2>&1 || fail "KDE session (plasmashell) not running"
[ -f "$HEARTBEAT" ] || fail "guard heartbeat missing"
age=$(( $(date +%s) - $(stat -c %Y "$HEARTBEAT") ))
[ "$age" -lt "$MAX_AGE" ] || fail "guard heartbeat stale (${age}s >= ${MAX_AGE}s)"
echo "healthy: bridge up, ${DST_DISPLAY} reachable, session alive (heartbeat ${age}s)"