Docker images / prepare-release (push) Successful in 10s
Docker images / backend-image (push) Successful in 2m14s
Docker images / frontend-image (push) Successful in 3m14s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 46s
Docker images / publish-release (push) Successful in 11s
Скорость между узлами считается как в Трафике (5 мин, без overlay/mesh), отдельно от ёмкости WAN и BT. Co-authored-by: Cursor <[email protected]>
104 lines
3.1 KiB
TypeScript
104 lines
3.1 KiB
TypeScript
import type { FlowMapHop } from "@mmapp/contracts/traffic-flow"
|
|
import { fmtRate } from "@/lib/fmt-rate"
|
|
|
|
export interface MatchedNetflowHop {
|
|
bps: number
|
|
bpsFwd: number
|
|
bpsRev: number
|
|
bytes: number
|
|
}
|
|
|
|
function ifaceNorm(s: string | undefined): string {
|
|
return (s ?? "").trim().toLowerCase()
|
|
}
|
|
|
|
function pairKey(a: string, b: string): string {
|
|
const x = String(a)
|
|
const y = String(b)
|
|
return x <= y ? `${x}\t${y}` : `${y}\t${x}`
|
|
}
|
|
|
|
function mergeDirected(hops: FlowMapHop[], mapFromId: string): MatchedNetflowHop {
|
|
let bytes = 0
|
|
let bpsFwd = 0
|
|
let bpsRev = 0
|
|
const from = String(mapFromId)
|
|
for (const h of hops) {
|
|
bytes += h.bytes
|
|
if (h.fromId === from) {
|
|
bpsFwd += h.bpsFwd
|
|
bpsRev += h.bpsRev
|
|
} else {
|
|
bpsFwd += h.bpsRev
|
|
bpsRev += h.bpsFwd
|
|
}
|
|
}
|
|
return { bytes, bpsFwd, bpsRev, bps: bpsFwd + bpsRev }
|
|
}
|
|
|
|
export function hopHasRate(h: MatchedNetflowHop | undefined): h is MatchedNetflowHop {
|
|
return h != null && Number.isFinite(h.bps) && h.bps > 0
|
|
}
|
|
|
|
export function formatNetflowRate(hop: MatchedNetflowHop): string {
|
|
return fmtRate(hop.bps / 1_000_000)
|
|
}
|
|
|
|
export function formatNetflowDir(hop: MatchedNetflowHop): string {
|
|
return `↓${fmtRate(hop.bpsFwd / 1_000_000)} ↑${fmtRate(hop.bpsRev / 1_000_000)}`
|
|
}
|
|
|
|
/** GRE: сначала имя интерфейса туннеля на любом конце, иначе пара узлов. */
|
|
export function matchNetflowForGreEdge(
|
|
edge: {
|
|
tunnel: { name: string }
|
|
fromServer: { id: string }
|
|
toServer: { id: string }
|
|
},
|
|
hops: FlowMapHop[],
|
|
): MatchedNetflowHop | undefined {
|
|
const name = ifaceNorm(edge.tunnel.name)
|
|
const fromId = String(edge.fromServer.id)
|
|
const toId = String(edge.toServer.id)
|
|
if (name) {
|
|
const ifaceHits = hops.filter((h) =>
|
|
h.kind === "iface"
|
|
&& ifaceNorm(h.iface) === name
|
|
&& (h.fromId === fromId || h.fromId === toId),
|
|
)
|
|
if (ifaceHits.length) return mergeDirected(ifaceHits, fromId)
|
|
const greNamed = hops.filter((h) =>
|
|
h.kind === "gre"
|
|
&& ifaceNorm(h.iface) === name
|
|
&& (h.fromId === fromId || h.fromId === toId || h.toId === fromId || h.toId === toId),
|
|
)
|
|
if (greNamed.length) return mergeDirected(greNamed, fromId)
|
|
}
|
|
const want = pairKey(fromId, toId)
|
|
const pairHits = hops.filter((h) =>
|
|
h.kind === "gre" && Boolean(h.toId) && pairKey(h.fromId, h.toId) === want,
|
|
)
|
|
if (pairHits.length) return mergeDirected(pairHits, fromId)
|
|
return undefined
|
|
}
|
|
|
|
/** WAN-аплинк HR: kind wan, иначе iface с тем же именем на homeId. */
|
|
export function matchNetflowForWan(
|
|
homeId: string,
|
|
wanIface: string,
|
|
hops: FlowMapHop[],
|
|
): MatchedNetflowHop | undefined {
|
|
const id = String(homeId)
|
|
const iface = ifaceNorm(wanIface)
|
|
if (!iface) return undefined
|
|
const wanHits = hops.filter((h) =>
|
|
h.kind === "wan" && h.fromId === id && ifaceNorm(h.iface) === iface,
|
|
)
|
|
if (wanHits.length) return mergeDirected(wanHits, id)
|
|
const ifaceHits = hops.filter((h) =>
|
|
h.kind === "iface" && h.fromId === id && ifaceNorm(h.iface) === iface,
|
|
)
|
|
if (ifaceHits.length) return mergeDirected(ifaceHits, id)
|
|
return undefined
|
|
}
|