174 lines
5.5 KiB
Python
174 lines
5.5 KiB
Python
|
#!/usr/bin/env python
|
|||
|
# -*- coding: utf-8 -*-
|
|||
|
|
|||
|
"""
|
|||
|
响应异常告警中间件模块
|
|||
|
拦截非正常响应并推送异常日志到主系统
|
|||
|
"""
|
|||
|
|
|||
|
import json
|
|||
|
import logging
|
|||
|
from typing import Any, Dict
|
|||
|
from fastapi import Request, Response
|
|||
|
from starlette.types import Message
|
|||
|
|
|||
|
from utils.logger import get_logger
|
|||
|
from utils.alert_sync import get_alert_sync_service
|
|||
|
|
|||
|
# 设置日志
|
|||
|
logger = get_logger("middleware.response_alert")
|
|||
|
|
|||
|
class ResponseAlertMiddleware:
|
|||
|
"""
|
|||
|
响应异常告警中间件
|
|||
|
监控所有API响应,当出现非正常响应时推送告警
|
|||
|
"""
|
|||
|
|
|||
|
def __init__(self, app):
|
|||
|
self.app = app
|
|||
|
self.alert_service = get_alert_sync_service()
|
|||
|
|
|||
|
async def __call__(self, scope, receive, send):
|
|||
|
if scope["type"] != "http":
|
|||
|
await self.app(scope, receive, send)
|
|||
|
return
|
|||
|
|
|||
|
# 创建请求对象获取路径信息
|
|||
|
request = Request(scope, receive)
|
|||
|
|
|||
|
# 捕获响应数据
|
|||
|
response_body = bytearray()
|
|||
|
|
|||
|
async def send_wrapper(message: Message):
|
|||
|
if message["type"] == "http.response.body":
|
|||
|
body = message.get("body", b"")
|
|||
|
response_body.extend(body)
|
|||
|
await send(message)
|
|||
|
|
|||
|
# 处理请求
|
|||
|
await self.app(scope, receive, send_wrapper)
|
|||
|
|
|||
|
# 检查响应并发送告警
|
|||
|
await self._check_and_alert(request, response_body)
|
|||
|
|
|||
|
async def _check_and_alert(self, request: Request, response_body: bytearray):
|
|||
|
"""
|
|||
|
检查响应并发送告警
|
|||
|
|
|||
|
Args:
|
|||
|
request: 请求对象
|
|||
|
response_body: 响应体
|
|||
|
"""
|
|||
|
try:
|
|||
|
# 解析响应体
|
|||
|
body_str = response_body.decode('utf-8')
|
|||
|
if not body_str:
|
|||
|
return
|
|||
|
|
|||
|
# 尝试解析JSON响应
|
|||
|
try:
|
|||
|
response_data = json.loads(body_str)
|
|||
|
except json.JSONDecodeError:
|
|||
|
# 非JSON响应,跳过检查
|
|||
|
return
|
|||
|
|
|||
|
# 检查是否为标准格式响应
|
|||
|
if not isinstance(response_data, dict) or 'code' not in response_data:
|
|||
|
return
|
|||
|
|
|||
|
response_code = response_data.get('code')
|
|||
|
message = response_data.get('message', '未知错误')
|
|||
|
|
|||
|
# 只处理非成功响应 (code != 200)
|
|||
|
if response_code == 200:
|
|||
|
return
|
|||
|
|
|||
|
# 构造告警信息
|
|||
|
alert_message = self._format_alert_message(request, response_code, message, response_data)
|
|||
|
|
|||
|
# 创建日志记录用于告警推送
|
|||
|
log_record = self._create_log_record(alert_message, response_code)
|
|||
|
|
|||
|
# 推送告警
|
|||
|
success = self.alert_service.sync_alert(log_record)
|
|||
|
if success:
|
|||
|
logger.debug(f"异常响应告警已推送: {request.method} {request.url.path} - {response_code}")
|
|||
|
else:
|
|||
|
logger.warning(f"异常响应告警推送失败: {request.method} {request.url.path} - {response_code}")
|
|||
|
|
|||
|
except Exception as e:
|
|||
|
# 避免告警中间件本身的错误影响正常流程
|
|||
|
logger.error(f"响应告警中间件处理错误: {str(e)}")
|
|||
|
|
|||
|
def _format_alert_message(self, request: Request, code: int, message: str, response_data: Dict[str, Any]) -> str:
|
|||
|
"""
|
|||
|
格式化告警消息
|
|||
|
|
|||
|
Args:
|
|||
|
request: 请求对象
|
|||
|
code: 响应码
|
|||
|
message: 响应消息
|
|||
|
response_data: 完整响应数据
|
|||
|
|
|||
|
Returns:
|
|||
|
格式化的告警消息
|
|||
|
"""
|
|||
|
method = request.method
|
|||
|
path = request.url.path
|
|||
|
client_ip = request.client.host if request.client else "unknown"
|
|||
|
|
|||
|
# 构建基本告警信息
|
|||
|
alert_msg = f"API异常响应: {method} {path} 返回错误码 {code}"
|
|||
|
alert_msg += f" | 错误信息: {message}"
|
|||
|
alert_msg += f" | 客户端IP: {client_ip}"
|
|||
|
|
|||
|
# 添加查询参数(如果有)
|
|||
|
if request.query_params:
|
|||
|
alert_msg += f" | 查询参数: {dict(request.query_params)}"
|
|||
|
|
|||
|
# 添加响应数据(如果有额外信息)
|
|||
|
if 'data' in response_data and response_data['data'] is not None:
|
|||
|
alert_msg += f" | 响应数据: {str(response_data['data'])[:200]}" # 限制长度
|
|||
|
|
|||
|
return alert_msg
|
|||
|
|
|||
|
def _create_log_record(self, message: str, response_code: int) -> logging.LogRecord:
|
|||
|
"""
|
|||
|
创建用于告警的日志记录
|
|||
|
|
|||
|
Args:
|
|||
|
message: 告警消息
|
|||
|
response_code: 响应码
|
|||
|
|
|||
|
Returns:
|
|||
|
日志记录对象
|
|||
|
"""
|
|||
|
# 根据响应码确定日志级别
|
|||
|
if response_code >= 500:
|
|||
|
level = logging.ERROR
|
|||
|
elif response_code >= 400:
|
|||
|
level = logging.WARNING
|
|||
|
else:
|
|||
|
level = logging.WARNING
|
|||
|
|
|||
|
# 创建日志记录
|
|||
|
record = logging.LogRecord(
|
|||
|
name="api.response_alert",
|
|||
|
level=level,
|
|||
|
pathname=__file__,
|
|||
|
lineno=0,
|
|||
|
msg=message,
|
|||
|
args=(),
|
|||
|
exc_info=None
|
|||
|
)
|
|||
|
|
|||
|
return record
|
|||
|
|
|||
|
def register_middleware(app):
|
|||
|
"""
|
|||
|
注册响应告警中间件到FastAPI应用
|
|||
|
|
|||
|
Args:
|
|||
|
app: FastAPI应用实例
|
|||
|
"""
|
|||
|
app.add_middleware(ResponseAlertMiddleware)
|