feat: improve bgp incident visibility

This commit is contained in:
linkong
2026-03-30 17:17:33 +08:00
parent 945786cee5
commit ac63bba2a2
12 changed files with 860 additions and 49 deletions

View File

@@ -7,10 +7,11 @@ const bgpGroup = new THREE.Group();
const bgpOverlayGroup = new THREE.Group();
const collectorMarkers = [];
const anomalyMarkers = [];
const anomalyCountByCollector = new Map();
const activeEventCountByCollector = new Map();
let showBGP = true;
let totalAnomalyCount = 0;
let totalIncidentCount = 0;
let textureCache = null;
let activeEventOverlay = null;
const relativeTimeFormatter = new Intl.RelativeTimeFormat("zh-CN", {
@@ -328,6 +329,78 @@ function buildAnomalyFeatureData(feature) {
};
}
function buildIncidentFeatureData(feature) {
const coordinates = feature?.geometry?.coordinates || [];
const [longitude, latitude] = coordinates;
if (
typeof latitude !== "number" ||
typeof longitude !== "number" ||
Number.isNaN(latitude) ||
Number.isNaN(longitude)
) {
return null;
}
const properties = feature?.properties || {};
const severity = normalizeSeverity(properties.severity);
const startedAt = properties.started_at || properties.created_at || null;
const affectedPrefixes = Array.isArray(properties.affected_prefixes)
? properties.affected_prefixes
: [];
const affectedAsns = Array.isArray(properties.affected_asns)
? properties.affected_asns
: [];
const affectedCollectors = Array.isArray(properties.affected_collectors)
? properties.affected_collectors
: [];
const affectedRegions = Array.isArray(properties.affected_regions)
? properties.affected_regions
: [];
const primaryRegion = affectedRegions[0] || {};
return {
latitude,
longitude,
rawSeverity: properties.severity || severity,
severity,
collector: affectedCollectors[0] || primaryRegion.collector || "-",
city: primaryRegion.city || "-",
country: primaryRegion.country || "-",
source: "bgp_incident",
anomaly_type: properties.incident_type || properties.title || "-",
incident_type: properties.incident_type || "-",
incident_key: properties.incident_key || "-",
status: properties.status || "-",
prefix: affectedPrefixes[0] || "-",
prefixes: affectedPrefixes,
origin_asn: affectedAsns[0] ?? "-",
new_origin_asn: affectedAsns[1] ?? "-",
affected_asns: affectedAsns,
as_path: [],
collectors: affectedCollectors,
collector_count: affectedCollectors.length || 1,
impacted_regions: affectedRegions,
confidence: properties.confidence ?? "-",
summary: properties.summary || properties.title || "-",
created_at: formatLocalDateTime(startedAt),
created_at_raw: startedAt,
route_change:
affectedAsns.length > 1
? affectedAsns.slice(0, 2).map((asn) => `AS${asn}`).join(" -> ")
: affectedPrefixes.length > 1
? `${affectedPrefixes.length}个前缀簇`
: properties.incident_type || "-",
observed_by: formatBGPObservedBy(affectedCollectors),
impacted_scope: formatBGPImpactedScope(affectedRegions),
location: formatBGPLocation(primaryRegion.city, primaryRegion.country),
id:
properties.id ||
properties.incident_key ||
`${properties.incident_type || "incident"}-${latitude}-${longitude}`,
};
}
function clearMarkerArray(markers) {
while (markers.length > 0) {
const marker = markers.pop();
@@ -454,9 +527,9 @@ function dedupeAnomalies(features) {
const data = buildAnomalyFeatureData(feature);
if (!data) return;
anomalyCountByCollector.set(
activeEventCountByCollector.set(
data.collector,
(anomalyCountByCollector.get(data.collector) || 0) + 1,
(activeEventCountByCollector.get(data.collector) || 0) + 1,
);
const dedupeKey = `${data.collector}|${data.latitude.toFixed(4)}|${data.longitude.toFixed(4)}`;
@@ -482,39 +555,85 @@ function dedupeAnomalies(features) {
.slice(0, BGP_CONFIG.maxRenderedMarkers);
}
function dedupeIncidents(features) {
const latestByKey = new Map();
features.forEach((feature) => {
const data = buildIncidentFeatureData(feature);
if (!data) return;
data.collectors.forEach((collector) => {
activeEventCountByCollector.set(
collector,
(activeEventCountByCollector.get(collector) || 0) + 1,
);
});
const dedupeKey = String(data.incident_key || data.id);
const previous = latestByKey.get(dedupeKey);
const currentTime = data.created_at_raw
? new Date(data.created_at_raw).getTime()
: 0;
const previousTime = previous?.created_at_raw
? new Date(previous.created_at_raw).getTime()
: 0;
if (!previous || currentTime >= previousTime) {
latestByKey.set(dedupeKey, data);
}
});
return Array.from(latestByKey.values())
.sort((a, b) => {
const timeA = a.created_at_raw ? new Date(a.created_at_raw).getTime() : 0;
const timeB = b.created_at_raw ? new Date(b.created_at_raw).getTime() : 0;
return timeB - timeA;
})
.slice(0, BGP_CONFIG.maxRenderedMarkers);
}
function applyCollectorCounts() {
collectorMarkers.forEach((marker) => {
marker.userData.anomaly_count =
anomalyCountByCollector.get(marker.userData.collector) || 0;
activeEventCountByCollector.get(marker.userData.collector) || 0;
});
}
export async function loadBGPAnomalies(scene, earth) {
clearBGPData(earth);
const [collectorsResponse, anomaliesResponse] = await Promise.all([
const [collectorsResponse, incidentsResponse, anomaliesResponse] = await Promise.all([
fetch(PATHS.bgpCollectorsApi),
fetch(`${PATHS.bgpIncidentsApi}?limit=${BGP_CONFIG.defaultFetchLimit}`),
fetch(`${PATHS.bgpApi}?limit=${BGP_CONFIG.defaultFetchLimit}`),
]);
if (!collectorsResponse.ok) {
throw new Error(`BGP collectors HTTP ${collectorsResponse.status}`);
}
if (!incidentsResponse.ok) {
throw new Error(`BGP incidents HTTP ${incidentsResponse.status}`);
}
if (!anomaliesResponse.ok) {
throw new Error(`BGP anomalies HTTP ${anomaliesResponse.status}`);
}
const collectorsPayload = await collectorsResponse.json();
const incidentsPayload = await incidentsResponse.json();
const anomaliesPayload = await anomaliesResponse.json();
const collectorFeatures = Array.isArray(collectorsPayload?.features)
? collectorsPayload.features
: [];
const incidentFeatures = Array.isArray(incidentsPayload?.features)
? incidentsPayload.features
: [];
const anomalyFeatures = Array.isArray(anomaliesPayload?.features)
? anomaliesPayload.features
: [];
totalAnomalyCount = anomaliesPayload?.count ?? anomalyFeatures.length;
anomalyCountByCollector.clear();
totalIncidentCount = incidentsPayload?.count ?? incidentFeatures.length;
activeEventCountByCollector.clear();
spreadCollectorPositions(
collectorFeatures
@@ -522,7 +641,12 @@ export async function loadBGPAnomalies(scene, earth) {
.filter(Boolean),
).forEach(createCollectorMarker);
dedupeAnomalies(anomalyFeatures).forEach(createAnomalyMarker);
const incidentMarkers = dedupeIncidents(incidentFeatures);
if (incidentMarkers.length > 0) {
incidentMarkers.forEach(createAnomalyMarker);
} else {
dedupeAnomalies(anomalyFeatures).forEach(createAnomalyMarker);
}
applyCollectorCounts();
if (!bgpGroup.parent) {
@@ -540,7 +664,8 @@ export async function loadBGPAnomalies(scene, earth) {
}
return {
totalCount: totalAnomalyCount,
totalCount: totalIncidentCount,
anomalyCount: totalAnomalyCount,
renderedCount: anomalyMarkers.length,
collectorCount: collectorMarkers.length,
};
@@ -644,8 +769,9 @@ export function clearBGPData(earth) {
clearMarkerArray(collectorMarkers);
clearMarkerArray(anomalyMarkers);
clearBGPEventOverlay();
anomalyCountByCollector.clear();
activeEventCountByCollector.clear();
totalAnomalyCount = 0;
totalIncidentCount = 0;
if (earth && bgpGroup.parent === earth) {
earth.remove(bgpGroup);
@@ -684,7 +810,27 @@ export function getBGPCollectorMarkers() {
}
export function getBGPCount() {
return totalAnomalyCount;
return totalIncidentCount;
}
export function getBGPCollectorCount() {
return collectorMarkers.length;
}
export function getBGPStatusSummary() {
if (totalIncidentCount > 0 && totalAnomalyCount > 0) {
return `${totalIncidentCount} 起活跃事件 / ${totalAnomalyCount} 条异常`;
}
if (totalIncidentCount > 0) {
return `${totalIncidentCount} 起活跃事件`;
}
if (totalAnomalyCount > 0) {
return `${totalAnomalyCount} 条活跃异常`;
}
if (collectorMarkers.length > 0) {
return "当前无活跃事件";
}
return "暂无观测数据";
}
export function showBGPEventOverlay(marker, earth) {
@@ -779,9 +925,9 @@ export function getBGPLegendItems() {
{ color: "#6db7ff", label: "观测站" },
{ color: "#8af5ff", label: "事件连线 / 枢纽" },
{ color: "#2dd4bf", label: "影响区域" },
{ color: "#ff4d4f", label: "严重异常" },
{ color: "#ff9f43", label: "高危异常" },
{ color: "#ffd166", label: "中危异常" },
{ color: "#4dabf7", label: "低危异常" },
{ color: "#ff4d4f", label: "严重事件" },
{ color: "#ff9f43", label: "高危事件" },
{ color: "#ffd166", label: "中危事件" },
{ color: "#4dabf7", label: "低危事件" },
];
}