// src/services/voiceDiag/checks/wan/wanAlarms.js // // Recent Prisma alarms surface. Not a threshold check — the // severity is a direct pass-through of what Prisma raised: // // - critical → error // - major → warn // - minor → info-only (stays 'ok', but details show the count) // // The window matches whatever `collectSdwanForStore` used — same // value the /voicediag "WAN window" banner shows. An "active // incident" that Prisma sees is materially more urgent than a // snapshot LQM anomaly, so this deliberately runs even when the // per-metric checks are all green — a critical alarm 45 min ago // is still the operator's problem. // // The message field DOES NOT dump raw JSON. Prisma populates each // alarm's `info` field with a nested object (e.g. `{ vpn_reasons: // [{ code: 'NETWORK_VPNLINK_DOWN', vpnlink_id: '...' }, …] }`) that // is useful to a person reading the Prisma UI but noise in a chat // message. Instead we roll samples up by (code + severity), humanize // the code, and mention the newest occurrence with an "N min ago" // style relative timestamp. // // When alarms fire but every physical Link State check is green, // the message calls out the mismatch and explains it (usually the // overlay/VPN mesh is flapping while the physical circuits stay up). // This addresses the "looks green but 20 alarms" cognitive dissonance // operators would otherwise have to reason through themselves. import { maybeSkippedByKillSwitch } from './_helpers.js'; import { categorizeAlarm, humanizeAlarmCode, rollupAlarms, countByCategory, humanizeAge, humanizeWindow, } from '../../../enrichment/alarmSemantics.js'; export const WAN_ALARMS_STANDARDS = Object.freeze({ criticalAllowed: 0, majorAllowed: 0, }); export const wanAlarmsCheck = { id: 'wanAlarms', // Label is intentionally generic (no window suffix) — the effective // window is dynamic (comes from ctx.sdwanData.window.alarmMinutes) // and gets baked into the message rather than the label, since // labels are static across the process lifetime. label: 'SD-WAN Alarms', requires: ['sdwanSite'], scope: null, standards: WAN_ALARMS_STANDARDS, async run(ctx) { const skip = maybeSkippedByKillSwitch(wanAlarmsCheck); if (skip) return skip; const alarms = ctx.sdwanData?.alarms; if (!alarms) { return { status: 'skipped', message: 'Alarms feed not available from Prisma (partial fetch).', details: null, remediation: null, }; } const { critical = 0, major = 0, minor = 0 } = alarms.last1h || {}; const samples = Array.isArray(alarms.samples) ? alarms.samples : []; const alarmWindowMin = ctx.sdwanData?.window?.alarmMinutes || 60; const windowLabel = humanizeWindow(alarmWindowMin); // Roll up samples so 20 identical NETWORK_ANYNETLINK_DOWN alarms // become 1 line with "×20" — same shape the WAN follow-up renderer // uses, kept consistent by pulling the helper from a shared module. const rollups = rollupAlarms(samples); const byCategory = countByCategory(samples); // Physical vs overlay heuristic. Only ever a HINT — a store where // Link State also reports paths down doesn't need us telling them // "physical is up". const linksAllUp = allPhysicalLinksUp(ctx?.sdwanData?.links); const details = { critical, major, minor, window: windowLabel, byCategory, recentSamples: rollups.slice(0, 3).map((r) => ({ code: r.code, humanized: humanizeAlarmCode(r.code), category: categorizeAlarm(r.code), severity: r.severity, count: r.count, age: humanizeAge(r.newestTs), ts: r.newestTs, })), }; if (critical > 0) { return { status: 'error', message: buildMessage({ count: critical, severityLabel: 'critical', windowLabel, rollups, byCategory, linksAllUp, }), details, remediation: null, }; } if (major > 0) { return { status: 'warn', message: buildMessage({ count: major, severityLabel: 'major', windowLabel, rollups, byCategory, linksAllUp, }), details, remediation: null, }; } if (minor > 0) { return { status: 'ok', message: `${minor} minor alarm${minor === 1 ? '' : 's'} in the last ` + `${windowLabel}, no critical or major.`, details, remediation: null, }; } return { status: 'ok', message: `No alarms raised in the last ${windowLabel}.`, details, remediation: null, }; }, }; /** * Build the WARN / ERROR message body. Structure: * alarm(s) in last — most common: * (×N, latest min ago). . */ function buildMessage({ count, severityLabel, windowLabel, rollups, byCategory, linksAllUp, }) { // Top rollup entry drives the human summary. Preserves severity // ordering (critical > major > minor) so the "most notable" alarm // is what we cite even if a lower-severity code has a higher count. const top = rollups[0]; const parts = [ `${count} ${severityLabel} alarm${count === 1 ? '' : 's'} in the last ${windowLabel}`, ]; if (top) { const label = humanizeAlarmCode(top.code); const suffix = []; if (top.count > 1) suffix.push(`×${top.count}`); if (top.newestTs) suffix.push(`latest ${humanizeAge(top.newestTs)}`); const suffixStr = suffix.length > 0 ? ` (${suffix.join(', ')})` : ''; parts.push(` — most common: ${label}${suffixStr}`); } // Physical-vs-overlay clarifier — the most common source of // "looks green but 20 alarms" confusion. Only emit when we // can confidently distinguish the two cases. const overlayHeavy = byCategory.overlay > 0 && byCategory.overlay >= byCategory.physical; if (overlayHeavy && linksAllUp === true) { parts.push( '. These affect SD-WAN overlay/VPN tunnels between sites; ' + 'physical WAN paths are all up per the Link State check.', ); } else if (byCategory.physical > 0 && linksAllUp === false) { parts.push( '. Physical WAN interfaces are affected — see the Link State ' + 'check for the impacted path(s).', ); } else if (byCategory.device > 0) { parts.push('. Device / ION appliance affected.'); } else { parts.push('.'); } return parts.join(''); } /** * True iff every physical link on the site reports up. Returns * `null` when the link array is missing or empty so the caller can * decide whether to emit the physical-vs-overlay hint at all. */ function allPhysicalLinksUp(links) { if (!Array.isArray(links) || links.length === 0) return null; let seen = 0; for (const l of links) { // Unknown state → treat as "cannot confirm" and short-circuit // rather than lie about the state. if (l?.up === null || l?.up === undefined) continue; seen += 1; if (l.up === false) return false; } return seen > 0 ? true : null; }