feat(metrics): add timeframe/bucket math and series aggregation
This commit is contained in:
@@ -45,3 +45,49 @@ def aggregate_fsinfo(disks):
|
|||||||
used = sum(int(d.get("used", 0) or 0) for d in disks or [])
|
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 [])
|
total = sum(int(d.get("total", 0) or 0) for d in disks or [])
|
||||||
return used, total
|
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
|
||||||
|
|||||||
@@ -38,3 +38,34 @@ def test_aggregate_fsinfo_sums():
|
|||||||
|
|
||||||
def test_aggregate_fsinfo_empty():
|
def test_aggregate_fsinfo_empty():
|
||||||
assert mh.aggregate_fsinfo([]) == (0, 0)
|
assert mh.aggregate_fsinfo([]) == (0, 0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_timeframe_to_range_hour():
|
||||||
|
assert mh.timeframe_to_range("hour", 10_000) == (10_000 - 3600, 10_000)
|
||||||
|
|
||||||
|
def test_timeframe_to_range_month():
|
||||||
|
assert mh.timeframe_to_range("month", 10_000_000) == (10_000_000 - 30 * 86400, 10_000_000)
|
||||||
|
|
||||||
|
def test_timeframe_to_range_unknown_defaults_hour():
|
||||||
|
assert mh.timeframe_to_range("nonsense", 10_000) == (10_000 - 3600, 10_000)
|
||||||
|
|
||||||
|
def test_pick_bucket_respects_floor():
|
||||||
|
# 1h window, target 150 -> 24s ideal, but floor 15 keeps >=15
|
||||||
|
assert mh.pick_bucket_seconds(0, 3600, target_points=150, floor=15) == 24
|
||||||
|
|
||||||
|
def test_pick_bucket_floor_applied():
|
||||||
|
assert mh.pick_bucket_seconds(0, 300, target_points=150, floor=15) == 15
|
||||||
|
|
||||||
|
def test_aggregate_series_averages_buckets():
|
||||||
|
rows = [
|
||||||
|
{"time": 0, "cpu": 10.0, "mem": 100},
|
||||||
|
{"time": 5, "cpu": 20.0, "mem": 200},
|
||||||
|
{"time": 20, "cpu": 40.0, "mem": None},
|
||||||
|
]
|
||||||
|
out = mh.aggregate_series(rows, from_ts=0, bucket=10, fields=["cpu", "mem"])
|
||||||
|
# bucket 0: times 0,5 -> cpu 15, mem 150 ; bucket 20: time 20 -> cpu 40, mem None
|
||||||
|
assert out[0] == {"time": 0, "cpu": 15.0, "mem": 150.0}
|
||||||
|
assert out[-1] == {"time": 20, "cpu": 40.0, "mem": None}
|
||||||
|
|
||||||
|
def test_aggregate_series_empty():
|
||||||
|
assert mh.aggregate_series([], 0, 10, ["cpu"]) == []
|
||||||
|
|||||||
Reference in New Issue
Block a user