ForcePilot/backend/package/yuxi/channels/services/stats_collector.py
Kris fb8bd90329 refactor(channel-services): 整理导入顺序并新增插件状态清理功能
1. 调整多个服务文件的导入顺序以符合规范
2. 新增Postgres插件状态存储实现,支持CRUD和过期清理
3. 在维护任务中添加插件过期状态自动清理逻辑
4. 新增统计收集器,支持请求/错误计数、响应时间统计和指标上报
2026-05-13 16:21:09 +08:00

82 lines
2.8 KiB
Python

from __future__ import annotations
import asyncio
import time
from collections import deque
from typing import TYPE_CHECKING
from yuxi.utils.logging_config import logger
if TYPE_CHECKING:
from yuxi.channels.services.runtime_state import RuntimeState
_MAX_RESPONSE_TIME_SAMPLES = 100
class StatsCollector:
def __init__(self, state: RuntimeState):
self._state = state
self._response_times: deque[float] = deque(maxlen=_MAX_RESPONSE_TIME_SAMPLES)
self._request_count_local: int = 0
self._error_count_local: int = 0
self._last_collect_at = time.monotonic()
def record_request(self) -> None:
self._state.request_count += 1
self._request_count_local += 1
def record_error(self) -> None:
self._state.error_count += 1
self._error_count_local += 1
def record_response_time(self, ms: float) -> None:
self._response_times.append(ms)
async def run(self, interval: float = 60) -> None:
logger.info("StatsCollector started")
while True:
await asyncio.sleep(interval)
try:
self._collect()
except asyncio.CancelledError:
break
except Exception:
logger.exception("StatsCollector error")
def _collect(self) -> None:
now = time.monotonic()
elapsed = now - self._last_collect_at
self._last_collect_at = now
rps = self._request_count_local / elapsed if elapsed > 0 else 0
eps = self._error_count_local / elapsed if elapsed > 0 else 0
avg_rt = sum(self._response_times) / len(self._response_times) if self._response_times else 0
p95_rt = 0.0
if len(self._response_times) >= 20:
sorted_times = sorted(self._response_times)
p95_idx = int(len(sorted_times) * 0.95)
p95_rt = sorted_times[p95_idx] if p95_idx < len(sorted_times) else sorted_times[-1]
error_rate = self._error_count_local / self._request_count_local if self._request_count_local else 0
logger.info(
f"Stats: channels={self._state.active_channels}, "
f"rps={rps:.1f}, eps={eps:.1f}, errors={self._error_count_local}, "
f"error_rate={error_rate:.2%}, "
f"avg_rt={avg_rt:.0f}ms, p95_rt={p95_rt:.0f}ms"
)
self._request_count_local = 0
self._error_count_local = 0
def get_summary(self) -> dict:
avg_rt = sum(self._response_times) / len(self._response_times) if self._response_times else 0
return {
"active_channels": self._state.active_channels,
"total_requests": self._state.request_count,
"total_errors": self._state.error_count,
"avg_response_time_ms": round(avg_rt, 1),
"phase": self._state.phase,
}