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
+1 -1
View File
@@ -15,7 +15,7 @@ QUIET = frozenset({"inactive", "resolved"})
# The worst thing an episode went through, most severe first.
_SEVERITY = ("firing", "acknowledged", "no_data", "pending")
_WORDS = {">": "above", ">=": "at or above", "<": "below", "<=": "at or below",
"==": "at", "!=": "other than"}
"==": "at", "!=": "other than", "rose": "on a rise of"}
def fires_when(rule) -> str:
+1 -1
View File
@@ -15,7 +15,7 @@ from steward.models.users import UserRole
alerts_bp = Blueprint("alerts", __name__, url_prefix="/alerts")
_OPERATORS = [(op.value, op.value) for op in AlertOperator]
_OPERATORS = [(op.value, op.said) for op in AlertOperator]
def _form_condition(form) -> dict:
"""The operator and threshold the form asks for. An up/down metric's form
+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))