fix(alerts): исключить неоднозначные и отключённые устройства из сигналов
This commit is contained in:
1 parent
65d4a17882
commit
8ffb8b5f00
11 files changed
+164
-14
No files matched your search
@@ -8,7 +8,7 @@
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "npm run build -w @mmapp/contracts && tsc",
|
||||
"test:alerts-telegram": "tsx src/services/alerts-outbox-retry.test.ts && tsx src/services/alerts-outbox-overview.test.ts && tsx src/services/alert-engine/outbox-execution.test.ts && tsx src/services/alerts-telegram-delivery.test.ts && tsx src/services/alerts-revision.test.ts && tsx src/services/alerts-overview.test.ts && tsx ../packages/contracts/src/alerts-normalization.test.ts && tsx src/services/alerts-config-execution.test.ts && tsx src/services/alerts-telegram-settings.test.ts && tsx src/services/alerts-telegram-send.test.ts && tsx src/routes/alerts.test.ts && tsx src/plugins/auth.alerts.test.ts",
|
||||
"test:alert-engine": "tsx src/services/alert-engine/groups.test.ts && tsx src/services/alert-engine/evaluate-rule.test.ts && tsx src/services/gre-bgp-snapshot-collector.test.ts && tsx src/services/gre-bgp-snapshot-history.test.ts && tsx src/services/gre-bgp-snapshot-sql.test.ts && tsx src/services/alert-engine/confirm-stability.test.ts",
|
||||
"test:alert-engine": "tsx src/services/alert-engine/server-signal-identity.test.ts && tsx src/services/alert-engine/groups.test.ts && tsx src/services/alert-engine/evaluate-rule.test.ts && tsx src/services/gre-bgp-snapshot-collector.test.ts && tsx src/services/gre-bgp-snapshot-history.test.ts && tsx src/services/gre-bgp-snapshot-sql.test.ts && tsx src/services/alert-engine/confirm-stability.test.ts",
|
||||
"start": "node dist/index.js",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import assert from "node:assert/strict"
|
||||
import { createServerSignalIdentityGuard, type AlertServerIdentity } from "./server-signal-identity.js"
|
||||
|
||||
const server = (id: number, name = "CHR", enabled = true): AlertServerIdentity => ({ id, name, host: `host-${id}`, enabled })
|
||||
const sample = (id: number, name = "CHR") => ({ serverId: id, name, host: `host-${id}`, status: "offline", sumRxMbps: 100 })
|
||||
|
||||
// A valid, uniquely named current device survives; disabled/deleted devices do not.
|
||||
const guard = createServerSignalIdentityGuard([server(1), server(2, "disabled", false)])
|
||||
assert.deepEqual(guard([sample(1), sample(2, "disabled"), sample(3, "deleted")]), [sample(1)])
|
||||
// Case/whitespace aliases of two enabled devices cannot mix history or traffic.
|
||||
for (const inventory of [[server(1), server(2, " chr ")], [server(2, " chr "), server(1)]]) {
|
||||
assert.deepEqual(createServerSignalIdentityGuard(inventory)([sample(1), sample(2, " chr ")]), [])
|
||||
}
|
||||
// Disabling the duplicate makes the remaining active device unambiguous.
|
||||
assert.deepEqual(createServerSignalIdentityGuard([server(1), server(2, " chr ", false)])([sample(1), sample(2, " chr ")]), [sample(1)])
|
||||
// Rename, host change and id reuse must not reassign old observations by display name.
|
||||
assert.deepEqual(guard([sample(3), { ...sample(1), host: "old-host" }, { ...sample(1), name: "old-name" }]), [])
|
||||
assert.deepEqual(createServerSignalIdentityGuard([server(1, "renamed")])([sample(1)]), [])
|
||||
assert.deepEqual(guard([sample(1), sample(1)]), [])
|
||||
assert.deepEqual(guard([{ ...sample(1), name: " chr " }]), [{ ...sample(1), name: " chr " }])
|
||||
assert.deepEqual(createServerSignalIdentityGuard([{ ...server(1), id: "1" }])([sample(1)]), [sample(1)])
|
||||
assert.deepEqual(createServerSignalIdentityGuard([{ ...server(1), id: "9007199254740993" }])([sample(1)]), [])
|
||||
// Filtering is immutable, and checks every run against the same current inventory.
|
||||
const rows = [sample(1), sample(2)]
|
||||
const before = JSON.stringify(rows)
|
||||
guard(rows)
|
||||
assert.equal(JSON.stringify(rows), before)
|
||||
console.log("server signal identity: current/disabled/deleted/ambiguous/renamed/host/duplicate/id/immutability passed")
|
||||
@@ -0,0 +1,36 @@
|
||||
export interface AlertServerIdentity {
|
||||
id: number | string
|
||||
name: string
|
||||
host: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
interface SnapshotServerIdentity { serverId: number; name: string; host: string }
|
||||
|
||||
const nameKey = (name: string) => name.trim().toLowerCase()
|
||||
|
||||
/** Rules currently address servers by name. Ambiguous names must never select an arbitrary device. */
|
||||
export function createServerSignalIdentityGuard(servers: AlertServerIdentity[]) {
|
||||
const active = servers.filter(s => s.enabled === true)
|
||||
const nameCounts = new Map<string, number>()
|
||||
for (const server of active) {
|
||||
const key = nameKey(server.name)
|
||||
nameCounts.set(key, (nameCounts.get(key) ?? 0) + 1)
|
||||
}
|
||||
const eligible = new Map<number, AlertServerIdentity>()
|
||||
for (const server of active) {
|
||||
const id = Number(server.id)
|
||||
const key = nameKey(server.name)
|
||||
if (Number.isSafeInteger(id) && id > 0 && key && nameCounts.get(key) === 1) eligible.set(id, server)
|
||||
}
|
||||
return <T extends SnapshotServerIdentity>(rows: T[]): T[] => {
|
||||
const counts = new Map<number, number>()
|
||||
for (const row of rows) if (row) counts.set(row.serverId, (counts.get(row.serverId) ?? 0) + 1)
|
||||
return rows.filter(row => {
|
||||
if (!row || counts.get(row.serverId) !== 1) return false
|
||||
const server = eligible.get(row.serverId)
|
||||
return server != null && typeof row.name === "string" && typeof row.host === "string"
|
||||
&& nameKey(row.name) === nameKey(server.name) && row.host.trim() === server.host.trim()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { dbAll } from "../../db/index.js"
|
||||
import { collectBgpSignalsFromRuns } from "./bgp-signals.js"
|
||||
import { collectGreSignalsFromRuns } from "./gre-signals.js"
|
||||
import { readGreBgpRunSnapshots } from "../gre-bgp-snapshot-history.js"
|
||||
import { createServerSignalIdentityGuard, type AlertServerIdentity } from "./server-signal-identity.js"
|
||||
import type {
|
||||
PingRunSnapshot,
|
||||
ResourcesRunSnapshot,
|
||||
@@ -187,14 +188,17 @@ function collectTrafficSignalsFromRuns(runs: TrafficRunSnapshot[]): TrafficServe
|
||||
/** Собирает сигналы alert_engine напрямую из результатов collector jobs (`scheduler_runs.result_json`). */
|
||||
export async function buildSignalSnapshotFromCollectors(): Promise<SignalSnapshot> {
|
||||
const sampledAt = new Date().toISOString()
|
||||
// Read only identity/configuration; credentials are not needed by the alert engine.
|
||||
const currentServers = await dbAll<AlertServerIdentity>("SELECT id, name, host, enabled FROM servers")
|
||||
const guardServerRows = createServerSignalIdentityGuard(currentServers)
|
||||
const resourceRuns = await readLatestOkRunSnapshots("uptime_resources", 16) as ResourcesRunSnapshot[]
|
||||
const restRuns = await readLatestOkRunSnapshots("servers_rest_ping", 16) as ServersRestPingRunSnapshot[]
|
||||
const pingRuns = await readLatestOkRunSnapshots("uptime_ping", 24) as PingRunSnapshot[]
|
||||
const trafficRuns = await readLatestOkRunSnapshots("traffic", 8) as TrafficRunSnapshot[]
|
||||
const greBgpRuns = await readGreBgpRunSnapshots()
|
||||
|
||||
const resourceByKey = collectServerSignalsFromResourceRuns(resourceRuns)
|
||||
const restByKey = collectServerSignalsFromRestRuns(restRuns)
|
||||
const resourceByKey = collectServerSignalsFromResourceRuns(resourceRuns.map(run => ({ ...run, servers: guardServerRows(run.servers ?? []) })))
|
||||
const restByKey = collectServerSignalsFromRestRuns(restRuns.map(run => ({ ...run, servers: guardServerRows(run.servers ?? []) })))
|
||||
const serverKeys = new Set([...resourceByKey.keys(), ...restByKey.keys()])
|
||||
const servers: ServerSignal[] = []
|
||||
for (const key of serverKeys) {
|
||||
@@ -216,7 +220,7 @@ export async function buildSignalSnapshotFromCollectors(): Promise<SignalSnapsho
|
||||
sampledAt,
|
||||
servers,
|
||||
probes: collectProbeSignalsFromRuns(pingRuns),
|
||||
trafficByServer: collectTrafficSignalsFromRuns(trafficRuns),
|
||||
trafficByServer: collectTrafficSignalsFromRuns(trafficRuns.map(run => ({ ...run, servers: guardServerRows(run.servers ?? []) }))),
|
||||
greTunnels: collectGreSignalsFromRuns(greBgpRuns),
|
||||
bgpSessions: collectBgpSignalsFromRuns(greBgpRuns),
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user