Files
000-platform/backend/app/routers/alerts.py
111 6c6c48cf71
All checks were successful
continuous-integration/drone/push Build is passing
feat: 新增告警、成本、配额、微信模块及缓存服务
- 新增告警模块 (alerts): 告警规则配置与触发
- 新增成本管理模块 (cost): 成本统计与分析
- 新增配额模块 (quota): 配额管理与限制
- 新增微信模块 (wechat): 微信相关功能接口
- 新增缓存服务 (cache): Redis 缓存封装
- 新增请求日志中间件 (request_logger)
- 新增异常处理和链路追踪中间件
- 更新 dashboard 前端展示
- 更新 SDK stats_client 功能
2026-01-24 16:53:47 +08:00

431 lines
13 KiB
Python

"""告警管理路由"""
from typing import Optional, List
from datetime import datetime, timedelta
from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks
from pydantic import BaseModel
from sqlalchemy.orm import Session
from sqlalchemy import desc, func
from ..database import get_db
from ..models.alert import AlertRule, AlertRecord, NotificationChannel
from ..services.alert import AlertService
from .auth import get_current_user, require_operator
from ..models.user import User
router = APIRouter(prefix="/alerts", tags=["告警管理"])
# ============= Schemas =============
class AlertRuleCreate(BaseModel):
name: str
description: Optional[str] = None
rule_type: str
scope_type: str = "global"
scope_value: Optional[str] = None
condition: dict
notification_channels: Optional[List[dict]] = None
cooldown_minutes: int = 30
max_alerts_per_day: int = 10
priority: str = "medium"
class AlertRuleUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
condition: Optional[dict] = None
notification_channels: Optional[List[dict]] = None
cooldown_minutes: Optional[int] = None
max_alerts_per_day: Optional[int] = None
priority: Optional[str] = None
status: Optional[int] = None
class NotificationChannelCreate(BaseModel):
name: str
channel_type: str
config: dict
class NotificationChannelUpdate(BaseModel):
name: Optional[str] = None
config: Optional[dict] = None
status: Optional[int] = None
# ============= Alert Rules API =============
@router.get("/rules")
async def list_alert_rules(
page: int = Query(1, ge=1),
size: int = Query(20, ge=1, le=100),
rule_type: Optional[str] = None,
status: Optional[int] = None,
user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""获取告警规则列表"""
query = db.query(AlertRule)
if rule_type:
query = query.filter(AlertRule.rule_type == rule_type)
if status is not None:
query = query.filter(AlertRule.status == status)
total = query.count()
rules = query.order_by(desc(AlertRule.created_at)).offset((page - 1) * size).limit(size).all()
return {
"total": total,
"page": page,
"size": size,
"items": [format_rule(r) for r in rules]
}
@router.get("/rules/{rule_id}")
async def get_alert_rule(
rule_id: int,
user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""获取告警规则详情"""
rule = db.query(AlertRule).filter(AlertRule.id == rule_id).first()
if not rule:
raise HTTPException(status_code=404, detail="告警规则不存在")
return format_rule(rule)
@router.post("/rules")
async def create_alert_rule(
data: AlertRuleCreate,
user: User = Depends(require_operator),
db: Session = Depends(get_db)
):
"""创建告警规则"""
rule = AlertRule(
name=data.name,
description=data.description,
rule_type=data.rule_type,
scope_type=data.scope_type,
scope_value=data.scope_value,
condition=data.condition,
notification_channels=data.notification_channels,
cooldown_minutes=data.cooldown_minutes,
max_alerts_per_day=data.max_alerts_per_day,
priority=data.priority,
status=1
)
db.add(rule)
db.commit()
db.refresh(rule)
return {"success": True, "id": rule.id}
@router.put("/rules/{rule_id}")
async def update_alert_rule(
rule_id: int,
data: AlertRuleUpdate,
user: User = Depends(require_operator),
db: Session = Depends(get_db)
):
"""更新告警规则"""
rule = db.query(AlertRule).filter(AlertRule.id == rule_id).first()
if not rule:
raise HTTPException(status_code=404, detail="告警规则不存在")
update_data = data.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(rule, key, value)
db.commit()
return {"success": True}
@router.delete("/rules/{rule_id}")
async def delete_alert_rule(
rule_id: int,
user: User = Depends(require_operator),
db: Session = Depends(get_db)
):
"""删除告警规则"""
rule = db.query(AlertRule).filter(AlertRule.id == rule_id).first()
if not rule:
raise HTTPException(status_code=404, detail="告警规则不存在")
db.delete(rule)
db.commit()
return {"success": True}
# ============= Alert Records API =============
@router.get("/records")
async def list_alert_records(
page: int = Query(1, ge=1),
size: int = Query(20, ge=1, le=100),
status: Optional[str] = None,
severity: Optional[str] = None,
alert_type: Optional[str] = None,
tenant_id: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""获取告警记录列表"""
query = db.query(AlertRecord)
if status:
query = query.filter(AlertRecord.status == status)
if severity:
query = query.filter(AlertRecord.severity == severity)
if alert_type:
query = query.filter(AlertRecord.alert_type == alert_type)
if tenant_id:
query = query.filter(AlertRecord.tenant_id == tenant_id)
if start_date:
query = query.filter(AlertRecord.created_at >= start_date)
if end_date:
query = query.filter(AlertRecord.created_at <= end_date + " 23:59:59")
total = query.count()
records = query.order_by(desc(AlertRecord.created_at)).offset((page - 1) * size).limit(size).all()
return {
"total": total,
"page": page,
"size": size,
"items": [format_record(r) for r in records]
}
@router.get("/records/summary")
async def get_alert_summary(
user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""获取告警摘要统计"""
today = datetime.now().date()
week_start = today - timedelta(days=7)
# 今日告警数
today_count = db.query(func.count(AlertRecord.id)).filter(
func.date(AlertRecord.created_at) == today
).scalar()
# 本周告警数
week_count = db.query(func.count(AlertRecord.id)).filter(
func.date(AlertRecord.created_at) >= week_start
).scalar()
# 活跃告警数
active_count = db.query(func.count(AlertRecord.id)).filter(
AlertRecord.status == 'active'
).scalar()
# 按严重程度统计
severity_stats = db.query(
AlertRecord.severity,
func.count(AlertRecord.id)
).filter(
func.date(AlertRecord.created_at) >= week_start
).group_by(AlertRecord.severity).all()
return {
"today_count": today_count,
"week_count": week_count,
"active_count": active_count,
"by_severity": {s: c for s, c in severity_stats}
}
@router.get("/records/{record_id}")
async def get_alert_record(
record_id: int,
user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""获取告警记录详情"""
record = db.query(AlertRecord).filter(AlertRecord.id == record_id).first()
if not record:
raise HTTPException(status_code=404, detail="告警记录不存在")
return format_record(record)
@router.post("/records/{record_id}/acknowledge")
async def acknowledge_alert(
record_id: int,
user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""确认告警"""
service = AlertService(db)
record = service.acknowledge_alert(record_id, user.username)
if not record:
raise HTTPException(status_code=404, detail="告警记录不存在")
return {"success": True}
@router.post("/records/{record_id}/resolve")
async def resolve_alert(
record_id: int,
user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""解决告警"""
service = AlertService(db)
record = service.resolve_alert(record_id)
if not record:
raise HTTPException(status_code=404, detail="告警记录不存在")
return {"success": True}
# ============= Check Alerts API =============
@router.post("/check")
async def trigger_alert_check(
background_tasks: BackgroundTasks,
user: User = Depends(require_operator),
db: Session = Depends(get_db)
):
"""手动触发告警检查"""
service = AlertService(db)
alerts = await service.check_all_rules()
# 异步发送通知
for alert in alerts:
rule = db.query(AlertRule).filter(AlertRule.id == alert.rule_id).first()
if rule:
background_tasks.add_task(service.send_notification, alert, rule)
return {
"success": True,
"triggered_count": len(alerts),
"alerts": [format_record(a) for a in alerts]
}
# ============= Notification Channels API =============
@router.get("/channels")
async def list_notification_channels(
user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""获取通知渠道列表"""
channels = db.query(NotificationChannel).order_by(desc(NotificationChannel.created_at)).all()
return [format_channel(c) for c in channels]
@router.post("/channels")
async def create_notification_channel(
data: NotificationChannelCreate,
user: User = Depends(require_operator),
db: Session = Depends(get_db)
):
"""创建通知渠道"""
channel = NotificationChannel(
name=data.name,
channel_type=data.channel_type,
config=data.config,
status=1
)
db.add(channel)
db.commit()
db.refresh(channel)
return {"success": True, "id": channel.id}
@router.put("/channels/{channel_id}")
async def update_notification_channel(
channel_id: int,
data: NotificationChannelUpdate,
user: User = Depends(require_operator),
db: Session = Depends(get_db)
):
"""更新通知渠道"""
channel = db.query(NotificationChannel).filter(NotificationChannel.id == channel_id).first()
if not channel:
raise HTTPException(status_code=404, detail="通知渠道不存在")
update_data = data.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(channel, key, value)
db.commit()
return {"success": True}
@router.delete("/channels/{channel_id}")
async def delete_notification_channel(
channel_id: int,
user: User = Depends(require_operator),
db: Session = Depends(get_db)
):
"""删除通知渠道"""
channel = db.query(NotificationChannel).filter(NotificationChannel.id == channel_id).first()
if not channel:
raise HTTPException(status_code=404, detail="通知渠道不存在")
db.delete(channel)
db.commit()
return {"success": True}
# ============= Helper Functions =============
def format_rule(rule: AlertRule) -> dict:
return {
"id": rule.id,
"name": rule.name,
"description": rule.description,
"rule_type": rule.rule_type,
"scope_type": rule.scope_type,
"scope_value": rule.scope_value,
"condition": rule.condition,
"notification_channels": rule.notification_channels,
"cooldown_minutes": rule.cooldown_minutes,
"max_alerts_per_day": rule.max_alerts_per_day,
"priority": rule.priority,
"status": rule.status,
"created_at": rule.created_at,
"updated_at": rule.updated_at
}
def format_record(record: AlertRecord) -> dict:
return {
"id": record.id,
"rule_id": record.rule_id,
"rule_name": record.rule_name,
"alert_type": record.alert_type,
"severity": record.severity,
"title": record.title,
"message": record.message,
"tenant_id": record.tenant_id,
"app_code": record.app_code,
"metric_value": record.metric_value,
"threshold_value": record.threshold_value,
"notification_status": record.notification_status,
"status": record.status,
"acknowledged_by": record.acknowledged_by,
"acknowledged_at": record.acknowledged_at,
"resolved_at": record.resolved_at,
"created_at": record.created_at
}
def format_channel(channel: NotificationChannel) -> dict:
return {
"id": channel.id,
"name": channel.name,
"channel_type": channel.channel_type,
"config": channel.config,
"status": channel.status,
"created_at": channel.created_at,
"updated_at": channel.updated_at
}