release: v1.9.0 — IRM-алерты отдельно от инцидентов
- Alembic 005: таблицы irm_alerts и incident_alert_links - Модуль alerts: API/UI, Ack/Resolve, привязка к инциденту через alert_ids - Вебхук Grafana: одна транзакция ingress + irm_alerts; разбор payload в grafana_payload - По умолчанию инцидент из вебхука не создаётся (AUTO_INCIDENT_FROM_ALERT) - Документация IRM_GRAFANA_PARITY.md, обновления IRM.md и CHANGELOG Made-with: Cursor
This commit is contained in:
@ -12,6 +12,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from onguard24.config import get_settings
|
||||
from onguard24.deps import get_pool
|
||||
from onguard24.domain.events import AlertReceived, DomainEvent, EventBus
|
||||
from onguard24.modules.ui_support import wrap_module_html_page
|
||||
@ -26,6 +27,7 @@ class IncidentCreate(BaseModel):
|
||||
title: str = Field(..., min_length=1, max_length=500)
|
||||
status: str = Field(default="open", max_length=64)
|
||||
severity: str = Field(default="warning", max_length=32)
|
||||
alert_ids: list[UUID] = Field(default_factory=list, description="Привязка к irm_alerts")
|
||||
|
||||
|
||||
class IncidentPatch(BaseModel):
|
||||
@ -39,6 +41,8 @@ def register_events(bus: EventBus, pool: asyncpg.Pool | None = None) -> None:
|
||||
return
|
||||
|
||||
async def on_alert(ev: DomainEvent) -> None:
|
||||
if not get_settings().auto_incident_from_alert:
|
||||
return
|
||||
if not isinstance(ev, AlertReceived) or ev.raw_payload_ref is None:
|
||||
return
|
||||
a = ev.alert
|
||||
@ -136,17 +140,29 @@ async def create_incident_api(
|
||||
if pool is None:
|
||||
raise HTTPException(status_code=503, detail="database disabled")
|
||||
async with pool.acquire() as conn:
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
INSERT INTO incidents (title, status, severity, source, grafana_org_slug, service_name)
|
||||
VALUES ($1, $2, $3, 'manual', NULL, NULL)
|
||||
RETURNING id, title, status, severity, source, ingress_event_id, created_at, updated_at,
|
||||
grafana_org_slug, service_name
|
||||
""",
|
||||
body.title.strip(),
|
||||
body.status,
|
||||
body.severity,
|
||||
)
|
||||
async with conn.transaction():
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
INSERT INTO incidents (title, status, severity, source, grafana_org_slug, service_name)
|
||||
VALUES ($1, $2, $3, 'manual', NULL, NULL)
|
||||
RETURNING id, title, status, severity, source, ingress_event_id, created_at, updated_at,
|
||||
grafana_org_slug, service_name
|
||||
""",
|
||||
body.title.strip(),
|
||||
body.status,
|
||||
body.severity,
|
||||
)
|
||||
iid = row["id"]
|
||||
for aid in body.alert_ids[:50]:
|
||||
await conn.execute(
|
||||
"""
|
||||
INSERT INTO incident_alert_links (incident_id, alert_id)
|
||||
VALUES ($1::uuid, $2::uuid)
|
||||
ON CONFLICT DO NOTHING
|
||||
""",
|
||||
iid,
|
||||
aid,
|
||||
)
|
||||
return {
|
||||
"id": str(row["id"]),
|
||||
"title": row["title"],
|
||||
@ -312,7 +328,7 @@ async def incidents_ui_home(request: Request):
|
||||
<thead><tr><th>ID</th><th>Заголовок</th><th>Статус</th><th>Важность</th><th>Источник</th><th>Grafana slug</th><th>Сервис</th><th>Создан</th></tr></thead>
|
||||
<tbody>{rows_html or '<tr><td colspan="8">Пока нет записей</td></tr>'}</tbody>
|
||||
</table>
|
||||
<p><small>Создание из Grafana: webhook → <code>ingress_events</code> → событие → строка здесь. Пустой заголовок бывает при тестовом JSON без полей алерта.</small></p>"""
|
||||
<p><small>Сначала вебхук создаёт <a href="/ui/modules/alerts/">алерт</a> (учёт, Ack/Resolve). Инцидент — отдельная сущность: создаётся вручную или из карточки алерта, к нему можно привязать один или несколько алертов. Пустой заголовок в списке — часто тестовый JSON без полей правила.</small></p>"""
|
||||
return HTMLResponse(
|
||||
wrap_module_html_page(
|
||||
document_title="Инциденты — onGuard24",
|
||||
|
||||
Reference in New Issue
Block a user