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")