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>
118 lines
4.7 KiB
Python
118 lines
4.7 KiB
Python
import asyncio
|
|
import logging
|
|
import time
|
|
from typing import Awaitable, Callable
|
|
|
|
from ..checkmk import CheckmkClient, ServiceState
|
|
from ..models import Event
|
|
from ..routing import Route, delay_provider
|
|
from .baseline import BaselineGuard, baseline_rows, open_rows
|
|
from .events import compute_events
|
|
from .notify import SystemNotifier
|
|
from .options import PollOptions, read_poll_options
|
|
from .outage import OutageTracker, classify
|
|
from .state import StateStore
|
|
from .status import PollerStatus
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
ERROR_MAX = 200
|
|
|
|
EventSink = Callable[[list[Event]], Awaitable[None]]
|
|
RuleSource = Callable[[], list[Route]]
|
|
|
|
|
|
class Poller:
|
|
def __init__(self, store, state: StateStore, sink: EventSink, status: PollerStatus,
|
|
client_factory: Callable[[], CheckmkClient | None] | None = None,
|
|
clock: Callable[[], float] = time.time, rules: RuleSource | None = None,
|
|
notifier: SystemNotifier | None = None, baseline: BaselineGuard | None = None,
|
|
tracker: OutageTracker | None = None):
|
|
self._store = store
|
|
self._state = state
|
|
self._sink = sink
|
|
self.status = status
|
|
self._client_factory = client_factory or (lambda: CheckmkClient.from_settings(store))
|
|
self._clock = clock
|
|
self._rules = rules or (lambda: [])
|
|
self._notifier = notifier
|
|
self._baseline = baseline
|
|
self._tracker = tracker or OutageTracker()
|
|
|
|
async def run(self) -> None:
|
|
while True:
|
|
options = read_poll_options(self._store)
|
|
self.status.interval = options.interval
|
|
try:
|
|
await self.run_cycle(options)
|
|
except asyncio.CancelledError:
|
|
raise
|
|
except Exception:
|
|
log.exception("Poller-Zyklus fehlgeschlagen")
|
|
await asyncio.sleep(options.interval)
|
|
|
|
async def run_cycle(self, options: PollOptions | None = None) -> None:
|
|
options = options or read_poll_options(self._store)
|
|
client = self._client_factory()
|
|
if client is None:
|
|
return
|
|
try:
|
|
current = await self._fetch(client)
|
|
except Exception as exc:
|
|
await self._on_failure(exc)
|
|
return
|
|
finally:
|
|
await client.aclose()
|
|
now = int(self._clock())
|
|
await self._on_reachable(now)
|
|
previous = await asyncio.to_thread(self._state.load)
|
|
if self._baseline is not None and await asyncio.to_thread(self._baseline.needed, not previous):
|
|
await self._record_baseline(previous, current, now)
|
|
return
|
|
rules = await asyncio.to_thread(self._rules)
|
|
delay = delay_provider(rules, options.default_delay_minutes, options.host_delay_minutes)
|
|
events, rows = compute_events(previous, current, now, delay)
|
|
await asyncio.to_thread(self._state.apply, previous, rows, now)
|
|
self._mark_success()
|
|
if events:
|
|
try:
|
|
await self._sink(events)
|
|
except Exception:
|
|
log.exception("Verarbeitung der Events fehlgeschlagen")
|
|
|
|
async def _fetch(self, client: CheckmkClient) -> list[ServiceState]:
|
|
services = await client.problem_services()
|
|
hosts = await client.problem_hosts()
|
|
return [*services, *(host.as_service() for host in hosts)]
|
|
|
|
def _mark_success(self) -> None:
|
|
self.status.last_success = self._clock()
|
|
self.status.last_error = None
|
|
|
|
async def _record_baseline(self, previous, current: list[ServiceState], now: int) -> None:
|
|
rows = baseline_rows(previous, current, now)
|
|
await asyncio.to_thread(self._state.apply, previous, rows, now, self._baseline.completion())
|
|
self._mark_success()
|
|
if self._notifier is not None:
|
|
await self._notify(self._notifier.baseline(open_rows(rows)))
|
|
|
|
async def _on_failure(self, exc: Exception) -> None:
|
|
self.status.last_error = f"{type(exc).__name__}: {exc}"[:ERROR_MAX]
|
|
log.warning("Checkmk-Abfrage fehlgeschlagen: %s", self.status.last_error)
|
|
kind, brief = classify(exc)
|
|
notice = self._tracker.failed(kind, brief, int(self._clock()))
|
|
if notice is not None and self._notifier is not None:
|
|
await self._notify(self._notifier.outage(notice))
|
|
|
|
async def _on_reachable(self, now: int) -> None:
|
|
notice = self._tracker.succeeded(now)
|
|
if notice is not None and self._notifier is not None:
|
|
await self._notify(self._notifier.restored(notice))
|
|
|
|
@staticmethod
|
|
async def _notify(call: Awaitable[None]) -> None:
|
|
try:
|
|
await call
|
|
except Exception:
|
|
log.exception("Systemmeldung konnte nicht gesendet werden")
|