Includes current frontend and backend work in progress and removes generated artifacts from tracking to keep the repository clean for дальнейшая разработка. Co-authored-by: Cursor <cursoragent@cursor.com>
266 lines
8.5 KiB
TypeScript
266 lines
8.5 KiB
TypeScript
import { sqliteDatabase } from "../../db/index.js"
|
|
import type {
|
|
GreBgpSnapshotRunSnapshot,
|
|
PingRunSnapshot,
|
|
ResourcesRunSnapshot,
|
|
SchedulerRunSnapshot,
|
|
ServersRestPingRunSnapshot,
|
|
TrafficRunSnapshot,
|
|
} from "../../types/scheduler-run-snapshot.js"
|
|
import type {
|
|
BgpPeerSignal,
|
|
GreTunnelSignal,
|
|
ProbeSignal,
|
|
ServerSignal,
|
|
SignalSnapshot,
|
|
TrafficServerSignal,
|
|
} from "./types.js"
|
|
|
|
function normalizeNameKey(v: string): string {
|
|
return v.trim().toLowerCase()
|
|
}
|
|
|
|
function safeParseRunSnapshot(raw: string | null): SchedulerRunSnapshot | null {
|
|
if (!raw) return null
|
|
try {
|
|
return JSON.parse(raw) as SchedulerRunSnapshot
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
function readLatestOkRunSnapshots(jobKey: string, limit = 32): SchedulerRunSnapshot[] {
|
|
const rows = sqliteDatabase
|
|
.prepare(
|
|
`SELECT result_json AS resultJson
|
|
FROM scheduler_runs
|
|
WHERE job_key = ? AND status = 'ok' AND result_json IS NOT NULL
|
|
ORDER BY finished_at DESC
|
|
LIMIT ?`,
|
|
)
|
|
.all(jobKey, limit) as { resultJson: string | null }[]
|
|
const out: SchedulerRunSnapshot[] = []
|
|
for (const row of rows) {
|
|
const parsed = safeParseRunSnapshot(row.resultJson)
|
|
if (!parsed || parsed.job !== jobKey) continue
|
|
out.push(parsed)
|
|
}
|
|
return out
|
|
}
|
|
|
|
type TriState = "online" | "offline" | "degraded"
|
|
|
|
function timelineToServerSignal(name: string, sampledAt: string, statuses: TriState[]): ServerSignal {
|
|
return {
|
|
name,
|
|
status: statuses[0] ?? "offline",
|
|
prevStatus: statuses[1] ?? null,
|
|
prev2Status: statuses[2] ?? null,
|
|
sampledAt,
|
|
}
|
|
}
|
|
|
|
function collectServerSignalsFromResourceRuns(runs: ResourcesRunSnapshot[]): Map<string, ServerSignal> {
|
|
const byKey = new Map<
|
|
string,
|
|
{ name: string; sampledAt: string; statuses: TriState[] }
|
|
>()
|
|
for (const run of runs) {
|
|
for (const s of run.servers ?? []) {
|
|
const name = String(s.name ?? "").trim()
|
|
if (!name) continue
|
|
const key = normalizeNameKey(name)
|
|
const rec = byKey.get(key) ?? { name, sampledAt: run.sampledAt, statuses: [] }
|
|
if (rec.statuses.length < 3) rec.statuses.push(s.status === "online" ? "online" : "offline")
|
|
if (!byKey.has(key)) rec.sampledAt = run.sampledAt
|
|
byKey.set(key, rec)
|
|
}
|
|
}
|
|
const out = new Map<string, ServerSignal>()
|
|
for (const [key, rec] of byKey) out.set(key, timelineToServerSignal(rec.name, rec.sampledAt, rec.statuses))
|
|
return out
|
|
}
|
|
|
|
function collectServerSignalsFromRestRuns(runs: ServersRestPingRunSnapshot[]): Map<string, ServerSignal> {
|
|
const byKey = new Map<
|
|
string,
|
|
{ name: string; sampledAt: string; statuses: TriState[] }
|
|
>()
|
|
for (const run of runs) {
|
|
for (const s of run.servers ?? []) {
|
|
const name = String(s.name ?? "").trim()
|
|
if (!name) continue
|
|
const key = normalizeNameKey(name)
|
|
const rec = byKey.get(key) ?? { name, sampledAt: run.sampledAt, statuses: [] }
|
|
if (rec.statuses.length < 3) rec.statuses.push(s.ok ? "online" : "offline")
|
|
if (!byKey.has(key)) rec.sampledAt = run.sampledAt
|
|
byKey.set(key, rec)
|
|
}
|
|
}
|
|
const out = new Map<string, ServerSignal>()
|
|
for (const [key, rec] of byKey) out.set(key, timelineToServerSignal(rec.name, rec.sampledAt, rec.statuses))
|
|
return out
|
|
}
|
|
|
|
function mergeResourceAndRestServer(resource: ServerSignal, rest: ServerSignal): ServerSignal {
|
|
const rUp = resource.status === "online"
|
|
const tUp = rest.status === "online"
|
|
if (tUp && !rUp) {
|
|
const rBad = resource.status === "offline" || resource.status === "degraded"
|
|
if (rBad) {
|
|
return {
|
|
name: rest.name,
|
|
status: "online",
|
|
prevStatus: "online",
|
|
prev2Status: resource.status,
|
|
sampledAt: rest.sampledAt,
|
|
}
|
|
}
|
|
return rest
|
|
}
|
|
if (rUp && !tUp) return resource
|
|
if (tUp && rUp) {
|
|
if (rest.sampledAt >= resource.sampledAt) {
|
|
if (resource.prevStatus === "offline" || resource.prevStatus === "degraded") {
|
|
return {
|
|
name: rest.name,
|
|
status: "online",
|
|
prevStatus: "online",
|
|
prev2Status: resource.prevStatus === "degraded" ? "degraded" : "offline",
|
|
sampledAt: rest.sampledAt,
|
|
}
|
|
}
|
|
if (
|
|
resource.prevStatus === "online" &&
|
|
resource.prev2Status != null &&
|
|
(resource.prev2Status === "offline" || resource.prev2Status === "degraded")
|
|
) {
|
|
return {
|
|
name: rest.name,
|
|
status: "online",
|
|
prevStatus: "online",
|
|
prev2Status: resource.prev2Status,
|
|
sampledAt: rest.sampledAt,
|
|
}
|
|
}
|
|
}
|
|
return rest.sampledAt >= resource.sampledAt ? rest : resource
|
|
}
|
|
return rest.sampledAt >= resource.sampledAt ? rest : resource
|
|
}
|
|
|
|
function collectProbeSignalsFromRuns(runs: PingRunSnapshot[]): ProbeSignal[] {
|
|
const out = new Map<string, ProbeSignal>()
|
|
for (const run of runs) {
|
|
for (const p of run.probes ?? []) {
|
|
const key = `${String(p.name ?? "").trim()} → ${String(p.target ?? "").trim()}`
|
|
if (!key.trim() || out.has(key)) continue
|
|
out.set(key, {
|
|
key,
|
|
status: p.status,
|
|
rttMs: p.rttMs ?? null,
|
|
lossPct: p.lossPct ?? null,
|
|
sampledAt: run.sampledAt,
|
|
})
|
|
}
|
|
}
|
|
return [...out.values()]
|
|
}
|
|
|
|
function collectTrafficSignalsFromRuns(runs: TrafficRunSnapshot[]): TrafficServerSignal[] {
|
|
const out = new Map<string, TrafficServerSignal>()
|
|
for (const run of runs) {
|
|
for (const s of run.servers ?? []) {
|
|
const name = String(s.name ?? "").trim()
|
|
if (!name) continue
|
|
const key = normalizeNameKey(name)
|
|
if (out.has(key)) continue
|
|
out.set(key, {
|
|
serverName: name,
|
|
rxMbps: Number(s.sumRxMbps ?? 0),
|
|
txMbps: Number(s.sumTxMbps ?? 0),
|
|
sampledAt: run.sampledAt,
|
|
})
|
|
}
|
|
}
|
|
return [...out.values()]
|
|
}
|
|
|
|
function collectGreSignalsFromRuns(runs: GreBgpSnapshotRunSnapshot[]): GreTunnelSignal[] {
|
|
const byTarget = new Map<string, GreTunnelSignal>()
|
|
for (const run of runs) {
|
|
const greRows = run.greTunnels ?? []
|
|
for (const row of greRows) {
|
|
const label = String(row.targetLabel ?? "").trim()
|
|
if (!label) continue
|
|
const prev = byTarget.get(label)
|
|
if (!prev) {
|
|
byTarget.set(label, {
|
|
targetLabel: label,
|
|
status: row.status,
|
|
prevStatus: null,
|
|
})
|
|
continue
|
|
}
|
|
if (prev.prevStatus == null) prev.prevStatus = row.status
|
|
}
|
|
}
|
|
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 function buildSignalSnapshotFromCollectors(): SignalSnapshot {
|
|
const sampledAt = new Date().toISOString()
|
|
const resourceRuns = readLatestOkRunSnapshots("uptime_resources", 16) as ResourcesRunSnapshot[]
|
|
const restRuns = readLatestOkRunSnapshots("servers_rest_ping", 16) as ServersRestPingRunSnapshot[]
|
|
const pingRuns = readLatestOkRunSnapshots("uptime_ping", 24) as PingRunSnapshot[]
|
|
const trafficRuns = readLatestOkRunSnapshots("traffic", 8) as TrafficRunSnapshot[]
|
|
const greBgpRuns = readLatestOkRunSnapshots("gre_bgp", 8) as GreBgpSnapshotRunSnapshot[]
|
|
|
|
const resourceByKey = collectServerSignalsFromResourceRuns(resourceRuns)
|
|
const restByKey = collectServerSignalsFromRestRuns(restRuns)
|
|
const serverKeys = new Set([...resourceByKey.keys(), ...restByKey.keys()])
|
|
const servers: ServerSignal[] = []
|
|
for (const key of serverKeys) {
|
|
const resource = resourceByKey.get(key)
|
|
const rest = restByKey.get(key)
|
|
if (!rest) {
|
|
if (resource) servers.push(resource)
|
|
continue
|
|
}
|
|
if (!resource) {
|
|
servers.push(rest)
|
|
continue
|
|
}
|
|
servers.push(mergeResourceAndRestServer(resource, rest))
|
|
}
|
|
servers.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: "base" }))
|
|
|
|
return {
|
|
sampledAt,
|
|
servers,
|
|
probes: collectProbeSignalsFromRuns(pingRuns),
|
|
trafficByServer: collectTrafficSignalsFromRuns(trafficRuns),
|
|
greTunnels: collectGreSignalsFromRuns(greBgpRuns),
|
|
bgpSessions: collectBgpSignalsFromRuns(greBgpRuns),
|
|
}
|
|
}
|