feat(syslog): devices through Swarm's ingress told apart by name; the send-to address is a setting (#6096)
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:
+32
-12
@@ -27,6 +27,7 @@ from sqlalchemy import func, select
|
||||
|
||||
from steward.core.scheduler import ScheduledTask
|
||||
|
||||
from .network import on_own_network
|
||||
from .parse import MAX_MESSAGE, Message, frames, parse
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -128,6 +129,14 @@ def settings(app: "Quart") -> tuple[int, bool, bool]:
|
||||
return int(cfg.get("port", 5514)), bool(cfg.get("udp", True)), bool(cfg.get("tcp", True))
|
||||
|
||||
|
||||
def send_to(app: "Quart") -> str:
|
||||
"""The address the set-up help tells devices to send to, as set in the
|
||||
plugin's settings: the web address Steward is reached by is often a name
|
||||
only its proxy answers, so it isn't guessed from that."""
|
||||
cfg = (app.config.get("PLUGINS") or {}).get("syslog") or {}
|
||||
return str(cfg.get("send_to") or "").strip()
|
||||
|
||||
|
||||
async def close() -> None:
|
||||
if LISTENER._udp_transport is not None:
|
||||
LISTENER._udp_transport.close()
|
||||
@@ -218,32 +227,43 @@ async def _store(app: "Quart", rounds: list[list[Received]]) -> None:
|
||||
await store_lines(session, [line for lines in rounds for line in lines])
|
||||
|
||||
|
||||
def sender_of(line: Received) -> tuple[str, str]:
|
||||
"""(address, sender) a line is filed under. A line from Docker's own
|
||||
address (a port published on Swarm's ingress) is any device's, so the
|
||||
name it carries tells the device; elsewhere the address alone does, and
|
||||
a device renamed stays one source."""
|
||||
if on_own_network(line.address):
|
||||
return line.address, line.message.hostname[:255]
|
||||
return line.address, ""
|
||||
|
||||
|
||||
async def store_lines(session, lines: list[Received]) -> None:
|
||||
from .models import SyslogLine, SyslogSource
|
||||
|
||||
if not lines:
|
||||
return
|
||||
by_address: dict[str, list[Received]] = {}
|
||||
by_sender: dict[tuple[str, str], list[Received]] = {}
|
||||
for line in lines:
|
||||
by_address.setdefault(line.address, []).append(line)
|
||||
sources = {s.address: s for s in (await session.execute(
|
||||
select(SyslogSource).where(SyslogSource.address.in_(list(by_address))))).scalars()}
|
||||
new = [a for a in by_address if a not in sources]
|
||||
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]
|
||||
if new:
|
||||
known = (await session.execute(select(func.count()).select_from(SyslogSource))).scalar_one()
|
||||
for address in new:
|
||||
for key in new:
|
||||
if known >= MAX_SENDERS:
|
||||
LISTENER.unoffered += len(by_address.pop(address))
|
||||
LISTENER.unoffered += len(by_sender.pop(key))
|
||||
continue
|
||||
first = by_address[address][0]
|
||||
source = SyslogSource(address=address, first_seen_at=first.at, last_seen_at=first.at)
|
||||
first = by_sender[key][0]
|
||||
source = SyslogSource(address=key[0], sender=key[1], first_seen_at=first.at, last_seen_at=first.at)
|
||||
session.add(source)
|
||||
sources[address] = source
|
||||
sources[key] = source
|
||||
known += 1
|
||||
await session.flush()
|
||||
|
||||
for address, received in by_address.items():
|
||||
source = sources[address]
|
||||
for key, received in by_sender.items():
|
||||
source = sources[key]
|
||||
newest = received[-1]
|
||||
source.lines_total = (source.lines_total or 0) + len(received)
|
||||
source.last_seen_at = max(source.last_seen_at, newest.at) if source.last_seen_at else newest.at
|
||||
|
||||
Reference in New Issue
Block a user