feat(syslog): devices through Swarm's ingress told apart by name; the send-to address is a setting (#6096)
CI / lint (push) Successful in 2s
CI / unit (push) Failing after 1m30s
CI / integration (push) Successful in 1m38s
CI / publish (push) Skipped

Through ingress every device arrives from Docker's own address, so a
source there is keyed by the hostname its lines carry (new column
sender, unique with address). The set-up help no longer guesses the
address from the web hostname: it shows the plugin's send_to setting,
covers ingress and host mode, ASUSTOR ADM, and both syslog formats.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 12:05:28 -04:00
co-authored by Claude Opus 5.5
parent d2926b6663
commit 0a647bbb82
11 changed files with 193 additions and 53 deletions
+35
View File
@@ -71,6 +71,41 @@ def test_an_unknown_sender_is_offered_and_an_added_one_kept(app):
asyncio.run(go())
def test_devices_through_dockers_address_are_told_apart_by_name(app, monkeypatch):
"""Through Swarm's ingress every device arrives from Docker's address
(#6096): there each name its lines carry is its own source."""
from sqlalchemy import delete, select
SyslogSource = _mod("models").SyslogSource
receiver = _mod("receiver")
parse = _mod("parse").parse
monkeypatch.setattr(receiver, "on_own_network", lambda address: address.startswith("10.0.0."))
now = datetime(2026, 10, 19, 3, 0, tzinfo=timezone.utc)
def line(address, host, text):
return receiver.Received(now, address, parse(f"<14>Oct 19 01:12:03 {host} kernel: {text}"))
async def go():
async with app.db_sessionmaker() as s:
async with s.begin():
await s.execute(delete(SyslogSource))
await receiver.store_lines(s, [line("10.0.0.2", "SynNAS", "a"), line("10.0.0.2", "AsusNAS", "b"),
line("10.0.0.2", "SynNAS", "c")])
async with app.db_sessionmaker() as s:
async with s.begin():
await receiver.store_lines(s, [line("10.0.0.2", "AsusNAS", "d"), line("10.9.0.1", "renamed", "e")])
async with app.db_sessionmaker() as s:
rows = (await s.execute(select(SyslogSource))).scalars().all()
got = sorted((r.address, r.sender, r.lines_total, r.last_line) for r in rows)
assert got == [("10.0.0.2", "AsusNAS", 2, "d"), ("10.0.0.2", "SynNAS", 2, "c"),
("10.9.0.1", "", 1, "e")]
async with app.db_sessionmaker() as s:
async with s.begin():
await s.execute(delete(SyslogSource))
asyncio.run(go())
def test_the_ring_keeps_each_source_within_its_window_and_cap(app):
from sqlalchemy import delete, select