The SNMP poll asks each device's sysName in the same GET and keeps it (snmp_003_sys_name). A syslog source told apart by name finds its own address from the SNMP device (reported or given name) or host of that name, so its lines reach the SNMP device's and host's pages again; the edit page gains Device address, which always wins (syslog_004). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
156 lines
12 KiB
HTML
156 lines
12 KiB
HTML
{# plugins/syslog/templates/syslog/index.html — whether Steward is listening,
|
|
the sources whose lines are kept, and the devices sending that haven't been
|
|
added: counted, never stored, until an admin adds one (#5999). #}
|
|
{% extends "base.html" %}
|
|
{% from "_macros.html" import page_header, status_badge, empty_state %}
|
|
{% from "syslog/_tone.html" import tone %}
|
|
{% block title %}Syslog — Steward{% endblock %}
|
|
{% block content %}
|
|
{% set admin = session.user_role == 'admin' %}
|
|
{{ page_header("Syslog", (sources|length ~ " source" ~ ("s" if sources|length != 1 else "")) if sources else none) }}
|
|
|
|
<section class="card" aria-labelledby="syslog-listener">
|
|
<div class="host-health-head">
|
|
{% if listener.error %}{{ status_badge('down', 'Not listening') }}
|
|
{% elif listener.listening %}{{ status_badge('up', 'Listening') }}
|
|
{% elif not listener.protocols %}{{ status_badge('inactive', 'Off') }}
|
|
{% else %}{{ status_badge('pending', 'Starting') }}{% endif %}
|
|
<h2 id="syslog-listener" class="section-title" style="margin:0;">Listener</h2>
|
|
{% if listener.protocols %}<span class="field-hint mono">{{ listener.protocols|join(" and ") }} port {{ listener.port }}</span>{% endif %}
|
|
{% if admin %}<span class="page-actions" style="margin-left:auto;"><a href="/settings/plugins/syslog/" class="btn btn-sm btn-ghost">Settings</a></span>{% endif %}
|
|
</div>
|
|
{% if listener.error %}
|
|
<p class="tone-crit" style="margin:0 0 0.4rem;">{{ listener.error }}</p>
|
|
<p class="field-hint" style="margin:0;">Tried again every minute. Another program may hold the port, or it is below 1024, which Steward can't open: choose another in Settings.</p>
|
|
{% elif not listener.protocols %}
|
|
<p class="field-hint" style="margin:0;">UDP and TCP are both off in Settings, so nothing is received.</p>
|
|
{% else %}
|
|
<p class="field-hint" style="margin:0;">
|
|
Point a device's remote syslog at this server, {{ listener.protocols|join(" or ") }} port {{ listener.port }}.
|
|
{%- if listener.bound_at %} Open since {{ listener.bound_at|ago }}: {{ "{:,}".format(listener.received) }} line{{ "s" if listener.received != 1 }} received{% if listener.connections %}, {{ listener.connections }} TCP sender{{ "s" if listener.connections != 1 }} connected{% endif %}.{% endif %}
|
|
</p>
|
|
{% endif %}
|
|
{% if listener.dropped %}<p class="tone-warn" style="margin:0.4rem 0 0;">{{ "{:,}".format(listener.dropped) }} line{{ "s" if listener.dropped != 1 }} lost: they arrived faster than they could be read.</p>{% endif %}
|
|
{% if listener.senders_full %}<p class="tone-warn" style="margin:0.4rem 0 0;">Steward knows {{ listener.max_senders }} senders, the most it keeps; a new one is counted, not offered{% if listener.unoffered %} ({{ "{:,}".format(listener.unoffered) }} line{{ "s" if listener.unoffered != 1 }} so far){% endif %}. Remove ones you don't need.</p>{% endif %}
|
|
</section>
|
|
|
|
{% set unnamed = (offered + sources)|selectattr("docker_address")|rejectattr("sender")|list %}
|
|
{% if unnamed %}
|
|
<p class="tone-warn" role="status">Lines from {{ unnamed|map(attribute="address")|unique|join(", ") }} come through Docker's own address and carry no device name, so the devices sending them can't be told apart. Have each device put its hostname in its lines, or publish the port in host mode, as <a href="#syslog-setup">Point a device here</a> shows.</p>
|
|
{% endif %}
|
|
|
|
<details class="card" id="syslog-setup"{% if not sources and not offered %} open{% endif %}>
|
|
<summary class="section-title" style="cursor:pointer;">Point a device here</summary>
|
|
{% set port = listener.port %}
|
|
{% set proto = listener.protocols|join(" or ") or "UDP or TCP" %}
|
|
{% set to = server or "ADDRESS" %}
|
|
<p style="margin:0.75rem 0 0.5rem;">In the device's remote syslog settings, send to {% if server %}<span class="mono">{{ server }}</span>{% else %}the IP address of a Docker host that publishes the port (<span class="mono">ADDRESS</span> below; set it under Settings → Plugins → Syslog, "Send to"){% endif %}, port <span class="mono">{{ port }}</span>, {{ proto }}. Either syslog format is read: the BSD one (RFC 3164) or the IETF one (RFC 5424), which is better where a device offers the choice, as its lines carry their zone. Once a device sends, it appears above as sending, and Add keeps its lines.</p>
|
|
<table class="table" style="width:100%;font-size:0.82rem;">
|
|
<thead><tr><th>Device</th><th>Where</th></tr></thead>
|
|
<tbody>
|
|
<tr><td>Synology (DSM)</td><td>Log Center → Log Sending: send logs to a syslog server, with the server, port and protocol. Choose the IETF format if it is offered.</td></tr>
|
|
<tr><td>ASUSTOR (ADM)</td><td>In ADM's system log settings, send logs to a syslog server, with the server, port and protocol. ADM offers no format choice; Steward reads the BSD format it sends.</td></tr>
|
|
<tr><td>QNAP (QTS)</td><td>QuLog Center → Log Sender: add a destination with the server, port, protocol and RFC 5424.</td></tr>
|
|
<tr><td>TrueNAS</td><td>System → Advanced Settings → Syslog: the remote syslog server as <span class="mono">{{ to }}:{{ port }}</span>, and its transport.</td></tr>
|
|
<tr><td>OpenWrt</td><td>System → System → Logging: the external system log server, its port and protocol.</td></tr>
|
|
<tr><td>UniFi</td><td>Network settings, under remote logging (named Activity Logging or SIEM server in newer versions): the server and port.</td></tr>
|
|
<tr><td>Linux (rsyslog)</td><td><span class="mono">*.* @{{ to }}:{{ port }}</span> (UDP) or <span class="mono">*.* @@{{ to }}:{{ port }}</span> (TCP) in <span class="mono">/etc/rsyslog.d/90-steward.conf</span>, then restart rsyslog.</td></tr>
|
|
</tbody>
|
|
</table>
|
|
<h3 class="section-title" style="margin:1rem 0 0.4rem;font-size:0.9rem;">Nothing arriving?</h3>
|
|
<ul class="field-hint" style="margin:0;padding-left:1.1rem;">
|
|
<li>Steward's service must publish port {{ port }}, for UDP and TCP both. In Docker Swarm, either way works:
|
|
<ul style="padding-left:1.1rem;">
|
|
<li><strong>Ingress</strong> (Swarm's default; Steward can move between nodes): send to any node's address, or an address shared over the nodes. Every line then arrives from Docker's own address, so Steward tells the devices apart by the hostname in their lines; a device must send one.</li>
|
|
<li><strong>Host mode</strong> (add <span class="mono">mode: host</span>): each device keeps its own address, but only the node running Steward answers, so send to that node.</li>
|
|
</ul>
|
|
</li>
|
|
<li>A firewall on the server, or between the device's network and the server's, may drop it.</li>
|
|
<li>Ports below 1024, such as syslog's usual 514, can't be opened by Steward. Map 514 to {{ port }} where the port is published, or point devices at {{ port }}.</li>
|
|
</ul>
|
|
<pre class="mono" style="margin:0.6rem 0 0;font-size:0.78rem;white-space:pre-wrap;">ports:
|
|
- { target: {{ port }}, published: {{ port }}, protocol: udp }
|
|
- { target: {{ port }}, published: {{ port }}, protocol: tcp }</pre>
|
|
</details>
|
|
|
|
{% if offered %}
|
|
<section class="card" aria-labelledby="syslog-offered">
|
|
<h2 id="syslog-offered" class="section-title">Sending, not added</h2>
|
|
<p class="field-hint" style="margin:0 0 0.75rem;">These devices are sending syslog. Their lines are counted and not kept until you add them.</p>
|
|
<table class="table" style="width:100%;font-size:0.82rem;">
|
|
<thead><tr><th>Sender</th><th>Lines</th><th>Last heard</th><th>Newest line</th>{% if admin %}<th></th>{% endif %}</tr></thead>
|
|
<tbody>
|
|
{% for s in offered %}
|
|
<tr>
|
|
<td>{% if s.sender %}<span class="mono">{{ s.sender }}</span><div class="field-hint">{% if s.device_address %}<span class="mono">{{ s.device_address }}</span>, {% endif %}through Docker's ingress</div>{% else %}<span class="mono">{{ s.address }}</span>{% if s.hostname_seen %}<div class="field-hint">calls itself {{ s.hostname_seen }}</div>{% endif %}{% if s.docker_address %}<div class="tone-warn" style="font-size:0.78rem;">Docker's address, not the device's</div>{% endif %}{% endif %}</td>
|
|
<td>{{ "{:,}".format(s.lines_total) }}</td>
|
|
<td>{{ s.last_seen_at|ago }}</td>
|
|
<td class="mono" style="max-width:28rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="{{ s.last_line }}"><span class="{{ tone(s.level) }}">{{ s.severity or "" }}</span> {{ s.last_line or "—" }}</td>
|
|
{% if admin %}
|
|
<td style="white-space:nowrap;">
|
|
<form method="post" action="/plugins/syslog/sources/{{ s.id }}/add" style="display:inline-flex;gap:0.4rem;align-items:center;margin:0;">{{ csrf_field() }}
|
|
<input class="input" name="name" aria-label="Name for {{ s.sender or s.address }}" maxlength="100" value="{{ s.hostname_seen or '' }}" placeholder="Name" style="width:9rem;">
|
|
<select class="input" name="host_id" aria-label="Host for {{ s.sender or s.address }}" style="width:9rem;">
|
|
<option value="">No host</option>
|
|
{% for h in hosts %}<option value="{{ h.id }}"{% if s.suggested_host == h.id %} selected{% endif %}>{{ h.name }}</option>{% endfor %}
|
|
</select>
|
|
<button type="submit" class="btn btn-sm">Add</button>
|
|
</form>
|
|
<form method="post" action="/plugins/syslog/sources/{{ s.id }}/ignore" style="display:inline;margin:0;">{{ csrf_field() }}
|
|
<button type="submit" class="btn btn-sm btn-ghost">Ignore</button>
|
|
</form>
|
|
</td>
|
|
{% endif %}
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</section>
|
|
{% endif %}
|
|
|
|
<section class="card" aria-labelledby="syslog-sources">
|
|
<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 %}
|
|
<table class="table" style="width:100%;font-size:0.82rem;">
|
|
<thead><tr><th>Source</th><th>Host</th><th class="num" title="Lines kept in the last hour">Last hour</th><th>Last heard</th><th>Newest line</th>{% if admin %}<th></th>{% endif %}</tr></thead>
|
|
<tbody>
|
|
{% for s in sources %}
|
|
<tr>
|
|
<td><a href="/plugins/syslog/sources/{{ s.id }}">{{ s.name }}</a><div class="field-hint mono">{{ s.device_address or s.address }}</div>{% if s.sender %}<div class="field-hint">through Docker's ingress</div>{% elif s.docker_address %}<div class="tone-warn" style="font-size:0.78rem;">Docker's address, not the device's</div>{% endif %}</td>
|
|
<td>{% if s.host %}<a href="/hosts/{{ s.host.id }}">{{ s.host.name }}</a>{% else %}—{% endif %}</td>
|
|
<td class="num">{{ "{:,}".format(s.rate or 0) }}</td>
|
|
<td>{{ s.last_seen_at|ago }}</td>
|
|
<td class="mono" style="max-width:28rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="{{ s.last_line }}"><span class="{{ tone(s.level) }}">{{ s.severity or "" }}</span> {{ s.last_line or "—" }}</td>
|
|
{% if admin %}<td><a href="/plugins/syslog/sources/{{ s.id }}/edit" class="btn btn-sm btn-ghost">Edit</a></td>{% endif %}
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% endif %}
|
|
</section>
|
|
|
|
{% if ignored %}
|
|
<details class="card">
|
|
<summary class="section-title" style="cursor:pointer;">Ignored ({{ ignored|length }})</summary>
|
|
<table class="table" style="width:100%;font-size:0.82rem;margin-top:0.75rem;">
|
|
<thead><tr><th>Sender</th><th>Lines</th><th>Last heard</th>{% if admin %}<th></th>{% endif %}</tr></thead>
|
|
<tbody>
|
|
{% for s in ignored %}
|
|
<tr>
|
|
<td><span class="mono">{{ s.sender or s.address }}</span>{% if s.sender %}<div class="field-hint">{% if s.device_address %}<span class="mono">{{ s.device_address }}</span>, {% endif %}through Docker's ingress</div>{% elif s.hostname_seen %}<div class="field-hint">calls itself {{ s.hostname_seen }}</div>{% endif %}</td>
|
|
<td>{{ "{:,}".format(s.lines_total) }}</td>
|
|
<td>{{ s.last_seen_at|ago }}</td>
|
|
{% if admin %}<td><form method="post" action="/plugins/syslog/sources/{{ s.id }}/offer" style="margin:0;">{{ csrf_field() }}<button type="submit" class="btn btn-sm btn-ghost">Offer again</button></form></td>{% endif %}
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</details>
|
|
{% endif %}
|
|
{% endblock %}
|