Files
security-ai-edge/core/result_reporter.py
16337 0191e498f1 feat: 告警HTTP上报 + 日志精简 + 边缘节点统一为edge
- 新增 alarm_upload_worker.py 异步告警上报(COS+HTTP)
- result_reporter 重构为Redis队列模式
- config_sync 适配WVP直推的聚合配置格式
- settings 默认 EDGE_DEVICE_ID 改为 edge
- 日志设置非告警模块为WARNING级别减少噪音
- main.py 集成新的告警上报流程

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 15:21:45 +08:00

220 lines
6.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
结果上报模块
使用本地 Redis 缓冲 + HTTP 上报替代 MQTT
告警流程:
算法产出告警 → report_alarm() LPUSH 到 Redis → AlarmUploadWorker 异步消费
"""
import json
import logging
import uuid
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional
from dataclasses import dataclass, field
import numpy as np
import redis
from config.settings import get_settings
logger = logging.getLogger(__name__)
@dataclass
class AlarmInfo:
"""告警信息类(新格式,对齐云端 alarm_event 表)"""
alarm_id: str
alarm_type: str
device_id: str
scene_id: str
event_time: str # ISO8601
alarm_level: int # 1-4
snapshot_local_path: Optional[str] = None
algorithm_code: Optional[str] = None
confidence_score: Optional[float] = None
ext_data: Optional[Dict[str, Any]] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
"""转换为字典(写入 Redis 的 JSON 格式)"""
return {
"alarm_id": self.alarm_id,
"alarm_type": self.alarm_type,
"device_id": self.device_id,
"scene_id": self.scene_id,
"event_time": self.event_time,
"alarm_level": self.alarm_level,
"snapshot_local_path": self.snapshot_local_path,
"algorithm_code": self.algorithm_code,
"confidence_score": self.confidence_score,
"ext_data": self.ext_data,
}
def generate_alarm_id(device_id: str) -> str:
"""
生成告警ID
格式: edge_{device_id}_{YYYYMMDDHHmmss}_{6位uuid}
"""
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
uid = uuid.uuid4().hex[:6]
return f"edge_{device_id}_{timestamp}_{uid}"
# Redis 队列 Key 常量
REDIS_KEY_ALARM_PENDING = "local:alarm:pending"
REDIS_KEY_ALARM_RETRY = "local:alarm:retry"
REDIS_KEY_ALARM_DEAD = "local:alarm:dead"
class ResultReporter:
"""结果上报器
将告警写入本地 Redis 队列,由 AlarmUploadWorker 异步消费上传。
report_alarm() 方法使用 LPUSH算法线程零阻塞。
"""
def __init__(self):
self._settings = get_settings()
self._redis: Optional[redis.Redis] = None
self._logger = logging.getLogger("result_reporter")
self._performance_stats = {
"alerts_generated": 0,
"alerts_queued": 0,
"queue_failures": 0,
}
# 图片存储(本地保存截图供 worker 读取上传)
self._image_storage = None
self._db_manager = None
self._logger.info("ResultReporter 初始化完成Redis 缓冲模式)")
def initialize(self):
"""初始化 Redis 连接和本地存储"""
# 初始化本地存储(截图保存)
try:
from core.storage_manager import get_image_storage
self._image_storage = get_image_storage()
except Exception as e:
self._logger.warning(f"本地图片存储初始化失败: {e}")
try:
from config.database import get_sqlite_manager
self._db_manager = get_sqlite_manager()
except Exception as e:
self._logger.warning(f"本地数据库初始化失败: {e}")
# 初始化 Redis 连接(使用本地 Redis
redis_cfg = self._settings.local_redis
try:
self._redis = redis.Redis(
host=redis_cfg.host,
port=redis_cfg.port,
db=redis_cfg.db,
password=redis_cfg.password,
decode_responses=True,
socket_connect_timeout=5,
)
self._redis.ping()
self._logger.info(
f"Redis 连接成功: {redis_cfg.host}:{redis_cfg.port}/{redis_cfg.db}"
)
except Exception as e:
self._logger.error(f"Redis 连接失败: {e}")
self._redis = None
def report_alarm(self, alarm_info: AlarmInfo, screenshot: Optional[np.ndarray] = None) -> bool:
"""
上报告警(写入 Redis 队列)
Args:
alarm_info: 告警信息
screenshot: 抓拍图片numpy 数组)
Returns:
是否成功写入队列
"""
self._performance_stats["alerts_generated"] += 1
# 保存截图到本地,获取本地路径
if screenshot is not None and self._image_storage:
try:
local_path = self._image_storage.save_capture(
image=screenshot,
camera_id=alarm_info.device_id,
alert_id=alarm_info.alarm_id,
timestamp=datetime.now(),
)
if local_path:
alarm_info.snapshot_local_path = local_path
except Exception as e:
self._logger.error(f"保存截图失败: {e}")
# 写入 Redis 队列
if self._redis is None:
self._performance_stats["queue_failures"] += 1
self._logger.error("Redis 未连接,无法写入告警队列")
return False
try:
alarm_json = json.dumps(alarm_info.to_dict(), ensure_ascii=False)
self._redis.lpush(REDIS_KEY_ALARM_PENDING, alarm_json)
self._performance_stats["alerts_queued"] += 1
self._logger.info(
f"告警已入队: alarm_id={alarm_info.alarm_id}, "
f"type={alarm_info.alarm_type}, device={alarm_info.device_id}"
)
return True
except Exception as e:
self._performance_stats["queue_failures"] += 1
self._logger.error(f"写入 Redis 队列失败: {e}")
return False
def get_statistics(self) -> Dict[str, Any]:
"""获取统计信息"""
stats = self._performance_stats.copy()
if self._redis:
try:
stats["pending_count"] = self._redis.llen(REDIS_KEY_ALARM_PENDING)
stats["retry_count"] = self._redis.llen(REDIS_KEY_ALARM_RETRY)
stats["dead_count"] = self._redis.llen(REDIS_KEY_ALARM_DEAD)
except Exception:
pass
stats["redis_connected"] = self._redis is not None
return stats
def close(self):
"""关闭上报器"""
self._logger.info("ResultReporter 资源清理")
if self._image_storage:
try:
self._image_storage.close()
except Exception:
pass
if self._db_manager:
try:
self._db_manager.close()
except Exception:
pass
if self._redis:
try:
self._redis.close()
except Exception:
pass
self._logger.info("ResultReporter 清理完成")
def cleanup(self):
"""清理资源(别名)"""
self.close()