Files
FabledSteward/steward/templates/email/report.html
T
bvandeusenandClaude Opus 5.5 5569f9934e
CI / lint (push) Successful in 2s
CI / integration (push) Failing after 1m24s
CI / unit (push) Successful in 1m37s
CI / publish (push) Skipped
feat(anomalies): a metric out of its normal is an anomaly to review (#5740)
#5740 "A metric that departs from its normal is recorded as an anomaly to
review, and is findable after it ends", milestone 513.

- Every minute each learned key's last 15 minutes is judged against its
  hour's normal: above the usual hold x1.25 and by the metric's floor opens
  an occurrence, which ends when the level comes back.
- metric_anomalies holds one row per item to review; a repeat while it
  still waits joins it as an occurrence (metric_anomaly_spans).
- An ended row nobody reviews within anomalies.review_days (14, Settings ->
  Thresholds) lapses; an ongoing one never does.
- Learning skips every hour an occurrence covers unless its anomaly was
  marked normal: no normal is learned without review.
- Alerts -> Anomalies lists those to review first, then the reviewed and
  lapsed under day dividers; marking is operator/admin and audited.
- The Overview widget and get_overview warn of open ones and count those
  to review; the host page shows its own; the weekly report lists the
  week's and the lapsed; MCP list_anomalies and review_anomaly.
- Reviewed and lapsed rows are pruned at data.retention_days, and kept
  in backups like alert history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:41:57 -04:00

81 lines
3.3 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{# The weekly report's HTML part: d = reports.build_report_data(), alerts = its
active alerts with tone and label, period, sent, base = Steward's address or "". #}
{% extends "_layout.html" %}
{% from "_parts.html" import badge, button, section, rows, row with context %}
{% block title %}Weekly report, {{ period }}{% endblock %}
{% block body %}
<tr><td style="padding:10px 24px 0 24px;font:600 22px/1.3 {{ c.font }};color:{{ c.text }};">Weekly report</td></tr>
<tr><td style="padding:2px 24px 0 24px;font:14px/1.5 {{ c.font }};color:{{ c.muted }};">{{ period }}</td></tr>
{% if d.hosts_down %}
<tr><td style="padding:16px 24px 0 24px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"
style="border:1px solid {{ c.crit }};border-left:4px solid {{ c.crit }};border-radius:8px;">
<tr><td style="padding:10px 14px;font:14px/1.5 {{ c.font }};color:{{ c.text }};">
<strong style="color:{{ c.crit }};">{{ d.hosts_down|length }} {{ "monitor is" if d.hosts_down|length == 1 else "monitors are" }} down now:</strong>
{{ d.hosts_down|join(", ") }}
</td></tr></table>
</td></tr>
{% endif %}
{{ section("Active alerts", base ~ "/alerts/" if base else "") }}
{% if alerts %}
{% call rows() %}
{% for a in alerts %}
{{ row([badge(a.tone, a.state), a.name ~ " — " ~ a.resource, a.condition]) }}
{% endfor %}
{% endcall %}
{% else %}
<tr><td style="padding:0 24px;font:14px/1.5 {{ c.font }};color:{{ c.muted }};">None. Nothing is firing.</td></tr>
{% endif %}
<tr><td style="padding:8px 24px 0 24px;font:13px/1.5 {{ c.font }};color:{{ c.muted }};">
This week {{ d.alert_events.fired }} {{ "alert" if d.alert_events.fired == 1 else "alerts" }} fired and {{ d.alert_events.resolved }} resolved.
</td></tr>
{{ section("Uptime over 7 days", base ~ "/monitors/" if base else "") }}
{% if d.uptime_summary %}
{% call rows() %}
{% for m in d.uptime_summary %}
{{ row([m.name, "—" if m.pct_7d is none else "%.2f%%"|format(m.pct_7d)]) }}
{% endfor %}
{% endcall %}
{% else %}
<tr><td style="padding:0 24px;font:14px/1.5 {{ c.font }};color:{{ c.muted }};">No monitors are enabled.</td></tr>
{% endif %}
{% if anomalies.seen or anomalies.lapsed %}
{{ section("Metric anomalies this week", base ~ "/alerts/anomalies" if base else "") }}
{% call rows() %}
{% for a in anomalies.seen %}
{{ row([badge(a.tone, a.review), a.name ~ " — " ~ a.when, a.level]) }}
{% endfor %}
{% for a in anomalies.lapsed %}
{{ row([badge(a.tone, a.review), a.name ~ " — lapsed unreviewed; " ~ a.when, a.level]) }}
{% endfor %}
{% endcall %}
{% if anomalies.to_review %}
<tr><td style="padding:8px 24px 0 24px;font:13px/1.5 {{ c.font }};color:{{ c.muted }};">
{{ anomalies.to_review }} {{ "anomaly waits" if anomalies.to_review == 1 else "anomalies wait" }} to be marked normal or real.
</td></tr>
{% endif %}
{% endif %}
{% if d.top_routers %}
{{ section("Busiest Traefik routers, last 24 hours", base ~ "/plugins/traefik/" if base else "") }}
{% call rows() %}
{% for r in d.top_routers %}
{{ row([r.name, "%.2f req/s"|format(r.avg_rate)]) }}
{% endfor %}
{% endcall %}
{% endif %}
{% if base %}
<tr><td style="padding:24px 24px 24px 24px;">{{ button(base ~ "/", "Open Steward") }}</td></tr>
{% else %}
<tr><td style="padding:0 0 16px 0;"></td></tr>
{% endif %}
{% endblock %}
{% block footer %}
Sent {{ sent }}. The weekly report is turned on, and its day and hour set, in Settings › Reports.
{% endblock %}