A supply nobody could read is unknown, not empty
This commit is contained in:
@@ -208,18 +208,64 @@ class ZabbixService:
|
||||
continue
|
||||
if str(item.get("state", "0")) != "0":
|
||||
continue
|
||||
try:
|
||||
level = int(float(item.get("lastvalue", 0)))
|
||||
except (ValueError, TypeError):
|
||||
level = 0
|
||||
level, lastseen = self._readlevel(item)
|
||||
supplies.append({
|
||||
"name": item.get("name", "Unknown"),
|
||||
"level": level,
|
||||
"lastseen": lastseen,
|
||||
"color": self._extract_color(item),
|
||||
"itemid": item.get("itemid"),
|
||||
})
|
||||
return supplies
|
||||
|
||||
# How old the newest reading may be and still be presented as the level.
|
||||
# Generous on purpose: supply items are polled in hours, not seconds, and
|
||||
# calling a slow poller "unknown" would be its own lie. This exists for the
|
||||
# host that stopped answering, not for one that answers rarely.
|
||||
STALE_AFTER_SECONDS = 24 * 3600
|
||||
|
||||
@classmethod
|
||||
def _readlevel(cls, item):
|
||||
"""(level, lastseen) for one supply item; level is None when unknown.
|
||||
|
||||
An item that has never collected returns lastvalue as an EMPTY STRING,
|
||||
and one whose host went quiet keeps a lastvalue that is arbitrarily old.
|
||||
Both used to become 0 - the empty string because float('') raises and
|
||||
the handler substituted zero, the stale one because nothing ever read
|
||||
lastclock, which was fetched and then ignored.
|
||||
|
||||
Zero is not a safe stand-in for "no reading". It is a legitimate value
|
||||
meaning the cartridge is spent, so the substitution was indistinguishable
|
||||
from a real empty - and it hit every item on the host at once, which is
|
||||
what a printer that has gone dark looks like. Downstream that reached
|
||||
`level <= EMPTY_LEVEL`, so the forecast gave it daysleft 0 and the order
|
||||
list asked purchasing to buy a full set of cartridges for a printer
|
||||
nobody had measured.
|
||||
|
||||
None says "unknown" and every consumer already handles it: the detail
|
||||
page renders N/A, the alert loop skips it, and analyse() falls back to
|
||||
the last real level in history rather than inventing one.
|
||||
"""
|
||||
raw = item.get("lastvalue")
|
||||
if raw is None or str(raw).strip() == "":
|
||||
return None, None
|
||||
try:
|
||||
level = int(float(raw))
|
||||
except (ValueError, TypeError):
|
||||
return None, None
|
||||
|
||||
try:
|
||||
lastseen = int(item.get("lastclock") or 0)
|
||||
except (ValueError, TypeError):
|
||||
lastseen = 0
|
||||
if lastseen <= 0:
|
||||
# A value with no clock cannot be aged. Trust the value and say the
|
||||
# age is unknown rather than discarding a reading that may be fine.
|
||||
return level, None
|
||||
if time.time() - lastseen > cls.STALE_AFTER_SECONDS:
|
||||
return None, lastseen
|
||||
return level, lastseen
|
||||
|
||||
# Per item, per call. Zabbix applies `limit` to the WHOLE result set, not
|
||||
# per item, so the budget is multiplied by the number of items asked for.
|
||||
HISTORY_PER_ITEM = 2000
|
||||
|
||||
Reference in New Issue
Block a user