fix(prometheus, syslog): a target or pattern pauses with a Pause/Resume button, not an "on" tick box in its form (#6419)
Monitors, schedules and maintenance plans already pause with a Pause/Resume button and say "Paused" where they're listed. Prometheus targets and syslog patterns only had a tick box inside the edit form, so a target refused by its endpoint couldn't be stopped from its page. - Both get a Pause/Resume button (target page and list; patterns list) posting to /toggle, recorded in the audit log, and a Paused badge. - The forms lose the tick box. A new one starts running; an edit keeps whether it is paused. - A target's "off" state is now "paused", in its pages and MCP tools. A resumed target reads "not scraped yet" until its next scrape, and its counter rates start over. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -372,12 +372,13 @@ def test_patterns_page_adds_edits_and_removes_and_a_rename_moves_counts(app):
|
||||
assert "at least one phrase" in await refused.get_data(as_text=True)
|
||||
made = await admin.post("/plugins/syslog/patterns/new", headers=csrf,
|
||||
form={"name": "Quota full", "phrases": "100%\nquota", "max_severity": "4",
|
||||
"window_minutes": "60", "enabled": "on"})
|
||||
"window_minutes": "60"})
|
||||
assert made.status_code == 302
|
||||
async with app.db_sessionmaker() as s:
|
||||
rows = {p.key: p for p in (await s.execute(select(models.SyslogPattern))).scalars()}
|
||||
assert set(rows) == {"nfs", "quota_full"}
|
||||
assert (rows["quota_full"].phrases, rows["quota_full"].max_severity) == (["100%", "quota"], 4)
|
||||
assert rows["quota_full"].enabled # a new pattern counts
|
||||
assert rows["nfs"].phrases[0] == "nfsd:"
|
||||
|
||||
await patterns.record_counts(app, now)
|
||||
@@ -391,13 +392,24 @@ def test_patterns_page_adds_edits_and_removes_and_a_rename_moves_counts(app):
|
||||
)).get_data(as_text=True)
|
||||
assert '<option value="syslog" selected>' in rule_form # aimed at the pattern's count
|
||||
|
||||
# Edited: the key, and so the metric, stay.
|
||||
edited = await admin.post(f"/plugins/syslog/patterns/{rows['quota_full'].id}/edit", headers=csrf,
|
||||
# Paused from the list (#6419), not from a tick box in the form.
|
||||
quota = rows["quota_full"].id
|
||||
assert (await admin.post(f"/plugins/syslog/patterns/{quota}/toggle", headers=csrf)).status_code == 302
|
||||
page = " ".join((await (await admin.get("/plugins/syslog/patterns")).get_data(as_text=True)).split())
|
||||
assert '<span class="badge badge-dim">Paused</span>' in page and ">Resume</button>" in page
|
||||
form_page = await (await admin.get(f"/plugins/syslog/patterns/{quota}/edit")).get_data(as_text=True)
|
||||
assert 'name="enabled"' not in form_page
|
||||
|
||||
# Edited: the key, and so the metric, stay, and so does the pause.
|
||||
edited = await admin.post(f"/plugins/syslog/patterns/{quota}/edit", headers=csrf,
|
||||
form={"name": "Quota", "phrases": "quota", "window_minutes": "15"})
|
||||
assert edited.status_code == 302
|
||||
async with app.db_sessionmaker() as s:
|
||||
p = await s.get(models.SyslogPattern, rows["quota_full"].id)
|
||||
p = await s.get(models.SyslogPattern, quota)
|
||||
assert (p.key, p.name, p.enabled, p.max_severity) == ("quota_full", "Quota", False, None)
|
||||
assert (await admin.post(f"/plugins/syslog/patterns/{quota}/toggle", headers=csrf)).status_code == 302
|
||||
async with app.db_sessionmaker() as s:
|
||||
assert (await s.get(models.SyslogPattern, quota)).enabled
|
||||
|
||||
# A renamed source takes its counts with it.
|
||||
renamed = await admin.post(f"/plugins/syslog/sources/{src_id}/edit", headers=csrf,
|
||||
|
||||
@@ -22,7 +22,7 @@ def _target(name, *, enabled=True, scraped=timedelta(seconds=30), error=None,
|
||||
def test_only_a_target_whose_scrape_failed_is_an_overview_finding():
|
||||
views = [t._target_view(x, {}, NOW) | {"_ok_at": x.last_ok_at} for x in (
|
||||
_target("archives", error="connection refused"), _target("fine"),
|
||||
_target("off", enabled=False, error="connection refused"),
|
||||
_target("paused", enabled=False, error="connection refused"),
|
||||
_target("stale", scraped=timedelta(days=1), error="connection refused"),
|
||||
_target("new", scraped=None))]
|
||||
[f] = t.target_findings(views)
|
||||
|
||||
@@ -11,6 +11,7 @@ from tests.test_style_tokens import hex_colours
|
||||
def _view(name, reach, says="Answering.", ok_minutes=None, capped=0):
|
||||
now = datetime.now(timezone.utc)
|
||||
return {"id": f"id-{name}", "name": name, "url": f"http://{name}:8081/metrics", "reach": reach,
|
||||
"enabled": reach != "paused",
|
||||
"says": says, "scraped_at": now - timedelta(seconds=20),
|
||||
"ok_at": now - timedelta(minutes=ok_minutes) if ok_minutes is not None else None,
|
||||
"interval": 60, "kept": 120, "dropped": 30, "capped": capped, "cap": 1000,
|
||||
@@ -20,7 +21,8 @@ def _view(name, reach, says="Answering.", ok_minutes=None, capped=0):
|
||||
async def test_a_target_not_answering_says_why_and_the_cap_is_shown(app):
|
||||
from quart import session
|
||||
targets = [_view("archives", "down", "Answered 401: the token was refused.", ok_minutes=180),
|
||||
_view("busy", "up", capped=37), _view("new", "pending")]
|
||||
_view("busy", "up", capped=37), _view("new", "pending"),
|
||||
_view("resting", "paused", "Paused: not scraped until it's resumed.")]
|
||||
async with app.test_request_context("/plugins/prometheus/"):
|
||||
session["user_role"] = "admin"
|
||||
page = await render_plugin_template(app, "prometheus", "prometheus/index.html", targets=targets)
|
||||
@@ -30,6 +32,10 @@ async def test_a_target_not_answering_says_why_and_the_cap_is_shown(app):
|
||||
assert "37 series weren't stored" in page and "the cap of 1000" in page
|
||||
assert '<span class="stat-tile-value">42 ms</span>' in page
|
||||
assert "the first scrape runs within seconds of saving" in page
|
||||
# Paused and resumed from the list (#6419), each row saying which it is.
|
||||
assert '<span class="badge badge-dim">Paused</span>' in page and "not scraped until it's resumed" in page
|
||||
assert 'action="/plugins/prometheus/targets/id-resting/toggle"' in page
|
||||
assert page.count(">Pause</button>") == 3 and page.count(">Resume</button>") == 1
|
||||
assert 'href="/plugins/prometheus/targets/new"' in page and 'href="/plugins/prometheus/targets/id-busy/edit"' in page
|
||||
main = page[page.index("<main"):]
|
||||
assert not hex_colours(main), hex_colours(main)
|
||||
@@ -43,7 +49,7 @@ async def test_a_viewer_sees_no_editing_and_an_empty_page_offers_to_add(app):
|
||||
targets=[_view("archives", "up")])
|
||||
session["user_role"] = "admin"
|
||||
empty = await render_plugin_template(app, "prometheus", "prometheus/index.html", targets=[])
|
||||
assert "/edit" not in page and "targets/new" not in page
|
||||
assert "/edit" not in page and "targets/new" not in page and "/toggle" not in page
|
||||
assert "No targets yet." in empty and 'href="/plugins/prometheus/targets/new"' in empty
|
||||
|
||||
|
||||
@@ -103,3 +109,4 @@ async def test_the_form_never_echoes_the_token_and_offers_test(app):
|
||||
assert 'name="token" type="password"' in page and "kept; type to change" in page
|
||||
assert 'name="clear_token"' in page and 'hx-post="/plugins/prometheus/targets/test"' in page
|
||||
assert "fabledarchives_*" in page and "Delete target" in page
|
||||
assert 'name="enabled"' not in page # paused from the target's page, not here (#6419)
|
||||
|
||||
@@ -128,13 +128,13 @@ class _Form(dict):
|
||||
def test_the_form_checks_what_it_stores_and_keeps_a_blank_token():
|
||||
values = parse_form(_Form(name="Archives", url="http://archives:8081/metrics", token="s3cret",
|
||||
interval_seconds="30", series_cap="500", keep="fabledarchives_*",
|
||||
enabled="on", verify_tls="on"))
|
||||
verify_tls="on"))
|
||||
assert values["token"].startswith("enc:") or values["token"] == "s3cret"
|
||||
assert (values["interval_seconds"], values["series_cap"], values["keep"], values["enabled"]) == (
|
||||
30, 500, ["fabledarchives_*"], True)
|
||||
assert (values["interval_seconds"], values["series_cap"], values["keep"]) == (30, 500, ["fabledarchives_*"])
|
||||
assert "enabled" not in values # Pause/Resume sets it, not the form (#6419)
|
||||
existing = PrometheusTarget(name="Archives", url="http://a/metrics", token="enc:v1:kept")
|
||||
again = parse_form(_Form(name="Archives", url="http://a/metrics"), existing)
|
||||
assert again["token"] == "enc:v1:kept" and again["enabled"] is False
|
||||
assert again["token"] == "enc:v1:kept" and "enabled" not in again
|
||||
cleared = parse_form(_Form(name="Archives", url="http://a/metrics", clear_token="on"), existing)
|
||||
assert cleared["token"] == ""
|
||||
for bad in (dict(name="", url="http://a"), dict(name="a:b", url="http://a"),
|
||||
@@ -158,7 +158,7 @@ def test_state_and_due():
|
||||
t.last_scraped_at = now - timedelta(minutes=10)
|
||||
assert target_state(t, now).reach == "stale"
|
||||
t.enabled = False
|
||||
assert target_state(t, now).reach == "off" and not is_due(t, now)
|
||||
assert target_state(t, now).reach == "paused" and not is_due(t, now)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -11,12 +11,12 @@ from plugins.syslog.patterns import PRESETS, PatternError, Tally, key_for, match
|
||||
|
||||
def test_a_form_reads_into_a_pattern():
|
||||
v = parse_form(MultiDict({"name": " NFS ", "phrases": "nfsd:\n\n lockd: \n", "max_severity": "3",
|
||||
"source_id": "", "window_minutes": "60", "enabled": "on"}))
|
||||
"source_id": "", "window_minutes": "60"}))
|
||||
assert v == {"name": "NFS", "phrases": ["nfsd:", "lockd:"], "max_severity": 3, "source_id": None,
|
||||
"window_minutes": 60, "enabled": True}
|
||||
"window_minutes": 60} # paused or not is the toggle's, not the form's (#6419)
|
||||
# A window or severity not offered falls back rather than failing.
|
||||
v = parse_form(MultiDict({"name": "x", "phrases": "a", "max_severity": "9", "window_minutes": "7"}))
|
||||
assert (v["max_severity"], v["window_minutes"], v["enabled"]) == (None, 15, False)
|
||||
assert (v["max_severity"], v["window_minutes"]) == (None, 15)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("form, says", [
|
||||
|
||||
Reference in New Issue
Block a user