feat(ui): shared |bytes |rate |duration |ago filters and status_class from Settings → Thresholds (#5093)
#5093 "Shared format filters and status_class driven by Settings → Thresholds". - steward/core/formatting.py: human_bytes (binary units), human_rate, human_duration (two largest units), ago (a <time> titled with the absolute UTC time) and status_class(value, kind, thresholds) -> ok/warn/crit/unknown, which raises on a kind the thresholds don't name. - app.py registers them as the bytes, rate, duration and ago filters and the status_class global; threshold_style is now graded by status_class (and caps its weight at 500). - Replaces the copies: docker's _human_bytes/_human_uptime, core alerts' _duration, the host-agent fmt_bps/fmt_bytes macros, the Traefik and UniFi byte-rate arithmetic, the swarm-failures ago macro and two uptime spans. - Tests: a table per helper, and every template compiled against the app's filters, so an unknown filter fails CI rather than a page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+5
-13
@@ -17,6 +17,7 @@ from steward.models.alerts import (
|
||||
AlertStateEnum,
|
||||
MaintenanceWindow,
|
||||
)
|
||||
from steward.core.formatting import human_duration
|
||||
from steward.models.metrics import PluginMetric
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -256,15 +257,6 @@ def silent_since(last_sample_at: datetime | None, interval: float | None,
|
||||
return last + limit if now - last > limit else None
|
||||
|
||||
|
||||
def _duration(seconds: float) -> str:
|
||||
seconds = int(seconds)
|
||||
if seconds < 120:
|
||||
return f"{seconds}s"
|
||||
if seconds < 7200:
|
||||
return f"{seconds // 60}m"
|
||||
return f"{seconds // 3600}h{(seconds % 3600) // 60:02d}m"
|
||||
|
||||
|
||||
def _track_samples(session: AsyncSession, rule: AlertRule, state: AlertState,
|
||||
samples: list[PluginMetric], now: datetime, muted: str | None = None
|
||||
) -> tuple[str, float, str] | None:
|
||||
@@ -281,7 +273,7 @@ def _track_samples(session: AsyncSession, rule: AlertRule, state: AlertState,
|
||||
state.last_sample_at = sample.recorded_at
|
||||
if state.no_data_since is None or not samples:
|
||||
return None
|
||||
note = f"data resumed after {_duration((now - state.no_data_since).total_seconds())} silent"
|
||||
note = f"data resumed after {human_duration((now - state.no_data_since).total_seconds())} silent"
|
||||
value = samples[-1].value
|
||||
event_id = _add_event(session, rule.id, "no_data", state.state.value, value, now,
|
||||
note=f"{note}; {muted}" if muted else note)
|
||||
@@ -559,12 +551,12 @@ async def check_silence(app: "Quart") -> None:
|
||||
continue
|
||||
state.no_data_since = since
|
||||
if state.last_sample_at is None:
|
||||
note = (f"no sample yet, {_duration((now - watching_since).total_seconds())}"
|
||||
note = (f"no sample yet, {human_duration((now - watching_since).total_seconds())}"
|
||||
" after the rule started watching")
|
||||
else:
|
||||
note = f"no sample for {_duration((now - state.last_sample_at).total_seconds())}"
|
||||
note = f"no sample for {human_duration((now - state.last_sample_at).total_seconds())}"
|
||||
if state.sample_interval_s:
|
||||
note += f" (usually every {_duration(state.sample_interval_s)})"
|
||||
note += f" (usually every {human_duration(state.sample_interval_s)})"
|
||||
muted = _muted_by(windows, rule)
|
||||
event_id = _add_event(session, rule.id, state.state.value, "no_data", None,
|
||||
now, note=f"{note}; {muted}" if muted else note)
|
||||
|
||||
Reference in New Issue
Block a user