feat(syslog): pattern alerts: telling phrases counted per source each minute for alert rules, storage-trouble presets (#6001)
A pattern (phrases, a severity, a source, a window) is counted every minute into plugin_metrics as lines_matching_<key> per added source, zero included, so an ordinary alert rule fires on a match and clears once the window passes. Each count reads only the lines stored since the last, by line id; a new or changed pattern reads its window once. Patterns page with ready-made storage-trouble presets and "Alert on it", which opens the rule form aimed at the count. Source names are kept unique, and a rename moves its counts and the rules on them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -70,7 +70,10 @@
|
||||
{% endif %}
|
||||
|
||||
<section class="card" aria-labelledby="syslog-sources">
|
||||
<h2 id="syslog-sources" class="section-title">Sources</h2>
|
||||
<div class="host-health-head">
|
||||
<h2 id="syslog-sources" class="section-title" style="margin:0;">Sources</h2>
|
||||
<span class="page-actions" style="margin-left:auto;"><a href="/plugins/syslog/patterns" class="btn btn-sm btn-ghost">Patterns</a></span>
|
||||
</div>
|
||||
{% if not sources %}
|
||||
{{ empty_state("No sources yet. Point a NAS, router or other device at this server's syslog port: it shows above as sending, and Add keeps its lines.") }}
|
||||
{% else %}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
{# plugins/syslog/templates/syslog/pattern_form.html — a pattern's phrases,
|
||||
severity, source and window, and Remove (#6001). #}
|
||||
{% extends "base.html" %}
|
||||
{% from "_macros.html" import crumbs, page_header %}
|
||||
{% set heading = ("Edit " ~ pattern.name) if pattern else "New pattern" %}
|
||||
{% block title %}{{ heading }} — Syslog — Steward{% endblock %}
|
||||
{% block breadcrumb %}{{ crumbs([("Syslog", "/plugins/syslog/"), ("Patterns", "/plugins/syslog/patterns"), (pattern.name if pattern else "New", "")]) }}{% endblock %}
|
||||
{% block content %}
|
||||
{{ page_header(heading) }}
|
||||
|
||||
<div class="form-page">
|
||||
{% if error %}<p class="tone-crit" role="alert">{{ error }}</p>{% endif %}
|
||||
<form method="post" action="{{ '/plugins/syslog/patterns/' ~ pattern.id ~ '/edit' if pattern else '/plugins/syslog/patterns/new' }}">{{ csrf_field() }}
|
||||
<section class="card form-section" aria-labelledby="syslog-pattern">
|
||||
<h2 id="syslog-pattern" class="section-title">Pattern</h2>
|
||||
<div class="form-pair">
|
||||
<div class="field">
|
||||
<label class="field-label" for="pattern-name">Name</label>
|
||||
<input class="input" id="pattern-name" name="name" maxlength="100" value="{{ v.name or '' }}" required autocomplete="off">
|
||||
{% if pattern %}<span class="field-hint">Alert rules watch <span class="mono">{{ pattern.metric }}</span>, which stays as it is when the name changes.</span>{% endif %}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label" for="pattern-phrases">Phrases</label>
|
||||
<textarea class="input mono" id="pattern-phrases" name="phrases" rows="6">{{ v.phrases or '' }}</textarea>
|
||||
<span class="field-hint">One per line. A line matches if it contains any of them, in any case. Leave empty to count every line at the severity below.</span>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label" for="pattern-severity">Severity</label>
|
||||
<select class="input" id="pattern-severity" name="max_severity">
|
||||
<option value="">Any</option>
|
||||
{% for level, name in severities %}<option value="{{ level }}"{% if v.max_severity|string == level|string %} selected{% endif %}>{{ name }} and worse</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label" for="pattern-source">Sources</label>
|
||||
<select class="input" id="pattern-source" name="source_id">
|
||||
<option value="">Every added source</option>
|
||||
{% for s in sources %}<option value="{{ s.id }}"{% if v.source_id == s.id %} selected{% endif %}>{{ s.name or s.address }}</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label" for="pattern-window">Window</label>
|
||||
<select class="input" id="pattern-window" name="window_minutes">
|
||||
{% for w in windows %}<option value="{{ w }}"{% if v.window_minutes == w %} selected{% endif %}>{{ w if w < 60 else w // 60 }} {{ "min" if w < 60 else ("hour" if w == 60 else "hours") }}</option>{% endfor %}
|
||||
</select>
|
||||
<span class="field-hint">The count is of matches this far back, so an alert on it clears this long after the last one.</span>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-check">
|
||||
<input type="checkbox" name="enabled"{% if v.enabled %} checked{% endif %}>
|
||||
Counted: off keeps the pattern without counting it
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="page-actions">
|
||||
<button type="submit" class="btn">Save</button>
|
||||
<a href="/plugins/syslog/patterns" class="btn btn-ghost">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if pattern %}
|
||||
<section class="card form-section" aria-labelledby="pattern-remove" style="margin-top:1.5rem;">
|
||||
<h2 id="pattern-remove" class="section-title">Remove</h2>
|
||||
<p class="field-hint" style="margin:0 0 0.75rem;">Stops counting. An alert rule on it then reads no data.</p>
|
||||
<form method="post" action="/plugins/syslog/patterns/{{ pattern.id }}/delete" data-confirm="Remove the pattern {{ pattern.name }}?" style="margin:0;">{{ csrf_field() }}
|
||||
<button type="submit" class="btn btn-danger btn-sm">Remove pattern</button>
|
||||
</form>
|
||||
</section>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,71 @@
|
||||
{# plugins/syslog/templates/syslog/patterns.html — telling phrases counted in
|
||||
each source's lines every minute, so an alert rule can watch them (#6001). #}
|
||||
{% extends "base.html" %}
|
||||
{% from "_macros.html" import crumbs, page_header, empty_state %}
|
||||
{% block title %}Patterns — Syslog — Steward{% endblock %}
|
||||
{% block breadcrumb %}{{ crumbs([("Syslog", "/plugins/syslog/"), ("Patterns", "")]) }}{% endblock %}
|
||||
{% block content %}
|
||||
{% set admin = session.user_role == 'admin' %}
|
||||
{% set can_alert = session.user_role in ('admin', 'operator') %}
|
||||
{{ page_header("Patterns", (patterns|length ~ " pattern" ~ ("s" if patterns|length != 1 else "")) if patterns else none) }}
|
||||
|
||||
<section class="card" aria-labelledby="syslog-patterns">
|
||||
<div class="host-health-head">
|
||||
<h2 id="syslog-patterns" class="section-title" style="margin:0;">Counted</h2>
|
||||
{% if admin %}<span class="page-actions" style="margin-left:auto;"><a href="/plugins/syslog/patterns/new" class="btn btn-sm">New pattern</a></span>{% endif %}
|
||||
</div>
|
||||
<p class="field-hint" style="margin:0 0 0.75rem;">Each minute, every added source's lines matching a pattern in its window are counted. Alert on a count over 0 to hear of the first match; the alert clears once the window has passed it.</p>
|
||||
{% if not patterns %}
|
||||
{{ empty_state("No patterns yet. Add one of the ready-made ones below, or write your own.") }}
|
||||
{% else %}
|
||||
<table class="table" style="width:100%;font-size:0.82rem;">
|
||||
<thead><tr><th>Pattern</th><th>Matches</th><th>Window</th><th>Count now</th>{% if can_alert %}<th></th>{% endif %}</tr></thead>
|
||||
<tbody>
|
||||
{% for p in patterns %}
|
||||
<tr>
|
||||
<td>{{ p.name }}{% if not p.enabled %} <span class="field-hint">(off)</span>{% endif %}<div class="field-hint mono">{{ p.metric }}</div></td>
|
||||
<td>
|
||||
{% if p.phrases %}<span class="mono" title="{{ p.phrases|join('\n') }}">{{ p.phrases[:3]|join(" · ") }}{% if p.phrases|length > 3 %} · +{{ p.phrases|length - 3 }} more{% endif %}</span>{% endif %}
|
||||
{% if p.severity %}<div class="field-hint">{{ p.severity }} and worse</div>{% endif %}
|
||||
{% if p.scope %}<div class="field-hint">from {{ p.scope }} only</div>{% endif %}
|
||||
</td>
|
||||
<td>{{ p.window }} min</td>
|
||||
<td>
|
||||
{% if not p.counts %}<span class="field-hint">No added source</span>
|
||||
{% else %}{% for c in p.counts %}<div><span class="{{ 'tone-crit' if c.value }}">{{ "—" if c.value is none else c.value }}</span> <span class="field-hint">{{ c.source }}</span></div>{% endfor %}{% endif %}
|
||||
</td>
|
||||
{% if can_alert %}
|
||||
<td style="white-space:nowrap;">
|
||||
{% if p.alert_resource %}<a class="btn btn-sm btn-ghost" href="/alerts/rules/new?source_module=syslog&resource_name={{ p.alert_resource|urlencode }}&metric_name={{ p.metric|urlencode }}">Alert on it</a>{% endif %}
|
||||
{% if admin %}<a class="btn btn-sm btn-ghost" href="/plugins/syslog/patterns/{{ p.id }}/edit">Edit</a>{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
{% if admin and presets %}
|
||||
<section class="card" aria-labelledby="syslog-presets">
|
||||
<h2 id="syslog-presets" class="section-title">Ready-made</h2>
|
||||
<p class="field-hint" style="margin:0 0 0.75rem;">How NAS and Linux kernels word storage trouble. Add one, then change its phrases if your devices word it differently.</p>
|
||||
<table class="table" style="width:100%;font-size:0.82rem;">
|
||||
<tbody>
|
||||
{% for p in presets %}
|
||||
<tr>
|
||||
<td>{{ p.name }}<div class="field-hint">{{ p.about }}</div></td>
|
||||
<td class="mono">{{ p.phrases|join(" · ") if p.phrases else "err, crit, alert or emerg" }}</td>
|
||||
<td style="white-space:nowrap;">
|
||||
<form method="post" action="/plugins/syslog/patterns/preset/{{ p.key }}" style="margin:0;">{{ csrf_field() }}
|
||||
<button type="submit" class="btn btn-sm">Add</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user