feat(syslog): a device through Docker's address is placed by its name, or a device address set on it (#6096)
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 1m32s
CI / integration (push) Successful in 1m42s
CI / publish (push) Successful in 20s

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>
This commit is contained in:
2026-10-19 12:24:58 -04:00
co-authored by Claude Opus 5.5
parent 5ce3dfe69d
commit fcd58224de
14 changed files with 184 additions and 32 deletions
+15 -3
View File
@@ -77,6 +77,7 @@ class Polled:
missing: list = field(default_factory=list)
texts: dict = field(default_factory=dict)
values: list = field(default_factory=list) # (metric, value)
sys_name: str | None = None
def _snapshot(device) -> SimpleNamespace:
@@ -118,7 +119,7 @@ async def _do_poll(app: "Quart", poll=None, walk=None) -> None:
global RATES
from steward.core.holding import holding
from .poller import poll_device, walk_tables
from .poller import SYS_NAME, poll_device, walk_tables
from .readings import state_of, tables_for
from .service import RateKeeper, credentials, readings_for
@@ -139,7 +140,9 @@ async def _do_poll(app: "Quart", poll=None, walk=None) -> None:
scans[d.id] = await walk(d.address, d.port, creds, tables_for(d.presets or []))
found = scans.get(d.id)
asked[d.id] = readings_for(d, found.rows if found is not None and found.error is None else None)
return await poll(d.address, d.port, creds, [(r.oid, r.key) for r in _asks(asked[d.id])])
# The device's own name rides along in the same GET.
return await poll(d.address, d.port, creds,
[(r.oid, r.key) for r in _asks(asked[d.id])] + [(SYS_NAME, SYS_NAME_KEY)])
results = await asyncio.gather(*(one(d) for d in devices), return_exceptions=True)
@@ -151,7 +154,10 @@ async def _do_poll(app: "Quart", poll=None, walk=None) -> None:
polls.append(Polled(now, device.id, device.name, failed=f"SNMP poll failed: {result}"))
device.last_error = polls[-1].failed
continue
p = Polled(now, device.id, device.name, error=result.error, scan=scans.get(device.id),
sys_name = result.texts.pop(SYS_NAME_KEY, None)
if SYS_NAME_KEY in result.missing:
result.missing.remove(SYS_NAME_KEY)
p = Polled(now, device.id, device.name, error=result.error, scan=scans.get(device.id), sys_name=sys_name,
answered=bool(result.readings or result.texts), missing=list(result.missing))
if p.scan is not None and p.scan.error is None:
# Kept on the copy too, so a stall doesn't walk it again every poll.
@@ -195,6 +201,10 @@ async def _do_poll(app: "Quart", poll=None, walk=None) -> None:
await held.flush(STORE_WAIT_S)
# The key the device's sysName is asked under: never a reading's.
SYS_NAME_KEY = "_sys_name"
def _asks(readings: list) -> list:
"""What the device is asked for: each reading, or the inputs of one
worked out from others."""
@@ -224,6 +234,8 @@ async def _store(app: "Quart", rounds: list[list[Polled]]) -> None:
row.last_error = p.error
if p.answered:
row.last_ok_at = p.at
if p.sys_name:
row.sys_name = p.sys_name[:255]
# A failed poll keeps the words it had; one that answered says them afresh.
texts = dict(row.texts or {}) if p.error else {}
texts.update(p.texts)