Files
checkbot/app/poller/loop.py
T
patrickandClaude Sonnet 5.5 347f55cf7c
test / pytest (3.11) (push) Waiting to run
test / pytest (3.13) (push) Waiting to run
feat: Phase 6c baseline, ack, host alarms, outage notice, send limit, privacy mode, UI link
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>
2026-11-06 03:06:23 +01:00

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