feat(bot): show actual response, flap threshold, pause/rename/baseline, komodo fleet and alerts

This commit is contained in:
hh committed 2026-07-23 03:45:08 +02:00
1 parent a46bd92b4c
commit a6968ebbe6
18 files changed
+1105 -103

No files matched your search

+17
View File
@@ -32,6 +32,13 @@ CREATE TABLE IF NOT EXISTS incidents (
CREATE INDEX IF NOT EXISTS idx_incidents_service ON incidents(service_id);
"""
MIGRATIONS: tuple[tuple[str, str, str], ...] = (
("services", "fail_streak", "INTEGER NOT NULL DEFAULT 0"),
("services", "is_paused", "INTEGER NOT NULL DEFAULT 0"),
("services", "last_latency_ms", "INTEGER"),
("services", "last_checked_at", "TEXT"),
)
class Database:
def __init__(self, path: str) -> None:
@@ -46,10 +53,20 @@ class Database:
self._conn.row_factory = aiosqlite.Row
await self._conn.execute("PRAGMA foreign_keys = ON")
await self._conn.executescript(SCHEMA)
await self._migrate()
await self._conn.commit()
self.services = ServiceRepository(self._conn)
self.incidents = IncidentRepository(self._conn)
async def _migrate(self) -> None:
assert self._conn is not None
for table, column, decl in MIGRATIONS:
cursor = await self._conn.execute(f"PRAGMA table_info({table})")
existing = {row["name"] for row in await cursor.fetchall()}
if column in existing:
continue
await self._conn.execute(f"ALTER TABLE {table} ADD COLUMN {column} {decl}")
async def close(self) -> None:
if self._conn is not None:
await self._conn.close()
+13 -3
View File
@@ -3,9 +3,9 @@ from enum import StrEnum
class MatchType(StrEnum):
STATUS = "status" # следим только за HTTP-статусом
BODY = "body" # тело ответа должно совпадать целиком
JSON = "json" # конкретное поле JSON должно быть равно эталону
STATUS = "status"
BODY = "body"
JSON = "json"
@dataclass(slots=True)
@@ -20,6 +20,10 @@ class Service:
json_value: str | None
is_up: bool
created_at: str
fail_streak: int = 0
is_paused: bool = False
last_latency_ms: int | None = None
last_checked_at: str | None = None
@property
def rule(self) -> str:
@@ -29,6 +33,12 @@ class Service:
return f"<code>{self.json_path}</code> = <code>{self.json_value}</code>"
return "тело ответа неизменно"
@property
def mark(self) -> str:
if self.is_paused:
return "⏸"
return "🟢" if self.is_up else "🔴"
@dataclass(slots=True)
class Incident:
+43 -1
View File
@@ -5,7 +5,8 @@ from utils.format import now
_COLUMNS = (
"id, name, url, match_type, ok_status, ok_body, "
"json_path, json_value, is_up, created_at"
"json_path, json_value, is_up, created_at, "
"fail_streak, is_paused, last_latency_ms, last_checked_at"
)
@@ -21,6 +22,10 @@ def _row_to_service(row: aiosqlite.Row) -> Service:
json_value=row["json_value"],
is_up=bool(row["is_up"]),
created_at=row["created_at"],
fail_streak=row["fail_streak"],
is_paused=bool(row["is_paused"]),
last_latency_ms=row["last_latency_ms"],
last_checked_at=row["last_checked_at"],
)
@@ -87,6 +92,43 @@ class ServiceRepository:
)
await self._conn.commit()
async def set_streak(self, service_id: int, streak: int) -> None:
await self._conn.execute(
"UPDATE services SET fail_streak = ? WHERE id = ?", (streak, service_id)
)
await self._conn.commit()
async def set_paused(self, service_id: int, *, is_paused: bool) -> None:
await self._conn.execute(
"UPDATE services SET is_paused = ?, fail_streak = 0 WHERE id = ?",
(int(is_paused), service_id),
)
await self._conn.commit()
async def touch_probe(self, service_id: int, latency_ms: int | None) -> None:
await self._conn.execute(
"UPDATE services SET last_latency_ms = ?, last_checked_at = ? WHERE id = ?",
(latency_ms, now().isoformat(), service_id),
)
await self._conn.commit()
async def rename(self, service_id: int, name: str) -> None:
await self._conn.execute(
"UPDATE services SET name = ? WHERE id = ?", (name, service_id)
)
await self._conn.commit()
async def set_baseline(
self, service_id: int, *, ok_status: int, ok_body: str, json_value: str | None
) -> None:
"""Переснять эталон с текущего ответа."""
await self._conn.execute(
"UPDATE services SET ok_status = ?, ok_body = ?, json_value = ? "
"WHERE id = ?",
(ok_status, ok_body, json_value, service_id),
)
await self._conn.commit()
async def delete(self, service_id: int) -> None:
await self._conn.execute("DELETE FROM services WHERE id = ?", (service_id,))
await self._conn.commit()