fix(anomalies): a burst within what a metric reaches in most hours opens nothing (#6727)
#6727 "A metric that bursts now and then opens anomalies at hours whose usual peak reads 0". usual_peak is the p90 of a slot's hourly max, so a metric that bursts in under about 1 hour in 10 reads 0 there however often it bursts across the day. #6723 "An anomaly opens on a burst of minutes inside the metric's usual peak, so most rows aren't outside the norm" therefore still opened on those. usual_anywhere is the median, across the hours of the day, of each hour's highest peak. Height is now judged against the greater of it and the slot's usual peak. Bursts at any hour lift the median; a job at a time of its own raises a few hours and leaves it low, so the same level at another time of day is still an anomaly, as System 182 rules. A held level still opens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -41,11 +41,13 @@ def src():
|
||||
norms.LEARNED.pop((name, METRIC), None)
|
||||
|
||||
|
||||
def _norms(src: str, res: str, hold: float = 10.0, peak: float = 20.0):
|
||||
"""A learned day normal for every hour: holds `hold`, peaks at `peak`."""
|
||||
def _norms(src: str, res: str, hold: float = 10.0, peak: float = 20.0, anywhere: float = 0.0):
|
||||
"""A learned day normal for every hour: holds `hold`, peaks at `peak`, and
|
||||
reaches `anywhere` in most hours of the day."""
|
||||
from steward.models.norms import MetricNorm
|
||||
return [MetricNorm(source_module=src, resource_name=res, metric_name=METRIC, slot=h,
|
||||
kind="day", usual_hold=hold, usual_peak=peak, days_seen=7, updated_at=NOW)
|
||||
kind="day", usual_hold=hold, usual_peak=peak, usual_anywhere=anywhere,
|
||||
days_seen=7, updated_at=NOW)
|
||||
for h in range(24)]
|
||||
|
||||
|
||||
@@ -104,7 +106,8 @@ def test_a_level_within_its_normal_is_no_anomaly(app, src):
|
||||
assert rows == []
|
||||
|
||||
|
||||
def _hour_of(app, src: str, res: str, *, level: float, for_minutes: int, then: float = 10.0):
|
||||
def _hour_of(app, src: str, res: str, *, level: float, for_minutes: int, then: float = 10.0,
|
||||
anywhere: float = 0.0):
|
||||
"""A detection pass after an hour reading `then`, with its last
|
||||
`for_minutes` at `level`, against the normal that holds 10 and peaks at 20."""
|
||||
from sqlalchemy import select
|
||||
@@ -115,7 +118,7 @@ def _hour_of(app, src: str, res: str, *, level: float, for_minutes: int, then: f
|
||||
async def _go():
|
||||
async with app.db_sessionmaker() as s:
|
||||
async with s.begin():
|
||||
s.add_all(_norms(src, res))
|
||||
s.add_all(_norms(src, res, anywhere=anywhere))
|
||||
s.add_all([PluginMetric(source_module=src, resource_name=res, metric_name=METRIC,
|
||||
value=level if m < for_minutes else then,
|
||||
recorded_at=NOW - timedelta(minutes=m)) for m in range(60)])
|
||||
@@ -137,6 +140,15 @@ def test_a_burst_past_its_usual_peak_is_an_anomaly(app, src):
|
||||
assert a.started_at == NOW - timedelta(minutes=15)
|
||||
|
||||
|
||||
def test_a_burst_past_its_slots_peak_but_within_what_most_hours_reach_is_no_anomaly(app, src):
|
||||
assert _hour_of(app, src, _res("anywhere"), level=30.0, for_minutes=15, anywhere=40.0) == []
|
||||
|
||||
|
||||
def test_a_level_within_what_most_hours_reach_is_an_anomaly_once_held(app, src):
|
||||
(a,) = _hour_of(app, src, _res("anyheld"), level=30.0, for_minutes=45, anywhere=40.0)
|
||||
assert a.peak_level == 30.0
|
||||
|
||||
|
||||
def test_a_level_within_its_usual_peak_is_an_anomaly_once_held(app, src):
|
||||
# The system running at its peaked rate: never past the peak, but not a burst.
|
||||
(a,) = _hour_of(app, src, _res("held"), level=18.0, for_minutes=45)
|
||||
@@ -374,6 +386,42 @@ def test_a_metric_learns_only_from_its_since(app, src):
|
||||
assert held_back is None
|
||||
|
||||
|
||||
def test_bursts_at_any_hour_are_told_from_a_job_with_a_time_of_its_own(app, src):
|
||||
"""Fourteen days of a metric at 0. One burst to 50 in each hour of the day
|
||||
leaves every slot's usual peak at 0 and is usual anywhere. The same burst
|
||||
every night at 02:00 is usual at 02:00, and still stands out at 14:00."""
|
||||
from steward.core.norms import norms_at, rebuild_norms
|
||||
from steward.models.metrics import PluginMetricHourly
|
||||
sporadic, nightly = _res("sporadic"), _res("nightly")
|
||||
midnight = NOW.replace(hour=0)
|
||||
|
||||
def hours(res, burst):
|
||||
return [PluginMetricHourly(source_module=src, resource_name=res, metric_name=METRIC,
|
||||
bucket=midnight - timedelta(days=d) + timedelta(hours=h),
|
||||
value_avg=0.0, value_p95=0.0, sample_count=60,
|
||||
value_max=50.0 if burst(d, h) else 0.0)
|
||||
for d in range(1, 15) for h in range(24)]
|
||||
|
||||
async def _go():
|
||||
async with app.db_sessionmaker() as s:
|
||||
async with s.begin():
|
||||
s.add_all(hours(sporadic, lambda d, h: h % 14 == d - 1))
|
||||
s.add_all(hours(nightly, lambda d, h: h == 2))
|
||||
async with s.begin():
|
||||
await rebuild_norms(s, NOW)
|
||||
at = {}
|
||||
for hour in (2, 14):
|
||||
at[hour] = await norms_at(s, [(src, sporadic, METRIC), (src, nightly, METRIC)],
|
||||
NOW.replace(hour=hour))
|
||||
return at
|
||||
|
||||
at = asyncio.run(_go())
|
||||
assert at[14][(src, sporadic, METRIC)].usual_peak == 0.0
|
||||
assert at[14][(src, sporadic, METRIC)].usual_height == 50.0
|
||||
assert at[2][(src, nightly, METRIC)].usual_height == 50.0
|
||||
assert at[14][(src, nightly, METRIC)].usual_height == 0.0
|
||||
|
||||
|
||||
def test_a_metric_learned_without_its_parts_ignores_them(app, src):
|
||||
"""A host's CPU learns a normal and each of its cores doesn't (#5742):
|
||||
with parts=False, "<host>:core0" is neither learned nor judged, and the
|
||||
|
||||
Reference in New Issue
Block a user