feat(alerts): a rule can fire when a reading rises, "rose by N" (#6724)

A rule compared a reading with a fixed number, so Synology Disk 1's bad
sectors going from 3,160 to 3,392 could say nothing: a count that only grows
is soon past any line. "rose by" compares each reading with the one before it,
kept on the alert's state and only for such a rule. A fall (a replaced disk)
doesn't fire. On the rule form, through MCP, and worded in emails, the alert
sentence, history and the timeline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-30 19:03:29 -04:00
co-authored by Claude Opus 5.5
parent d83f8ffe9b
commit e51d969c43
15 changed files with 133 additions and 12 deletions
+4 -2
View File
@@ -154,7 +154,7 @@ def condition_text(metric: str, operator: AlertOperator | str, threshold: float)
if _is_breached(v, operator, threshold)]
if len(fires_on) == 1:
return f"is {fires_on[0]}"
return f"{metric_label(metric)} {operator.value} {metric_value(metric, threshold)}"
return f"{metric_label(metric)} {operator.said} {metric_value(metric, threshold)}"
def alert_sentence(rule: AlertRule, label: str, value: float | None,
@@ -175,7 +175,7 @@ def alert_sentence(rule: AlertRule, label: str, value: float | None,
# not how long ago someone acknowledged it (the page says that apart).
word = "breaching" if label == "acknowledged" else label
for_how_long = f", {word} for {held}" if held else f", {word}"
condition = f" ({rule.operator.value} {metric_value(metric, rule.threshold)})"
condition = f" ({rule.operator.said} {metric_value(metric, rule.threshold)})"
counted = count_words(metric, value)
if counted and rule.resource_name in WHOLE_RESOURCES:
# The count is the whole of it, and links where the swarm is shown.
@@ -599,6 +599,8 @@ async def rule_fields(db, *, name: str, source_module: str, resource_name: str,
threshold = float(threshold)
except (TypeError, ValueError):
raise ValueError(f"threshold must be a number, got {threshold!r}.") from None
if op == AlertOperator.rose and threshold <= 0:
raise ValueError("A rule on a rise needs how much it rises by, above 0.")
try:
consecutive = (1 if consecutive_failures_required in (None, "")
else int(consecutive_failures_required))