Initial commit: server-monitor v1 (Python)
Kompletter Neustart als Python-Projekt. Portiert von zfs-monitor Bash v3, saubere Architektur: - config.py: TOML-Config mit dataclasses - state.py: zentrales State-Management (JSON statt Einzeldateien) - notify.py: Telegram + Persistent Alerting als wiederverwendbare Klasse - checks/zfs.py: ZFS Pool-Health, I/O-Fehler, Kapazität - checks/smart.py: SATA + NVMe SMART-Monitoring - checks/nut.py: NUT/USV-Monitoring (APC Back-UPS RS 1500G)
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from datetime import datetime
|
||||
|
||||
from config import Config
|
||||
from state import State
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
ICONS = {"CRITICAL": "🔴", "WARNING": "🟡", "INFO": "🟢"}
|
||||
|
||||
|
||||
class Alerter:
|
||||
def __init__(self, config: Config, state: State) -> None:
|
||||
self.config = config
|
||||
self.state = state
|
||||
|
||||
# ── Persistent Alerting ───────────────────────────────────
|
||||
# Gleiche Logik wie im Bash-Script:
|
||||
# - Neuer Fehler → sofort melden, runs = 0
|
||||
# - Fehler bleibt → runs++, melden wenn runs >= repeat_runs
|
||||
# - Fehler weg → Entwarnung, State löschen
|
||||
|
||||
def should_alert(self, key: str, level: str) -> bool:
|
||||
repeat = (
|
||||
self.config.alert.critical_repeat_runs
|
||||
if level == "CRITICAL"
|
||||
else self.config.alert.warning_repeat_runs
|
||||
)
|
||||
active = self.state.get(f"{key}_active")
|
||||
runs: int = self.state.get(f"{key}_runs", 0)
|
||||
|
||||
if active != "1":
|
||||
self.state.set(f"{key}_active", "1")
|
||||
self.state.set(f"{key}_runs", 0)
|
||||
return True
|
||||
|
||||
runs += 1
|
||||
self.state.set(f"{key}_runs", runs)
|
||||
|
||||
if repeat == 0:
|
||||
return False
|
||||
if runs >= repeat:
|
||||
self.state.set(f"{key}_runs", 0)
|
||||
return True
|
||||
return False
|
||||
|
||||
def clear_alert(self, key: str) -> bool:
|
||||
"""Clear state and return True if the alert was previously active."""
|
||||
was_active = self.state.get(f"{key}_active") == "1"
|
||||
self.state.delete(f"{key}_active", f"{key}_runs")
|
||||
return was_active
|
||||
|
||||
# ── Sending ───────────────────────────────────────────────
|
||||
|
||||
def send_alert(self, level: str, title: str, body: str, key: str = "") -> None:
|
||||
icon = ICONS.get(level, "⚪")
|
||||
suffix = ""
|
||||
if key:
|
||||
runs: int = self.state.get(f"{key}_runs", 0)
|
||||
if runs > 0:
|
||||
suffix = f" <i>(Erinnerung #{runs})</i>"
|
||||
|
||||
ts = datetime.now().strftime("%d.%m.%Y %H:%M:%S")
|
||||
msg = (
|
||||
f"{icon} <b>[{level}] {self.config.hostname}</b>{suffix}\n"
|
||||
f"<b>{title}</b>\n\n"
|
||||
f"{body}\n\n"
|
||||
f"🕐 {ts}"
|
||||
)
|
||||
log.info("ALERT %s: %s", level, title)
|
||||
self._send_telegram(msg)
|
||||
|
||||
def _send_telegram(self, text: str) -> bool:
|
||||
url = f"https://api.telegram.org/bot{self.config.telegram.token}/sendMessage"
|
||||
data = urllib.parse.urlencode({
|
||||
"chat_id": self.config.telegram.chat_id,
|
||||
"text": text,
|
||||
"parse_mode": "HTML",
|
||||
}).encode()
|
||||
|
||||
for attempt in range(1, 4):
|
||||
try:
|
||||
with urllib.request.urlopen(url, data=data, timeout=10) as resp:
|
||||
result = json.loads(resp.read())
|
||||
if result.get("ok"):
|
||||
return True
|
||||
except Exception as e:
|
||||
log.warning("Telegram Fehler (Versuch %d/3): %s", attempt, e)
|
||||
if attempt < 3:
|
||||
time.sleep(attempt * 2)
|
||||
|
||||
log.error("Telegram: Alert konnte nicht gesendet werden!")
|
||||
return False
|
||||
Reference in New Issue
Block a user