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>
131 lines
6.0 KiB
Python
131 lines
6.0 KiB
Python
from dataclasses import replace
|
|
from typing import Callable, Mapping, Sequence
|
|
|
|
from ..checkmk import ServiceState
|
|
from ..models import (HOST_SERVICE_NAME, KIND_FLAPPING, KIND_PROBLEM, KIND_RECOVERY, STATE_OK, SUPPRESS_ACK,
|
|
SUPPRESS_DOWNTIME, SUPPRESS_HOST_DOWN, Event, Key, StateRow, ack_holds)
|
|
|
|
FLAP_QUIET_SECONDS = 600
|
|
|
|
DelayProvider = Callable[[str, str, int, str], int]
|
|
|
|
|
|
def no_delay(host: str, service: str, state: int, output: str) -> int:
|
|
return 0
|
|
|
|
|
|
def _problem_since(cur: ServiceState, prev: StateRow | None, now: int) -> int:
|
|
if prev is not None and prev.state != STATE_OK:
|
|
return prev.problem_since or prev.last_state_change
|
|
return cur.last_state_change or now
|
|
|
|
|
|
def _ack_state(cur: ServiceState, prev: StateRow | None) -> int | None:
|
|
if not cur.acknowledged:
|
|
return None
|
|
return prev.ack_state if prev is not None and prev.ack_state is not None else cur.state
|
|
|
|
|
|
def build_row(cur: ServiceState, prev: StateRow | None, now: int, reported_state: int, flap_reported: bool,
|
|
reported: tuple[int, int] | None = None, held: bool = False) -> StateRow:
|
|
reported_at, reported_since = reported or ((prev.reported_at, prev.reported_since) if prev else (0, 0))
|
|
return StateRow(cur.host, cur.service, cur.state, cur.output, cur.last_state_change, cur.in_downtime,
|
|
cur.flapping, reported_state, flap_reported, _problem_since(cur, prev, now),
|
|
reported_at, reported_since, prev.last_reminder_ts if prev else 0,
|
|
_ack_state(cur, prev), held)
|
|
|
|
|
|
def _reason(cur: ServiceState, ack_state: int | None, down_hosts: frozenset[str]) -> str | None:
|
|
if cur.in_downtime:
|
|
return SUPPRESS_DOWNTIME
|
|
if cur.service != HOST_SERVICE_NAME and cur.host in down_hosts:
|
|
return SUPPRESS_HOST_DOWN
|
|
if ack_holds(cur.state, ack_state):
|
|
return SUPPRESS_ACK
|
|
return None
|
|
|
|
|
|
def _event(now: int, kind: str, cur: ServiceState, row: StateRow, reason: str | None, **extra) -> Event:
|
|
return Event(ts=now, host=cur.host, service=cur.service, kind=kind, state=cur.state, output=cur.output,
|
|
suppressed=reason is not None, suppress_reason=reason,
|
|
since_ts=cur.last_state_change, problem_since_ts=row.problem_since, **extra)
|
|
|
|
|
|
def _problem_event(now: int, cur: ServiceState, prev: StateRow | None, row: StateRow,
|
|
reason: str | None) -> Event:
|
|
if prev is None or prev.reported_state == STATE_OK:
|
|
return _event(now, KIND_PROBLEM, cur, row, reason)
|
|
duration = max(cur.last_state_change - prev.reported_since, 0) if prev.reported_since else None
|
|
return _event(now, KIND_PROBLEM, cur, row, reason, previous_state=prev.reported_state,
|
|
previous_duration=duration)
|
|
|
|
|
|
def _handle_current(cur: ServiceState, prev: StateRow | None, now: int, delay_seconds: DelayProvider,
|
|
down_hosts: frozenset[str]) -> tuple[StateRow | None, Event | None]:
|
|
if not cur.hard:
|
|
return prev, None
|
|
reported = prev.reported_state if prev else STATE_OK
|
|
flap_reported = prev.flap_reported if prev else False
|
|
ack = _ack_state(cur, prev)
|
|
reason = _reason(cur, ack, down_hosts)
|
|
if cur.flapping:
|
|
row = build_row(cur, prev, now, reported, True)
|
|
return row, (None if flap_reported else _event(now, KIND_FLAPPING, cur, row, reason))
|
|
delay = delay_seconds(cur.host, cur.service, cur.state, cur.output)
|
|
if now - cur.last_state_change < delay or cur.state == reported:
|
|
return build_row(cur, prev, now, reported, False), None
|
|
if reason == SUPPRESS_HOST_DOWN:
|
|
# Nicht als gemeldet markieren: bleibt das Problem nach der Rückkehr des Hosts bestehen, wird es normal gemeldet.
|
|
already = prev is not None and prev.host_held and prev.state == cur.state
|
|
row = build_row(cur, prev, now, reported, False, held=True)
|
|
return row, (None if already else _problem_event(now, cur, prev, row, reason))
|
|
row = build_row(cur, prev, now, cur.state, False, (now, cur.last_state_change))
|
|
return row, _problem_event(now, cur, prev, row, reason)
|
|
|
|
|
|
def _handle_gone(prev: StateRow, now: int) -> tuple[StateRow | None, Event | None]:
|
|
if prev.flap_reported:
|
|
if prev.state == STATE_OK and now - prev.last_state_change >= FLAP_QUIET_SECONDS:
|
|
return None, None
|
|
if prev.state == STATE_OK:
|
|
return prev, None
|
|
held = replace(prev, state=STATE_OK, output="", last_state_change=now, in_downtime=False,
|
|
flapping=True, flap_reported=True, ack_state=None, host_held=False)
|
|
return held, None
|
|
if prev.reported_state == STATE_OK:
|
|
return None, None
|
|
event = Event(ts=now, host=prev.host, service=prev.service, kind=KIND_RECOVERY, state=STATE_OK,
|
|
suppressed=prev.in_downtime, suppress_reason=SUPPRESS_DOWNTIME if prev.in_downtime else None,
|
|
previous_state=prev.reported_state,
|
|
problem_since_ts=prev.problem_since or prev.last_state_change)
|
|
return None, event
|
|
|
|
|
|
def hosts_not_up(current: Sequence[ServiceState]) -> frozenset[str]:
|
|
return frozenset(c.host for c in current if c.service == HOST_SERVICE_NAME and c.state != STATE_OK)
|
|
|
|
|
|
def compute_events(previous: Mapping[Key, StateRow], current: Sequence[ServiceState], now: int,
|
|
delay_seconds: DelayProvider = no_delay) -> tuple[list[Event], list[StateRow]]:
|
|
events: list[Event] = []
|
|
rows: dict[Key, StateRow] = {}
|
|
seen: set[Key] = set()
|
|
down_hosts = hosts_not_up(current)
|
|
for cur in current:
|
|
key = (cur.host, cur.service)
|
|
seen.add(key)
|
|
row, event = _handle_current(cur, previous.get(key), now, delay_seconds, down_hosts)
|
|
if row is not None:
|
|
rows[key] = row
|
|
if event is not None:
|
|
events.append(event)
|
|
for key, prev in previous.items():
|
|
if key in seen:
|
|
continue
|
|
row, event = _handle_gone(prev, now)
|
|
if row is not None:
|
|
rows[key] = row
|
|
if event is not None:
|
|
events.append(event)
|
|
return events, list(rows.values())
|