fix(netflow): изолировать коллектор IPFIX и срезать раздувание базы
Docker images / prepare-release (push) Successful in 8s
Docker images / backend-image (push) Successful in 2m23s
Docker images / frontend-image (push) Successful in 3m16s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 57s
Docker images / publish-release (push) Successful in 12s
Docker images / prepare-release (push) Successful in 8s
Docker images / backend-image (push) Successful in 2m23s
Docker images / frontend-image (push) Successful in 3m16s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 57s
Docker images / publish-release (push) Successful in 12s
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
+71
-17
@@ -19,11 +19,11 @@ import { useDataSource } from "@/lib/data-source"
|
||||
import { useTrafficLive } from "@/hooks/use-traffic-live"
|
||||
import { useFlowLive } from "@/hooks/use-flow-live"
|
||||
import { requestJson } from "@/shared/api/http-client"
|
||||
import { getFlowAnalytics, getFlowClients, getFlowExporters, getTrafficFlows } from "@/shared/api/traffic-flow"
|
||||
import { getFlowAnalytics, getFlowClients, getFlowExporters, getFlowMonthly, getTrafficFlows } from "@/shared/api/traffic-flow"
|
||||
import { listServers } from "@/shared/api/servers"
|
||||
import { FlowOverlaySheet } from "@/components/traffic/flow-overlay-sheet"
|
||||
import { FlowAnalyticsDetail, FlowEntityCardView } from "@/components/traffic/flow-analytics-panel"
|
||||
import type { FlowAnalyticsDto, FlowEntityCard, FlowStatsDto } from "@mmapp/contracts/traffic-flow"
|
||||
import type { FlowAnalyticsDto, FlowEntityCard, FlowMonthlyDto, FlowStatsDto } from "@mmapp/contracts/traffic-flow"
|
||||
import type { ServerRead } from "@mmapp/contracts/servers"
|
||||
import { Badge } from "@/components/reui/badge"
|
||||
import {
|
||||
@@ -63,18 +63,52 @@ function flowIngestLine(stats: FlowStatsDto | null): string | null {
|
||||
return `Коллектор: ${listener} · пакеты ${stats.packetsReceived ?? 0} · последний ${last}${exporter}${err}`
|
||||
}
|
||||
|
||||
function flowEmptyHint(stats: FlowStatsDto | null): string | undefined {
|
||||
function flowEmptyHint(stats: FlowStatsDto | null, collectorAlive?: boolean): string | undefined {
|
||||
if (!stats) return undefined
|
||||
if (stats.lastError) return stats.lastError
|
||||
if (stats.packetsReceived) {
|
||||
return `IPFIX приходит (${stats.lastExporterIp ?? "экспортёр"}), но сессии ещё не записаны.`
|
||||
}
|
||||
if (stats.listenerBound === false) {
|
||||
if (stats.listenerBound === false && !collectorAlive) {
|
||||
return "Коллектор UDP не слушает. Подключите JH ещё раз — ingest включится автоматически."
|
||||
}
|
||||
if (stats.listenerBound || collectorAlive) {
|
||||
return "Коллектор жив, IPFIX ещё не доходит. На jump-host у target Src должен быть 0.0.0.0 (авто)."
|
||||
}
|
||||
return "IPFIX ещё не доходит до коллектора. На jump-host у target Src должен быть 0.0.0.0 (авто). На хосте MM проверьте bind 10.255.254.1:4739 после wg-flow."
|
||||
}
|
||||
|
||||
function monthlyToAnalytics(m: FlowMonthlyDto): FlowAnalyticsDto {
|
||||
const emptySeries = Array(60).fill(0) as number[]
|
||||
return {
|
||||
bpsNow: 0,
|
||||
bytes: m.bytes,
|
||||
packets: 0,
|
||||
conversations: 0,
|
||||
conversationsRaw: 0,
|
||||
uniqueSrc: 0,
|
||||
uniqueDst: 0,
|
||||
topProto: "—",
|
||||
topCategory: "—",
|
||||
rxSeries: emptySeries,
|
||||
txSeries: emptySeries,
|
||||
applications: [],
|
||||
protocols: [],
|
||||
sources: [],
|
||||
destinations: [],
|
||||
interfaces: [],
|
||||
asns: m.asns,
|
||||
countries: m.countries,
|
||||
categories: [],
|
||||
services: m.services,
|
||||
mapEdges: [],
|
||||
conversationsList: [],
|
||||
ifaces: [],
|
||||
live: false,
|
||||
degraded: false,
|
||||
}
|
||||
}
|
||||
|
||||
// ─── data model ───────────────────────────────────────────────────────────────
|
||||
|
||||
interface BoundIfaceTraffic {
|
||||
@@ -298,9 +332,9 @@ const userTraffic: UserTraffic[] = INIT_USERS.map((u) =>
|
||||
|
||||
/** Ключи совпадают с `rangeToMinutes` в API (`/api/traffic/...`). */
|
||||
const TRAFFIC_RANGE_KEYS = ["5m", "15m", "1h", "4h", "24h"] as const
|
||||
type Range = (typeof TRAFFIC_RANGE_KEYS)[number]
|
||||
type Range = (typeof TRAFFIC_RANGE_KEYS)[number] | "30d"
|
||||
|
||||
const TRAFFIC_RANGE_LABELS: Record<Range, string> = {
|
||||
const TRAFFIC_RANGE_LABELS: Record<(typeof TRAFFIC_RANGE_KEYS)[number], string> = {
|
||||
"5m": "5м",
|
||||
"15m": "15м",
|
||||
"1h": "1ч",
|
||||
@@ -781,7 +815,7 @@ export default function TrafficPage() {
|
||||
serverId: selectedId,
|
||||
iface: selectedIface,
|
||||
})
|
||||
const flowLiveEnabled = isLive && effectiveMode === "flows" && Boolean(selectedId)
|
||||
const flowLiveEnabled = isLive && effectiveMode === "flows" && Boolean(selectedId) && range === "5m"
|
||||
const { sample: flowLiveSample, error: flowLiveError } = useFlowLive({
|
||||
enabled: flowLiveEnabled,
|
||||
backendUrl,
|
||||
@@ -835,7 +869,7 @@ export default function TrafficPage() {
|
||||
setLiveBusy(true)
|
||||
setLiveError(null)
|
||||
try {
|
||||
const q = encodeURIComponent(targetRange)
|
||||
const q = encodeURIComponent(targetRange === "30d" ? "24h" : targetRange)
|
||||
const [srvRes, usersRes, ifacesRes] = await Promise.all([
|
||||
apiFetch<{ servers: LiveTrafficServer[] }>(`/api/traffic/servers?range=${q}`),
|
||||
apiFetch<{ users: UserTraffic[] }>(`/api/traffic/users?range=${q}`),
|
||||
@@ -902,8 +936,6 @@ export default function TrafficPage() {
|
||||
useEffect(() => {
|
||||
if (!isLive || effectiveMode !== "flows") return
|
||||
void loadFlows()
|
||||
const t = window.setInterval(() => { void loadFlows() }, 5000)
|
||||
return () => window.clearInterval(t)
|
||||
}, [isLive, effectiveMode, loadFlows])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -911,6 +943,18 @@ export default function TrafficPage() {
|
||||
setFlowAnalytics(null)
|
||||
return
|
||||
}
|
||||
if (range === "5m") {
|
||||
setFlowAnalytics(null)
|
||||
return
|
||||
}
|
||||
if (range === "30d") {
|
||||
const month = new Date().toISOString().slice(0, 7)
|
||||
void getFlowMonthly(backendUrl, {
|
||||
month,
|
||||
serverId: flowScope === "servers" ? selectedId : undefined,
|
||||
}).then((m) => setFlowAnalytics(monthlyToAnalytics(m))).catch(() => setFlowAnalytics(null))
|
||||
return
|
||||
}
|
||||
void getFlowAnalytics(backendUrl, {
|
||||
range,
|
||||
serverId: flowScope === "servers" ? selectedId : undefined,
|
||||
@@ -969,12 +1013,19 @@ export default function TrafficPage() {
|
||||
|
||||
const handleModeChange = (next: GroupMode) => {
|
||||
setGroupMode(next)
|
||||
if (next === "servers") setSelectedId(activeServerTraffic[0]?.id ?? "srv1")
|
||||
else if (next === "users") setSelectedId((isLive ? liveUsers : userTraffic)[0]?.id ?? "u1")
|
||||
else if (next === "ifaces") setSelectedId((isLive ? liveBoundIfaces : boundIfaces)[0]?.id ?? "")
|
||||
else if (next === "flows") {
|
||||
if (next === "servers") {
|
||||
setSelectedId(activeServerTraffic[0]?.id ?? "srv1")
|
||||
if (range === "30d") setRange("1h")
|
||||
} else if (next === "users") {
|
||||
setSelectedId((isLive ? liveUsers : userTraffic)[0]?.id ?? "u1")
|
||||
if (range === "30d") setRange("1h")
|
||||
} else if (next === "ifaces") {
|
||||
setSelectedId((isLive ? liveBoundIfaces : boundIfaces)[0]?.id ?? "")
|
||||
if (range === "30d") setRange("1h")
|
||||
} else if (next === "flows") {
|
||||
setFlowScope("servers")
|
||||
setFlowIface("__all__")
|
||||
setRange("5m")
|
||||
setSelectedId(flowExporters[0]?.id ?? "")
|
||||
}
|
||||
setSortField("rx")
|
||||
@@ -1064,7 +1115,10 @@ export default function TrafficPage() {
|
||||
|
||||
const visibleSortFields = SORT_FIELDS.filter(s => !s.modesOnly || s.modesOnly.includes(effectiveMode))
|
||||
const ingestLine = flowIngestLine(flowStats)
|
||||
const flowError = liveError || flowLiveError
|
||||
const collectorAlive = Boolean(flowStats?.listenerBound || flowStats?.packetsReceived)
|
||||
const flowError = liveError
|
||||
|| (flowLiveError && !(collectorAlive && /live HTTP 500/.test(flowLiveError)) ? flowLiveError : null)
|
||||
|| (displayedFlow?.degraded ? "Коллектор перегружен: упрощённая аналитика" : null)
|
||||
|
||||
const flowKpiItems = [
|
||||
{
|
||||
@@ -1258,7 +1312,7 @@ export default function TrafficPage() {
|
||||
))}
|
||||
{sortedFlowCards.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{flowEmptyHint(flowStats) ?? "Нет экспортёров IPFIX. Подключите jump-host."}
|
||||
{flowEmptyHint(flowStats, collectorAlive) ?? "Нет экспортёров IPFIX. Подключите jump-host."}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -1275,7 +1329,7 @@ export default function TrafficPage() {
|
||||
dedup={flowDedup}
|
||||
onDedup={setFlowDedup}
|
||||
liveHint={displayedFlow?.live ? "live" : undefined}
|
||||
emptyHint={flowEmptyHint(flowStats)}
|
||||
emptyHint={flowEmptyHint(flowStats, collectorAlive)}
|
||||
/>
|
||||
</FramePanel>
|
||||
</Frame>
|
||||
|
||||
Reference in New Issue
Block a user