#5485 "A rule's page waits up to 25 s for its metric preview: a cold 24 h series read from plugin_metrics". Opening "Agent offline" took 25.6 s; a cold day of one series measured 18.3 s over MCP, a warm one 63 ms. The pickers still come with the page from plugin_metric_latest; the preview is its own request (hx-trigger=load), showing "Reading the last day…" until it lands. Picker swaps no longer read it either. #5486 "Send test where it can be found": the button moves from inside "What happens" to the page header beside History, its result under the verdict. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -81,7 +81,8 @@ whole host", then its mounts, CPU cores, disks and so on, under a heading for
|
||||
each kind. A part that reports nothing a rule can watch, such as an interface's
|
||||
traffic, isn't listed. The **Metric** list holds only what the picked resource
|
||||
reports. Under it, a preview shows the metric's current reading and its last
|
||||
24 hours (lowest to highest, with an hourly sparkline), so the threshold can be
|
||||
24 hours (lowest to highest, with an hourly sparkline; it loads just after the
|
||||
page, since a day of samples can take a while to read), so the threshold can be
|
||||
set against real numbers.
|
||||
|
||||
### Rules that cover hosts
|
||||
@@ -176,7 +177,7 @@ bursts: each alert posts on its own, at once.
|
||||
|
||||
### Send test
|
||||
|
||||
**Send test** on a rule's page (operator role), or `send_test_alert` over MCP,
|
||||
**Send test** at the top of a rule's page (operator role), or `send_test_alert` over MCP,
|
||||
sends a `TEST` alert for that rule now, by email and webhook, the same way a
|
||||
real alert would go. It uses the rule's name and condition and the latest
|
||||
reading of what the rule watches. A rule that covers hosts uses its first
|
||||
|
||||
@@ -281,9 +281,11 @@ async def rule_target_api():
|
||||
@alerts_bp.get("/api/metric_preview")
|
||||
@require_role(UserRole.viewer)
|
||||
async def metric_preview_api():
|
||||
"""What the picked metric reads now and over the last day (#5475)."""
|
||||
"""What the picked metric reads now and over the last day (#5475). Its own
|
||||
request, after the page: a cold day of one series can take many seconds
|
||||
to read (#5485), and the page doesn't wait for it."""
|
||||
async with current_app.db_sessionmaker() as db:
|
||||
ctx = await _target_from_args(db)
|
||||
ctx = await _target_from_args(db, with_preview=True)
|
||||
return await render_template("alerts/_metric_preview.html", **ctx)
|
||||
|
||||
|
||||
@@ -300,28 +302,30 @@ async def cover_groups_api():
|
||||
|
||||
|
||||
async def _target(db, source: str, *, host: str = "", resource: str = "", metric: str = "",
|
||||
covers: str = "resource", keep: bool = False) -> dict:
|
||||
"""The rule form's target pickers and the chosen metric's preview (#5475);
|
||||
`keep` on a rule's own page, so its resource and metric stay on offer."""
|
||||
covers: str = "resource", keep: bool = False, with_preview: bool = False) -> dict:
|
||||
"""The rule form's target pickers (#5475), from the latest-value table;
|
||||
`keep` on a rule's own page, so its resource and metric stay on offer.
|
||||
The chosen metric's preview only `with_preview`: it reads a day of raw
|
||||
samples, so pages and picker swaps leave it to its own request (#5485)."""
|
||||
from steward.core.status import sparkline_svg
|
||||
target = await alert_svc.rule_target(db, source, host=host, resource=resource, metric=metric,
|
||||
keep=keep)
|
||||
preview = None
|
||||
if target["resource"] and target["metric"]:
|
||||
if with_preview and target["resource"] and target["metric"]:
|
||||
preview = await alert_svc.metric_preview(db, source, target["resource"], target["metric"])
|
||||
if preview:
|
||||
# From zero to the series' own peak (UI & Design ruling 8), so a
|
||||
# disk at 61-62% reads flat rather than as a cliff.
|
||||
preview["spark"] = sparkline_svg(preview["series"], width=120, height=20,
|
||||
stroke="currentColor", floor=1.0)
|
||||
return {**target, "preview": preview, "current_covers": covers}
|
||||
return {**target, "preview": preview, "preview_read": with_preview, "current_covers": covers}
|
||||
|
||||
|
||||
async def _target_from_args(db) -> dict:
|
||||
async def _target_from_args(db, *, with_preview: bool = False) -> dict:
|
||||
a = request.args
|
||||
return await _target(db, a.get("source_module", ""), host=a.get("pick_host", ""),
|
||||
resource=a.get("resource_name", ""), metric=a.get("metric_name", ""),
|
||||
covers=a.get("covers", "resource"))
|
||||
covers=a.get("covers", "resource"), with_preview=with_preview)
|
||||
|
||||
|
||||
async def _cover_choices(db, source: str, rule: AlertRule | None = None) -> dict:
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif metric and resource and not preview_read %}
|
||||
<p class="field-hint">Reading the last day…</p>
|
||||
{% elif metric and resource %}
|
||||
<p class="field-hint">No reading of this yet from {{ resource }}.</p>
|
||||
{% endif %}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
so on), so a host's parts never crowd the list of hosts. Any other source:
|
||||
one Resource picker. Then the metrics that resource reports, and a preview
|
||||
of the chosen one. Picking a host or part swaps this block alone
|
||||
(/alerts/api/rule_target); picking a metric swaps the preview. #}
|
||||
(/alerts/api/rule_target); picking a metric swaps the preview, which
|
||||
is read in its own request after the block is drawn (#5485). #}
|
||||
{% set one = current_covers|default("resource") == "resource" %}
|
||||
<div data-cover-resource {% if not one %}hidden{% endif %}>
|
||||
{% if host_source %}
|
||||
@@ -68,5 +69,10 @@
|
||||
{% if metric and metric_about(metric) %}
|
||||
<p class="field-hint" data-metric-about>{{ metric_about(metric) }}</p>
|
||||
{% else %}<p class="field-hint" data-metric-about hidden></p>{% endif %}
|
||||
<div id="metric-preview" aria-live="polite">{% include "alerts/_metric_preview.html" %}</div>
|
||||
{# The preview comes in its own request once the page is up (#5485). #}
|
||||
<div id="metric-preview" aria-live="polite"
|
||||
{%- if metric and resource %} hx-get="/alerts/api/metric_preview" hx-trigger="load"
|
||||
hx-include="closest form" hx-params="source_module,resource_name,pick_host,metric_name,covers"{% endif %}>
|
||||
{%- include "alerts/_metric_preview.html" -%}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
<div class="form-page">
|
||||
{% if rule %}
|
||||
{% call page_header(rule.name) %}
|
||||
{# Send test (#5466), up here where it is found (#5486); its result shows under the verdict. #}
|
||||
<button type="button" class="btn btn-sm btn-ghost" hx-post="/alerts/rules/{{ rule.id }}/test"
|
||||
hx-target="#rule-test-result" hx-swap="innerHTML"
|
||||
title="Send a TEST alert for this rule as saved, now. Nothing fires and its history doesn't change.">Send test</button>
|
||||
<a class="btn btn-sm btn-ghost" href="/alerts/history?rule={{ rule.id }}">History</a>
|
||||
{% endcall %}
|
||||
<p class="page-verdict">
|
||||
@@ -17,6 +21,7 @@
|
||||
{%- if fired %}Last fired {{ fired|ago }}{% else %}Never fired{% endif -%}
|
||||
{%- if history_count is defined and history_count is not none %} · {{ history_count }} change{{ "s" if history_count != 1 }} of state on record{% endif -%}
|
||||
</p>
|
||||
<div id="rule-test-result" aria-live="polite"></div>
|
||||
{% if rule.covers != "resource" %}
|
||||
<p class="field-hint" style="margin-top:-0.5rem;">
|
||||
Covers {% if rule.covers == "group" %}every host in <strong>{{ rule.cover_group }}</strong>{% else %}every host{% endif %}:
|
||||
@@ -139,15 +144,8 @@
|
||||
{%- else %}<span class="tone-warn">It notifies nobody yet:</span> no email server with recipients and no webhook is set
|
||||
{%- if session.user_role == 'admin' %} (<a href="/settings/notifications/">Settings → Notifications</a>){% endif %}.{% endif -%}
|
||||
</p>
|
||||
{% if rule %}
|
||||
<div class="form-group">
|
||||
<button type="button" class="btn btn-sm btn-ghost" hx-post="/alerts/rules/{{ rule.id }}/test"
|
||||
hx-target="#rule-test-result" hx-swap="innerHTML">Send test</button>
|
||||
<p class="field-hint">Sends a TEST alert for this rule as saved, now, to everywhere above.
|
||||
Nothing fires, and the rule's history doesn't change.</p>
|
||||
<div id="rule-test-result" aria-live="polite"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if rule %}<p class="field-hint" style="margin-top:-0.5rem;">To check that it arrives, use
|
||||
<strong>Send test</strong> at the top of this page.</p>{% endif %}
|
||||
<div class="form-group">
|
||||
<label style="display:flex;align-items:center;gap:0.5rem;font-weight:normal;">
|
||||
<input type="checkbox" name="notify_no_data"
|
||||
|
||||
@@ -316,5 +316,8 @@ async def test_a_rule_covering_hosts_lists_its_hosts_under_it(app):
|
||||
assert 'aria-expanded="false" aria-controls="hosts-p1"' in quiet
|
||||
assert '<tbody id="hosts-p1" class="row-children" hidden>' in quiet and "never fired" in quiet
|
||||
assert "Covers every host in <strong>swarm</strong>: 2 now (node-a, node-b)" in page
|
||||
# #5486: Send test sits in the page's header, its result under the verdict.
|
||||
assert page.index('hx-post="/alerts/rules/p1/test"') < page.index('id="rule-test-result"') \
|
||||
< page.index('id="rule-watches"')
|
||||
assert 'name="covers" value="group"' in page and "fixed once the rule exists" in page
|
||||
assert 'data-cover-resource hidden' in page
|
||||
|
||||
@@ -51,11 +51,14 @@ def test_the_pickers_and_the_preview_read_what_the_host_reports(app):
|
||||
_csrf="test-token", login_at=time.time())
|
||||
|
||||
resp = await client.get("/alerts/api/rule_target", query_string={
|
||||
"source_module": "host_agent", "pick_host": host})
|
||||
"source_module": "host_agent", "pick_host": host, "metric_name": "cpu_pct"})
|
||||
page = " ".join((await resp.get_data(as_text=True)).split())
|
||||
assert resp.status_code == 200
|
||||
assert f'<option value="{host}" selected>The whole host</option>' in page
|
||||
assert f'<optgroup label="Mounts"> <option value="{host}:/data" >/data</option>' in page
|
||||
# #5485: the pickers come without the preview, which loads in its own request.
|
||||
assert 'hx-get="/alerts/api/metric_preview" hx-trigger="load"' in page
|
||||
assert "Reading the last day" in page and "Now <strong>" not in page
|
||||
|
||||
resp = await client.get("/alerts/api/metric_preview", query_string={
|
||||
"source_module": "host_agent", "pick_host": host, "resource_name": f"{host}:/data",
|
||||
|
||||
Reference in New Issue
Block a user