199 lines
6.3 KiB
Python
199 lines
6.3 KiB
Python
"""
|
|
알림 규칙 CRUD API (모바일 기능 #45).
|
|
|
|
엔드포인트:
|
|
GET /api/alert-rules/ — 내 알림 규칙 목록 (tenant 필터)
|
|
POST /api/alert-rules/ — 알림 규칙 생성
|
|
PUT /api/alert-rules/{id} — 알림 규칙 수정
|
|
DELETE /api/alert-rules/{id} — 알림 규칙 삭제
|
|
PATCH /api/alert-rules/{id}/toggle — 활성/비활성 토글
|
|
|
|
AlertRule: target_type(server/service/sr), metric(cpu/memory/disk/sla),
|
|
threshold, operator(>/</=), channel(push/inapp/sms), enabled
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from typing import List, Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel, ConfigDict, field_validator
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from core.auth import get_current_user
|
|
from database import get_db
|
|
from models import AlertRule, User
|
|
|
|
router = APIRouter(prefix="/api/alert-rules", tags=["Alert Rules"])
|
|
|
|
_VALID_TARGET = {"server", "service", "sr"}
|
|
_VALID_METRIC = {"cpu", "memory", "disk", "sla"}
|
|
_VALID_OPERATOR = {">", "<", "="}
|
|
_VALID_CHANNEL = {"push", "inapp", "sms"}
|
|
|
|
|
|
def _tenant_of(user: User) -> str:
|
|
"""사용자의 테넌트 식별자 — inst_code 우선, 없으면 username 단위 격리."""
|
|
return user.inst_code or f"user:{user.username}"
|
|
|
|
|
|
class AlertRuleCreate(BaseModel):
|
|
target_type: str
|
|
target_id: Optional[str] = None
|
|
metric: str
|
|
threshold: float
|
|
operator: str = ">"
|
|
channel: str = "inapp"
|
|
enabled: bool = True
|
|
|
|
@field_validator("target_type")
|
|
@classmethod
|
|
def _v_target(cls, v: str) -> str:
|
|
if v not in _VALID_TARGET:
|
|
raise ValueError(f"target_type은 {_VALID_TARGET} 중 하나여야 합니다.")
|
|
return v
|
|
|
|
@field_validator("metric")
|
|
@classmethod
|
|
def _v_metric(cls, v: str) -> str:
|
|
if v not in _VALID_METRIC:
|
|
raise ValueError(f"metric은 {_VALID_METRIC} 중 하나여야 합니다.")
|
|
return v
|
|
|
|
@field_validator("operator")
|
|
@classmethod
|
|
def _v_op(cls, v: str) -> str:
|
|
if v not in _VALID_OPERATOR:
|
|
raise ValueError(f"operator는 {_VALID_OPERATOR} 중 하나여야 합니다.")
|
|
return v
|
|
|
|
@field_validator("channel")
|
|
@classmethod
|
|
def _v_ch(cls, v: str) -> str:
|
|
if v not in _VALID_CHANNEL:
|
|
raise ValueError(f"channel은 {_VALID_CHANNEL} 중 하나여야 합니다.")
|
|
return v
|
|
|
|
|
|
class AlertRuleUpdate(BaseModel):
|
|
target_type: Optional[str] = None
|
|
target_id: Optional[str] = None
|
|
metric: Optional[str] = None
|
|
threshold: Optional[float] = None
|
|
operator: Optional[str] = None
|
|
channel: Optional[str] = None
|
|
enabled: Optional[bool] = None
|
|
|
|
|
|
class AlertRuleOut(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
target_type: str
|
|
target_id: Optional[str]
|
|
metric: str
|
|
threshold: float
|
|
operator: str
|
|
channel: str
|
|
enabled: bool
|
|
created_by: Optional[str]
|
|
created_at: Optional[datetime]
|
|
|
|
|
|
@router.get("/", response_model=List[AlertRuleOut])
|
|
async def list_alert_rules(
|
|
enabled: Optional[bool] = None,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""내 테넌트의 알림 규칙 목록."""
|
|
q = select(AlertRule).where(AlertRule.tenant_id == _tenant_of(current_user))
|
|
if enabled is not None:
|
|
q = q.where(AlertRule.enabled == enabled)
|
|
q = q.order_by(AlertRule.created_at.desc())
|
|
rows = (await db.execute(q)).scalars().all()
|
|
return rows
|
|
|
|
|
|
@router.post("/", response_model=AlertRuleOut, status_code=201)
|
|
async def create_alert_rule(
|
|
payload: AlertRuleCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
rule = AlertRule(
|
|
tenant_id=_tenant_of(current_user),
|
|
target_type=payload.target_type,
|
|
target_id=payload.target_id,
|
|
metric=payload.metric,
|
|
threshold=payload.threshold,
|
|
operator=payload.operator,
|
|
channel=payload.channel,
|
|
enabled=payload.enabled,
|
|
created_by=current_user.username,
|
|
)
|
|
db.add(rule)
|
|
await db.commit()
|
|
await db.refresh(rule)
|
|
return rule
|
|
|
|
|
|
async def _get_owned_rule(rule_id: int, db: AsyncSession, user: User) -> AlertRule:
|
|
rule = await db.get(AlertRule, rule_id)
|
|
if not rule or rule.tenant_id != _tenant_of(user):
|
|
raise HTTPException(404, "알림 규칙을 찾을 수 없습니다.")
|
|
return rule
|
|
|
|
|
|
@router.put("/{rule_id}", response_model=AlertRuleOut)
|
|
async def update_alert_rule(
|
|
rule_id: int,
|
|
payload: AlertRuleUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
rule = await _get_owned_rule(rule_id, db, current_user)
|
|
data = payload.model_dump(exclude_unset=True)
|
|
# 유효성 검증
|
|
if "target_type" in data and data["target_type"] not in _VALID_TARGET:
|
|
raise HTTPException(422, f"target_type은 {_VALID_TARGET} 중 하나여야 합니다.")
|
|
if "metric" in data and data["metric"] not in _VALID_METRIC:
|
|
raise HTTPException(422, f"metric은 {_VALID_METRIC} 중 하나여야 합니다.")
|
|
if "operator" in data and data["operator"] not in _VALID_OPERATOR:
|
|
raise HTTPException(422, f"operator는 {_VALID_OPERATOR} 중 하나여야 합니다.")
|
|
if "channel" in data and data["channel"] not in _VALID_CHANNEL:
|
|
raise HTTPException(422, f"channel은 {_VALID_CHANNEL} 중 하나여야 합니다.")
|
|
for k, v in data.items():
|
|
setattr(rule, k, v)
|
|
rule.updated_at = datetime.now()
|
|
await db.commit()
|
|
await db.refresh(rule)
|
|
return rule
|
|
|
|
|
|
@router.delete("/{rule_id}", status_code=204)
|
|
async def delete_alert_rule(
|
|
rule_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
rule = await _get_owned_rule(rule_id, db, current_user)
|
|
await db.delete(rule)
|
|
await db.commit()
|
|
|
|
|
|
@router.patch("/{rule_id}/toggle", response_model=AlertRuleOut)
|
|
async def toggle_alert_rule(
|
|
rule_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""알림 규칙 활성/비활성 토글."""
|
|
rule = await _get_owned_rule(rule_id, db, current_user)
|
|
rule.enabled = not rule.enabled
|
|
rule.updated_at = datetime.now()
|
|
await db.commit()
|
|
await db.refresh(rule)
|
|
return rule
|