164 lines
5.6 KiB
Python
164 lines
5.6 KiB
Python
"""Pure helpers + read query for instance metric history."""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from datetime import timezone as _tz
|
|
|
|
_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
|
|
|
|
|
|
_TIMEFRAME_SECONDS = {
|
|
"hour": 3600,
|
|
"day": 86400,
|
|
"week": 7 * 86400,
|
|
"month": 30 * 86400,
|
|
}
|
|
|
|
|
|
def timeframe_to_range(timeframe, now_ts):
|
|
"""Convert timeframe string to (from_ts, to_ts) tuple."""
|
|
span = _TIMEFRAME_SECONDS.get(timeframe, 3600)
|
|
return now_ts - span, now_ts
|
|
|
|
|
|
def pick_bucket_seconds(from_ts, to_ts, target_points=150, floor=15):
|
|
"""Calculate bucket size in seconds for time-series aggregation."""
|
|
span = max(1, to_ts - from_ts)
|
|
ideal = span // max(1, target_points)
|
|
return max(floor, int(ideal))
|
|
|
|
|
|
def aggregate_series(rows, from_ts, bucket, fields):
|
|
"""Aggregate time-series data into buckets, averaging numeric fields."""
|
|
if not rows:
|
|
return []
|
|
buckets: dict[int, dict] = {}
|
|
order: list[int] = []
|
|
for r in rows:
|
|
b = from_ts + ((int(r["time"]) - from_ts) // bucket) * bucket
|
|
if b not in buckets:
|
|
buckets[b] = {f: [] for f in fields}
|
|
order.append(b)
|
|
for f in fields:
|
|
v = r.get(f)
|
|
if v is not None:
|
|
buckets[b][f].append(float(v))
|
|
out = []
|
|
for b in order:
|
|
point = {"time": b}
|
|
for f in fields:
|
|
vals = buckets[b][f]
|
|
point[f] = (sum(vals) / len(vals)) if vals else None
|
|
out.append(point)
|
|
return out
|
|
|
|
|
|
_AGG_FIELDS = ["cpu", "mem", "maxmem", "netin", "netout",
|
|
"diskread", "diskwrite", "diskpct", "iops_read", "iops_write"]
|
|
|
|
|
|
def _to_ts(dt):
|
|
if dt.tzinfo is None:
|
|
dt = dt.replace(tzinfo=_tz.utc)
|
|
return int(dt.timestamp())
|
|
|
|
|
|
def query_instance_metrics(db, server_id, vmid, from_ts, to_ts, nic="all"):
|
|
from ..models import InstanceMetric, InstanceNicMetric
|
|
from datetime import datetime
|
|
|
|
start = datetime.fromtimestamp(from_ts, tz=_tz.utc)
|
|
end = datetime.fromtimestamp(to_ts, tz=_tz.utc)
|
|
|
|
base = (db.query(InstanceMetric)
|
|
.filter(InstanceMetric.server_id == server_id,
|
|
InstanceMetric.vmid == vmid,
|
|
InstanceMetric.ts >= start, InstanceMetric.ts <= end)
|
|
.order_by(InstanceMetric.ts.asc()).all())
|
|
|
|
# Distinct NICs seen in the window (for the selector).
|
|
if nic == "all":
|
|
# Only fetch distinct dev names — avoids loading all NIC rows.
|
|
dev_rows = (db.query(InstanceNicMetric.dev)
|
|
.filter(InstanceNicMetric.server_id == server_id,
|
|
InstanceNicMetric.vmid == vmid,
|
|
InstanceNicMetric.ts >= start, InstanceNicMetric.ts <= end)
|
|
.distinct().all())
|
|
nics = sorted({r.dev for r in dev_rows})
|
|
nic_by_ts: dict[int, tuple] = {}
|
|
else:
|
|
# Load full NIC rows for the requested dev to build per-ts override map.
|
|
nic_rows = (db.query(InstanceNicMetric)
|
|
.filter(InstanceNicMetric.server_id == server_id,
|
|
InstanceNicMetric.vmid == vmid,
|
|
InstanceNicMetric.ts >= start, InstanceNicMetric.ts <= end)
|
|
.order_by(InstanceNicMetric.ts.asc()).all())
|
|
nics = sorted({r.dev for r in nic_rows})
|
|
nic_by_ts = {}
|
|
for r in nic_rows:
|
|
if r.dev == nic:
|
|
nic_by_ts[_to_ts(r.ts)] = (r.in_rate, r.out_rate)
|
|
|
|
rows = []
|
|
for m in base:
|
|
ts = _to_ts(m.ts)
|
|
netin, netout = m.netin_rate, m.netout_rate
|
|
if nic != "all":
|
|
ov = nic_by_ts.get(ts)
|
|
netin, netout = (ov if ov else (None, None))
|
|
diskpct = (m.disk_used / m.disk_total * 100.0) if (m.disk_total or 0) > 0 else None
|
|
rows.append({
|
|
"time": ts,
|
|
"cpu": (m.cpu * 100.0) if m.cpu is not None else None,
|
|
"mem": m.mem, "maxmem": m.maxmem,
|
|
"netin": netin, "netout": netout,
|
|
"diskread": m.diskread_rate, "diskwrite": m.diskwrite_rate,
|
|
"diskpct": diskpct,
|
|
"iops_read": m.iops_read, "iops_write": m.iops_write,
|
|
})
|
|
|
|
bucket = pick_bucket_seconds(from_ts, to_ts)
|
|
data = aggregate_series(rows, from_ts, bucket, _AGG_FIELDS)
|
|
return {"data": data, "from": from_ts, "to": to_ts, "meta": {"nics": nics}}
|