feat(snmp): a NAS's disk and volume I/O, CPU waiting on disk, available memory, interface errors (#5997)
- Synology: what each disk and volume reads and writes, bytes and requests a second and the load DSM gives it (SYNOLOGY-STORAGEIO- and -SPACEIO-MIB, 64-bit byte counters), and each disk's reconnects and sectors not found. - Linux server: CPU waiting on disk, the wait tick counter's share of the CPU's, and memory available (free + buffers + cache). A reading may now be worked out from others: the poll asks for its inputs, rates each counter, and stores only the result. - Interface traffic: errors and discards in and out, per second. - byte_count unit: a byte counter shown as bytes a second. Asustor's MIB has nothing more for this; its Linux server readings gain the same CPU wait and available memory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -139,7 +139,7 @@ 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 asked[d.id]])
|
||||
return await poll(d.address, d.port, creds, [(r.oid, r.key) for r in _asks(asked[d.id])])
|
||||
|
||||
results = await asyncio.gather(*(one(d) for d in devices), return_exceptions=True)
|
||||
|
||||
@@ -158,23 +158,35 @@ async def _do_poll(app: "Quart", poll=None, walk=None) -> None:
|
||||
device.tables, device.tables_at = p.scan.rows, now
|
||||
device.last_error = result.error
|
||||
missing = set(result.missing)
|
||||
for reading in asked[device.id]:
|
||||
|
||||
def read(reading):
|
||||
value = result.readings.get(reading.key)
|
||||
if reading.unit == "state":
|
||||
# Its word is shown as answered; its level is stored,
|
||||
# unless Steward doesn't know the word (level None).
|
||||
raw = value if value is not None else result.texts.get(reading.key)
|
||||
if raw is None:
|
||||
continue
|
||||
return None
|
||||
p.texts[reading.key], value = state_of(reading, raw)
|
||||
elif value is None:
|
||||
return value
|
||||
if value is None:
|
||||
if reading.key in result.texts: # a word where a number was asked for
|
||||
missing.add(reading.key)
|
||||
continue
|
||||
elif reading.counter:
|
||||
value = RATES.rate(device.id, reading, value, now)
|
||||
return None
|
||||
if reading.counter:
|
||||
return RATES.rate(device.id, reading, value, now)
|
||||
return value * reading.scale + reading.offset
|
||||
|
||||
for reading in asked[device.id]:
|
||||
if reading.inputs:
|
||||
# Every input is read, so each counter keeps its last sample.
|
||||
got = {i.key: read(i) for i in reading.inputs}
|
||||
if any(i.key in missing for i in reading.inputs):
|
||||
missing -= {i.key for i in reading.inputs}
|
||||
missing.add(reading.key)
|
||||
value = None if None in got.values() else reading.combine(got)
|
||||
else:
|
||||
value = value * reading.scale + reading.offset
|
||||
value = read(reading)
|
||||
if value is not None:
|
||||
p.values.append((reading.key, value))
|
||||
p.missing = sorted(missing)
|
||||
@@ -183,6 +195,12 @@ async def _do_poll(app: "Quart", poll=None, walk=None) -> None:
|
||||
await held.flush(STORE_WAIT_S)
|
||||
|
||||
|
||||
def _asks(readings: list) -> list:
|
||||
"""What the device is asked for: each reading, or the inputs of one
|
||||
worked out from others."""
|
||||
return [i for r in readings for i in (r.inputs or (r,))]
|
||||
|
||||
|
||||
async def _store(app: "Quart", rounds: list[list[Polled]]) -> None:
|
||||
"""Write held polls, oldest first, in one transaction."""
|
||||
from steward.core.alerts import MetricBatch
|
||||
|
||||
Reference in New Issue
Block a user