1. 调整多个服务文件的导入顺序以符合规范 2. 新增Postgres插件状态存储实现,支持CRUD和过期清理 3. 在维护任务中添加插件过期状态自动清理逻辑 4. 新增统计收集器,支持请求/错误计数、响应时间统计和指标上报
82 lines
2.8 KiB
Python
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,
|
|
}
|