Files
iAOP/core/edge-gateway/collector/metrics.py
T
yunmei f49c0920d4 feat: 完成 issue #3 边缘采集网关模板化封装
- 点位字典 CSV schema/加载/自动校验(缺失字段/量纲/重复点号,PRD 5.1)
- 协议可插拔只读驱动:OPC UA(S7-1200 适配)/S7/Modbus/称重/能源/模拟
- 周期采集引擎:只读+背压保护+健康度指标(丢失率/P99/可用性)
- Kafka 流式上行 + 本地 spool 断点续传(丢失率≤0.02% 保障)
- 模板配置外置(gateway.yaml + 点位字典 CSV),换行业零改码
- 18 个单元测试全绿;端到端运行 SLA 达标
2026-08-04 15:32:16 +08:00

96 lines
3.8 KiB
Python

# -*- coding: utf-8 -*-
"""采集健康度统计 —— 丢失率 / P99 延迟 / 可用性(PRD 9 章 NFR 落点)。
指标口径(对齐 PRD 5.1 验收):
- 丢失率:未读到(含超时/失败)样本数 / 应采集样本总数,目标 ≤ 0.02%;
- P99 延迟:单轮采集批处理耗时(入队到读取完成)的 P99,目标 ≤ 1.8s;
- 可用性:成功轮次 / 总轮次(连续两轮成功间不间断视为可用),目标 ≥ 99.8%。
"""
from __future__ import annotations
import threading
import time
from typing import List, Optional
class HealthMetrics:
"""线程安全的采集健康度统计器。"""
def __init__(self, window_size: int = 4096):
self._lock = threading.Lock()
# 轮次耗时(秒),用于 P99 分位计算
self._round_latencies: List[float] = []
self._window_size = max(64, window_size)
# 样本计数
self.total_samples = 0 # 应采集样本总数
self.lost_samples = 0 # 未读到样本数
self.failed_rounds = 0 # 失败轮次(驱动异常/超时)
self.total_rounds = 0 # 总轮次数
def record_round(self, latency_sec: float, expected: int, got: int, failed: bool = False) -> None:
"""记录一轮采集结果。
Args:
latency_sec: 本轮批处理耗时(秒);
expected: 本轮应采集样本数;
got: 本轮实际读到样本数;
failed: 本轮是否整体失败(驱动异常等)。
"""
with self._lock:
self.total_rounds += 1
self.total_samples += expected
self.lost_samples += max(0, expected - got)
if failed:
self.failed_rounds += 1
self._round_latencies.append(latency_sec)
if len(self._round_latencies) > self._window_size:
# 只保留最近窗口,避免无限增长
self._round_latencies = self._round_latencies[-self._window_size:]
# ------------------------------------------------------------------
@property
def loss_rate(self) -> float:
"""丢失率(0~1 区间的小数,如 0.0002 表示 0.02%)。"""
with self._lock:
if self.total_samples == 0:
return 0.0
return self.lost_samples / self.total_samples
@property
def availability(self) -> float:
"""可用性(0~1 区间小数,如 0.998 表示 99.8%)。"""
with self._lock:
if self.total_rounds == 0:
return 1.0
return 1.0 - self.failed_rounds / self.total_rounds
def p99_latency(self) -> float:
"""最近窗口内采集批处理耗时的 P99(秒)。"""
with self._lock:
if not self._round_latencies:
return 0.0
ordered = sorted(self._round_latencies)
idx = max(0, min(len(ordered) - 1, int(len(ordered) * 0.99)))
return ordered[idx]
def snapshot(self) -> dict:
"""一次性导出全部健康度指标(供驾驶舱/日志上报)。"""
return {
"total_rounds": self.total_rounds,
"total_samples": self.total_samples,
"lost_samples": self.lost_samples,
"loss_rate": round(self.loss_rate, 6),
"p99_latency_sec": round(self.p99_latency(), 4),
"availability": round(self.availability, 6),
"failed_rounds": self.failed_rounds,
"window_size": self._window_size,
}
def meets_sla(self) -> bool:
"""是否满足 PRD 验收基线(P99≤1.8s / 丢失率≤0.02% / 可用性≥99.8%)。"""
return (
self.p99_latency() <= 1.8
and self.loss_rate <= 0.0002
and self.availability >= 0.998
)