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
+7 -4
View File
@@ -14,8 +14,8 @@ from steward.models.users import UserRole
from .models import SyslogLine, SyslogSource
from .parse import facility_name, severity_name
from .service import (AROUND_LINES, AROUND_SEVERITY, LineFilter, filtered, lines_between, place_for, rates,
severity_level, shown_on, source_name, sources_for)
from .service import (AROUND_LINES, AROUND_SEVERITY, LineFilter, addresses_by_name, device_address, filtered,
lines_between, place_for, rates, severity_level, shown_on, source_name, sources_for)
async def list_syslog_sources(state: str = "") -> dict:
@@ -28,6 +28,8 @@ async def list_syslog_sources(state: str = "") -> dict:
A device whose lines reach Steward through Docker's own address (a port
published on Swarm's ingress) is told apart by the name its lines carry:
`through_docker` is true and `address` is Docker's, shared with the others.
`device_address` is the device's own: set on the source, or found from the
SNMP device or host its name is (else the address its lines come from).
Each gives its address, the host it is shown on, lines received in all
(`lines_total`) and, for an added source, in the last hour, when it was
@@ -43,9 +45,10 @@ async def list_syslog_sources(state: str = "") -> dict:
sources = list((await db.execute(stmt)).scalars())
per_hour = await rates(db, [s.id for s in sources if s.state == "accepted"], now)
hosts = {h.id: h.name for h in (await db.execute(select(Host))).scalars()}
named = await addresses_by_name(db) if any(s.sender for s in sources) else None
return {"sources": [{
"source_id": s.id, "name": source_name(s), "address": s.address, "state": s.state,
"through_docker": bool(s.sender),
"through_docker": bool(s.sender), "device_address": device_address(s, named),
"host_id": s.host_id, "host_name": hosts.get(s.host_id), "calls_itself": s.hostname_seen or None,
"lines_total": s.lines_total, "lines_last_hour": per_hour.get(s.id, 0) if s.state == "accepted" else None,
"last_heard": iso(s.last_seen_at), "last_heard_age_s": age_s(s.last_seen_at, now),
@@ -99,7 +102,7 @@ async def search_syslog(source: str = "", host: str = "", text: str = "", severi
list_tool="list_syslog_sources", name=source_name)]
if host:
h = pick(host, list((await db.execute(select(Host))).scalars()), kind="host", list_tool="list_hosts")
chosen = sources_for(chosen, h.id, h.address)
chosen = sources_for(chosen, h.id, h.address, await addresses_by_name(db))
if not chosen:
return {"range": key, "matches": [], "returned": 0, "truncated": False,
"note": f"No added syslog source is shown on {h.name}."}