feat(syslog): lines beside the readings: around an anomaly, in get_anomaly, and marked on host and SNMP charts (#6002)
get_anomaly's circumstances gain the host's or SNMP device's syslog warnings and worse (mcp_events / mcp_resource_events). An anomaly row opens "Syslog around it", read only when opened. A shared Chart.js plugin (chart-marks.js) draws a dashed line at each syslog error on the host metrics charts and an SNMP device's charts, with a line saying how many. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -110,3 +110,59 @@ def sources_for(sources: list[SyslogSource], host_id: str | None, address: str |
|
||||
address = (address or "").strip().lower()
|
||||
return [s for s in sources if s.state == "accepted" and (
|
||||
s.host_id == host_id if s.host_id else bool(address) and s.address.lower() == address)]
|
||||
|
||||
|
||||
# ── Lines beside the readings: an anomaly's circumstances, a chart's marks (#6002)
|
||||
|
||||
# What an anomaly's circumstances take from the log: warnings and worse, so a
|
||||
# chatty device's info lines don't crowd out the line that explains it.
|
||||
AROUND_SEVERITY = 4
|
||||
AROUND_LINES = 200
|
||||
# What a chart marks: errors and worse. Past MAX_MARKS the newest are kept.
|
||||
MARK_SEVERITY = 3
|
||||
MAX_MARKS = 300
|
||||
|
||||
|
||||
def source_name(s: SyslogSource) -> str:
|
||||
return s.name or s.hostname_seen or s.address
|
||||
|
||||
|
||||
async def shown_on(db, host_id: str | None, address: str | None) -> list[SyslogSource]:
|
||||
"""The added sources a host's page (and its address) or a device's page
|
||||
shows; the host's own address when only the host is given."""
|
||||
from steward.models.hosts import Host
|
||||
if host_id and not address:
|
||||
host = await db.get(Host, host_id)
|
||||
address = host.address if host is not None else None
|
||||
accepted = list((await db.execute(select(SyslogSource).where(SyslogSource.state == "accepted"))).scalars())
|
||||
return sources_for(accepted, host_id, address)
|
||||
|
||||
|
||||
async def place_for(db, source: str, resource: str) -> tuple[str | None, str | None]:
|
||||
"""(host id, address) whose sources a metric's resource is shown with:
|
||||
an SNMP device's own binding and address, else the host the resource
|
||||
lives on, as get_anomaly finds it."""
|
||||
if source == "snmp":
|
||||
from steward.core.plugin_manager import loaded_plugin_submodule
|
||||
snmp = loaded_plugin_submodule("snmp", "models")
|
||||
if snmp is None:
|
||||
return None, None
|
||||
device = (await db.execute(select(snmp.SnmpDevice).where(
|
||||
snmp.SnmpDevice.name == resource).limit(1))).scalar_one_or_none()
|
||||
return (device.host_id, device.address) if device is not None else (None, None)
|
||||
from steward.mcp.tools._events import place_of
|
||||
place = await place_of(db, source, resource)
|
||||
return place.host_id, None
|
||||
|
||||
|
||||
async def lines_between(db, sources: list[SyslogSource], since: datetime, until: datetime,
|
||||
worst: int, limit: int) -> list[SyslogLine]:
|
||||
"""Lines of these sources received in [since, until] at `worst` or worse,
|
||||
newest first, at most `limit`."""
|
||||
if not sources:
|
||||
return []
|
||||
return list((await db.execute(
|
||||
select(SyslogLine).where(SyslogLine.source_id.in_([s.id for s in sources]),
|
||||
SyslogLine.received_at >= since, SyslogLine.received_at <= until,
|
||||
SyslogLine.severity <= worst)
|
||||
.order_by(SyslogLine.received_at.desc(), SyslogLine.id.desc()).limit(limit))).scalars())
|
||||
|
||||
Reference in New Issue
Block a user