Zum Inhalt

Runbook: USV-Metriken einrichten

Einmalige Einrichtung des Textfile-Collectors, der die Werte der CyberPower OR650ERM1U nach Prometheus bringt. Erstellt für homeserver#200, Teil 2.

Läuft auf dem NUC (10.10.10.3). Alle Schritte sind idempotent und lassen sich wiederholen.

Warum

Bis jetzt wusste Prometheus über die USV nur, ob der Container existiert. Ladung, Restlaufzeit und Netzstatus — die Werte, die im Ernstfall zählen — wurden gar nicht erfasst. Ebensowenig die Staleness des Treibers selbst: Dass usbhid-ups 3–5×/Tag verstummt, war nur durch Zählen im Journal sichtbar.

upsc scheitert mit Error: Data stale genau dann, wenn der Treiber still ist. Daraus wird nuc_ups_scrape_success — und damit aus dem Problem aus Teil 1 eine Zeitreihe.

Voraussetzung

Der Code muss auf dem NUC liegen:

cd /opt/rpi-docker-compose && git pull

Schritt 1 — Ablageverzeichnis anlegen

Es gibt nur einen Schreiber (den Timer als root), deshalb reichen normale Rechte:

sudo mkdir -p /mnt/ssd/container-data/monitoring-stack/textfile
sudo chown root:root /mnt/ssd/container-data/monitoring-stack/textfile
sudo chmod 755 /mnt/ssd/container-data/monitoring-stack/textfile

Rechte sind hier schon einmal zur Falle geworden

Auf dem aiserver lief der Backup-Zeitstempel vom 14.08. bis 04.09.2026 ins Leere, weil die Unit ohne CAP_DAC_OVERRIDE lief und root damit den normalen Rechte-Bits unterliegt. Diese Unit hat aus demselben Grund CapabilityBoundingSet= (leer) — sie schreibt als Eigentümer, nicht per Capability. Verzeichnis deshalb root lassen.

Schritt 2 — Skript ausführbar machen

Git bewahrt das Ausführbar-Bit, nach einem frischen clone aber sicherheitshalber prüfen:

test -x /opt/rpi-docker-compose/nut/scripts/ups-textfile-collector.sh \
  || sudo chmod +x /opt/rpi-docker-compose/nut/scripts/ups-textfile-collector.sh

Einmal von Hand laufen lassen, bevor der Timer scharf geschaltet wird:

sudo /opt/rpi-docker-compose/nut/scripts/ups-textfile-collector.sh
cat /mnt/ssd/container-data/monitoring-stack/textfile/ups.prom

Erwartet: rund 20 Zeilen, nuc_ups_scrape_success 1, nuc_ups_status_info{status="OL"} 1, eine Ladung von 100 und eine Restlaufzeit um 1900 Sekunden.

Schritt 3 — Units installieren und starten

sudo cp /opt/rpi-docker-compose/nut/host-config/ups-textfile-collector.service \
        /opt/rpi-docker-compose/nut/host-config/ups-textfile-collector.timer \
        /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now ups-textfile-collector.timer

Wirkung prüfen, nicht Dateiinhalt — eine Unit-Datei ohne Sektionskopf wird zeilenweise still verworfen und die Unit läuft unbemerkt mit Vorgabewerten weiter:

systemctl show ups-textfile-collector.timer -p TimersMonotonic
# erwartet: ... OnUnitActiveSec ... 30s ... (nicht leer)

systemctl list-timers ups-textfile-collector.timer
# erwartet: NEXT in unter 30 s

Nach einer Minute:

systemctl status ups-textfile-collector.service   # erwartet: inactive (dead), Result: success
stat -c '%y' /mnt/ssd/container-data/monitoring-stack/textfile/ups.prom
# erwartet: Zeitstempel jünger als 30 s

Schritt 4 — node-exporter neu erzeugen

cd /opt/rpi-docker-compose/monitoring-stack
docker compose up -d node-exporter

up -d genügt nicht immer: Nur weil die Compose-Datei neu ist, wird der Container neu erzeugt — bei einem reinen restart bliebe die alte Konfiguration bestehen. Prüfen, dass die Option wirklich angekommen ist:

docker inspect node-exporter --format '{{.Args}}' | tr ' ' '\n' | grep textfile
# erwartet: --collector.textfile.directory=/var/lib/node-exporter/textfile

Der Container muss danach auch wirklich laufen

Am 06.09. hat ein :ro an einem Verzeichnis-Mount das Entrypoint von nut-upsd zerlegt und eine zweistündige Restart-Schleife ausgelöst — die USV-Überwachung war in dieser Zeit tot. Dass ein Mount auflöst, heißt nicht, dass der Container damit startet. Hier ist :ro gefahrlos (prom/node-exporter ist ein statisches Binary ohne Entrypoint-Skript), aber geprüft wird trotzdem:

docker ps --filter name=node-exporter --format '{{.Status}}'
# erwartet: Up ... — nicht "Restarting"

Schritt 5 — Prometheus die Regeln laden lassen

rule_files in der prometheus.yml ist ein Glob (/etc/prometheus/rules/*.yml), die neue Datei wird also allein durch git pull gefunden. Nötig ist nur der Reload:

docker exec prometheus promtool check rules \
  /etc/prometheus/rules/ups-alerts.yml          # erwartet: SUCCESS: 7 rules found

docker exec prometheus wget --post-data="" -qO- http://localhost:9090/-/reload
# leere Antwort = erfolgreich (--web.enable-lifecycle ist gesetzt)

Alle Abfragen laufen über docker exec

Prometheus hat kein Port-Mapping auf den Host — curl localhost:9090 läuft ins Leere. Erreichbar ist es nur containerintern oder über prometheus.home.robinwerner.net. Im Image gibt es wget (busybox), aber weder curl noch kill; jq läuft auf dem Host am Ende der Pipe.

Verifikation

# 1. Metriken kommen im Exporter an
docker exec prometheus wget -qO- http://node-exporter:9100/metrics | grep -c '^nuc_ups_'
# erwartet: 15

# 2. Der Textfile-Collector meldet keinen Parse-Fehler
docker exec prometheus wget -qO- http://node-exporter:9100/metrics \
  | grep node_textfile_scrape_error
# erwartet: node_textfile_scrape_error 0
#   1 bedeutet: eine .prom-Datei war unparsbar und wurde KOMPLETT verworfen

# 3. Prometheus hat die Serie
docker exec prometheus wget -qO- \
  'http://localhost:9090/api/v1/query?query=nuc_ups_battery_charge_percent' \
  | jq -r '.data.result[0].value[1]'
# erwartet: 100

# 4. Die Regeln sind geladen
docker exec prometheus wget -qO- http://localhost:9090/api/v1/rules \
  | jq -r '.data.groups[] | select(.name=="ups-alerts") | .rules[].name'
# erwartet: 7 Namen, beginnend mit Ups

Danach: Teil 1 auswerten

Sobald ein paar Tage Historie vorliegen, beantwortet diese Abfrage die eigentliche Frage aus #200 — ob die Stale-Ereignisse nach dem Reboot vom 06.09. weg sind:

# Ereignisse pro Tag (changes zählt beide Flanken, also durch 2 teilen)
changes(nuc_ups_scrape_success[24h]) / 2

Vergleichswert vor dem Reboot: rund 3,3 Ereignisse pro Tag über vier Monate.

Rückbau

sudo systemctl disable --now ups-textfile-collector.timer
sudo rm /etc/systemd/system/ups-textfile-collector.{service,timer}
sudo systemctl daemon-reload
sudo rm -f /mnt/ssd/container-data/monitoring-stack/textfile/ups.prom

Die Alarmregeln und die node-exporter-Option müssten zusätzlich per Git zurückgenommen werden — sonst feuert UpsMetricMissing nach 30 Minuten.