Files
patrickandClaude Sonnet 5.5 347f55cf7c
test / pytest (3.11) (push) Canceled after 0s
test / pytest (3.13) (push) Canceled after 0s
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

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