Files
stack/infra/grafana/dashboards/zotero-desktop.json
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

89 lines
2.5 KiB
JSON

{
"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 }
}
]
}