guardia-itsm/routers/smart_notify.py
2026-06-02 19:49:59 +09:00

261 lines
8.8 KiB
Python

"""
스마트 알림 규칙 편집기 + 지능형 필터
노코드 방식으로 알림 규칙을 정의하고
AI 기반 스마트 필터로 알림 피로도를 관리한다.
엔드포인트:
GET /api/smart-notify/rules — 규칙 목록
POST /api/smart-notify/rules — 규칙 생성
PUT /api/smart-notify/rules/{id} — 규칙 수정
DELETE /api/smart-notify/rules/{id}— 규칙 삭제
POST /api/smart-notify/test/{id} — 테스트 발송
GET /api/smart-notify/logs — 발송 이력
POST /api/smart-notify/silence — 무음 기간 설정
GET /api/smart-notify/digest — 일괄 요약 설정
"""
from __future__ import annotations
import json
import logging
from datetime import datetime
from typing import List, Optional
import httpx
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
from pydantic import BaseModel, Field
from sqlalchemy import select, desc
from sqlalchemy.ext.asyncio import AsyncSession
from core.auth import get_current_user, require_admin_role
from database import get_db
from models import User, SmartNotifyRule, NotifyLog
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/smart-notify", tags=["스마트 알림"])
OLLAMA_URL = "http://localhost:11434"
ITSM_NOTIFY = "http://127.0.0.1:9001/api/messenger/webhook"
class NotifyRuleCreate(BaseModel):
name: str = Field(..., max_length=200)
trigger_type: str = Field(..., description="SR_CREATED|SR_UPDATED|INCIDENT|DRIFT|KPI_BREACH|CUSTOM")
conditions: dict = Field(default_factory=dict)
channels: List[str] = Field(default_factory=list, description=["messenger","email","slack","kakao"])
priority_filter: str = Field("ALL", description="HIGH|MEDIUM|ALL")
silence_hours: Optional[List[int]] = Field(None, description="무음 시간 목록 [22,23,0,1,...,7]")
digest_mode: bool = Field(False, description="묶음 발송 모드")
digest_interval_min: int = Field(60, description="묶음 발송 간격(분)")
is_active: bool = True
class SilenceRequest(BaseModel):
rule_id: Optional[int] = None # None = 전체
hours: List[int] = Field(..., description="무음 시간 [22,23,0,1,...,7]")
async def _send_to_messenger(message: str, room: str = "ops") -> bool:
try:
async with httpx.AsyncClient(timeout=5) as c:
r = await c.post(ITSM_NOTIFY, json={
"event": "smart_notify", "room": room,
"success": True, "result_summary": message
})
return r.status_code == 200
except Exception as e:
logger.warning(f"메신저 알림 실패: {e}")
return False
async def _ai_classify_importance(notification: dict) -> str:
"""Ollama로 알림 중요도 재평가 (HIGH/MEDIUM/LOW)."""
try:
async with httpx.AsyncClient(timeout=10) as c:
r = await c.post(f"{OLLAMA_URL}/api/generate", json={
"model": "llama3",
"system": "IT 운영 알림 중요도 분류. HIGH/MEDIUM/LOW 중 하나만 답변.",
"prompt": f"알림: {json.dumps(notification, ensure_ascii=False)[:200]}",
"stream": False,
})
if r.status_code == 200:
resp = r.json().get("response", "").strip().upper()
if "HIGH" in resp: return "HIGH"
if "LOW" in resp: return "LOW"
except Exception:
pass
return "MEDIUM"
def _check_conditions(rule: SmartNotifyRule, event: dict) -> bool:
"""규칙 조건 충족 여부 확인."""
conditions = rule.conditions or {}
for key, expected in conditions.items():
actual = event.get(key)
if isinstance(expected, list):
if actual not in expected: return False
elif actual != expected:
return False
return True
def _is_silent_hour(rule: SmartNotifyRule) -> bool:
"""현재 시각이 무음 시간대인지 확인."""
silent = rule.silence_hours
if not silent:
return False
current_hour = datetime.utcnow().hour + 9 # KST
current_hour = current_hour % 24
return current_hour in silent
@router.get("/rules")
async def list_rules(
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
rows = await db.execute(
select(SmartNotifyRule).where(SmartNotifyRule.tenant_id == user.tenant_id)
.order_by(SmartNotifyRule.name)
)
rules = rows.scalars().all()
return [
{
"id": r.id, "name": r.name, "trigger_type": r.trigger_type,
"conditions": r.conditions, "channels": r.channels,
"priority_filter": r.priority_filter,
"silence_hours": r.silence_hours,
"digest_mode": r.digest_mode,
"is_active": r.is_active, "created_at": r.created_at,
}
for r in rules
]
@router.post("/rules")
async def create_rule(
req: NotifyRuleCreate,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_admin_role),
):
rule = SmartNotifyRule(
tenant_id=user.tenant_id,
name=req.name,
trigger_type=req.trigger_type,
conditions=req.conditions,
channels=req.channels,
priority_filter=req.priority_filter,
silence_hours=req.silence_hours or [],
digest_mode=req.digest_mode,
digest_interval_min=req.digest_interval_min,
is_active=req.is_active,
created_at=datetime.utcnow(),
)
db.add(rule)
await db.commit()
await db.refresh(rule)
return {"ok": True, "id": rule.id}
@router.put("/rules/{rule_id}")
async def update_rule(
rule_id: int,
req: NotifyRuleCreate,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_admin_role),
):
row = await db.execute(
select(SmartNotifyRule).where(SmartNotifyRule.id == rule_id, SmartNotifyRule.tenant_id == user.tenant_id)
)
rule = row.scalar_one_or_none()
if not rule:
raise HTTPException(404)
rule.name = req.name; rule.trigger_type = req.trigger_type
rule.conditions = req.conditions; rule.channels = req.channels
rule.priority_filter = req.priority_filter
rule.silence_hours = req.silence_hours or []
rule.digest_mode = req.digest_mode; rule.is_active = req.is_active
await db.commit()
return {"ok": True}
@router.delete("/rules/{rule_id}")
async def delete_rule(
rule_id: int,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_admin_role),
):
row = await db.execute(
select(SmartNotifyRule).where(SmartNotifyRule.id == rule_id, SmartNotifyRule.tenant_id == user.tenant_id)
)
rule = row.scalar_one_or_none()
if not rule: raise HTTPException(404)
await db.delete(rule); await db.commit()
return {"ok": True}
@router.post("/test/{rule_id}")
async def test_rule(
rule_id: int,
background_tasks: BackgroundTasks,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""테스트 알림 발송."""
row = await db.execute(
select(SmartNotifyRule).where(SmartNotifyRule.id == rule_id, SmartNotifyRule.tenant_id == user.tenant_id)
)
rule = row.scalar_one_or_none()
if not rule: raise HTTPException(404)
msg = f"[테스트] 알림 규칙 '{rule.name}' 테스트 발송"
sent = False
if "messenger" in (rule.channels or []):
sent = await _send_to_messenger(msg)
log = NotifyLog(
rule_id=rule.id, channel="messenger",
recipient="ops", message=msg,
status="SENT" if sent else "FAILED",
sent_at=datetime.utcnow(),
)
db.add(log); await db.commit()
return {"ok": sent, "message": msg}
@router.get("/logs")
async def notify_logs(
limit: int = 50,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
rows = await db.execute(
select(NotifyLog, SmartNotifyRule.name.label("rule_name")).join(
SmartNotifyRule, NotifyLog.rule_id == SmartNotifyRule.id, isouter=True
).where(SmartNotifyRule.tenant_id == user.tenant_id)
.order_by(desc(NotifyLog.sent_at)).limit(limit)
)
return [
{"id": r.NotifyLog.id, "rule": r.rule_name,
"channel": r.NotifyLog.channel, "status": r.NotifyLog.status,
"message": r.NotifyLog.message[:100], "sent_at": r.NotifyLog.sent_at}
for r in rows.all()
]
@router.post("/silence")
async def set_silence(
req: SilenceRequest,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_admin_role),
):
"""무음 시간대 설정."""
q = select(SmartNotifyRule).where(SmartNotifyRule.tenant_id == user.tenant_id)
if req.rule_id:
q = q.where(SmartNotifyRule.id == req.rule_id)
rows = await db.execute(q)
rules = rows.scalars().all()
for rule in rules:
rule.silence_hours = req.hours
await db.commit()
return {"ok": True, "updated": len(rules), "silence_hours": req.hours}