feat(metrics): add rate + blockstats/netstat/fsinfo parsing helpers
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
47
backend/app/services/metrics_history.py
Normal file
47
backend/app/services/metrics_history.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
"""Pure helpers + read query for instance metric history."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
_RD_OPS = re.compile(r"rd_operations=(\d+)")
|
||||||
|
_WR_OPS = re.compile(r"wr_operations=(\d+)")
|
||||||
|
|
||||||
|
|
||||||
|
def compute_rate(prev, cur, dt):
|
||||||
|
"""Per-second rate from cumulative counters. None if unknowable; 0 on reset."""
|
||||||
|
if prev is None or cur is None or not dt or dt <= 0:
|
||||||
|
return None
|
||||||
|
delta = float(cur) - float(prev)
|
||||||
|
if delta < 0:
|
||||||
|
return 0.0
|
||||||
|
return delta / dt
|
||||||
|
|
||||||
|
|
||||||
|
def parse_blockstats(text: str):
|
||||||
|
"""Sum rd_operations / wr_operations across all block devices in HMP output."""
|
||||||
|
rd = sum(int(m) for m in _RD_OPS.findall(text or ""))
|
||||||
|
wr = sum(int(m) for m in _WR_OPS.findall(text or ""))
|
||||||
|
return rd, wr
|
||||||
|
|
||||||
|
|
||||||
|
def parse_netstat(rows, vmid: int):
|
||||||
|
"""Map dev -> (in_bytes, out_bytes) for the given vmid from /nodes/{node}/netstat."""
|
||||||
|
out: dict[str, tuple[int, int]] = {}
|
||||||
|
for r in rows or []:
|
||||||
|
try:
|
||||||
|
if int(r.get("vmid")) != int(vmid):
|
||||||
|
continue
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
dev = r.get("dev")
|
||||||
|
if not dev:
|
||||||
|
continue
|
||||||
|
out[dev] = (int(float(r.get("in", 0) or 0)), int(float(r.get("out", 0) or 0)))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def aggregate_fsinfo(disks):
|
||||||
|
"""Sum used/total bytes across guest filesystems."""
|
||||||
|
used = sum(int(d.get("used", 0) or 0) for d in disks or [])
|
||||||
|
total = sum(int(d.get("total", 0) or 0) for d in disks or [])
|
||||||
|
return used, total
|
||||||
40
backend/tests/test_metrics_history.py
Normal file
40
backend/tests/test_metrics_history.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
from app.services import metrics_history as mh
|
||||||
|
|
||||||
|
|
||||||
|
def test_compute_rate_normal():
|
||||||
|
assert mh.compute_rate(100, 160, 2.0) == 30.0
|
||||||
|
|
||||||
|
def test_compute_rate_no_prev():
|
||||||
|
assert mh.compute_rate(None, 160, 2.0) is None
|
||||||
|
|
||||||
|
def test_compute_rate_counter_reset():
|
||||||
|
assert mh.compute_rate(500, 10, 2.0) == 0.0
|
||||||
|
|
||||||
|
def test_compute_rate_zero_dt():
|
||||||
|
assert mh.compute_rate(100, 200, 0) is None
|
||||||
|
|
||||||
|
def test_parse_blockstats_sums_devices():
|
||||||
|
text = (
|
||||||
|
"drive-scsi0: rd_bytes=10 wr_bytes=20 rd_operations=100 wr_operations=50 flush_operations=1\n"
|
||||||
|
"drive-scsi1: rd_bytes=5 wr_bytes=5 rd_operations=7 wr_operations=3 flush_operations=0\n"
|
||||||
|
)
|
||||||
|
assert mh.parse_blockstats(text) == (107, 53)
|
||||||
|
|
||||||
|
def test_parse_blockstats_empty():
|
||||||
|
assert mh.parse_blockstats("") == (0, 0)
|
||||||
|
|
||||||
|
def test_parse_netstat_filters_by_vmid():
|
||||||
|
rows = [
|
||||||
|
{"dev": "tap114i0", "vmid": "114", "in": "1000", "out": "2000"},
|
||||||
|
{"dev": "tap114i1", "vmid": "114", "in": "10", "out": "20"},
|
||||||
|
{"dev": "tap200i0", "vmid": "200", "in": "9", "out": "9"},
|
||||||
|
]
|
||||||
|
out = mh.parse_netstat(rows, 114)
|
||||||
|
assert out == {"tap114i0": (1000, 2000), "tap114i1": (10, 20)}
|
||||||
|
|
||||||
|
def test_aggregate_fsinfo_sums():
|
||||||
|
disks = [{"used": 10, "total": 40}, {"used": 5, "total": 20}]
|
||||||
|
assert mh.aggregate_fsinfo(disks) == (15, 60)
|
||||||
|
|
||||||
|
def test_aggregate_fsinfo_empty():
|
||||||
|
assert mh.aggregate_fsinfo([]) == (0, 0)
|
||||||
Reference in New Issue
Block a user