#5691 "Generic Prometheus dashboard widget: pick a target and a template", step 3 of milestone 508 "Prometheus widget templates". - /plugins/prometheus/widget draws one target with one template in widget shape: the verdict (led by "isn't answering" when the target is down), then each block with problems first, figures beside a share bar or the last 24h of bars, and the healthy rest folding to "+N more healthy" past six. A missing target or template, or one that no longer checks, says so. - library.py: bundled templates from plugins/prometheus/widgets/*.yaml, the picker's target and template lists, and the default title. - widgets/fabled_archives.yaml: the first bundled template, against the series Archives reports (refined in #5693). - Dashboard: a widget setting may take `options_from` (read when the picker opens) with an `empty` sentence; `default_title`; a Details link may name settings ("/plugins/prometheus/targets/{target}"). Prometheus is grouped with the integrations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
100 lines
5.0 KiB
Python
100 lines
5.0 KiB
Python
"""Integration: the Prometheus dashboard widget (#5691) draws a target's
|
|
stored series with the bundled Fabled Archives template, the picker lists the
|
|
targets and templates, and a widget whose target is gone says so.
|
|
Requires STEWARD_DATABASE_URL."""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import importlib
|
|
import os
|
|
import time
|
|
import uuid
|
|
|
|
import pytest
|
|
|
|
pytestmark = pytest.mark.integration
|
|
|
|
|
|
@pytest.fixture
|
|
def app():
|
|
if not os.environ.get("STEWARD_DATABASE_URL"):
|
|
pytest.skip("needs Postgres")
|
|
from steward.app import create_app
|
|
return create_app(testing=False)
|
|
|
|
|
|
def _prom(sub):
|
|
"""A submodule of the prometheus plugin as the app loaded it (see
|
|
test_prometheus_scrape)."""
|
|
from steward.core.plugin_manager import get_loaded_plugin_modules
|
|
loaded = get_loaded_plugin_modules().get("prometheus")
|
|
return importlib.import_module(f"{loaded.__name__ if loaded else 'plugins.prometheus'}.{sub}")
|
|
|
|
|
|
def _series(name: str) -> list[tuple[str, str, float]]:
|
|
"""(resource, metric, value) as a scrape of Archives stores them."""
|
|
rows = [(name, "fabledarchives_data_disk_free_bytes", 17.8e12),
|
|
(name, "fabledarchives_data_disk_size_bytes", 46e12),
|
|
(name, "fabledarchives_files", 94116.0), (name, "fabledarchives_files_checked", 93310.0),
|
|
(name, "fabledarchives_malware_held_files", 2.0),
|
|
(name, "fabledarchives_notification_channels_failing", 0.0),
|
|
(f"{name}:version=2026.10.12.1717,channel=dev,commit=3605d0a", "fabledarchives_build_info", 1.0),
|
|
(f"{name}:person=Bryan,person_id=9,kind=files", "fabledarchives_person_stored_bytes", 4.7e11)]
|
|
for state in ("disabled", "fresh", "late", "missing", "waiting"):
|
|
rows.append((f"{name}:target=Steward,target_id=1,state={state}", "fabledarchives_backup_target_state",
|
|
float(state == "late")))
|
|
for state in ("ok", "failing", "late", "running"):
|
|
rows.append((f"{name}:job=Nightly,job_id=5,state={state}", "fabledarchives_replication_job_state",
|
|
float(state == "ok")))
|
|
return rows
|
|
|
|
|
|
def test_the_widget_draws_a_target_with_a_template_and_the_picker_lists_them(app):
|
|
from steward.core.alerts import record_metrics
|
|
from steward.core.widgets import WIDGET_REGISTRY
|
|
from steward.dashboard.routes import with_options
|
|
from steward.models.metrics import PluginMetric
|
|
from steward.models.users import User, UserRole
|
|
|
|
PrometheusTarget = _prom("models").PrometheusTarget
|
|
tag = uuid.uuid4().hex[:8]
|
|
name = f"arch-{tag}"
|
|
|
|
async def go():
|
|
uid = str(uuid.uuid4())
|
|
async with app.db_sessionmaker() as s:
|
|
async with s.begin():
|
|
s.add(User(id=uid, username=f"pw-{tag}", email=f"pw-{tag}@test", password_hash="x",
|
|
role=UserRole.viewer, is_active=True))
|
|
target = PrometheusTarget(name=name, url=f"http://{name}/metrics")
|
|
s.add(target)
|
|
await s.flush()
|
|
target_id = target.id
|
|
await record_metrics(s, [PluginMetric(source_module="prometheus", resource_name=r,
|
|
metric_name=m, value=v) for r, m, v in _series(name)])
|
|
async with app.db_sessionmaker() as db:
|
|
picker = await with_options(db, WIDGET_REGISTRY["prometheus"])
|
|
client = app.test_client()
|
|
async with client.session_transaction() as sess:
|
|
sess.update(user_id=uid, user_role="viewer", username=f"pw-{tag}", login_at=time.time(), tz="UTC")
|
|
ok = await client.get(f"/plugins/prometheus/widget?target={target_id}&template=bundled:fabled_archives")
|
|
page = " ".join((await ok.get_data(as_text=True)).split())
|
|
gone = await client.get("/plugins/prometheus/widget?target=no-such-target&template=bundled:fabled_archives")
|
|
async with app.db_sessionmaker() as s:
|
|
async with s.begin():
|
|
await s.delete(await s.get(PrometheusTarget, target_id))
|
|
return target_id, picker, ok.status_code, page, gone.status_code, await gone.get_data(as_text=True)
|
|
|
|
target_id, picker, status, page, gone_status, gone = asyncio.run(go())
|
|
targets, templates = picker["params"]
|
|
assert {"value": target_id, "label": name} in targets["options"]
|
|
assert {"value": "bundled:fabled_archives", "label": "Fabled Archives (built in)"} in templates["options"]
|
|
assert status == 200
|
|
# The late backup target and the held malware are the problems, the late target first.
|
|
assert "2 need a look. Backup targets: Steward is late." in page
|
|
assert page.index("2 need a look") < page.index("Backup targets</div>") < page.index("Replication</div>")
|
|
assert "16.2 TiB of 41.8 TiB" in page and "93,310 of 94,116" in page
|
|
assert "version 2026.10.12.1717 · channel dev · commit 3605d0a" in page
|
|
assert f'<a href="/plugins/prometheus/targets/{target_id}">{name}</a>' in page
|
|
assert gone_status == 200 and "The target this widget showed has been deleted." in gone
|