feat(alerts): алерты всей команде — детекторы, дедупликация, список (#15)
CI / ruff · mypy · pytest (pull_request) Successful in 4m49s
CI / ruff · mypy · pytest (pull_request) Successful in 4m49s
- детекторы по состоянию после каждого раунда снапшотов: краш, OOM, цикл рестартов, unhealthy, перегрузка ≥ 1 мин, мало места, Docker недоступен; пороги в config/alerts.yaml - AlertService: ключ kind+service, повтор в 10 мин — счётчик в исходном сообщении, «✅ восстановлено» ответом; разовые события (🛡) со счётчиком - рассылка всем активным (🛡 — только админам), темп ~25/с, RetryAfter, заблокировавший бота пропускается; alert_deliveries для правок и ответов - «🚨 Алерты»: активные + 7 дней по 10, карточка; кнопки «📜 Логи», «🩺 Статус» под алертом открывают новое сообщение, а не затирают алерт - 🛡 атаки через агента идут через общий сервис алертов - убраны заглушки «Раздел в разработке» — у всех разделов свои роутеры Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+15
-22
@@ -6,19 +6,19 @@ import logging
|
||||
from datetime import timedelta
|
||||
from enum import StrEnum
|
||||
|
||||
from aiogram import Bot, F, Router
|
||||
from aiogram import F, Router
|
||||
from aiogram.enums import ParseMode
|
||||
from aiogram.exceptions import TelegramAPIError
|
||||
from aiogram.filters.callback_data import CallbackData
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
from aiogram.types import BufferedInputFile, CallbackQuery, InlineKeyboardMarkup, Message
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from app.access.service import AccessService
|
||||
from app.agent.service import Agent, Outcome
|
||||
from app.alerts.detectors import Condition
|
||||
from app.alerts.service import AlertService
|
||||
from app.bot.menu import Nav, NavAction, free_input
|
||||
from app.db.models import LogMode, Role, User, utcnow
|
||||
from app.db.models import AlertKind, LogMode, User, utcnow
|
||||
from app.logs.grep import GrepError, GrepErrorCode, GrepResult, parse_grep, run, to_command
|
||||
from app.logs.journal import LogJournal
|
||||
from app.runtime.docker import Container, DockerClient, DockerError
|
||||
@@ -146,7 +146,7 @@ def build_router(
|
||||
docker: DockerClient | None,
|
||||
journal: LogJournal,
|
||||
agent: Agent | None = None,
|
||||
access: AccessService | None = None,
|
||||
alerts: AlertService | None = None,
|
||||
) -> Router:
|
||||
router = Router(name="logs")
|
||||
t = texts.logs
|
||||
@@ -156,23 +156,16 @@ def build_router(
|
||||
raise DockerError("docker is not configured (DOCKER_URL)")
|
||||
return docker
|
||||
|
||||
async def alert_admins(bot: Bot, member: User, prompt: str, stage: str, reason: str) -> None:
|
||||
"""🛡 to the admins only (SPEC AL-2, L-12). The asker sees just a polite refusal."""
|
||||
if access is None:
|
||||
async def alert_admins(member: User, prompt: str, stage: str, reason: str) -> None:
|
||||
"""🛡 to the admins only (SPEC AL-2, L-12); repeats by the same member bump one alert's counter."""
|
||||
if alerts is None:
|
||||
return
|
||||
text = t.attack_alert.format(
|
||||
who=html.escape(who(member)),
|
||||
stage=stage,
|
||||
reason=html.escape(reason),
|
||||
prompt=html.escape(prompt[:1000]),
|
||||
condition = Condition(
|
||||
AlertKind.AGENT_ATTACK,
|
||||
f"user {member.tg_id}",
|
||||
{"who": who(member), "stage": stage, "reason": reason, "log": prompt[:1000]},
|
||||
)
|
||||
for admin in await access.list_members():
|
||||
if admin.role is not Role.ADMIN:
|
||||
continue
|
||||
try:
|
||||
await bot.send_message(admin.tg_id, text, parse_mode=ParseMode.HTML)
|
||||
except TelegramAPIError as e: # one blocked admin must not hide the alert from the others
|
||||
log.warning("attack alert to %s failed: %s", admin.tg_id, e)
|
||||
await alerts.event(condition, admins_only=True)
|
||||
|
||||
@router.message(F.text == texts.menu.logs)
|
||||
async def open_logs(message: Message, state: FSMContext) -> None:
|
||||
@@ -275,7 +268,7 @@ def build_router(
|
||||
await deliver(message, texts, head, query.count, result, container_keyboard(texts, c.short_id), c.name)
|
||||
|
||||
@router.message(LogsFlow.agent, F.text, free_input(texts))
|
||||
async def got_question(message: Message, state: FSMContext, member: User, bot: Bot) -> None:
|
||||
async def got_question(message: Message, state: FSMContext, member: User) -> None:
|
||||
if agent is None: # pragma: no cover - the button doesn't open this state without an agent
|
||||
await state.clear()
|
||||
return
|
||||
@@ -296,7 +289,7 @@ def build_router(
|
||||
match answer.outcome:
|
||||
case Outcome.ATTACK:
|
||||
await message.answer(t.agent_refused, reply_markup=kb)
|
||||
await alert_admins(bot, member, prompt, answer.stage, answer.reason)
|
||||
await alert_admins(member, prompt, answer.stage, answer.reason)
|
||||
return
|
||||
case Outcome.QUOTA:
|
||||
await message.answer(t.agent_quota.format(quota=agent.quota), reply_markup=kb)
|
||||
|
||||
Reference in New Issue
Block a user