fix(bgp): отображать только подтверждённые показатели сессий

This commit is contained in:
Denozordec committed 2026-09-30 16:56:28 +07:00
1 parent 7046808429
commit 8fdc4b917b
13 files changed
+148 -169

No files matched your search

+4 -2
View File
@@ -2,7 +2,7 @@ import { eq } from "drizzle-orm"
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
import { db } from "../db/index.js"
import { servers } from "../db/schema.js"
import { parseBgpSessions } from "../services/bgp-parse-sessions.js"
import { parseBgpSessions, parseBgpCatalogSessions, type BgpCatalogSessionRead } from "../services/bgp-parse-sessions.js"
import { MikrotikClient } from "../services/mikrotik.js"
import { ServerIdParamSchema, type ServerIdParams } from "../types/server.js"
import type { BgpSessionRead } from "../types/server.js"
@@ -13,11 +13,13 @@ type ServerRow = typeof servers.$inferSelect
interface BgpDependencies {
listEnabledServers: () => Promise<ServerRow[]>
findServer: (id: number) => Promise<ServerRow | undefined>
readCatalogSessions?: (server: ServerRow) => Promise<BgpCatalogSessionRead[]>
readSessions: (server: ServerRow) => Promise<BgpSessionRead[]>
}
const defaults: BgpDependencies = {
listEnabledServers: () => db.select().from(servers).where(eq(servers.enabled, true)),
findServer: async id => (await db.select().from(servers).where(eq(servers.id, id)).limit(1))[0],
readCatalogSessions: async server => parseBgpCatalogSessions(server, await MikrotikClient.fromServer(server).getBgpSessions()),
readSessions: async server => parseBgpSessions(server, await MikrotikClient.fromServer(server).getBgpSessions()),
}
export function createBgpRoutes(dependencies: BgpDependencies = defaults): FastifyPluginAsyncZod {
@@ -25,7 +27,7 @@ export function createBgpRoutes(dependencies: BgpDependencies = defaults): Fasti
// Отдельный каталог сохраняет совместимость старого массива для оповещений.
app.get("/bgp/catalog", async (_req, reply) => {
const devices = await dependencies.listEnabledServers()
return reply.send(await pollBgpCatalog(devices, dependencies.readSessions))
return reply.send(await pollBgpCatalog<ServerRow, BgpCatalogSessionRead | BgpSessionRead>(devices, dependencies.readCatalogSessions ?? dependencies.readSessions))
})
// GET /api/bgp/sessions/raw/:id — raw RouterOS response for a single server (debug)
@@ -0,0 +1,21 @@
import assert from "node:assert/strict"
import { parseBgpCatalogSessions, parseBgpSessions } from "./bgp-parse-sessions.js"
import { servers } from "../db/schema.js"
const server = { id: 1, name: "Тест", host: "192.0.2.1", site: "", country: "RU" } as typeof servers.$inferSelect
const raw = { ".id": "*1", "total-updates-received": "42", "total-updates-sent": "99" }
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)
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)
assert.equal(observed.holdTime, 0)
assert.equal(observed.keepalive, 90)
assert.equal(observed.inputMessages, 0)
assert.equal(observed.afi, "ip,ipv6")
const invalid = parseBgpCatalogSessions(server, [{ ".id": "*3", "prefix-count": "12bad", "hold-time": "invalid", "remote.messages": "-1" }])[0]
assert.equal(invalid.prefixesRx, null)
assert.equal(invalid.holdTime, null)
assert.equal(invalid.inputMessages, null)
console.log("BGP parser: unknown/observed zero/timers/AFI, no UPDATE-prefix substitution, legacy preserved")
@@ -135,3 +135,33 @@ export function parseBgpSessions(server: ServerRow, raw: RosBgpSession[]): BgpSe
}
})
}
/** Каталог не заменяет отсутствующие показатели значениями по умолчанию старого API. */
export type BgpCatalogSessionRead = Omit<BgpSessionRead, "holdTime" | "keepalive" | "prefixesRx" | "prefixesTx" | "inputMessages" | "outputMessages"> & {
holdTime: number | null; keepalive: number | null; prefixesRx: number | null; prefixesTx: number | null;
inputMessages: number | null; outputMessages: number | null; afi: string | null;
}
function readCounter(value: string | undefined): number | null {
if (value === undefined || !/^\d+$/.test(value.trim())) return null
const parsed = Number(value)
return Number.isSafeInteger(parsed) ? parsed : null
}
function readTimer(value: string | undefined): number | null {
if (value === "0" || value === "infinity") return 0
if (!value || !/^(?:\d+(?:w|d|h|ms|m|s))+$/.test(value)) return null
return parseDuration(value)
}
export function parseBgpCatalogSessions(server: ServerRow, raw: RosBgpSession[]): BgpCatalogSessionRead[] {
return parseBgpSessions(server, raw).map((session, index) => {
const s = raw[index]
return { ...session,
state: s.established === "true" ? "Established" : s.state?.trim() ? detectState(s) : "Unknown",
holdTime: readTimer(s["hold-time"]) ?? readCounter(s["active-holdtime"]),
keepalive: readTimer(s["keepalive-time"]),
prefixesRx: readCounter(s["prefix-count"]), prefixesTx: null,
inputMessages: readCounter(s["remote.messages"] ?? s["total-messages-received"]),
outputMessages: readCounter(s["local.messages"] ?? s["total-messages-sent"]),
afi: s["remote.afi"]?.trim() || null,
}
})
}