fix(alerts): краш при restart-политике, гистерезис закрытия, редкие правки
CI / ruff · mypy · pytest (pull_request) Successful in 24m47s

Найдено смоуком на живых контейнерах (examples/, #19):
- контейнер с restart: on-failure поднимается за секунду — между раундами
  его не видно в exited; краш теперь распознаётся и по росту RestartCount
  (сравнение со снапшотами до текущего раунда), повторы — счётчик
- алерт закрывается, только когда условия нет resolve_after (60 с): иначе
  флапающий сервис получал бы «✅ восстановлено» после каждого раунда
- правки сообщений со счётчиком — не чаще раза в минуту на алерт

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-17 15:52:54 +03:00
co-authored by Claude Opus 5.5
parent 6c6f766abc
commit 79ea5ce2a5
7 changed files with 77 additions and 17 deletions
+4 -3
View File
@@ -7,7 +7,7 @@ Old rows are removed by `app.db.maintenance`.
import asyncio
import logging
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from dataclasses import dataclass, replace
from datetime import datetime
from sqlalchemy import select
@@ -62,6 +62,7 @@ class Observation:
details: Details
stats: Stats | None # None when not running or stats failed
at: datetime | None = None # the round's time; its own snapshot row carries the same `at`
async def _observe(docker: DockerClient, container_id: str) -> Observation | None:
@@ -76,7 +77,7 @@ async def _observe(docker: DockerClient, container_id: str) -> Observation | Non
stats = await docker.stats(container_id)
except DockerError as e:
log.warning("stats %s: %s", container_id[:12], e)
return Observation(details, stats)
return Observation(details, stats) # `at` is set by collect_once
def _row(o: Observation, at: datetime) -> ContainerSnapshot | None:
@@ -96,7 +97,7 @@ async def collect_once(docker: DockerClient, store: SnapshotStore, now: datetime
"""Inspect every managed container, stats for the running ones; a snapshot row per running container."""
at = now or utcnow()
found = await asyncio.gather(*(_observe(docker, c.id) for c in await docker.containers()))
observations = [o for o in found if o is not None]
observations = [replace(o, at=at) for o in found if o is not None]
await store.add([row for o in observations if (row := _row(o, at)) is not None])
return observations