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>
54 lines
2.1 KiB
Python
54 lines
2.1 KiB
Python
import asyncio
|
|
from dataclasses import replace
|
|
from typing import Callable, Iterable
|
|
|
|
from ..models import (KIND_FLAPPING, KIND_PROBLEM, KIND_RECOVERY, KIND_REMINDER, SUPPRESS_NOT_REPORTED,
|
|
SUPPRESS_RECOVERY_OFF, Event, Key)
|
|
|
|
REPORTED_KINDS = (KIND_PROBLEM, KIND_FLAPPING, KIND_REMINDER)
|
|
|
|
ReportCounts = Callable[[str, str], tuple[int, int]]
|
|
|
|
|
|
class ReportGate:
|
|
"""Merkt pro Service, ob das laufende Problem tatsächlich gesendet wurde (Grundlage für Recovery)."""
|
|
|
|
def __init__(self, counts: ReportCounts):
|
|
self._counts = counts
|
|
self._reported: dict[Key, bool] = {}
|
|
|
|
def reset(self, reported: Iterable[Key]) -> None:
|
|
self._reported = {key: True for key in reported}
|
|
|
|
async def apply(self, event: Event, recovery_enabled: bool) -> Event:
|
|
if event.kind != KIND_RECOVERY or event.suppressed:
|
|
return event
|
|
if not recovery_enabled:
|
|
return replace(event, suppressed=True, suppress_reason=SUPPRESS_RECOVERY_OFF)
|
|
if not await self._was_reported((event.host, event.service)):
|
|
return replace(event, suppressed=True, suppress_reason=SUPPRESS_NOT_REPORTED)
|
|
return event
|
|
|
|
async def track(self, event: Event, sent: bool) -> None:
|
|
key = (event.host, event.service)
|
|
if event.kind == KIND_RECOVERY:
|
|
self._reported.pop(key, None)
|
|
elif event.kind in REPORTED_KINDS:
|
|
before = await self._sent_before(key)
|
|
self._reported[key] = before or sent
|
|
|
|
async def _sent_before(self, key: Key) -> bool:
|
|
known = self._reported.get(key)
|
|
if known is not None:
|
|
return known
|
|
_total, sent = await asyncio.to_thread(self._counts, *key)
|
|
return sent > 0
|
|
|
|
async def _was_reported(self, key: Key) -> bool:
|
|
known = self._reported.get(key)
|
|
if known is not None:
|
|
return known
|
|
total, sent = await asyncio.to_thread(self._counts, *key)
|
|
# Ohne jede Historie (z. B. durch Retention gelöscht) wird die Recovery nicht verschluckt.
|
|
return total == 0 or sent > 0
|