fix(bgp): сохранять неизвестное состояние и достоверную историю оповещений
This commit is contained in:
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)
|
||||
|
||||
@@ -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. */
|
||||
|
||||
Reference in new issue
Block a user