/** * Built-in monitors — scheduling celilo's own audit checks. * * `celilo system audit` already implements fourteen checks that know how to * find a broken fleet; until now nothing ran them on a schedule or told anyone * what they found. A `builtin_check` monitor runs ONE of those categories and * projects its findings into alert keys, so the same machinery that carries a * module's failing health check carries "machine iot is unreachable". * * See openspec/changes/add-alerting/design.md (Gap 6 → D4). */ import type { AlertSeverity } from '../../db/schema'; import type { DriftCategory, DriftFinding, DriftSeverity } from '../audit/types'; import { type FailingKey, builtinAlertKey } from './keys'; /** * What kind of entity a category's findings are about. * * This is not cosmetic: suppression resolves a key's ancestors through the * deployment topology, and it can only do that if it knows whether * `builtin:…/x:iot` names a machine or a module. Categories absent from the map * are about the management system as a whole and have no narrower subject. */ const TARGET_KIND_BY_CATEGORY: Partial> = { machines_reachable: 'machine', // Subject is a hostname, which since celilo#1133 may be a machine OR a // celilo-provisioned container. A container's hostname finds no machine // ancestor, so its alert is never suppressed by one — which is the safe // direction: a full disk that pages when it did not have to beats one that // stays silent because an unrelated ancestor happened to be firing. disk_space: 'machine', // Subject is an FQDN, which is neither a machine nor a module: a served // name can outlive any single module that asked for it. public_dns: 'hostname', transport_reads: 'module', services_reachable: 'service', services_credentials: 'service', backups: 'module', abandoned_operations: 'module', health: 'module', module_versions: 'module', module_configs: 'module', undeployed_modules: 'module', unconfigured_modules: 'module', }; const SYSTEM_TARGET_KIND = 'system'; export function targetKindForCategory(category: DriftCategory): string { return TARGET_KIND_BY_CATEGORY[category] ?? SYSTEM_TARGET_KIND; } /** * Map a drift severity to the severity its alert carries. * * `todo` findings are next-step reminders ("you imported this and haven't * deployed it"), not divergence — they are recorded but never page, mirroring * how a `warn` health-check item behaves. `drift` and `blocked` take the * monitor's configured severity. */ export function severityForDriftSeverity( driftSeverity: DriftSeverity, monitorSeverity: AlertSeverity, ): AlertSeverity { return driftSeverity === 'todo' ? 'warning' : monitorSeverity; } /** * Project the findings of ONE audit category into the complete set of currently * failing keys for that monitor. * * Findings from other categories are ignored rather than silently folded in: a * `builtin_check` monitor owns exactly one category, and resolution works by * set difference over the keys a monitor reports. Letting a stray category's * findings through would make another monitor's alerts resolve at random. */ export function failingKeysFromFindings( category: DriftCategory, findings: DriftFinding[], monitorSeverity: AlertSeverity, ): FailingKey[] { const kind = targetKindForCategory(category); const failing: FailingKey[] = []; for (const finding of findings) { if (finding.category !== category) continue; failing.push({ key: builtinAlertKey(category, kind, finding.subject), severity: severityForDriftSeverity(finding.severity, monitorSeverity), message: finding.message, details: finding.details, }); } return failing; }