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.
This commit is contained in:
17
compose.yml
17
compose.yml
@@ -317,6 +317,13 @@ services:
|
|||||||
# the UI in a FATAL restart loop. "auto" tells kasmvnc to figure it
|
# the UI in a FATAL restart loop. "auto" tells kasmvnc to figure it
|
||||||
# out itself.
|
# out itself.
|
||||||
- TURN_EXTERNAL_IP=auto
|
- TURN_EXTERNAL_IP=auto
|
||||||
|
# OTLP tracing of the desktop-bridge lifecycle is OFF by default. To
|
||||||
|
# enable: add `observability` to `networks` below and uncomment the
|
||||||
|
# endpoint. Left off deliberately — zotero is a GPU desktop kept on
|
||||||
|
# `gateway` only; attaching it to `observability` widens its blast
|
||||||
|
# radius. The structured Loki logs + Grafana alert cover the same
|
||||||
|
# failure without crossing that isolation boundary.
|
||||||
|
# - OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
reservations:
|
reservations:
|
||||||
@@ -324,6 +331,16 @@ services:
|
|||||||
- driver: nvidia
|
- driver: nvidia
|
||||||
count: 1
|
count: 1
|
||||||
capabilities: [gpu]
|
capabilities: [gpu]
|
||||||
|
# Detects the blank-desktop failure (dead :20->:21 kasmxproxy bridge) that
|
||||||
|
# container/nginx/Xvnc liveness all miss. The image bakes an equivalent
|
||||||
|
# HEALTHCHECK; this override tunes timing for the GPU desktop's slow boot
|
||||||
|
# (first start may install the NVIDIA userspace driver).
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "/usr/local/bin/zotero-healthcheck"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
start_period: 180s
|
||||||
labels:
|
labels:
|
||||||
- "promtail=true"
|
- "promtail=true"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
88
infra/grafana/dashboards/zotero-desktop.json
Normal file
88
infra/grafana/dashboards/zotero-desktop.json
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
{
|
||||||
|
"title": "Zotero Desktop",
|
||||||
|
"uid": "zotero-desktop",
|
||||||
|
"tags": ["zotero", "desktop", "kasmvnc"],
|
||||||
|
"timezone": "browser",
|
||||||
|
"schemaVersion": 39,
|
||||||
|
"time": { "from": "now-6h", "to": "now" },
|
||||||
|
"refresh": "30s",
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "Bridge down events (1h)",
|
||||||
|
"type": "stat",
|
||||||
|
"gridPos": { "x": 0, "y": 0, "w": 6, "h": 4 },
|
||||||
|
"datasource": { "uid": "loki" },
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "sum(count_over_time({container=\"zotero\", zotero_event=\"bridge_down\"} [1h]))",
|
||||||
|
"queryType": "instant"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "background" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{ "color": "green", "value": null },
|
||||||
|
{ "color": "red", "value": 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"title": "Bridge start failures (1h)",
|
||||||
|
"type": "stat",
|
||||||
|
"gridPos": { "x": 6, "y": 0, "w": 6, "h": 4 },
|
||||||
|
"datasource": { "uid": "loki" },
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "sum(count_over_time({container=\"zotero\", zotero_event=\"bridge_start_failed\"} [1h]))",
|
||||||
|
"queryType": "instant"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "background" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{ "color": "green", "value": null },
|
||||||
|
{ "color": "red", "value": 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"title": "Desktop-bridge events",
|
||||||
|
"type": "timeseries",
|
||||||
|
"gridPos": { "x": 12, "y": 0, "w": 12, "h": 8 },
|
||||||
|
"datasource": { "uid": "loki" },
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "sum by (zotero_event) (count_over_time({container=\"zotero\"} | zotero_event=~\".+\" [5m]))",
|
||||||
|
"legendFormat": "{{zotero_event}}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"title": "Bridge + watchdog log tail",
|
||||||
|
"type": "logs",
|
||||||
|
"gridPos": { "x": 0, "y": 4, "w": 12, "h": 12 },
|
||||||
|
"datasource": { "uid": "loki" },
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "{container=\"zotero\"} | zotero_event=~\".+\"",
|
||||||
|
"queryType": "range"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"options": { "showTime": true, "sortOrder": "Descending", "wrapLogMessage": true }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
85
infra/grafana/provisioning/alerting/zotero.yml
Normal file
85
infra/grafana/provisioning/alerting/zotero.yml
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# Grafana unified-alerting provisioning for the Zotero desktop bridge.
|
||||||
|
#
|
||||||
|
# Fires when the kasmxproxy-guard watchdog reports the :20->:21 mirror as
|
||||||
|
# genuinely failing (kasmxproxy won't start, or a display is down) for a
|
||||||
|
# sustained window — i.e. the browser would see a blank desktop. The
|
||||||
|
# watchdog's self-healing bridge_down -> bridge_up blips are intentionally
|
||||||
|
# NOT alerted (they recover within seconds); we key off bridge_start_failed
|
||||||
|
# and *_display_down, gated by `for: 5m`.
|
||||||
|
#
|
||||||
|
# NOTE: alert-rule provisioning JSON shifts between Grafana versions (this
|
||||||
|
# stack tracks grafana:latest). Verify in the UI after deploy:
|
||||||
|
# Alerting -> Alert rules -> "Zotero desktop bridge failing".
|
||||||
|
# Routing to an actual channel additionally needs a contact point /
|
||||||
|
# notification policy; unrouted, the rule still shows as Firing in the UI.
|
||||||
|
apiVersion: 1
|
||||||
|
|
||||||
|
groups:
|
||||||
|
- orgId: 1
|
||||||
|
name: zotero
|
||||||
|
folder: Zotero
|
||||||
|
interval: 1m
|
||||||
|
rules:
|
||||||
|
- uid: zotero-bridge-failing
|
||||||
|
title: Zotero desktop bridge failing
|
||||||
|
condition: C
|
||||||
|
for: 5m
|
||||||
|
noDataState: OK
|
||||||
|
execErrState: OK
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
service: zotero
|
||||||
|
annotations:
|
||||||
|
summary: >-
|
||||||
|
Zotero KDE desktop bridge (:20->:21) is failing — the browser sees a
|
||||||
|
blank screen at zotero.fhirworx.io.
|
||||||
|
description: >-
|
||||||
|
kasmxproxy-guard reported bridge_start_failed or a display down for
|
||||||
|
5m+. The KasmVNC display the browser connects to is not being
|
||||||
|
driven. Check `docker logs zotero` and the "Zotero Desktop"
|
||||||
|
dashboard.
|
||||||
|
data:
|
||||||
|
- refId: A
|
||||||
|
relativeTimeRange:
|
||||||
|
from: 600
|
||||||
|
to: 0
|
||||||
|
datasourceUid: loki
|
||||||
|
model:
|
||||||
|
refId: A
|
||||||
|
datasource:
|
||||||
|
type: loki
|
||||||
|
uid: loki
|
||||||
|
queryType: instant
|
||||||
|
expr: >-
|
||||||
|
sum(count_over_time({container="zotero",
|
||||||
|
zotero_event=~"bridge_start_failed|dst_display_down|src_display_down"}
|
||||||
|
[5m]))
|
||||||
|
- refId: B
|
||||||
|
relativeTimeRange:
|
||||||
|
from: 600
|
||||||
|
to: 0
|
||||||
|
datasourceUid: __expr__
|
||||||
|
model:
|
||||||
|
refId: B
|
||||||
|
type: reduce
|
||||||
|
reducer: last
|
||||||
|
expression: A
|
||||||
|
datasource:
|
||||||
|
type: __expr__
|
||||||
|
uid: __expr__
|
||||||
|
- refId: C
|
||||||
|
relativeTimeRange:
|
||||||
|
from: 600
|
||||||
|
to: 0
|
||||||
|
datasourceUid: __expr__
|
||||||
|
model:
|
||||||
|
refId: C
|
||||||
|
type: threshold
|
||||||
|
expression: B
|
||||||
|
datasource:
|
||||||
|
type: __expr__
|
||||||
|
uid: __expr__
|
||||||
|
conditions:
|
||||||
|
- evaluator:
|
||||||
|
type: gt
|
||||||
|
params: [0]
|
||||||
@@ -32,4 +32,36 @@ RUN mkdir -p /home/ubuntu/Desktop \
|
|||||||
&& chmod +x /home/ubuntu/Desktop/zotero.desktop \
|
&& chmod +x /home/ubuntu/Desktop/zotero.desktop \
|
||||||
&& chown -R ubuntu:ubuntu /home/ubuntu/Desktop
|
&& chown -R ubuntu:ubuntu /home/ubuntu/Desktop
|
||||||
|
|
||||||
|
# Desktop-bridge watchdog + healthcheck deps.
|
||||||
|
# x11-utils → xdpyinfo (probe X reachability of :20/:21)
|
||||||
|
# procps → pgrep (detect kasmxproxy / plasmashell)
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
x11-utils \
|
||||||
|
procps \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# otel-cli: enables OTLP span emission from the bridge watchdog. Dormant by
|
||||||
|
# default (the guard only emits when OTEL_EXPORTER_OTLP_ENDPOINT is set and
|
||||||
|
# this container is attached to the `observability` network — see compose.yml).
|
||||||
|
# Best-effort install so a release-asset hiccup never breaks the image build.
|
||||||
|
ARG OTEL_CLI_VERSION=0.4.5
|
||||||
|
RUN arch="$(dpkg --print-architecture | sed -e 's/amd64/amd64/' -e 's/arm64/arm64/')" \
|
||||||
|
&& curl -fsSL "https://github.com/equinix-labs/otel-cli/releases/download/v${OTEL_CLI_VERSION}/otel-cli_${OTEL_CLI_VERSION}_linux_${arch}.tar.gz" \
|
||||||
|
-o /tmp/otel-cli.tgz \
|
||||||
|
&& tar -xzf /tmp/otel-cli.tgz -C /usr/local/bin otel-cli \
|
||||||
|
&& chmod +x /usr/local/bin/otel-cli \
|
||||||
|
&& rm -f /tmp/otel-cli.tgz \
|
||||||
|
|| echo 'WARN: otel-cli install failed; OTLP tracing will be unavailable'
|
||||||
|
|
||||||
|
# Desktop-bridge watchdog (fixes the kasmxproxy startup race) + healthcheck.
|
||||||
|
COPY infra/images/zotero/kasmxproxy-guard.sh /usr/local/bin/kasmxproxy-guard.sh
|
||||||
|
COPY infra/images/zotero/zotero-healthcheck.sh /usr/local/bin/zotero-healthcheck
|
||||||
|
COPY infra/images/zotero/kasmxproxy-guard.conf /etc/supervisor/conf.d/kasmxproxy-guard.conf
|
||||||
|
RUN chmod +x /usr/local/bin/kasmxproxy-guard.sh /usr/local/bin/zotero-healthcheck
|
||||||
|
|
||||||
|
# Detect a blank desktop (dead :20->:21 bridge), which container/nginx/Xvnc
|
||||||
|
# liveness all miss. Compose overrides interval/start_period as needed.
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=180s --retries=5 \
|
||||||
|
CMD /usr/local/bin/zotero-healthcheck
|
||||||
|
|
||||||
USER ubuntu
|
USER ubuntu
|
||||||
|
|||||||
20
infra/images/zotero/kasmxproxy-guard.conf
Normal file
20
infra/images/zotero/kasmxproxy-guard.conf
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
; Supervised watchdog that keeps the KDE desktop (:20) mirrored onto the
|
||||||
|
; KasmVNC display (:21). Fixes the startup race where the stock
|
||||||
|
; kasmvnc-entrypoint.sh never launches kasmxproxy. See kasmxproxy-guard.sh.
|
||||||
|
;
|
||||||
|
; priority 25 > kasmvnc's 20 so we start after Xvnc; autorestart keeps the
|
||||||
|
; watchdog itself alive. Logs go to /dev/fd/1 (the container's stdout) so
|
||||||
|
; promtail -> Loki picks them up — supervisord child logs default to files
|
||||||
|
; under /tmp that never reach `docker logs`.
|
||||||
|
[program:kasmxproxy-guard]
|
||||||
|
command=/usr/local/bin/kasmxproxy-guard.sh
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
startsecs=3
|
||||||
|
startretries=9999
|
||||||
|
stopasgroup=true
|
||||||
|
killasgroup=true
|
||||||
|
priority=25
|
||||||
|
stdout_logfile=/dev/fd/1
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
redirect_stderr=true
|
||||||
92
infra/images/zotero/kasmxproxy-guard.sh
Executable file
92
infra/images/zotero/kasmxproxy-guard.sh
Executable file
@@ -0,0 +1,92 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# kasmxproxy-guard — keep the KDE desktop (Xvfb :20) mirrored onto the
|
||||||
|
# KasmVNC display (:21) that the browser actually connects to.
|
||||||
|
#
|
||||||
|
# Why this exists
|
||||||
|
# ---------------
|
||||||
|
# The stock kasmvnc-entrypoint.sh launches kasmxproxy (the :20->:21 mirror)
|
||||||
|
# only after `until [ -S /tmp/.X11-unix/X21 ]` — a guard that tests the
|
||||||
|
# *filesystem* socket path. entrypoint.sh runs `rm -rf /tmp/.X*` concurrently
|
||||||
|
# (both are supervisord programs with no ordering barrier between them) and
|
||||||
|
# unlinks /tmp/.X11-unix/X21 while Xvnc keeps its fd + abstract socket
|
||||||
|
# (@/tmp/.X11-unix/X21) open and fully serving. The stock guard then loops
|
||||||
|
# forever, kasmxproxy never starts, and :21 stays blank — the desktop
|
||||||
|
# "doesn't open" even though every process reports healthy. It's a startup
|
||||||
|
# race, so it's intermittent.
|
||||||
|
#
|
||||||
|
# This watchdog ignores the socket *file* and probes X reachability directly
|
||||||
|
# (xdpyinfo), (re)launching kasmxproxy whenever both displays are up. It also
|
||||||
|
# emits structured, single-line logs to stdout (scraped by promtail -> Loki)
|
||||||
|
# and maintains a heartbeat file consumed by the container healthcheck.
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
SRC_DISPLAY="${DISPLAY:-:20}" # where KDE actually renders (Xvfb)
|
||||||
|
DST_DISPLAY="${KASMVNC_DISPLAY:-:21}" # what KasmVNC serves to the browser
|
||||||
|
FPS="${DISPLAY_REFRESH:-60}"
|
||||||
|
HEARTBEAT="${ZOTERO_HEARTBEAT:-/tmp/kasmxproxy-guard.heartbeat}"
|
||||||
|
POLL="${ZOTERO_GUARD_POLL:-5}"
|
||||||
|
OTEL_ENDPOINT="${OTEL_EXPORTER_OTLP_ENDPOINT:-}"
|
||||||
|
|
||||||
|
# Structured, promtail-friendly: `level=` and `zotero_event=` are extracted
|
||||||
|
# into Loki labels by infra/loki/promtail-config.yml.
|
||||||
|
log() {
|
||||||
|
local level="$1" event="$2"; shift 2
|
||||||
|
printf 'level=%s zotero_event=%s %s\n' "$level" "$event" "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Dormant unless an OTLP endpoint is configured AND otel-cli is installed.
|
||||||
|
# Enabling tracing also requires attaching this container to the
|
||||||
|
# `observability` network — see compose.yml. Best-effort; never fatal.
|
||||||
|
emit_span() {
|
||||||
|
[ -n "$OTEL_ENDPOINT" ] || return 0
|
||||||
|
command -v otel-cli >/dev/null 2>&1 || return 0
|
||||||
|
otel-cli span \
|
||||||
|
--service zotero \
|
||||||
|
--name "$1" \
|
||||||
|
--kind internal \
|
||||||
|
--attrs "zotero.event=$1,zotero.src_display=${SRC_DISPLAY},zotero.dst_display=${DST_DISPLAY}" \
|
||||||
|
--endpoint "$OTEL_ENDPOINT" --protocol grpc --timeout 2s >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
x_reachable() { DISPLAY="$1" xdpyinfo >/dev/null 2>&1; }
|
||||||
|
|
||||||
|
# A live KDE session means something is actually drawing on :20. Without this
|
||||||
|
# the bridge could be "up" while mirroring a blank screen (KDE crash).
|
||||||
|
session_alive() {
|
||||||
|
pgrep -x plasmashell >/dev/null 2>&1 || pgrep -x kwin_x11 >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
start_bridge() {
|
||||||
|
log error bridge_down "msg=\"kasmxproxy not running; starting it\" src=${SRC_DISPLAY} dst=${DST_DISPLAY}"
|
||||||
|
emit_span bridge_down
|
||||||
|
DISPLAY="$SRC_DISPLAY" kasmxproxy -a "$SRC_DISPLAY" -v "$DST_DISPLAY" -f "$FPS" \
|
||||||
|
>/tmp/kasmxproxy.log 2>&1 &
|
||||||
|
sleep 2
|
||||||
|
if pgrep -x kasmxproxy >/dev/null 2>&1; then
|
||||||
|
log info bridge_up "pid=$(pgrep -x kasmxproxy | head -n1)"
|
||||||
|
emit_span bridge_up
|
||||||
|
else
|
||||||
|
log error bridge_start_failed "msg=\"kasmxproxy exited immediately; see /tmp/kasmxproxy.log\""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
log info guard_start "msg=\"watching ${SRC_DISPLAY}->${DST_DISPLAY}\" poll=${POLL}s"
|
||||||
|
|
||||||
|
while :; do
|
||||||
|
if ! x_reachable "$SRC_DISPLAY"; then
|
||||||
|
log warn src_display_down "display=${SRC_DISPLAY}"
|
||||||
|
elif ! x_reachable "$DST_DISPLAY"; then
|
||||||
|
log warn dst_display_down "display=${DST_DISPLAY}"
|
||||||
|
elif ! pgrep -x kasmxproxy >/dev/null 2>&1; then
|
||||||
|
start_bridge
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Heartbeat reflects a *fully* healthy bridge: proxy running, the display
|
||||||
|
# the browser sees is reachable, and a real session is drawing to it.
|
||||||
|
if pgrep -x kasmxproxy >/dev/null 2>&1 && x_reachable "$DST_DISPLAY" && session_alive; then
|
||||||
|
: > "$HEARTBEAT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep "$POLL"
|
||||||
|
done
|
||||||
25
infra/images/zotero/zotero-healthcheck.sh
Executable file
25
infra/images/zotero/zotero-healthcheck.sh
Executable file
@@ -0,0 +1,25 @@
|
|||||||
|
#!/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)"
|
||||||
@@ -151,6 +151,18 @@ scrape_configs:
|
|||||||
mail_event:
|
mail_event:
|
||||||
mail_count:
|
mail_count:
|
||||||
|
|
||||||
|
# --- zotero (KDE desktop / kasmxproxy bridge watchdog) ---
|
||||||
|
- match:
|
||||||
|
selector: '{container="zotero"}'
|
||||||
|
stages:
|
||||||
|
# kasmxproxy-guard.sh emits `zotero_event=<state>` on each bridge
|
||||||
|
# transition (bridge_up / bridge_down / bridge_start_failed /
|
||||||
|
# src_display_down / dst_display_down / guard_start).
|
||||||
|
- regex:
|
||||||
|
expression: 'zotero_event=(?P<zotero_event>\w+)'
|
||||||
|
- labels:
|
||||||
|
zotero_event:
|
||||||
|
|
||||||
# --- act-runner ---
|
# --- act-runner ---
|
||||||
- match:
|
- match:
|
||||||
selector: '{container="act-runner"}'
|
selector: '{container="act-runner"}'
|
||||||
|
|||||||
Reference in New Issue
Block a user