feat(ui): audit, alert rules and the rule form say things in words (#5212)
#5212 "Plain labels: no raw ids, scopes, metric keys or unexplained jargon", batch 1: - Audit log: each action reads as a sentence ("paused monitor"), with the code as its tooltip. Detail shows flags as yes/no, lists joined, a run's scope by name, spans as spans, and no ids the entity column already gives. The filter is now a picker of areas. - Alert rules, history and Status say the condition in words: "is down", "Fullest disk >= 85%". Readings use metric_value, and history times use |ago. - Rule form: sources and metrics by name, the threshold's unit, "fires when it is down/up" for an up/down metric instead of 0.5, and a metric reference built from the catalog. - Schedules and Maintenance: viewers no longer see Run now, Pause, Edit, Delete or New. Intervals and next runs read as "every 2h" and "in 3h"; a scope reads "Every alert on Host agent". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,12 +25,12 @@ def _said(says):
|
||||
|
||||
|
||||
@pytest.mark.parametrize("op, threshold, value, reads", [
|
||||
(AlertOperator.gt, 90.0, 94.0, "cpu_pct on web-1 is 94% (> 90%), firing for 12m"),
|
||||
(AlertOperator.gte, 90.0, 90.0, "cpu_pct on web-1 is 90% (>= 90%), firing for 12m"),
|
||||
(AlertOperator.lt, 5.0, 2.5, "cpu_pct on web-1 is 2.5% (< 5.0%), firing for 12m"),
|
||||
(AlertOperator.lte, 5.0, 5.0, "cpu_pct on web-1 is 5.0% (<= 5.0%), firing for 12m"),
|
||||
(AlertOperator.eq, 100.0, 100.0, "cpu_pct on web-1 is 100% (== 100%), firing for 12m"),
|
||||
(AlertOperator.ne, 0.0, 3.0, "cpu_pct on web-1 is 3.0% (!= 0.0%), firing for 12m"),
|
||||
(AlertOperator.gt, 90.0, 94.0, "CPU on web-1 is 94% (> 90%), firing for 12m"),
|
||||
(AlertOperator.gte, 90.0, 90.0, "CPU on web-1 is 90% (>= 90%), firing for 12m"),
|
||||
(AlertOperator.lt, 5.0, 2.5, "CPU on web-1 is 2.5% (< 5.0%), firing for 12m"),
|
||||
(AlertOperator.lte, 5.0, 5.0, "CPU on web-1 is 5.0% (<= 5.0%), firing for 12m"),
|
||||
(AlertOperator.eq, 100.0, 100.0, "CPU on web-1 is 100% (== 100%), firing for 12m"),
|
||||
(AlertOperator.ne, 0.0, 3.0, "CPU on web-1 is 3.0% (!= 0.0%), firing for 12m"),
|
||||
])
|
||||
def test_a_firing_alert_reads_as_one_sentence(op, threshold, value, reads):
|
||||
now = datetime.now(timezone.utc)
|
||||
@@ -44,7 +44,7 @@ def test_units_follow_the_metric_name():
|
||||
now = datetime.now(timezone.utc)
|
||||
says = alert_sentence(_rule("response_ms", threshold=200.0, resource="api"), "pending", 431.0,
|
||||
now - timedelta(seconds=45), now)
|
||||
assert _said(says) == "response_ms on api is 431 ms (> 200 ms), pending for 45s"
|
||||
assert _said(says) == "Response time on api is 431 ms (> 200 ms), pending for 45s"
|
||||
|
||||
|
||||
def test_an_up_or_down_metric_says_the_word():
|
||||
@@ -58,13 +58,13 @@ def test_an_up_or_down_metric_says_the_word():
|
||||
def test_no_reading_yet_is_said_not_shown_as_a_number():
|
||||
now = datetime.now(timezone.utc)
|
||||
says = alert_sentence(_rule(), "firing", None, now - timedelta(minutes=3), now)
|
||||
assert _said(says) == "cpu_pct on web-1 has no reading yet (> 90%), firing for 3m"
|
||||
assert _said(says) == "CPU on web-1 has no reading yet (> 90%), firing for 3m"
|
||||
|
||||
|
||||
def test_a_silent_series_says_how_long_it_has_been_quiet():
|
||||
now = datetime.now(timezone.utc)
|
||||
says = alert_sentence(_rule(), "no_data", 50.0, now - timedelta(minutes=20), now)
|
||||
assert _said(says) == "No cpu_pct from web-1 for 20m"
|
||||
assert _said(says) == "No CPU reading from web-1 for 20m"
|
||||
|
||||
|
||||
def _row(rule, label, url, state=AlertStateEnum.firing):
|
||||
@@ -82,7 +82,7 @@ async def test_an_operator_sees_the_sentence_linked_and_can_acknowledge(app):
|
||||
page = await render_template("alerts/list.html", active=[_row(rule, "firing", "/hosts/h1")],
|
||||
rules=[rule], operators=[])
|
||||
|
||||
assert 'cpu_pct on <a href="/hosts/h1">web-1</a> is 94% (> 90%), firing for 12m' in page
|
||||
assert 'CPU on <a href="/hosts/h1">web-1</a> is 94% (> 90%), firing for 12m' in page
|
||||
assert '<span class="badge badge-crit">Firing</span>' in page
|
||||
assert 'action="/alerts/r1/acknowledge"' in page
|
||||
assert 'href="/alerts/rules/new"' in page and 'href="/alerts/rules/r1/edit"' in page
|
||||
@@ -98,7 +98,7 @@ async def test_a_viewer_reads_but_gets_no_actions(app):
|
||||
page = await render_template("alerts/list.html", active=[_row(rule, "firing", None)],
|
||||
rules=[rule], operators=[])
|
||||
|
||||
assert "cpu_pct on <strong>web-1</strong>" in page # nothing to link to: no bare href
|
||||
assert "CPU on <strong>web-1</strong>" in page # nothing to link to: no bare href
|
||||
for action in ("/acknowledge", "/alerts/rules/new", "/edit", "/toggle", "/delete"):
|
||||
assert action not in page, action
|
||||
assert 'href="/alerts/history?rule=r1"' in page
|
||||
@@ -165,3 +165,43 @@ async def test_rules_say_what_each_is_doing_now(app):
|
||||
assert 'title="Within its threshold">quiet</span>' in rules
|
||||
assert '<span class="tone-dim" style="font-size:0.8rem;">off</span>' in rules
|
||||
assert 'hx-get="/alerts/active"' in page
|
||||
|
||||
|
||||
def test_a_rule_condition_reads_in_words():
|
||||
"""#5212: "is down", not "is_up < 0.5"; a threshold in its unit."""
|
||||
from steward.alerts.service import rule_condition
|
||||
assert rule_condition(_rule("is_up", AlertOperator.lt, 0.5)) == "is down"
|
||||
assert rule_condition(_rule("is_healthy", AlertOperator.gte, 0.5)) == "is healthy"
|
||||
assert rule_condition(_rule("disk_used_pct_worst", AlertOperator.gte, 85.0)) == "Fullest disk >= 85%"
|
||||
assert rule_condition(_rule("seconds_since_seen", AlertOperator.gt, 180.0)) == \
|
||||
"Time since the agent reported > 3m"
|
||||
assert rule_condition(_rule("hrProcessorLoad", AlertOperator.gt, 3.0)) == "hrProcessorLoad > 3"
|
||||
|
||||
|
||||
def test_the_form_turns_up_or_down_into_a_condition():
|
||||
from steward.alerts.routes import _form_condition
|
||||
assert _form_condition({"metric_name": "is_up", "binary_when": "down"}) == \
|
||||
{"operator": "<", "threshold": "0.5"}
|
||||
assert _form_condition({"metric_name": "is_healthy", "binary_when": "up"}) == \
|
||||
{"operator": ">=", "threshold": "0.5"}
|
||||
# Any other metric keeps what was typed, whatever binary_when says.
|
||||
assert _form_condition({"metric_name": "cpu_pct", "binary_when": "down", "operator": ">",
|
||||
"threshold": "90"}) == {"operator": ">", "threshold": "90"}
|
||||
|
||||
|
||||
async def test_the_rule_form_speaks_in_names_and_units(app):
|
||||
from steward.alerts.routes import _OPERATORS
|
||||
rule = _rule("is_up", AlertOperator.lt, 0.5, "nas")
|
||||
rule.source_module, rule.ansible_action, rule.notify_no_data = "http", None, True
|
||||
async with app.test_request_context("/alerts/rules/r1/edit"):
|
||||
session["user_role"] = "operator"
|
||||
page = await render_template(
|
||||
"alerts/rules_form.html", rule=rule, operators=_OPERATORS,
|
||||
source_modules=["http", "host_agent"], initial_source="http", resources=["nas"],
|
||||
metrics=["is_up", "response_ms"], ansible_sources=[],
|
||||
catalog={"http": ["is_up", "response_ms"], "snmp": []})
|
||||
assert ">HTTP monitors</option>" in page and ">Host agent</option>" in page
|
||||
assert ">Up or down</option>" in page and 'data-unit="ms"' in page
|
||||
assert '<option value="down" selected>' in page # the binary choice, shown
|
||||
assert 'name="threshold" step="any" required disabled' in page # the raw threshold, off
|
||||
assert ">is_up<" not in page and "SNMP" not in page # no raw key; empty source skipped
|
||||
|
||||
Reference in New Issue
Block a user