Includes atomic migrations, per-rule reminder semantics, first-run helper script and the note on the undecided UI chat idea. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
80 lines
3.8 KiB
Python
80 lines
3.8 KiB
Python
from dataclasses import dataclass
|
|
from typing import Sequence
|
|
|
|
from ..db import Database
|
|
from ..models import STATE_OK, Key, StateRow
|
|
|
|
RECENT_CHANGES_DEFAULT = 5
|
|
|
|
# last_reminder_ts bleibt beim Update unangetastet, damit der Erinnerungs-Worker nicht überschrieben wird.
|
|
UPSERT_SQL = (
|
|
"INSERT INTO service_states(host, service, state, output, last_state_change, in_downtime, "
|
|
"flapping, reported_state, flap_reported, problem_since, reported_at, reported_since, "
|
|
"last_reminder_ts, ack_state, host_held) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) "
|
|
"ON CONFLICT(host, service) DO UPDATE SET state=excluded.state, output=excluded.output, "
|
|
"last_state_change=excluded.last_state_change, in_downtime=excluded.in_downtime, "
|
|
"flapping=excluded.flapping, reported_state=excluded.reported_state, "
|
|
"flap_reported=excluded.flap_reported, problem_since=excluded.problem_since, "
|
|
"reported_at=excluded.reported_at, reported_since=excluded.reported_since, "
|
|
"ack_state=excluded.ack_state, host_held=excluded.host_held"
|
|
)
|
|
LOG_SQL = "INSERT INTO state_log(host, service, state, ts) VALUES (?,?,?,?)"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class StateChange:
|
|
ts: int
|
|
state: int
|
|
|
|
|
|
def _from_db(row) -> StateRow:
|
|
return StateRow(row["host"], row["service"], row["state"], row["output"], row["last_state_change"],
|
|
bool(row["in_downtime"]), bool(row["flapping"]), row["reported_state"],
|
|
bool(row["flap_reported"]), row["problem_since"], row["reported_at"],
|
|
row["reported_since"], row["last_reminder_ts"], row["ack_state"], bool(row["host_held"]))
|
|
|
|
|
|
class StateStore:
|
|
def __init__(self, db: Database):
|
|
self._db = db
|
|
|
|
def load(self) -> dict[Key, StateRow]:
|
|
rows = self._db.query("SELECT * FROM service_states")
|
|
return {(r["host"], r["service"]): _from_db(r) for r in rows}
|
|
|
|
def apply(self, previous: dict[Key, StateRow], rows: list[StateRow], now: int,
|
|
extra: Sequence[tuple[str, tuple]] = ()) -> None:
|
|
statements: list[tuple[str, tuple]] = []
|
|
new_keys = {row.key for row in rows}
|
|
for key, prev in previous.items():
|
|
if key in new_keys:
|
|
continue
|
|
statements.append(("DELETE FROM service_states WHERE host=? AND service=?", key))
|
|
if prev.state != STATE_OK:
|
|
statements.append((LOG_SQL, (*key, STATE_OK, now)))
|
|
for row in rows:
|
|
prev = previous.get(row.key)
|
|
if prev == row:
|
|
continue
|
|
if prev is None or prev.state != row.state:
|
|
statements.append((LOG_SQL, (row.host, row.service, row.state, row.last_state_change or now)))
|
|
statements.append((UPSERT_SQL, (row.host, row.service, row.state, row.output,
|
|
row.last_state_change, int(row.in_downtime), int(row.flapping),
|
|
row.reported_state, int(row.flap_reported), row.problem_since,
|
|
row.reported_at, row.reported_since, row.last_reminder_ts,
|
|
row.ack_state, int(row.host_held))))
|
|
statements.extend(extra)
|
|
if statements:
|
|
self._db.execute_many(statements)
|
|
|
|
def mark_reminded(self, host: str, service: str, ts: int) -> None:
|
|
self._db.execute("UPDATE service_states SET last_reminder_ts=? WHERE host=? AND service=?",
|
|
(ts, host, service))
|
|
|
|
def recent_changes(self, host: str, service: str, limit: int = RECENT_CHANGES_DEFAULT) -> list[StateChange]:
|
|
rows = self._db.query(
|
|
"SELECT ts, state FROM state_log WHERE host=? AND service=? ORDER BY ts DESC, id DESC LIMIT ?",
|
|
(host, service, limit),
|
|
)
|
|
return [StateChange(r["ts"], r["state"]) for r in rows]
|