v1.1.0: Alembic, pytest, домен и документация
- Миграции PostgreSQL через Alembic; DDL убран из lifespan приложения. - Тесты: health, status, ingress Grafana; моки Vault/Grafana/Forgejo. - Пакет onguard24/domain/ (сущности, шина событий), docs/DOMAIN.md. - Обновлены README, CHANGELOG, ARCHITECTURE. Made-with: Cursor
This commit is contained in:
23
onguard24/domain/__init__.py
Normal file
23
onguard24/domain/__init__.py
Normal file
@ -0,0 +1,23 @@
|
||||
"""Доменные сущности и шина событий (задел под модули)."""
|
||||
|
||||
from onguard24.domain.entities import Alert, EscalationPolicy, EscalationStep, Incident, Severity
|
||||
from onguard24.domain.events import (
|
||||
AlertReceived,
|
||||
DomainEvent,
|
||||
EventBus,
|
||||
InMemoryEventBus,
|
||||
Module,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"Severity",
|
||||
"Alert",
|
||||
"Incident",
|
||||
"EscalationPolicy",
|
||||
"EscalationStep",
|
||||
"DomainEvent",
|
||||
"AlertReceived",
|
||||
"Module",
|
||||
"EventBus",
|
||||
"InMemoryEventBus",
|
||||
]
|
||||
59
onguard24/domain/entities.py
Normal file
59
onguard24/domain/entities.py
Normal file
@ -0,0 +1,59 @@
|
||||
"""Сущности домена (пока без таблиц БД — контракт для следующих версий)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class Severity(str, Enum):
|
||||
"""Грубая шкала для алертов и инцидентов."""
|
||||
|
||||
INFO = "info"
|
||||
WARNING = "warning"
|
||||
CRITICAL = "critical"
|
||||
|
||||
|
||||
class Alert(BaseModel):
|
||||
"""Нормализованный алерт после парсинга ingress (Grafana и др.)."""
|
||||
|
||||
id: UUID = Field(default_factory=uuid4)
|
||||
source: str = Field(..., description="grafana, manual, …")
|
||||
external_ref: str | None = Field(None, description="uid правила, fingerprint")
|
||||
title: str = ""
|
||||
severity: Severity = Severity.WARNING
|
||||
labels: dict[str, str] = Field(default_factory=dict)
|
||||
payload: dict[str, Any] = Field(default_factory=dict)
|
||||
received_at: datetime | None = None
|
||||
|
||||
|
||||
class Incident(BaseModel):
|
||||
"""Инцидент в продукте (отдельно от сырого ingress_events)."""
|
||||
|
||||
id: UUID = Field(default_factory=uuid4)
|
||||
title: str = ""
|
||||
status: str = Field("open", description="open, acknowledged, resolved, …")
|
||||
severity: Severity = Severity.WARNING
|
||||
alert_ids: list[UUID] = Field(default_factory=list)
|
||||
created_at: datetime | None = None
|
||||
updated_at: datetime | None = None
|
||||
|
||||
|
||||
class EscalationStep(BaseModel):
|
||||
"""Один шаг цепочки (уведомление, пауза, повтор)."""
|
||||
|
||||
order: int = 0
|
||||
kind: str = Field(..., description="notify, wait, repeat, …")
|
||||
config: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class EscalationPolicy(BaseModel):
|
||||
"""Политика эскалации, привязанная к команде/сервису."""
|
||||
|
||||
id: UUID = Field(default_factory=uuid4)
|
||||
name: str = ""
|
||||
steps: list[EscalationStep] = Field(default_factory=list)
|
||||
64
onguard24/domain/events.py
Normal file
64
onguard24/domain/events.py
Normal file
@ -0,0 +1,64 @@
|
||||
"""События домена и подписка модулей (задел; пока in-memory)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Protocol
|
||||
from uuid import UUID
|
||||
|
||||
from onguard24.domain.entities import Alert
|
||||
|
||||
|
||||
@dataclass
|
||||
class DomainEvent:
|
||||
"""Базовый тип события."""
|
||||
|
||||
name: str = "domain.generic"
|
||||
occurred_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
|
||||
|
||||
@dataclass
|
||||
class AlertReceived(DomainEvent):
|
||||
"""Алерт принят в систему (после нормализации)."""
|
||||
|
||||
name: str = "alert.received"
|
||||
alert: Alert | None = None
|
||||
raw_payload_ref: UUID | None = None
|
||||
|
||||
|
||||
Handler = Callable[[DomainEvent], Awaitable[None]]
|
||||
|
||||
|
||||
class Module(Protocol):
|
||||
"""Модуль (schedules, contacts, …) может подписаться на события."""
|
||||
|
||||
@property
|
||||
def name(self) -> str: ...
|
||||
|
||||
async def on_event(self, event: DomainEvent) -> None: ...
|
||||
|
||||
|
||||
class EventBus(Protocol):
|
||||
async def publish(self, event: DomainEvent) -> None: ...
|
||||
|
||||
def subscribe(self, event_name: str, handler: Handler) -> None: ...
|
||||
|
||||
|
||||
class InMemoryEventBus:
|
||||
"""Простая шина для тестов и раннего прототипа."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._subs: dict[str, list[Handler]] = {}
|
||||
|
||||
def subscribe(self, event_name: str, handler: Handler) -> None:
|
||||
self._subs.setdefault(event_name, []).append(handler)
|
||||
|
||||
async def publish(self, event: DomainEvent) -> None:
|
||||
for h in self._subs.get(event.name, []):
|
||||
await h(event)
|
||||
|
||||
async def publish_alert_received(self, alert: Alert, raw_payload_ref: UUID | None = None) -> None:
|
||||
ev = AlertReceived(alert=alert, raw_payload_ref=raw_payload_ref)
|
||||
await self.publish(ev)
|
||||
Reference in New Issue
Block a user