@daemonitor/plugins 1.1.10 → 1.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -204,19 +204,20 @@ async function snmpWalkIndexed(t, oid, timeout = 10000) {
204
204
  async function collectQnap(t) {
205
205
  const tempWarn = t.tempWarnC ?? 70;
206
206
  const volWarn = t.volumeWarnPct ?? 90;
207
- const [model, cpuTemp, sysTemp, cpuLoads, storDescrs, memTotal, memFree, fanSpeeds] = await Promise.all([
208
- snmpGet(t, QNAP.model),
209
- snmpGet(t, QNAP.cpuTemp),
210
- snmpGet(t, QNAP.sysTemp),
211
- snmpWalk(t, "1.3.6.1.2.1.25.3.3.1.2"), // hrProcessorLoad (per core)
212
- snmpWalkIndexed(t, "1.3.6.1.2.1.25.2.3.1.3"), // hrStorageDescr
213
- snmpGet(t, QNAP.memTotal),
214
- snmpGet(t, QNAP.memFree),
215
- snmpWalk(t, "1.3.6.1.4.1.24681.1.2.15.1.3"), // SystemFanSpeed table
207
+ // The light scalar gets can run together, but the WALKS must be SERIALIZED:
208
+ // QNAP's SNMP agent silently drops rows (and whole tables) under concurrent
209
+ // sessions, producing partial reads that flip a failing NAS back to a false
210
+ // "healthy". 5-min cadence — sequential latency is fine.
211
+ const [model, cpuTemp, sysTemp, memTotal, memFree] = await Promise.all([
212
+ snmpGet(t, QNAP.model), snmpGet(t, QNAP.cpuTemp), snmpGet(t, QNAP.sysTemp),
213
+ snmpGet(t, QNAP.memTotal), snmpGet(t, QNAP.memFree),
216
214
  ]);
217
- if (model == null && cpuTemp == null && !cpuLoads.length) {
215
+ if (model == null && cpuTemp == null) {
218
216
  return { name: t.name, ip: t.ip, kind: "qnap", reachable: false };
219
217
  }
218
+ const cpuLoads = await snmpWalk(t, "1.3.6.1.2.1.25.3.3.1.2"); // hrProcessorLoad (per core)
219
+ const storDescrs = await snmpWalkIndexed(t, "1.3.6.1.2.1.25.2.3.1.3"); // hrStorageDescr
220
+ const fanSpeeds = await snmpWalk(t, "1.3.6.1.4.1.24681.1.2.15.1.3"); // SystemFanSpeed table
220
221
  // CPU: average per-core load.
221
222
  const cores = cpuLoads.map(Number).filter((n) => !isNaN(n));
222
223
  const cpuPct = cores.length ? Math.round(cores.reduce((a, b) => a + b, 0) / cores.length) : undefined;
@@ -233,10 +234,14 @@ async function collectQnap(t) {
233
234
  // Presence keys on MODEL/CAPACITY ("--" = empty bay), so a FAILING disk
234
235
  // (unreadable temp, negative status) is still reported, not mistaken for empty.
235
236
  const DT = "1.3.6.1.4.1.24681.1.2.11.1."; // disk table columns: 2=descr 3=temp 4=status 5=model 6=capacity 7=smart
236
- const [wStatus, wTemp, wSmart, wDescr, wModel, wCap] = await Promise.all([
237
- snmpWalkIndexed(t, DT + "4"), snmpWalkIndexed(t, DT + "3"), snmpWalkIndexed(t, DT + "7"),
238
- snmpWalkIndexed(t, DT + "2"), snmpWalkIndexed(t, DT + "5"), snmpWalkIndexed(t, DT + "6"),
239
- ]);
237
+ // Serialized (not Promise.all) — concurrent column walks are what make the
238
+ // agent drop rows.
239
+ const wStatus = await snmpWalkIndexed(t, DT + "4");
240
+ const wTemp = await snmpWalkIndexed(t, DT + "3");
241
+ const wSmart = await snmpWalkIndexed(t, DT + "7");
242
+ const wDescr = await snmpWalkIndexed(t, DT + "2");
243
+ const wModel = await snmpWalkIndexed(t, DT + "5");
244
+ const wCap = await snmpWalkIndexed(t, DT + "6");
240
245
  const col = (rows) => {
241
246
  const m = {};
242
247
  for (const r of rows)
@@ -257,6 +262,12 @@ async function collectQnap(t) {
257
262
  const tempC = parseC(cTp[i] || null);
258
263
  disks.push({ slot: Number(i), descr: cDs[i] || undefined, model, capacity, tempC, status, smart: cSm[i] && cSm[i] !== "--" ? cSm[i] : undefined });
259
264
  }
265
+ // A reachable QNAP always has ≥1 disk. Zero means the disk-table walks came
266
+ // back empty (agent timed out) — treat as an INCOMPLETE read, not a clean
267
+ // bill of health, so a real disk error isn't falsely cleared to green.
268
+ if (!disks.length) {
269
+ return { name: t.name, ip: t.ip, kind: "qnap", reachable: true, error: "SNMP read incomplete — disk table empty" };
270
+ }
260
271
  // A present disk is bad only on a real error status, an explicitly-bad SMART
261
272
  // verdict, or overheating — not a blank "--" or an empty bay.
262
273
  const badDisks = disks.filter((d) => (d.status != null && d.status !== 0) ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daemonitor/plugins",
3
- "version": "1.1.10",
3
+ "version": "1.1.11",
4
4
  "description": "daemonitor plugins",
5
5
  "types": "types/index.d.ts",
6
6
  "main": "dist/index.js",