feat: improve bgp incident visibility
This commit is contained in:
@@ -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: "低危事件" },
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user