All checks were successful
continuous-integration/drone/push Build is passing
- 新增告警模块 (alerts): 告警规则配置与触发 - 新增成本管理模块 (cost): 成本统计与分析 - 新增配额模块 (quota): 配额管理与限制 - 新增微信模块 (wechat): 微信相关功能接口 - 新增缓存服务 (cache): Redis 缓存封装 - 新增请求日志中间件 (request_logger) - 新增异常处理和链路追踪中间件 - 更新 dashboard 前端展示 - 更新 SDK stats_client 功能
431 lines
13 KiB
Python
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
|
|
}
|