402 lines
17 KiB
Python
402 lines
17 KiB
Python
from database import Database
|
||
from srs_logger import get_logger
|
||
import requests
|
||
import time
|
||
import threading
|
||
from datetime import datetime, timedelta
|
||
from typing import Optional, Dict, Any
|
||
from datetime import datetime, timedelta
|
||
|
||
# ============================================================================
|
||
# System Statistics Management Module
|
||
# ============================================================================
|
||
|
||
class SystemStatsManager:
|
||
"""系统统计管理器"""
|
||
|
||
def __init__(self, db: Database):
|
||
self.db = db
|
||
self.logger = get_logger()
|
||
self.api_url = "http://python_stats:wMePq3ahpoLRzgsVg7BY9eE82uuJHT0YukD2ZE1JfMY2RjP4e6QnUaKg3V9x5s9M@localhost:1985/api/v1/summaries"
|
||
self.previous_data: Optional[Dict[str, Any]] = None
|
||
self.previous_timestamp: Optional[float] = None
|
||
self.is_running = False
|
||
self.poll_thread: Optional[threading.Thread] = None
|
||
self.cleanup_thread: Optional[threading.Thread] = None
|
||
|
||
# 启动定期清理过期数据的线程
|
||
self._start_cleanup_thread()
|
||
|
||
def _fetch_system_stats(self):
|
||
"""获取系统统计数据并插入数据库"""
|
||
max_retries = 5
|
||
retry_count = 0
|
||
|
||
while retry_count < max_retries:
|
||
try:
|
||
# 发送HTTP请求获取SRS统计数据
|
||
response = requests.get(self.api_url, timeout=10)
|
||
response.raise_for_status()
|
||
|
||
data = response.json()
|
||
|
||
# 检查返回状态
|
||
if data.get('code') != 0:
|
||
self.logger.error(f"SRS API returned error code: {data.get('code')}")
|
||
retry_count += 1
|
||
time.sleep(2 ** retry_count) # 指数退避
|
||
continue
|
||
|
||
# 提取有用的信息
|
||
stats_data = self._extract_stats_data(data)
|
||
if stats_data:
|
||
# 插入数据库
|
||
success = self.db.insert_system_stats(**stats_data)
|
||
if success:
|
||
self.logger.debug("Successfully inserted system stats")
|
||
return True
|
||
else:
|
||
self.logger.error("Failed to insert system stats to database")
|
||
retry_count += 1
|
||
else:
|
||
self.logger.error("Failed to extract stats data")
|
||
retry_count += 1
|
||
|
||
except requests.exceptions.RequestException as e:
|
||
self.logger.error(f"HTTP request failed: {e}")
|
||
retry_count += 1
|
||
except Exception as e:
|
||
self.logger.exception(f"Unexpected error in fetch_system_stats: {e}")
|
||
retry_count += 1
|
||
|
||
if retry_count < max_retries:
|
||
time.sleep(2 ** retry_count) # 指数退避重试
|
||
|
||
self.logger.error(f"Failed to fetch system stats after {max_retries} retries")
|
||
return False
|
||
|
||
def _extract_stats_data(self, api_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||
"""从API返回数据中提取有用的统计信息"""
|
||
try:
|
||
data_section = api_data.get('data', {})
|
||
self_section = data_section.get('self', {})
|
||
system_section = data_section.get('system', {})
|
||
|
||
current_timestamp = data_section.get('now_ms', 0) / 1000.0 # 转换为秒
|
||
|
||
# SRS相关数据
|
||
srs_uptime = self_section.get('srs_uptime', 0.0)
|
||
srs_cpu_percent = self_section.get('cpu_percent', 0.0)
|
||
srs_memory_percent = self_section.get('mem_percent', 0.0)
|
||
|
||
# 网络流量数据(需要计算KBps)
|
||
srs_recv_bytes = system_section.get('srs_recv_bytes', 0)
|
||
srs_send_bytes = system_section.get('srs_send_bytes', 0)
|
||
srs_sample_time = system_section.get('srs_sample_time', 0) / 1000.0 # 转换为秒
|
||
|
||
# 计算KBps(需要与上一次的数据比较)
|
||
srs_recv_KBps = 0.0
|
||
srs_send_KBps = 0.0
|
||
|
||
if (self.previous_data and
|
||
self.previous_timestamp and
|
||
current_timestamp > self.previous_timestamp):
|
||
|
||
time_diff = current_timestamp - self.previous_timestamp
|
||
prev_recv = self.previous_data.get('srs_recv_bytes', 0)
|
||
prev_send = self.previous_data.get('srs_send_bytes', 0)
|
||
|
||
if time_diff > 0:
|
||
# 计算字节差值并转换为KBps
|
||
srs_recv_KBps = max(0, (srs_recv_bytes - prev_recv) / time_diff / 1024)
|
||
srs_send_KBps = max(0, (srs_send_bytes - prev_send) / time_diff / 1024)
|
||
|
||
# print(srs_recv_KBps, srs_send_KBps) # DEBUG
|
||
|
||
# 磁盘IO数据
|
||
disk_read_KBps = system_section.get('disk_read_KBps', 0.0)
|
||
disk_write_KBps = system_section.get('disk_write_KBps', 0.0)
|
||
|
||
# 操作系统相关数据
|
||
os_uptime = system_section.get('uptime', 0.0)
|
||
os_cpu_percent = system_section.get('cpu_percent', 0.0)
|
||
os_memory_percent = system_section.get('mem_ram_percent', 0.0)
|
||
|
||
# 更新上一次的数据
|
||
self.previous_data = {
|
||
'srs_recv_bytes': srs_recv_bytes,
|
||
'srs_send_bytes': srs_send_bytes,
|
||
'srs_sample_time': srs_sample_time
|
||
}
|
||
self.previous_timestamp = current_timestamp
|
||
|
||
return {
|
||
'srs_uptime': srs_uptime,
|
||
'srs_cpu_percent': srs_cpu_percent,
|
||
'srs_memory_percent': srs_memory_percent,
|
||
'srs_recv_KBps': srs_recv_KBps,
|
||
'srs_send_KBps': srs_send_KBps,
|
||
'disk_read_KBps': disk_read_KBps,
|
||
'disk_write_KBps': disk_write_KBps,
|
||
'os_uptime': os_uptime,
|
||
'os_cpu_percent': os_cpu_percent,
|
||
'os_memory_percent': os_memory_percent
|
||
}
|
||
|
||
except Exception as e:
|
||
self.logger.exception(f"Failed to extract stats data: {e}")
|
||
return None
|
||
|
||
def start_polling(self, interval: int = 3):
|
||
"""启动长轮询统计数据收集"""
|
||
if self.is_running:
|
||
self.logger.warn("Polling is already running")
|
||
return
|
||
|
||
self.is_running = True
|
||
self.poll_thread = threading.Thread(target=self._polling_loop, args=(interval,))
|
||
self.poll_thread.daemon = True
|
||
self.poll_thread.start()
|
||
self.logger.info(f"Started system stats polling with {interval}s interval")
|
||
|
||
def stop_polling(self):
|
||
"""停止长轮询"""
|
||
if not self.is_running:
|
||
self.logger.warn("Polling is not running")
|
||
return
|
||
|
||
self.is_running = False
|
||
if self.poll_thread:
|
||
self.poll_thread.join(timeout=5)
|
||
self.logger.info("Stopped system stats polling")
|
||
|
||
def _polling_loop(self, interval: int):
|
||
"""轮询循环"""
|
||
while self.is_running:
|
||
try:
|
||
self._fetch_system_stats()
|
||
except Exception as e:
|
||
self.logger.exception(f"Error in polling loop: {e}")
|
||
|
||
# 等待指定间隔,但允许提前停止
|
||
for _ in range(interval):
|
||
if not self.is_running:
|
||
break
|
||
time.sleep(1)
|
||
|
||
def _start_cleanup_thread(self):
|
||
"""启动清理过期数据的线程"""
|
||
self.cleanup_thread = threading.Thread(target=self._cleanup_loop)
|
||
self.cleanup_thread.daemon = True
|
||
self.cleanup_thread.start()
|
||
self.logger.info("Started system stats cleanup thread (5 minute interval)")
|
||
|
||
def _cleanup_loop(self):
|
||
"""定期清理过期数据的循环"""
|
||
while True:
|
||
try:
|
||
# 执行清理
|
||
success = self.db.delete_expired_system_stats()
|
||
if success:
|
||
self.logger.debug("Successfully cleaned up expired system stats")
|
||
else:
|
||
self.logger.warn("Failed to clean up expired system stats")
|
||
|
||
time.sleep(300)
|
||
|
||
except Exception as e:
|
||
self.logger.exception(f"Error in cleanup loop: {e}")
|
||
# 发生错误后等待一分钟再继续
|
||
time.sleep(60)
|
||
|
||
def _process_stats_data(self, raw_stats: list, time_delta: int, time_interval: int) -> list:
|
||
"""
|
||
处理原始统计数据,按指定时间间隔进行插值和过滤
|
||
|
||
Args:
|
||
raw_stats: 原始统计数据列表
|
||
time_delta: 时间范围(秒)
|
||
time_interval: 时间间隔(秒)
|
||
|
||
Returns:
|
||
处理后的统计数据列表
|
||
"""
|
||
try:
|
||
if not raw_stats:
|
||
return []
|
||
|
||
# 参数验证
|
||
if time_interval <= 0:
|
||
self.logger.warn("Invalid time_interval, using 1 second")
|
||
time_interval = 1
|
||
if time_delta <= 0:
|
||
self.logger.warn("Invalid time_delta, using 60 seconds")
|
||
time_delta = 60
|
||
|
||
# 转换时间戳并排序
|
||
for stat in raw_stats:
|
||
if isinstance(stat['timestamp'], str):
|
||
stat['timestamp'] = datetime.fromisoformat(stat['timestamp'])
|
||
elif not isinstance(stat['timestamp'], datetime):
|
||
# 如果是其他格式,尝试解析
|
||
stat['timestamp'] = datetime.fromisoformat(str(stat['timestamp']))
|
||
|
||
# 按时间戳排序
|
||
raw_stats.sort(key=lambda x: x['timestamp'])
|
||
|
||
# 确定时间范围
|
||
now = datetime.now().replace(microsecond=0) # 取整到秒
|
||
start_time = now - timedelta(seconds=time_delta)
|
||
|
||
# 过滤掉超出时间范围的数据
|
||
filtered_stats = [stat for stat in raw_stats if stat['timestamp'] >= start_time]
|
||
|
||
if not filtered_stats:
|
||
self.logger.warn("No data points in specified time range")
|
||
return []
|
||
|
||
# 生成目标时间点列表(从最近一次记录向前)
|
||
target_times = []
|
||
current_time = now
|
||
while current_time >= start_time:
|
||
target_times.append(current_time)
|
||
current_time -= timedelta(seconds=time_interval)
|
||
|
||
# 反转列表,使其按时间顺序排列
|
||
target_times.reverse()
|
||
|
||
# 对每个目标时间点进行插值
|
||
processed_stats = []
|
||
numeric_fields = [
|
||
'srs_uptime', 'srs_cpu_percent', 'srs_memory_percent',
|
||
'srs_recv_KBps', 'srs_send_KBps', 'disk_read_KBps',
|
||
'disk_write_KBps', 'os_uptime', 'os_cpu_percent', 'os_memory_percent'
|
||
]
|
||
|
||
for target_time in target_times:
|
||
interpolated_data = self._interpolate_data_point(filtered_stats, target_time, numeric_fields)
|
||
if interpolated_data:
|
||
processed_stats.append(interpolated_data)
|
||
|
||
self.logger.debug(f"Processed {len(raw_stats)} raw points into {len(processed_stats)} interpolated points")
|
||
return processed_stats
|
||
|
||
except Exception as e:
|
||
self.logger.exception(f"Error processing stats data: {e}")
|
||
return raw_stats # 如果处理失败,返回原始数据
|
||
|
||
def _interpolate_data_point(self, raw_stats: list, target_time: datetime, numeric_fields: list) -> Optional[Dict[str, Any]]:
|
||
"""
|
||
为指定时间点插值数据
|
||
|
||
Args:
|
||
raw_stats: 原始统计数据列表(已排序)
|
||
target_time: 目标时间点
|
||
numeric_fields: 需要插值的数值字段列表
|
||
|
||
Returns:
|
||
插值后的数据点
|
||
"""
|
||
try:
|
||
# 查找最接近的前后两个数据点
|
||
before_point = None
|
||
after_point = None
|
||
|
||
for i, stat in enumerate(raw_stats):
|
||
stat_time = stat['timestamp']
|
||
|
||
if stat_time <= target_time:
|
||
before_point = stat
|
||
elif stat_time > target_time:
|
||
after_point = stat
|
||
break
|
||
|
||
# 如果没有找到合适的数据点,使用最近的点
|
||
if before_point is None and after_point is None:
|
||
return None
|
||
elif before_point is None:
|
||
# 只有后面的点,直接使用
|
||
result = after_point.copy()
|
||
result['timestamp'] = target_time.replace(microsecond=0)
|
||
return result
|
||
elif after_point is None:
|
||
# 只有前面的点,直接使用
|
||
result = before_point.copy()
|
||
result['timestamp'] = target_time.replace(microsecond=0)
|
||
return result
|
||
|
||
# 计算时间差和权重
|
||
before_time = before_point['timestamp']
|
||
after_time = after_point['timestamp']
|
||
|
||
# 如果时间点完全匹配,直接返回
|
||
if before_time == target_time:
|
||
return before_point.copy()
|
||
if after_time == target_time:
|
||
return after_point.copy()
|
||
|
||
# 计算插值权重
|
||
total_diff = (after_time - before_time).total_seconds()
|
||
if total_diff == 0:
|
||
# 两个点时间相同,使用前一个点
|
||
result = before_point.copy()
|
||
result['timestamp'] = target_time.replace(microsecond=0)
|
||
return result
|
||
|
||
target_diff = (target_time - before_time).total_seconds()
|
||
weight = target_diff / total_diff
|
||
|
||
# 执行线性插值
|
||
result = {'timestamp': target_time.replace(microsecond=0)} # 确保时间戳取整到秒
|
||
|
||
for field in numeric_fields:
|
||
if field in before_point and field in after_point:
|
||
before_val = float(before_point[field]) if before_point[field] is not None else 0.0
|
||
after_val = float(after_point[field]) if after_point[field] is not None else 0.0
|
||
|
||
# 线性插值
|
||
interpolated_val = before_val + (after_val - before_val) * weight
|
||
result[field] = round(interpolated_val, 4) # 保留4位小数
|
||
else:
|
||
# 如果字段不存在,使用前一个点的值
|
||
result[field] = before_point.get(field, 0.0)
|
||
|
||
return result
|
||
|
||
except Exception as e:
|
||
self.logger.exception(f"Error interpolating data point: {e}")
|
||
return None
|
||
|
||
def get_recent_stats(self, user_group: list, time_delta: int = 5, time_interval: int = 1):
|
||
"""获取最近的统计数据"""
|
||
try:
|
||
if not any(group in user_group for group in ['streamer', 'manager', 'admin']):
|
||
return {"success": False, "message": "权限不足,无法获取统计数据"}
|
||
|
||
# 获取原始数据,多获取一些以便插值
|
||
raw_stats = self.db.get_system_stats(time_delta + 5) # 多获取60秒数据用于插值
|
||
if not raw_stats:
|
||
return {"success": False, "message": "没有找到相关的统计数据"}
|
||
|
||
# 处理数据,按指定间隔进行插值和过滤
|
||
processed_stats = self._process_stats_data(raw_stats, time_delta, time_interval)
|
||
|
||
# 转换时间戳为字符串格式,便于JSON序列化
|
||
for stat in processed_stats:
|
||
if isinstance(stat['timestamp'], datetime):
|
||
stat['timestamp'] = stat['timestamp'].isoformat()
|
||
|
||
return {
|
||
"success": True,
|
||
"system_stats": processed_stats,
|
||
"metadata": {
|
||
"time_delta": time_delta,
|
||
"time_interval": time_interval,
|
||
"data_points": len(processed_stats),
|
||
"original_points": len(raw_stats)
|
||
}
|
||
}
|
||
|
||
except Exception as e:
|
||
self.logger.exception(f"Error in get_recent_stats: {e}")
|
||
return {"success": False, "message": "服务器内部错误"} |