fix(bgp): сохранять неизвестное состояние и достоверную историю оповещений

This commit is contained in:
Denozordec committed 2026-10-12 10:14:39 +07:00
1 parent 8cb0fbd195
commit fef7bc5520
15 files changed
+1196 -40

No files matched your search

@@ -0,0 +1,22 @@
import type { GreBgpSnapshotRunSnapshot } from "../../types/scheduler-run-snapshot.js"
import type { BgpPeerSignal } from "./types.js"
export function isKnownBgpState(state: string): boolean {
return ["established", "idle", "active", "connect", "opensent", "openconfirm"].includes(state.trim().toLowerCase())
}
/** Runs arrive newest first. Keep current unknown, but use the last known previous state. */
export function collectBgpSignalsFromRuns(runs: GreBgpSnapshotRunSnapshot[]): BgpPeerSignal[] {
const byKey = new Map<string, BgpPeerSignal>()
for (const run of runs) {
for (const row of run.bgpPeers ?? []) {
const key = String(row.key ?? "").trim()
if (!key) continue
const state = String(row.state ?? "").trim()
const current = byKey.get(key)
if (!current) byKey.set(key, { key, state, prevState: null })
else if (current.prevState === null && isKnownBgpState(state)) current.prevState = state
}
}
return [...byKey.values()]
}
@@ -2,6 +2,8 @@ import assert from "node:assert/strict"
import type { ApiAlertRule } from "../alerts-service.js"
import { evaluateRule } from "./evaluate-rule.js"
import type { SignalSnapshot } from "./types.js"
import { collectBgpSignalsFromRuns } from "./bgp-signals.js"
import type { GreBgpSnapshotRunSnapshot } from "../../types/scheduler-run-snapshot.js"
function emptySnap(over: Partial<SignalSnapshot> = {}): SignalSnapshot {
return {
@@ -112,6 +114,27 @@ function rule(p: Partial<ApiAlertRule> & Pick<ApiAlertRule, "id" | "name" | "typ
assert.equal(evaluateRule(r, emptySnap()), null)
}
for (const state of ["Unknown", "", " ", "FutureState"]) {
const problem = rule({ id: "unknown-down", name: "BGP", type: "bgp-peer", targets: ["srv: peer1"], conditions: ["разорвал сессию"] })
const recovery = rule({ id: "unknown-up", name: "BGP", type: "bgp-peer", targets: ["srv: peer1"], conditions: ["восстановил сессию"] })
assert.equal(evaluateRule(problem, emptySnap({ bgpSessions: [{ key: "srv: peer1", state, prevState: "Established" }] })), null)
assert.equal(evaluateRule(recovery, emptySnap({ bgpSessions: [{ key: "srv: peer1", state: "Established", prevState: state }] })), null)
}
for (const [history, expectedPrevious, recoveryExpected] of [
[["Established", "Unknown", "Idle"], "Idle", true],
[["Established", "Unknown", "Established"], "Established", false],
[["Established", "Unknown", ""], null, false],
[["Unknown", "Idle", "Established"], "Idle", false],
] as const) {
const runs = history.map(state => ({ bgpPeers: [{ key: "srv: peer1", state }] })) as GreBgpSnapshotRunSnapshot[]
const signals = collectBgpSignalsFromRuns(runs)
assert.equal(signals[0].state, history[0])
assert.equal(signals[0].prevState, expectedPrevious)
const recovery = rule({ id: "gap-up", name: "BGP", type: "bgp-peer", targets: ["srv: peer1"], conditions: ["восстановил сессию"] })
assert.equal(evaluateRule(recovery, emptySnap({ bgpSessions: signals })) !== null, recoveryExpected)
}
{
const r = rule({
id: "srv-rec",
@@ -1,6 +1,7 @@
import { createHash } from "node:crypto"
import type { ApiAlertRule } from "../alerts-service.js"
import type { RuleEvalHit, SignalSnapshot } from "./types.js"
import { isKnownBgpState } from "./bgp-signals.js"
function hashPayload(parts: string[]): string {
return createHash("sha256").update(parts.join("\0")).digest("hex").slice(0, 24)
@@ -213,12 +214,13 @@ function evalBgpPeer(rule: ApiAlertRule, snap: SignalSnapshot, cond: string): Ru
const hits: string[] = []
for (const tgt of rule.targets) {
const b = findBgpPeer(snap, tgt)
if (!b) continue
if (!b || !isKnownBgpState(b.state)) continue
if (wantDown && !isBgpEstablished(b.state)) hits.push(tgt)
if (
wantUp &&
isBgpEstablished(b.state) &&
b.prevState != null &&
isKnownBgpState(b.prevState) &&
String(b.prevState).trim() !== "" &&
!isBgpEstablished(b.prevState)
) {
@@ -1,4 +1,5 @@
import { dbAll } from "../../db/index.js"
import { collectBgpSignalsFromRuns } from "./bgp-signals.js"
import type {
GreBgpSnapshotRunSnapshot,
PingRunSnapshot,
@@ -8,7 +9,6 @@ import type {
TrafficRunSnapshot,
} from "../../types/scheduler-run-snapshot.js"
import type {
BgpPeerSignal,
GreTunnelSignal,
ProbeSignal,
ServerSignal,
@@ -206,24 +206,6 @@ function collectGreSignalsFromRuns(runs: GreBgpSnapshotRunSnapshot[]): GreTunnel
return [...byTarget.values()]
}
function collectBgpSignalsFromRuns(runs: GreBgpSnapshotRunSnapshot[]): BgpPeerSignal[] {
const byKey = new Map<string, BgpPeerSignal>()
for (const run of runs) {
const peers = run.bgpPeers ?? []
for (const row of peers) {
const key = String(row.key ?? "").trim()
if (!key) continue
const prev = byKey.get(key)
if (!prev) {
byKey.set(key, { key, state: String(row.state ?? "").trim(), prevState: null })
continue
}
if (prev.prevState == null) prev.prevState = String(row.state ?? "").trim()
}
}
return [...byKey.values()]
}
/** Собирает сигналы alert_engine напрямую из результатов collector jobs (`scheduler_runs.result_json`). */
export async function buildSignalSnapshotFromCollectors(): Promise<SignalSnapshot> {
const sampledAt = new Date().toISOString()
@@ -7,6 +7,13 @@ const missing = parseBgpCatalogSessions(server, [raw])[0]
assert.equal(missing.state, "Unknown")
for (const field of ["prefixesRx", "prefixesTx", "holdTime", "keepalive", "inputMessages", "outputMessages", "afi"] as const) assert.equal(missing[field], null)
assert.equal(parseBgpSessions(server, [raw])[0].prefixesRx, 42)
for (const s of [{}, { uptime: "1m" }, { established: "false" }, { state: "" }, { state: " " }, { state: "ActiveFuture" }, { state: "new-native-state" }]) {
assert.equal(parseBgpSessions(server, [s])[0].state, "Unknown")
assert.equal(parseBgpCatalogSessions(server, [s])[0].state, "Unknown")
}
for (const [native, expected] of [["idle", "Idle"], [" ACTIVE ", "Active"], ["connect", "Connect"], ["Open Sent", "OpenSent"], ["openconfirm", "OpenConfirm"], ["established", "Established"]]) {
assert.equal(parseBgpSessions(server, [{ state: native }])[0].state, expected)
}
const observed = parseBgpCatalogSessions(server, [{ ".id": "*2", established: "true", "prefix-count": "0", "hold-time": "0", "keepalive-time": "1m30s", "remote.messages": "0", "local.messages": "2", "remote.afi": "ip,ipv6" }])[0]
assert.equal(observed.state, "Established")
assert.equal(observed.prefixesRx, 0)
+2 -7
View File
@@ -70,15 +70,10 @@ function detectState(s: RosBgpSession): string {
}
const key = s["state"].trim().toLowerCase()
if (map[key]) return map[key]
for (const [k, v] of Object.entries(map)) {
if (key.startsWith(k)) return v
}
return key.charAt(0).toUpperCase() + key.slice(1)
return "Unknown"
}
if (s["uptime"]) return "Active"
return "Idle"
return "Unknown"
}
/** Парсинг BGP-сессий с роутера — общая логика для HTTP-маршрута и alert-engine. */