@daemonitor/plugins 1.1.18 → 1.1.20

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.
@@ -28,6 +28,7 @@ const SYS = {
28
28
  name: "1.3.6.1.2.1.1.5.0", // sysName
29
29
  ifOper: "1.3.6.1.2.1.2.2.1.8", // ifOperStatus (walk): 1=up 2=down
30
30
  ifAdmin: "1.3.6.1.2.1.2.2.1.7", // ifAdminStatus (walk): 1=up (enabled)
31
+ ifName: "1.3.6.1.2.1.31.1.1.1.1", // ifName (walk) — eth0/eth1/itf0/...
31
32
  ifHCIn: "1.3.6.1.2.1.31.1.1.1.6", // ifHCInOctets (walk, Counter64)
32
33
  ifHCOut: "1.3.6.1.2.1.31.1.1.1.10", // ifHCOutOctets (walk, Counter64)
33
34
  };
@@ -45,9 +46,9 @@ const UCD = {
45
46
  memCached: "1.3.6.1.4.1.2021.4.15.0", // memCached (KB)
46
47
  hrProcLoad: "1.3.6.1.2.1.25.3.3.1.2", // hrProcessorLoad (walk, per-core %) — CPU fallback
47
48
  };
48
- // Per-target previous interface-octet sample, for throughput (bps) rate calc
49
- // across polls. Module-level: one collectDevice call per target per cycle.
50
- const netPrev = {};
49
+ // Previous cumulative octet counters PER INTERFACE (target ip -> ifIndex), for
50
+ // per-link throughput rates across polls.
51
+ const netPrevIf = {};
51
52
  // QNAP enterprise MIB (NAS-MIB, 1.3.6.1.4.1.24681.1.2). A full walk of this
52
53
  // subtree times out on QTS, so everything here is fetched by exact OID.
53
54
  const QNAP = {
@@ -167,20 +168,30 @@ async function collectDevice(t) {
167
168
  snmpGet(t, SYS.descr),
168
169
  snmpGet(t, SYS.name),
169
170
  snmpGet(t, SYS.uptime),
170
- snmpWalk(t, SYS.ifOper),
171
- snmpWalk(t, SYS.ifAdmin),
171
+ snmpWalkIndexed(t, SYS.ifOper),
172
+ snmpWalkIndexed(t, SYS.ifAdmin),
172
173
  ]);
173
174
  if (descr == null && name == null && !oper.length) {
174
175
  return { name: t.name, ip: t.ip, kind: t.kind || "device", reachable: false };
175
176
  }
177
+ // Index the link-state walks by ifIndex so each interface can carry its own
178
+ // up/down (positional zipping breaks whenever the two walks differ in length).
179
+ const byIndex = (rows) => {
180
+ const m = {};
181
+ for (const r of rows)
182
+ m[r.idx] = r.val;
183
+ return m;
184
+ };
185
+ const operByIdx = byIndex(oper);
186
+ const adminByIdx = byIndex(admin);
176
187
  // Only count ADMIN-enabled ports; an enabled port that's operationally down is
177
188
  // a real link-down (ignore the many disabled ports on a big switch).
178
189
  let portsTotal = 0;
179
190
  let portsUp = 0;
180
- for (let i = 0; i < oper.length; i++) {
181
- if (Number(admin[i]) === 1) {
191
+ for (const idx of Object.keys(adminByIdx)) {
192
+ if (Number(adminByIdx[idx]) === 1) {
182
193
  portsTotal++;
183
- if (Number(oper[i]) === 1)
194
+ if (Number(operByIdx[idx]) === 1)
184
195
  portsUp++;
185
196
  }
186
197
  }
@@ -189,10 +200,11 @@ async function collectDevice(t) {
189
200
  // CPU / RAM / load / network — best-effort UCD-SNMP + IF-MIB counters. Absent
190
201
  // on gear without the UCD MIB (returns "No Such Object" fast → null), so every
191
202
  // field is optional and simply omitted when unsupported.
192
- const [cpuIdle, load1, load5, load15, memTotal, memAvail, memBuffer, memCached, procLoads, hcIn, hcOut] = await Promise.all([
203
+ const [cpuIdle, load1, load5, load15, memTotal, memAvail, memBuffer, memCached, procLoads, ifNames, hcInIdx, hcOutIdx] = await Promise.all([
193
204
  snmpGet(t, UCD.cpuIdle), snmpGet(t, UCD.load1), snmpGet(t, UCD.load5), snmpGet(t, UCD.load15),
194
205
  snmpGet(t, UCD.memTotal), snmpGet(t, UCD.memAvail), snmpGet(t, UCD.memBuffer), snmpGet(t, UCD.memCached),
195
- snmpWalk(t, UCD.hrProcLoad), snmpWalk(t, SYS.ifHCIn), snmpWalk(t, SYS.ifHCOut),
206
+ snmpWalk(t, UCD.hrProcLoad), snmpWalkIndexed(t, SYS.ifName),
207
+ snmpWalkIndexed(t, SYS.ifHCIn), snmpWalkIndexed(t, SYS.ifHCOut),
196
208
  ]);
197
209
  // CPU %: prefer ssCpuIdle (100 - idle); fall back to avg per-core hrProcessorLoad.
198
210
  let cpuPct;
@@ -216,24 +228,55 @@ async function collectDevice(t) {
216
228
  memPct = Math.round((usedKb / mTot) * 100);
217
229
  memTotalMB = Math.round(mTot / 1024);
218
230
  }
219
- // Network throughput (aggregate bytes/sec, to match the OS plugin's NET
220
- // metric): delta of summed HC octets vs the prior poll. First poll (or a
221
- // counter reset) yields no rate.
222
- let netIn, netOut;
223
- const sumIn = hcIn.map(Number).filter((n) => !isNaN(n)).reduce((a, b) => a + b, 0);
224
- const sumOut = hcOut.map(Number).filter((n) => !isNaN(n)).reduce((a, b) => a + b, 0);
225
- if (hcIn.length || hcOut.length) {
226
- const now = Date.now();
227
- const prev = netPrev[t.ip];
228
- if (prev && sumIn >= prev.in && sumOut >= prev.out && now > prev.ts) {
229
- const dt = (now - prev.ts) / 1000;
231
+ // Network throughput, PER INTERFACE (bytes/sec).
232
+ //
233
+ // Summing every interface is meaningless on a router: a transiting packet is
234
+ // counted once arriving on the WAN and again leaving on the LAN, so
235
+ // sum(in) === sum(out) by construction — and vendors add aggregate pseudo
236
+ // interfaces (EdgeOS `itf0`) that mirror the whole load a second time. The
237
+ // observed effect was ~5x inflation with in/out identical to the eye.
238
+ //
239
+ // So: rate each interface separately, skip loopback/pseudo, and let the target
240
+ // name its WAN (`wanInterface`) to headline the figure that actually means
241
+ // "internet traffic". Without one, the headline is left undefined rather than
242
+ // reporting a number we know to be wrong.
243
+ const PSEUDO = /^(lo|imq\d*|itf\d*|ifb\d*|sit\d*|gre\d*|teql\d*|tunl\d*)$/i;
244
+ const nameByIdx = byIndex(ifNames);
245
+ const inByIdx = byIndex(hcInIdx);
246
+ const outByIdx = byIndex(hcOutIdx);
247
+ const now = Date.now();
248
+ const prevIf = netPrevIf[t.ip] || {};
249
+ const nextIf = {};
250
+ const interfaces = [];
251
+ for (const idx of Object.keys(inByIdx)) {
252
+ const name = (nameByIdx[idx] || `if${idx}`).replace(/^"|"$/g, "");
253
+ const cin = Number(inByIdx[idx]), cout = Number(outByIdx[idx]);
254
+ if (isNaN(cin) || isNaN(cout))
255
+ continue;
256
+ nextIf[idx] = { in: cin, out: cout, ts: now };
257
+ if (PSEUDO.test(name))
258
+ continue;
259
+ const p = prevIf[idx];
260
+ let inBps, outBps;
261
+ if (p && cin >= p.in && cout >= p.out && now > p.ts) {
262
+ const dt = (now - p.ts) / 1000;
230
263
  if (dt > 0) {
231
- netIn = Math.round((sumIn - prev.in) / dt);
232
- netOut = Math.round((sumOut - prev.out) / dt);
264
+ inBps = Math.round((cin - p.in) / dt);
265
+ outBps = Math.round((cout - p.out) / dt);
233
266
  }
234
267
  }
235
- netPrev[t.ip] = { in: sumIn, out: sumOut, ts: now };
268
+ // ifOperStatus 1=up, ifAdminStatus 1=enabled. A link that's admin-enabled but
269
+ // operationally down is a real fault; an admin-disabled port is just off.
270
+ const adminUp = adminByIdx[idx] != null ? Number(adminByIdx[idx]) === 1 : undefined;
271
+ const linkUp = operByIdx[idx] != null ? Number(operByIdx[idx]) === 1 : undefined;
272
+ interfaces.push({ name, inBps, outBps, up: linkUp, adminUp });
236
273
  }
274
+ netPrevIf[t.ip] = nextIf;
275
+ // Headline = the WAN link when the target names one.
276
+ const wanName = t.wanInterface;
277
+ const wan = wanName ? interfaces.find((i) => i.name === wanName) : undefined;
278
+ const netIn = wan?.inBps;
279
+ const netOut = wan?.outBps;
237
280
  return {
238
281
  name: t.name,
239
282
  ip: t.ip,
@@ -252,6 +295,11 @@ async function collectDevice(t) {
252
295
  memTotalMB,
253
296
  netIn,
254
297
  netOut,
298
+ wanInterface: wanName,
299
+ // Per-link rates (loopback/pseudo excluded), busiest first.
300
+ interfaces: interfaces
301
+ .slice()
302
+ .sort((a, b) => ((b.inBps || 0) + (b.outBps || 0)) - ((a.inBps || 0) + (a.outBps || 0))),
255
303
  };
256
304
  }
257
305
  // Walk that keeps each row's index (numeric OID suffix) — needed to resolve the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daemonitor/plugins",
3
- "version": "1.1.18",
3
+ "version": "1.1.20",
4
4
  "description": "daemonitor plugins",
5
5
  "types": "types/index.d.ts",
6
6
  "main": "dist/index.js",