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:
+30
-13
@@ -23,7 +23,7 @@ from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy import func, or_, select
|
||||
|
||||
from steward.core.scheduler import ScheduledTask
|
||||
|
||||
@@ -246,21 +246,36 @@ async def store_lines(session, lines: list[Received]) -> None:
|
||||
for line in lines:
|
||||
by_sender.setdefault(sender_of(line), []).append(line)
|
||||
addresses = list({address for address, _ in by_sender})
|
||||
sources = {(s.address, s.sender): s for s in (await session.execute(
|
||||
select(SyslogSource).where(SyslogSource.address.in_(addresses)))).scalars()}
|
||||
new = [k for k in by_sender if k not in sources]
|
||||
for key in [k for k in new if k[1]]:
|
||||
# A row from before devices at Docker's address were told apart by
|
||||
# name, last seen under this one, is this device: it keeps its state
|
||||
# and name rather than a new row being offered beside it.
|
||||
before = sources.get((key[0], ""))
|
||||
if before is not None and before.hostname_seen == key[1]:
|
||||
before.sender = key[1]
|
||||
sources[key] = sources.pop((key[0], ""))
|
||||
new.remove(key)
|
||||
names = list({name for _, name in by_sender if name})
|
||||
rows = (await session.execute(select(SyslogSource).where(
|
||||
or_(SyslogSource.address.in_(addresses), SyslogSource.sender.in_(names))))).scalars().all()
|
||||
sources = {(s.address, s.sender): s for s in rows}
|
||||
by_name = {s.sender: s for s in rows if s.sender}
|
||||
new = []
|
||||
for key in by_sender:
|
||||
address, name = key
|
||||
if name in by_name:
|
||||
# Docker's address depends on the node Steward's task is on, so a
|
||||
# device is its name wherever it now arrives from.
|
||||
by_name[name].address = address
|
||||
sources[key] = by_name[name]
|
||||
elif key not in sources:
|
||||
# A row from before devices at Docker's address were told apart by
|
||||
# name, last seen under this one, is this device: it keeps its state
|
||||
# and name rather than a new row being offered beside it.
|
||||
before = sources.get((address, ""))
|
||||
if name and before is not None and before.hostname_seen == name:
|
||||
before.sender = name
|
||||
by_name[name] = sources[key] = sources.pop((address, ""))
|
||||
else:
|
||||
new.append(key)
|
||||
if new:
|
||||
known = (await session.execute(select(func.count()).select_from(SyslogSource))).scalar_one()
|
||||
for key in new:
|
||||
if key[1] in by_name: # the same name from a second address in this round
|
||||
by_name[key[1]].address = key[0]
|
||||
sources[key] = by_name[key[1]]
|
||||
continue
|
||||
if known >= MAX_SENDERS:
|
||||
LISTENER.unoffered += len(by_sender.pop(key))
|
||||
continue
|
||||
@@ -268,6 +283,8 @@ async def store_lines(session, lines: list[Received]) -> None:
|
||||
source = SyslogSource(address=key[0], sender=key[1], first_seen_at=first.at, last_seen_at=first.at)
|
||||
session.add(source)
|
||||
sources[key] = source
|
||||
if key[1]:
|
||||
by_name[key[1]] = source
|
||||
known += 1
|
||||
await session.flush()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user