release: v1.9.0 — IRM-алерты отдельно от инцидентов
Some checks failed
Deploy / deploy (push) Has been cancelled
CI / test (push) Successful in 37s

- 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:
Alexandr
2026-04-03 15:26:38 +03:00
parent 3cb75eb7b7
commit a8ccf1d35c
19 changed files with 722 additions and 60 deletions

View File

@ -0,0 +1,74 @@
"""IRM: алерты отдельно от инцидентов (ack/resolve), связь N:M инцидент↔алерт
Revision ID: 005_irm_alerts
Revises: 004_grafana_catalog
Create Date: 2026-04-03
"""
from typing import Sequence, Union
from alembic import op
revision: str = "005_irm_alerts"
down_revision: Union[str, None] = "004_grafana_catalog"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.execute(
"""
CREATE TABLE IF NOT EXISTS irm_alerts (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
ingress_event_id uuid NOT NULL UNIQUE REFERENCES ingress_events(id) ON DELETE CASCADE,
status text NOT NULL DEFAULT 'firing'
CHECK (status IN ('firing', 'acknowledged', 'resolved', 'silenced')),
title text NOT NULL DEFAULT '',
severity text NOT NULL DEFAULT 'warning',
source text NOT NULL DEFAULT 'grafana',
grafana_org_slug text,
service_name text,
labels jsonb NOT NULL DEFAULT '{}'::jsonb,
fingerprint text,
acknowledged_at timestamptz,
acknowledged_by text,
resolved_at timestamptz,
resolved_by text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
"""
)
op.execute(
"""
CREATE INDEX IF NOT EXISTS irm_alerts_status_created_idx
ON irm_alerts (status, created_at DESC);
"""
)
op.execute(
"""
CREATE INDEX IF NOT EXISTS irm_alerts_ingress_event_id_idx
ON irm_alerts (ingress_event_id);
"""
)
op.execute(
"""
CREATE TABLE IF NOT EXISTS incident_alert_links (
incident_id uuid NOT NULL REFERENCES incidents(id) ON DELETE CASCADE,
alert_id uuid NOT NULL REFERENCES irm_alerts(id) ON DELETE CASCADE,
PRIMARY KEY (incident_id, alert_id)
);
"""
)
op.execute(
"""
CREATE INDEX IF NOT EXISTS incident_alert_links_alert_idx
ON incident_alert_links (alert_id);
"""
)
def downgrade() -> None:
op.execute("DROP TABLE IF EXISTS incident_alert_links;")
op.execute("DROP TABLE IF EXISTS irm_alerts;")