fix(syslog): a device through Docker's address stays one source when that address changes (#6717)
Docker's ingress address depends on the node Steward's task is on, and sources were keyed by (address, sender). After a task move the same device arrived under a new key and became a new "offered" row, so its lines were counted, not kept. A named sender is now one row whatever address it last came from, enforced by a partial unique index. Migration syslog_005 folds rows sharing a name into the added one and moves their lines and patterns across. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -120,6 +120,52 @@ def test_devices_through_dockers_address_are_told_apart_by_name(app, monkeypatch
|
||||
asyncio.run(go())
|
||||
|
||||
|
||||
def test_a_device_stays_its_source_when_dockers_address_changes(app, monkeypatch):
|
||||
"""Docker's address depends on the node Steward's task is on: an added
|
||||
device keeps its row, its state and its lines after the task moves, even
|
||||
when a round carries both addresses."""
|
||||
from sqlalchemy import delete, select
|
||||
|
||||
SyslogLine, SyslogSource = _mod("models").SyslogLine, _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))
|
||||
s.add(SyslogSource(address="10.0.0.3", sender="SynNAS", state="accepted", name="Mine",
|
||||
first_seen_at=now, last_seen_at=now, lines_total=1))
|
||||
async with app.db_sessionmaker() as s:
|
||||
async with s.begin():
|
||||
await receiver.store_lines(s, [line("10.0.0.3", "SynNAS", "a"), line("10.0.0.4", "SynNAS", "b")])
|
||||
async with app.db_sessionmaker() as s:
|
||||
async with s.begin():
|
||||
await receiver.store_lines(s, [line("10.0.0.4", "SynNAS", "c")])
|
||||
async with app.db_sessionmaker() as s:
|
||||
src = (await s.execute(select(SyslogSource))).scalar_one()
|
||||
assert (src.address, src.state, src.name, src.lines_total) == ("10.0.0.4", "accepted", "Mine", 4)
|
||||
kept = (await s.execute(select(SyslogLine.message))).scalars().all()
|
||||
assert sorted(kept) == ["a", "b", "c"]
|
||||
# A name first heard from two addresses in one round is still one source.
|
||||
async with app.db_sessionmaker() as s:
|
||||
async with s.begin():
|
||||
await receiver.store_lines(s, [line("10.0.0.3", "AsusNAS", "d"), line("10.0.0.4", "AsusNAS", "e")])
|
||||
async with app.db_sessionmaker() as s:
|
||||
src = (await s.execute(select(SyslogSource).where(SyslogSource.sender == "AsusNAS"))).scalar_one()
|
||||
assert (src.address, src.state, src.lines_total) == ("10.0.0.4", "offered", 2)
|
||||
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user