From a5add937f8da81fe5eac281c389c6a0963aa018b Mon Sep 17 00:00:00 2001 From: Micha Date: Sat, 16 May 2026 13:26:49 +0200 Subject: [PATCH] Add Loki Alloy logging baseline --- HOMELAB_ARCHITECTURE_MASTER_V2.md | 9 ++++ docs/REPO_MAP.md | 12 ++++- docs/RESTORE_MATRIX.md | 1 + docs/SERVICE_CATALOG.md | 3 ++ docs/STORAGE_LAYOUT.draft.md | 2 +- ops/grafana-influxdb/README.md | 9 +++- ops/grafana-influxdb/docker-compose.yml | 3 ++ .../dashboards/container-error-rate.json | 23 ++++++++++ .../dashboards/logs-last-60m.json | 43 ++++++++++++++++++ .../provisioning/dashboards/providers.yml | 12 +++++ .../dashboards/restart-events.json | 23 ++++++++++ .../provisioning/datasources/influxdb.yml | 8 ++++ ops/loki/README.md | 23 ++++++++++ ops/loki/config/config.alloy | 43 ++++++++++++++++++ ops/loki/config/loki-config.yml | 45 +++++++++++++++++++ ops/loki/docker-compose.yml | 43 ++++++++++++++++++ .../posture-check/docker-critical-events.sh | 33 ++++++++++++++ 17 files changed, 330 insertions(+), 5 deletions(-) create mode 100644 ops/grafana-influxdb/provisioning/dashboards/container-error-rate.json create mode 100644 ops/grafana-influxdb/provisioning/dashboards/logs-last-60m.json create mode 100644 ops/grafana-influxdb/provisioning/dashboards/providers.yml create mode 100644 ops/grafana-influxdb/provisioning/dashboards/restart-events.json create mode 100644 ops/loki/README.md create mode 100644 ops/loki/config/config.alloy create mode 100644 ops/loki/config/loki-config.yml create mode 100644 ops/loki/docker-compose.yml create mode 100755 services/posture-check/docker-critical-events.sh diff --git a/HOMELAB_ARCHITECTURE_MASTER_V2.md b/HOMELAB_ARCHITECTURE_MASTER_V2.md index 88e77af..22efcca 100644 --- a/HOMELAB_ARCHITECTURE_MASTER_V2.md +++ b/HOMELAB_ARCHITECTURE_MASTER_V2.md @@ -296,6 +296,8 @@ Legende Status: | `speedtest-tracker` | ✅ | `frontend_net` | Traefik + Middleware | aktiv via `speedtest.kaleschke.info` | — | | `grafana` | ✅ | `frontend_net`, `grafana_influx_internal` | Traefik + Middleware | aktiv via `grafana.kaleschke.info`, InfluxDB-Datenquelle provisioniert; laeuft aktuell als `user: "0"` wegen Host-Appdata-Permissions | Wetter-/HA-Dashboard aufbauen; UID/GID-Hardening spaeter pruefen | | `influxdb3-core` | ✅ | `grafana_influx_internal`, `grafana_influx_lan` + LAN-Bind | LAN-Port nur fuer interne Writer | InfluxDB 3 Core fuer Metriken; keine Traefik-/Public-Freigabe; Port 8181 nur via `INFLUXDB_BIND_IP`; laeuft aktuell als `user: "0"` wegen Host-Appdata-Permissions | HA-Write-Token und Sensor-Export finalisieren; UID/GID-Hardening spaeter pruefen | +| `loki` | ✅ | `backend_net` | intern | interner Container-Logspeicher ohne Public Route; Grafana greift ueber Loki-Datasource zu | Retention/Storage nach erstem Produktivlauf beobachten | +| `alloy` | ✅ | `backend_net` | intern | Docker-Log-Collector mit read-only Docker-Socket-Ausnahme; schreibt nach Loki | keine Secrets; Socket-Ausnahme regelmaessig pruefen | ### 7.7 Noch offene Sonderfälle @@ -395,6 +397,7 @@ Für den laufenden Betrieb gilt stattdessen: | `nextcloud` | keine zentrale ForwardAuth-Middleware | Nextcloud bringt eigene Auth, Clients und WebDAV/CardDAV-Endpunkte mit; Traefik bleibt Reverse Proxy, Auth bleibt app-nativ | | `influxdb3-core` | Host-Port 8181 auf LAN-IP | Home Assistant laeuft in einer VM ausserhalb des Compose-Netzes und muss Metriken schreiben koennen; keine Traefik-Route, kein `frontend_net`, Zugriff nur ueber Token und LAN-IP `INFLUXDB_BIND_IP` | | `grafana`, `influxdb3-core` | `user: "0"` | aktueller Live-Stand fuer Host-Appdata-/Plugin-Permissions; als Ausnahme dokumentiert, spaeter separat mit UID/GID-Test haerten | +| `alloy` | Docker-Socket read-only | Docker-Log-Discovery fuer Loki; keine Schreibrechte, keine Appdaten-Persistenz ueber den Socket | --- @@ -553,6 +556,12 @@ Mutable Tags wie `latest`, `stable`, `release` oder reine Major-Tags wurden auf - InfluxDB 3 Core nutzt einen festen Versionstag statt `latest`, weil der InfluxDB-`latest`-Tag versionsstrategisch im Umbruch ist. - `grafana` und `influxdb3-core` laufen aktuell als `user: "0"`; das ist als Host-Appdata-Permissions-Ausnahme dokumentiert und wird nicht nebenbei geaendert. +### Loki/Alloy Logging-Baseline (2026-05-16) +- `loki` laeuft intern auf `backend_net`, ohne Traefik-Route und ohne Host-Port. +- `alloy` sammelt Docker-Logs ueber `/var/run/docker.sock:ro` und schreibt sie an Loki. +- Grafana bekommt eine provisionierte Loki-Datasource und einfache Start-Dashboards fuer Logs, Restart-Events und Error-Rate. +- Loki-Logdaten sind Diagnosematerial mit begrenzter Retention, keine primaere Restore-Quelle. + ### Authelia ohne Redis-Session-Backend (2026-05-04) - Authelia nutzt PostgreSQL fuer persistente Storage-Daten, aber bewusst kein Redis-Session-Backend. - Das haelt den Tier-1-Auth-Pfad einfacher; nach einem Authelia-Restart muessen aktive Sessions neu aufgebaut werden. diff --git a/docs/REPO_MAP.md b/docs/REPO_MAP.md index 87b17f2..1cb9421 100644 --- a/docs/REPO_MAP.md +++ b/docs/REPO_MAP.md @@ -45,9 +45,13 @@ Secret-Werte werden hier nicht dokumentiert. Aufgefuehrt werden nur Variablennam | `traefik/dynamic/dashboards.yml` | leer; File-Provider-Platzhalter | | `traefik/dynamic/tls.yml` | leer; File-Provider-Platzhalter | | `security/authelia/configuration.yml` | versionierte Authelia-Baseline fuer nicht geheime ACL-/Session-/Storage-Einstellungen; manuelle Host-Merge-Pflicht, User-Daten, OIDC-Client-Konfiguration und Secret-Werte bleiben ausserhalb von Git | -| `ops/grafana-influxdb/provisioning/datasources/influxdb.yml` | Grafana Datasource Provisioning fuer InfluxDB 3 Core | +| `ops/grafana-influxdb/provisioning/datasources/influxdb.yml` | Grafana Datasource Provisioning fuer InfluxDB 3 Core und Loki | +| `ops/grafana-influxdb/provisioning/dashboards/*.json` | Grafana Dashboards fuer Container-Logs, Restart-Events und Error-Rate | +| `ops/loki/config/loki-config.yml` | Loki Filesystem/Retention-Konfiguration fuer internen Logspeicher | +| `ops/loki/config/config.alloy` | Alloy Docker-Log-Collector-Konfiguration | | `ops/borg-ui/scripts/pre-backup-dumps.sh` | Host-seitiges Dump-Skript fuer PostgreSQL, SQLite-Container-Dumps und Komodo Mongo | | `services/posture-check/posture-check.sh` | Host-seitiger Posture-Check fuer Filesystem, Mover-Drift, NVMe-SMART, Fuellstand und ntfy-Alarmierung | +| `services/posture-check/docker-critical-events.sh` | Host-seitiger Docker-Event-Watcher fuer kritische ntfy-Alarme | | `services/posture-check/posture_check.sh` | Kompatibilitaets-Wrapper fuer die Schreibweise aus `STORAGE_LAYOUT.draft.md` | | `ops/hermes-agent/config/hermes/config.yaml` | Hermes Agent Konfiguration mit Env-Platzhaltern | | `ops/hermes-agent/hermes.env.example` | Beispiel fuer Hermes `.env`; echte Datei liegt auf Host-Appdata | @@ -99,6 +103,7 @@ Secret-Werte werden hier nicht dokumentiert. Aufgefuehrt werden nur Variablennam | Filebrowser | `ops/filebrowser/docker-compose.yml` | `filebrowser` -> `filebrowser/filebrowser:v2.63.2@sha256:...` | `files.kaleschke.info` | `frontend_net` | keine | Documents/Photos/Projekte-Mounts, Admin-UI hinter Authelia | | Glances | `ops/glances/docker-compose.yml` | `glances` -> `nicolargo/glances:latest-full@sha256:...` | `glances.kaleschke.info` | `frontend_net` | keine | Rootfs/Docker-Socket fuer Monitoring | | Grafana/InfluxDB | `ops/grafana-influxdb/docker-compose.yml` | `grafana`, `influxdb3-core` | `grafana.kaleschke.info` | `frontend_net`, `grafana_influx_internal`, `grafana_influx_lan` | `influxdb3-core`: `${INFLUXDB_BIND_IP:-127.0.0.1}:8181:8181` | InfluxDB LAN-only fuer Home Assistant; Grafana datasource token; beide Container laufen aktuell als `user: "0"` | +| Loki/Alloy | `ops/loki/docker-compose.yml` | `loki`, `alloy` | keine | `backend_net` | keine | interner Logspeicher und Docker-Log-Collector; Alloy nutzt Docker socket read-only | | Hermes Agent | `ops/hermes-agent/docker-compose.yml` | `hermes-gateway`, `hermes-dashboard` -> local build from Dockerfile | `hermes.kaleschke.info` via `${HERMES_DASHBOARD_HOST}` | `hermes_net`, dashboard zusaetzlich `frontend_net` | `8642` nur expose intern | SSH runner, Home Assistant optional, LLM provider env; Dashboard hinter Authelia | | Komodo | `ops/komodo/docker-compose.yml` | `komodo-core`, `komodo-mongo`, `komodo-periphery` | `komodo.kaleschke.info` | `frontend_net`, `komodo_net` | keine | Mongo, Docker socket, `/mnt/user/services` workspace mount, Gitea DNS override | | Scrutiny | `ops/scrutiny/docker-compose.yml` | `scrutiny` -> `ghcr.io/starosdev/scrutiny:latest-omnibus@sha256:...` | `scrutiny.kaleschke.info` | `frontend_net` | keine | `privileged: true`, device mounts fuer SMART | @@ -144,7 +149,7 @@ Secret-Werte werden hier nicht dokumentiert. Aufgefuehrt werden nur Variablennam | Network | Typ / Status | Nutzer | |---|---|---| | `frontend_net` | external bridge | Web-/Proxy-Netz fuer Traefik und alle gerouteten UIs | -| `backend_net` | external/internal laut Architektur | PostgreSQL 17, Redis, Authelia, Paperless, Mail Archiver, Traefik | +| `backend_net` | external/internal laut Architektur | PostgreSQL 17, Redis, Authelia, Paperless, Mail Archiver, Traefik, Loki, Alloy, Grafana-Loki-Datasource | | `dns_net` | App-/Host-Netz | AdGuard Home und Unbound | | `immich_default` | Compose-intern, `internal: true` | Immich Server, ML, Postgres, Redis | | `mealie_internal` | Compose-intern | Mealie und Mealie Postgres | @@ -183,6 +188,7 @@ Secret-Werte werden hier nicht dokumentiert. Aufgefuehrt werden nur Variablennam | Speedtest | `/mnt/user/appdata/speedtest-tracker/config` | | Uptime Kuma | `/mnt/user/appdata/uptime-kuma` | | Grafana/InfluxDB | `/mnt/user/appdata/grafana`, Grafana provisioning, `/mnt/user/appdata/influxdb3/data`, `/mnt/user/appdata/influxdb3/plugins` | +| Loki/Alloy | `/mnt/user/appdata/loki/config`, `/mnt/user/appdata/loki/data`, `/mnt/user/appdata/alloy/config`, `/mnt/user/appdata/alloy/data` | | Hermes Agent | `/mnt/user/appdata/hermes-agent/data`, `/mnt/user/appdata/hermes-agent/ssh`, SSH private key path | | Komodo | `komodo_keys`, `/mnt/user/appdata/komodo/core`, `/mnt/user/appdata/komodo/mongo`, `/mnt/user/appdata/komodo/periphery`, `/mnt/user/services` | @@ -205,6 +211,7 @@ Secret-Werte werden hier nicht dokumentiert. Aufgefuehrt werden nur Variablennam | Borg UI | Borg Credentials, Admin Login, SSH Keys in persistentem Appdata, nicht im Git | | Hermes Agent | provider keys, API server key, messaging tokens, Home Assistant token in Host `.env`; SSH private key als Host-Secret | | Grafana/InfluxDB | Grafana Admin Password, InfluxDB Admin Token JSON, Grafana Datasource Token | +| Loki/Alloy | keine zusaetzlichen Secrets; Zugriff nur intern ueber `backend_net` | ## Skripte @@ -212,6 +219,7 @@ Secret-Werte werden hier nicht dokumentiert. Aufgefuehrt werden nur Variablennam |---|---|---| | `ops/borg-ui/scripts/pre-backup-dumps.sh` | Unraid Host, nicht Borg-UI Inline-Hook | erzeugt aktuelle Dumps unter `/mnt/user/backups/borg/dumps/latest` | | `services/posture-check/posture-check.sh` | Unraid Host | schreibt `/mnt/user/services/posture-check/last.json` und alarmiert via ntfy bei Warning/Critical | +| `services/posture-check/docker-critical-events.sh` | Unraid Host | beobachtet Docker `die`/`oom`/`kill` Events und alarmiert via `kallilab-critical` | Das Skript liest Secret-Dateien auf dem Host und schreibt Dump-Artefakte. Bei Analyse niemals Secret-Inhalte ausgeben. diff --git a/docs/RESTORE_MATRIX.md b/docs/RESTORE_MATRIX.md index 6e8b23c..c56fc3b 100644 --- a/docs/RESTORE_MATRIX.md +++ b/docs/RESTORE_MATRIX.md @@ -66,6 +66,7 @@ Sie ist die fachliche Ergaenzung zu `docs/DISASTER_RECOVERY.md`. | BentoPDF | Rebuildbar | keine kritische Persistenz; alte Stirling-PDF-Daten unter `/mnt/user/appdata/stirling-pdf` bis zur Abnahme behalten | keine | keine separaten Secret-Dateien dokumentiert | Traefik, Authelia | UI startet, PDF-Tools verfuegbar, Office-Konvertierung ueber HTTPS funktioniert | | Grafana | Share + Dump | `/mnt/user/appdata/grafana`, inklusive `provisioning/datasources/influxdb.yml` | `grafana.sqlite` | `grafana_admin_password.txt`, `grafana_influxdb_token.txt` | Traefik, Authelia, InfluxDB 3 Core | UI startet, InfluxDB-Datenquelle testet erfolgreich | | InfluxDB 3 Core | Share | `/mnt/user/appdata/influxdb3/data`, `/mnt/user/appdata/influxdb3/plugins` | dateibasierter Object Store | `influxdb3_admin_token.json` | internes `grafana_influx_internal` Netz | `homelab`-Datenbank vorhanden, Grafana kann SQL-Abfrage ausfuehren | +| Loki / Alloy | Rebuild + begrenzte Logdaten | `/mnt/user/appdata/loki/config`, `/mnt/user/appdata/loki/data`, `/mnt/user/appdata/alloy/config` | keine primaere DB; Loki-Dateispeicher mit 30 Tagen Retention | keine zusaetzlichen Secrets | `backend_net`, Docker socket read-only, Grafana | Loki `/ready` ist gruen, Grafana-Datasource `Loki` kann Logs abfragen | | Hermes Agent | VM-seitig offen | `/mnt/user/appdata/hermes-agent/data`, `/mnt/user/appdata/hermes-agent/ssh` | keine eigene DB | Host-`.env` fuer Provider-/API-/Home-Assistant-Tokens, `hermes_runner_id_ed25519`, `HERMES_DASHBOARD_HOST` | separate Hermes-VM/Runner, Traefik, Authelia, `hermes_net` | NAS-Stack nicht starten, solange Runner-VM und echte `.env` fehlen | | ddns-updater | Rebuildbar | geringe Persistenzrelevanz | keine | Provider-Zugang ueber Stack ENV | Internetzugang | Update-Job laeuft | diff --git a/docs/SERVICE_CATALOG.md b/docs/SERVICE_CATALOG.md index 041d8bc..6668b9e 100644 --- a/docs/SERVICE_CATALOG.md +++ b/docs/SERVICE_CATALOG.md @@ -67,6 +67,8 @@ Secret-Werte sind nicht enthalten. Es werden nur Secret-Namen, Env-Key-Namen und | `code-server` | Web-Editor / Operations Workspace | `ops/code-server/docker-compose.yml` | `https://code.kaleschke.info` | Traefik + Authelia | `/mnt/user/appdata/code-server`, `/mnt/user/services/dev` | Tier 3 | ja + Authelia | Passwort ueber LSIO `FILE__PASSWORD`; Workspaces beachten | | `grafana` | Metrik-Dashboard | `ops/grafana-influxdb/docker-compose.yml` | `https://grafana.kaleschke.info` | Traefik + Authelia, InfluxDB 3 Core | `/mnt/user/appdata/grafana`, Grafana provisioning | Tier 3, `grafana.sqlite` | ja + Authelia | Datasource wird provisioniert, Token ueber Secret; laeuft aktuell als `user: "0"` wegen Host-Appdata-Permissions | | `influxdb3-core` | Zeitreihen-/Metrikdaten fuer Grafana und Home Assistant | `ops/grafana-influxdb/docker-compose.yml` | LAN `8181` je `INFLUXDB_BIND_IP`, keine Public URL | Grafana, Home Assistant Writer | `/mnt/user/appdata/influxdb3/data`, `/mnt/user/appdata/influxdb3/plugins` | Tier 3 | nein | LAN-only Host-Port-Ausnahme; `401 Unauthorized` beim Curl ohne Token ist erwarteter Reachability-Test; laeuft aktuell als `user: "0"` wegen Host-Appdata-Permissions | +| `loki` | zentraler Container-Logspeicher | `ops/loki/docker-compose.yml`, `ops/loki/config/loki-config.yml` | intern `http://loki:3100`, keine Public URL | `backend_net`, Grafana | `/mnt/user/appdata/loki/config`, `/mnt/user/appdata/loki/data` | Tier 3, transiente Logs mit 30 Tagen Retention | nein | Nur intern erreichbar; Grafana nutzt Loki als Datasource; Logs sind Diagnosematerial, keine primaere Restore-Quelle | +| `alloy` | Docker-Log-Collector fuer Loki | `ops/loki/docker-compose.yml`, `ops/loki/config/config.alloy` | intern | Docker socket read-only, Loki, `backend_net` | `/mnt/user/appdata/alloy/config`, `/mnt/user/appdata/alloy/data` | rebuildbar | nein | Dokumentierte Host-Observability-Ausnahme: `/var/run/docker.sock:/var/run/docker.sock:ro`; keine Appdaten, nur Log-Discovery | | `hermes-gateway` | Hermes Agent Gateway/API intern | `ops/hermes-agent/docker-compose.yml` | intern `8642` auf `hermes_net` | SSH Runner (VM 192.168.178.143), LLM Provider, optional Home Assistant | `/mnt/user/appdata/hermes-agent/data`, SSH key path | Tier 3, Borg/Share | nein | NAS-Stack bleibt deaktiviert, solange die separate Hermes-VM/Runner-Seite nicht wiederhergestellt ist; kein Docker-Socket | | `hermes-dashboard` | Hermes Dashboard | `ops/hermes-agent/docker-compose.yml` | `https://hermes.kaleschke.info` via `${HERMES_DASHBOARD_HOST}` | `hermes-gateway`, Traefik + Authelia | shared read-only data mount | Tier 3, Borg/Share | ja + Authelia | Compose-Profil `dashboard`; aktuell VM-seitig offen, nicht Teil des NAS-Finalstarts | @@ -75,6 +77,7 @@ Secret-Werte sind nicht enthalten. Es werden nur Secret-Namen, Env-Key-Namen und | Service | Zweck | Autoritativer Pfad | URL / Zugang | Abhaengigkeiten | Datenpfade | Backup / Restore | Traefik | Besonderheiten / TODOs | |---|---|---|---|---|---|---|---|---| | `posture-check` | Host-Posture-Audit fuer Filesystem, Mover-Drift, NVMe-SMART und Fuellstand | `services/posture-check/posture-check.sh` | Unraid User-Script / Cron / Borg Pre-Hook | `findmnt`, `df`, `nvme`, optional `curl` fuer ntfy | `/mnt/user/services/posture-check/last.json` | Repo-Skript + letzter JSON-Status | nein | Muss auf dem Unraid-Host bei Boot, stuendlich und vor Borg laufen; `ALLOW_DISK1_NTFS=1` ist die dokumentierte Uebergangsausnahme bis Disk1-Migration Phase 2; Warning/Critical alarmieren via ntfy | +| `docker-critical-events` | Live-Alarmierung fuer Docker `die`/`oom`/`kill` Events | `services/posture-check/docker-critical-events.sh` | Unraid User-Script / Hintergrundprozess | Docker CLI, ntfy | `/mnt/user/services/posture-check/docker-critical-events-last.log` | Repo-Skript + letzter Event-Log | nein | Optional als Unraid User-Script `at array start` starten; sendet nach `kallilab-critical` | ## Backup- und Restore-Hinweise diff --git a/docs/STORAGE_LAYOUT.draft.md b/docs/STORAGE_LAYOUT.draft.md index 4e0459a..765fc41 100644 --- a/docs/STORAGE_LAYOUT.draft.md +++ b/docs/STORAGE_LAYOUT.draft.md @@ -292,7 +292,7 @@ Diese Regeln sind nicht optional. Verstoß ist Incident, kein Feature-Request. 12. **Kein Backup-Lauf ohne vorgeschalteten Posture-Check (siehe §11).** Backup auf kompromittiertem Filesystem überschreibt unter Umständen den letzten guten Stand und kontaminiert die Backup-Historie. **Dokumentierte Host-Observability-Ausnahmen (Operator-Entscheidung 2026-05-16):** -`glances` und `scrutiny` duerfen gezielt Host-/Device-Bind-Mounts ausserhalb `/mnt/user/...` nutzen, weil ihre Kernfunktion sonst nicht erfuellbar ist. Erlaubt sind nur die in `docs/SERVICE_CATALOG.md` pro Dienst genannten Binds. Diese Ausnahmen sind keine Datenpersistenz-Pfade und duerfen nicht fuer Appdaten, Backups oder normale Service-Konfiguration erweitert werden. Neue oder geaenderte Host-Binds brauchen eine explizite Doku-Aenderung im selben Commit. +`glances`, `scrutiny` und `alloy` duerfen gezielt Host-/Device-Bind-Mounts ausserhalb `/mnt/user/...` nutzen, weil ihre Kernfunktion sonst nicht erfuellbar ist. Erlaubt sind nur die in `docs/SERVICE_CATALOG.md` pro Dienst genannten Binds. Diese Ausnahmen sind keine Datenpersistenz-Pfade und duerfen nicht fuer Appdaten, Backups oder normale Service-Konfiguration erweitert werden. Neue oder geaenderte Host-Binds brauchen eine explizite Doku-Aenderung im selben Commit. ## 13. Soft Rules — Konventionen diff --git a/ops/grafana-influxdb/README.md b/ops/grafana-influxdb/README.md index e4f0946..e80b621 100644 --- a/ops/grafana-influxdb/README.md +++ b/ops/grafana-influxdb/README.md @@ -9,7 +9,7 @@ Monitoring-Stack fuer Grafana + InfluxDB 3 Core. InfluxDB bleibt ohne Public Rou - Grafana wird ueber Traefik + `authelia@file,secure-headers@file` unter `grafana.kaleschke.info` veroeffentlicht. - InfluxDB bleibt ohne Traefik-Route. Der HTTP-Port `8181` kann fuer interne Writer wie Home Assistant ueber `INFLUXDB_BIND_IP` auf eine LAN-Adresse gebunden werden; Default ist `127.0.0.1`. - InfluxDB haengt an zwei Compose-Netzen: `grafana_influx_internal` fuer Grafana und `grafana_influx_lan` fuer das Docker Host-Port-Publishing. Im laufenden Komodo-Stack heissen sie durch den Compose-Projektpraefix `grafana_grafana_influx_internal` und `grafana_grafana_influx_lan`. InfluxDB haengt bewusst nicht im `frontend_net`. -- Grafana provisioning legt eine SQL-Datenquelle fuer InfluxDB 3 Core mit der Datenbank `homelab` an. +- Grafana provisioning legt eine SQL-Datenquelle fuer InfluxDB 3 Core mit der Datenbank `homelab` und eine Loki-Datasource fuer Container-Logs an. - Der Grafana-Datasource-Token liegt als Secret-Datei auf dem Host und wird beim Containerstart nur containerintern in die fuer Grafana-Provisioning noetige Environment-Variable geladen. - Home Assistant schreibt mit der InfluxDB-v2-API-Kompatibilitaet nach InfluxDB 3; Details: `docs/HOME_ASSISTANT_INFLUXDB_ECOWITT.md`. @@ -39,12 +39,15 @@ Monitoring-Stack fuer Grafana + InfluxDB 3 Core. InfluxDB bleibt ohne Public Rou install -m 600 /dev/null /mnt/user/appdata/secrets/grafana_influxdb_token.txt ``` -4. Provisioning-Datei aus dem Git-Checkout auf den Host-Appdata-Pfad kopieren: +4. Provisioning-Dateien aus dem Git-Checkout auf den Host-Appdata-Pfad kopieren: ```bash mkdir -p /mnt/user/appdata/grafana/provisioning/datasources + mkdir -p /mnt/user/appdata/grafana/provisioning/dashboards cp /mnt/user/appdata/komodo/core/repos/homelab-infra/ops/grafana-influxdb/provisioning/datasources/influxdb.yml /mnt/user/appdata/grafana/provisioning/datasources/influxdb.yml + cp /mnt/user/appdata/komodo/core/repos/homelab-infra/ops/grafana-influxdb/provisioning/dashboards/* /mnt/user/appdata/grafana/provisioning/dashboards/ chmod 644 /mnt/user/appdata/grafana/provisioning/datasources/influxdb.yml + chmod 644 /mnt/user/appdata/grafana/provisioning/dashboards/* ``` 5. Nach dem ersten Start die Datenbank anlegen: @@ -57,6 +60,8 @@ Monitoring-Stack fuer Grafana + InfluxDB 3 Core. InfluxDB bleibt ohne Public Rou - `https://grafana.kaleschke.info` oeffnet nach Authelia die Grafana-Loginseite. - Grafana `Connections -> Data sources -> InfluxDB 3 Core -> Save & test` ist erfolgreich. +- Grafana `Connections -> Data sources -> Loki -> Save & test` ist erfolgreich, sobald der Loki/Alloy-Stack laeuft. +- Die provisionierten Dashboards `Logs - Last 60m`, `Container Restart Events` und `Container Error Rate` sind sichtbar. - InfluxDB bleibt ohne Public Route. Falls `INFLUXDB_BIND_IP` auf die LAN-IP gesetzt ist, ist Port `8181` nur im internen Netz fuer Writer wie Home Assistant erreichbar. - `docker ps` zeigt fuer `influxdb3-core` `192.168.178.58:8181->8181/tcp` oder den per `INFLUXDB_BIND_IP` gesetzten Host. - `ss -ltnp | grep 8181` zeigt einen Listener auf der gebundenen Host-IP. diff --git a/ops/grafana-influxdb/docker-compose.yml b/ops/grafana-influxdb/docker-compose.yml index 39e024f..f05d2bb 100644 --- a/ops/grafana-influxdb/docker-compose.yml +++ b/ops/grafana-influxdb/docker-compose.yml @@ -26,6 +26,7 @@ services: - grafana_influxdb_token networks: - frontend_net + - backend_net - grafana_influx_internal security_opt: - no-new-privileges:true @@ -82,6 +83,8 @@ secrets: networks: frontend_net: external: true + backend_net: + external: true grafana_influx_lan: driver: bridge grafana_influx_internal: diff --git a/ops/grafana-influxdb/provisioning/dashboards/container-error-rate.json b/ops/grafana-influxdb/provisioning/dashboards/container-error-rate.json new file mode 100644 index 0000000..308c4fc --- /dev/null +++ b/ops/grafana-influxdb/provisioning/dashboards/container-error-rate.json @@ -0,0 +1,23 @@ +{ + "uid": "kallilab-container-error-rate", + "title": "Container Error Rate", + "schemaVersion": 39, + "version": 1, + "refresh": "5m", + "time": { "from": "now-24h", "to": "now" }, + "panels": [ + { + "id": 1, + "type": "table", + "title": "Container Errors Last 24h", + "datasource": { "type": "loki", "uid": "loki" }, + "targets": [ + { + "refId": "A", + "expr": "sum by (container_name) (count_over_time({platform=\"docker\"} |~ \"(?i)(level=error|error|fatal|panic)\" [24h]))" + } + ], + "gridPos": { "h": 16, "w": 24, "x": 0, "y": 0 } + } + ] +} diff --git a/ops/grafana-influxdb/provisioning/dashboards/logs-last-60m.json b/ops/grafana-influxdb/provisioning/dashboards/logs-last-60m.json new file mode 100644 index 0000000..c182aa6 --- /dev/null +++ b/ops/grafana-influxdb/provisioning/dashboards/logs-last-60m.json @@ -0,0 +1,43 @@ +{ + "uid": "kallilab-logs-last-60m", + "title": "Last 60 min before now", + "schemaVersion": 39, + "version": 1, + "refresh": "30s", + "time": { "from": "now-60m", "to": "now" }, + "templating": { + "list": [ + { + "name": "container", + "type": "query", + "datasource": { "type": "loki", "uid": "loki" }, + "query": "label_values(container_name)", + "includeAll": true, + "allValue": ".+", + "refresh": 1 + } + ] + }, + "panels": [ + { + "id": 1, + "type": "logs", + "title": "Docker Log Stream", + "datasource": { "type": "loki", "uid": "loki" }, + "targets": [ + { + "refId": "A", + "expr": "{platform=\"docker\", container_name=~\"$container\"}" + } + ], + "gridPos": { "h": 20, "w": 24, "x": 0, "y": 0 }, + "options": { + "showTime": true, + "showLabels": true, + "wrapLogMessage": false, + "enableLogDetails": true, + "sortOrder": "Descending" + } + } + ] +} diff --git a/ops/grafana-influxdb/provisioning/dashboards/providers.yml b/ops/grafana-influxdb/provisioning/dashboards/providers.yml new file mode 100644 index 0000000..d18173b --- /dev/null +++ b/ops/grafana-influxdb/provisioning/dashboards/providers.yml @@ -0,0 +1,12 @@ +apiVersion: 1 + +providers: + - name: KalliLab Observability + orgId: 1 + folder: KalliLab Observability + type: file + disableDeletion: false + updateIntervalSeconds: 60 + allowUiUpdates: false + options: + path: /etc/grafana/provisioning/dashboards diff --git a/ops/grafana-influxdb/provisioning/dashboards/restart-events.json b/ops/grafana-influxdb/provisioning/dashboards/restart-events.json new file mode 100644 index 0000000..65bf755 --- /dev/null +++ b/ops/grafana-influxdb/provisioning/dashboards/restart-events.json @@ -0,0 +1,23 @@ +{ + "uid": "kallilab-restart-events", + "title": "Restart Events", + "schemaVersion": 39, + "version": 1, + "refresh": "5m", + "time": { "from": "now-24h", "to": "now" }, + "panels": [ + { + "id": 1, + "type": "heatmap", + "title": "Restart-like Log Events", + "datasource": { "type": "loki", "uid": "loki" }, + "targets": [ + { + "refId": "A", + "expr": "sum by (container_name) (count_over_time({platform=\"docker\"} |~ \"(?i)(restart|restarting|started|exited|oom)\" [5m]))" + } + ], + "gridPos": { "h": 16, "w": 24, "x": 0, "y": 0 } + } + ] +} diff --git a/ops/grafana-influxdb/provisioning/datasources/influxdb.yml b/ops/grafana-influxdb/provisioning/datasources/influxdb.yml index 1371367..d04baff 100644 --- a/ops/grafana-influxdb/provisioning/datasources/influxdb.yml +++ b/ops/grafana-influxdb/provisioning/datasources/influxdb.yml @@ -16,3 +16,11 @@ datasources: insecureGrpc: true secureJsonData: token: $GRAFANA_INFLUXDB_TOKEN + - name: Loki + uid: loki + type: loki + access: proxy + url: http://loki:3100 + isDefault: false + jsonData: + maxLines: 1000 diff --git a/ops/loki/README.md b/ops/loki/README.md new file mode 100644 index 0000000..98c091f --- /dev/null +++ b/ops/loki/README.md @@ -0,0 +1,23 @@ +# Loki / Alloy + +Internal logging stack for KalliLab CORE. + +## Services + +- `loki`: internal log store on `backend_net`, no Traefik route, `auth_enabled: false` because access is limited to internal Docker networking. +- `alloy`: Docker log collector. It mounts `/var/run/docker.sock:ro` as a documented observability exception and forwards Docker container logs to Loki. + +## Host sync + +Before first deploy, sync the checked-in config files to appdata: + +```bash +mkdir -p /mnt/user/appdata/loki/config /mnt/user/appdata/loki/data +mkdir -p /mnt/user/appdata/alloy/config /mnt/user/appdata/alloy/data +cp /mnt/user/services/homelab-infra/ops/loki/config/loki-config.yml /mnt/user/appdata/loki/config/loki-config.yml +cp /mnt/user/services/homelab-infra/ops/loki/config/config.alloy /mnt/user/appdata/alloy/config/config.alloy +``` + +## Restore posture + +Loki data is transient operational telemetry. Docker raw logs remain the first fallback, Loki chunks on disk are a convenience cache, and ntfy critical events provide the external first-crash marker. diff --git a/ops/loki/config/config.alloy b/ops/loki/config/config.alloy new file mode 100644 index 0000000..e38c7fd --- /dev/null +++ b/ops/loki/config/config.alloy @@ -0,0 +1,43 @@ +discovery.docker "containers" { + host = "unix:///var/run/docker.sock" +} + +discovery.relabel "docker_logs" { + targets = [] + + rule { + source_labels = ["__meta_docker_container_name"] + regex = "/(.*)" + target_label = "container_name" + } + + rule { + source_labels = ["__meta_docker_container_label_com_docker_compose_project"] + target_label = "compose_project" + } + + rule { + source_labels = ["__meta_docker_container_label_com_docker_compose_service"] + target_label = "compose_service" + } +} + +loki.source.docker "containers" { + host = "unix:///var/run/docker.sock" + targets = discovery.docker.containers.targets + labels = { platform = "docker", host = "kallilabcore" } + relabel_rules = discovery.relabel.docker_logs.rules + forward_to = [loki.process.docker.receiver] +} + +loki.process "docker" { + forward_to = [loki.write.local.receiver] + + stage.docker {} +} + +loki.write "local" { + endpoint { + url = "http://loki:3100/loki/api/v1/push" + } +} diff --git a/ops/loki/config/loki-config.yml b/ops/loki/config/loki-config.yml new file mode 100644 index 0000000..eeff22d --- /dev/null +++ b/ops/loki/config/loki-config.yml @@ -0,0 +1,45 @@ +auth_enabled: false + +server: + http_listen_port: 3100 + grpc_listen_port: 9096 + +common: + instance_addr: 127.0.0.1 + path_prefix: /loki + storage: + filesystem: + chunks_directory: /loki/chunks + rules_directory: /loki/rules + replication_factor: 1 + ring: + kvstore: + store: inmemory + +query_range: + results_cache: + cache: + embedded_cache: + enabled: true + max_size_mb: 100 + +schema_config: + configs: + - from: 2026-05-16 + store: tsdb + object_store: filesystem + schema: v13 + index: + prefix: index_ + period: 24h + +limits_config: + retention_period: 720h + allow_structured_metadata: true + +compactor: + working_directory: /loki/compactor + compaction_interval: 10m + retention_enabled: true + retention_delete_delay: 2h + delete_request_store: filesystem diff --git a/ops/loki/docker-compose.yml b/ops/loki/docker-compose.yml new file mode 100644 index 0000000..b5e5c22 --- /dev/null +++ b/ops/loki/docker-compose.yml @@ -0,0 +1,43 @@ +services: + loki: + image: grafana/loki:3.7.2@sha256:191d4fdfb7264f16989f0a57f320872620a5a7c2ceeec6229212c4190ec49b86 + container_name: loki + restart: unless-stopped + command: + - -config.file=/etc/loki/loki-config.yml + volumes: + - /mnt/user/appdata/loki/config:/etc/loki:ro + - /mnt/user/appdata/loki/data:/loki + networks: + - backend_net + security_opt: + - no-new-privileges:true + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://localhost:3100/ready"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 40s + + alloy: + image: grafana/alloy:v1.16.1@sha256:51aeb9d829239345070619dad3edd6873186f913c84f45b365b74574fcb38ec0 + container_name: alloy + restart: unless-stopped + command: + - run + - /etc/alloy/config.alloy + - --storage.path=/var/lib/alloy/data + volumes: + - /mnt/user/appdata/alloy/config:/etc/alloy:ro + - /mnt/user/appdata/alloy/data:/var/lib/alloy/data + - /var/run/docker.sock:/var/run/docker.sock:ro + networks: + - backend_net + security_opt: + - no-new-privileges:true + depends_on: + - loki + +networks: + backend_net: + external: true diff --git a/services/posture-check/docker-critical-events.sh b/services/posture-check/docker-critical-events.sh new file mode 100755 index 0000000..5921265 --- /dev/null +++ b/services/posture-check/docker-critical-events.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -euo pipefail + +NTFY_SCRIPT="${NTFY_SCRIPT:-/mnt/user/services/homelab-infra/ops/restore-tests/send-ntfy.sh}" +NTFY_TOPIC="${NTFY_TOPIC:-kallilab-critical}" +SEND_NTFY="${SEND_NTFY:-1}" +OUTPUT_PATH="${OUTPUT_PATH:-/mnt/user/services/posture-check/docker-critical-events-last.log}" +EVENT_FILTERS="${EVENT_FILTERS:---filter event=die --filter event=oom --filter event=kill}" + +mkdir -p "$(dirname "$OUTPUT_PATH")" + +send_event() { + local line="$1" + local timestamp + timestamp="$(date -Iseconds)" + + printf '%s %s\n' "$timestamp" "$line" | tee -a "$OUTPUT_PATH" >/dev/null + + if [ "$SEND_NTFY" = "1" ] && [ -x "$NTFY_SCRIPT" ]; then + "$NTFY_SCRIPT" "$NTFY_TOPIC" "Docker critical event" "$line" high || true + fi +} + +if ! command -v docker >/dev/null 2>&1; then + echo "docker command not found" >&2 + exit 127 +fi + +# shellcheck disable=SC2086 +docker events $EVENT_FILTERS --format '{{json .}}' | while IFS= read -r event; do + [ -n "$event" ] || continue + send_event "$event" +done