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: "低危事件" },
];
}

View File

@@ -27,6 +27,7 @@ export const PATHS = {
cablesApi: '/api/v1/visualization/geo/cables',
landingPointsApi: '/api/v1/visualization/geo/landing-points',
bgpApi: '/api/v1/visualization/geo/bgp-anomalies',
bgpIncidentsApi: '/api/v1/visualization/geo/bgp-incidents',
bgpCollectorsApi: '/api/v1/visualization/geo/bgp-collectors',
geoJSON: './geo.json',
landingPointsStatic: './landing-point-geo.geojson',

View File

@@ -32,23 +32,23 @@ const CARD_CONFIG = {
},
bgp: {
icon: '📡',
title: 'BGP异常详情',
title: 'BGP事件详情',
className: 'bgp',
fields: [
{ key: 'anomaly_type', label: '异常类型' },
{ key: 'anomaly_type', label: '事件类型' },
{ key: 'severity', label: '严重度' },
{ key: 'status', label: '状态' },
{ key: 'route_change', label: '路由变更' },
{ key: 'route_change', label: '事件特征' },
{ key: 'prefix', label: '前缀' },
{ key: 'as_path_display', label: '传播路径' },
{ key: 'origin_asn', label: '原始 ASN' },
{ key: 'new_origin_asn', label: ' ASN' },
{ key: 'origin_asn', label: '涉及 ASN' },
{ key: 'new_origin_asn', label: '关联 ASN' },
{ key: 'confidence', label: '置信度' },
{ key: 'collector', label: '采集器' },
{ key: 'collector', label: '主观测站' },
{ key: 'observed_by', label: '观测范围' },
{ key: 'impacted_scope', label: '影响区域' },
{ key: 'location', label: '观测位置' },
{ key: 'created_at', label: '发生时间' },
{ key: 'created_at', label: '事件时间' },
{ key: 'summary', label: '摘要' }
]
},
@@ -59,7 +59,7 @@ const CARD_CONFIG = {
fields: [
{ key: 'collector', label: '采集器' },
{ key: 'location', label: '观测位置' },
{ key: 'anomaly_count', label: '当前异常数' },
{ key: 'anomaly_count', label: '当前事件数' },
{ key: 'status', label: '状态' }
]
},

View File

@@ -72,6 +72,8 @@ import {
getBGPCollectorMarkers,
getBGPLegendItems,
getBGPCount,
getBGPCollectorCount,
getBGPStatusSummary,
getShowBGP,
clearBGPSelection,
setBGPMarkerState,
@@ -382,24 +384,44 @@ function showBGPInfo(marker) {
},
];
showInfoCard("bgp", {
anomaly_type: formatBGPAnomalyTypeLabel(marker.userData.anomaly_type),
anomaly_type: formatBGPAnomalyTypeLabel(
marker.userData.incident_type || marker.userData.anomaly_type,
),
severity: formatBGPSeverityLabel(
marker.userData.rawSeverity || marker.userData.severity,
),
status: formatBGPStatusLabel(marker.userData.status),
route_change: formatBGPRouteChange(
marker.userData.origin_asn,
marker.userData.new_origin_asn,
),
prefix: marker.userData.prefix,
as_path_display: formatBGPASPath(marker.userData.as_path),
origin_asn: marker.userData.origin_asn,
new_origin_asn: marker.userData.new_origin_asn,
route_change:
marker.userData.route_change ||
formatBGPRouteChange(
marker.userData.origin_asn,
marker.userData.new_origin_asn,
),
prefix:
Array.isArray(marker.userData.prefixes) && marker.userData.prefixes.length > 1
? `${marker.userData.prefixes[0]}${marker.userData.prefixes.length}`
: marker.userData.prefix,
as_path_display:
Array.isArray(marker.userData.as_path) && marker.userData.as_path.length > 0
? formatBGPASPath(marker.userData.as_path)
: "-",
origin_asn:
Array.isArray(marker.userData.affected_asns) && marker.userData.affected_asns.length > 0
? marker.userData.affected_asns.slice(0, 3).map((asn) => `AS${asn}`).join(", ")
: marker.userData.origin_asn,
new_origin_asn:
Array.isArray(marker.userData.affected_asns) && marker.userData.affected_asns.length > 3
? `${marker.userData.affected_asns.length}个ASN`
: marker.userData.new_origin_asn,
confidence: formatBGPConfidence(marker.userData.confidence),
collector: marker.userData.collector,
observed_by: formatBGPObservedBy(marker.userData.collectors),
observed_by:
marker.userData.observed_by ||
formatBGPObservedBy(marker.userData.collectors),
impacted_scope: formatBGPImpactedScope(impactedRegions),
location: formatBGPLocation(marker.userData.city, marker.userData.country),
location:
marker.userData.location ||
formatBGPLocation(marker.userData.city, marker.userData.country),
created_at: formatBGPObservedTime(marker.userData.created_at_raw),
summary: marker.userData.summary,
});
@@ -603,6 +625,8 @@ function updateStatsSummary() {
landingPointCount:
document.getElementById("landing-point-count")?.textContent || 0,
bgpAnomalyCount: `${getBGPCount()}`,
bgpCollectorCount: `${getBGPCollectorCount()}`,
bgpStatusSummary: getBGPStatusSummary(),
terrainOn: getShowTerrain(),
textureQuality: "8K 卫星图",
});
@@ -717,8 +741,8 @@ async function loadData(showWhiteSphere = false) {
setLoadingMessage(
showWhiteSphere ? "正在刷新全球态势数据..." : "正在初始化全球态势数据...",
showWhiteSphere
? "重新同步卫星、海底光缆、登陆点与BGP异常数据"
: "同步卫星、海底光缆、登陆点与BGP异常数据",
? "重新同步卫星、海底光缆、登陆点与BGP态势数据"
: "同步卫星、海底光缆、登陆点与BGP态势数据",
);
setLoading(true);
clearLockedObject();
@@ -746,7 +770,20 @@ async function loadData(showWhiteSphere = false) {
}
const bgpCountEl = document.getElementById("bgp-anomaly-count");
if (bgpCountEl) {
bgpCountEl.textContent = `${bgpResult.totalCount} `;
bgpCountEl.textContent = `${bgpResult.totalCount} `;
}
const bgpCollectorEl = document.getElementById("bgp-collector-count");
if (bgpCollectorEl) {
bgpCollectorEl.textContent = `${bgpResult.collectorCount}`;
}
const bgpStatusEl = document.getElementById("bgp-status-summary");
if (bgpStatusEl) {
bgpStatusEl.textContent =
bgpResult.totalCount > 0
? `${bgpResult.totalCount} 起活跃事件`
: bgpResult.anomalyCount > 0
? `${bgpResult.anomalyCount} 条活跃异常`
: "当前无活跃事件";
}
return bgpResult;
})(),
@@ -765,7 +802,7 @@ async function loadData(showWhiteSphere = false) {
errors.push({ label: "卫星", reason: results[1].reason });
}
if (results[2].status === "rejected") {
errors.push({ label: "BGP异常", reason: results[2].reason });
errors.push({ label: "BGP态势", reason: results[2].reason });
}
if (errors.length > 0) {
@@ -1175,7 +1212,7 @@ function onClick(event) {
showBGPEventOverlay(clickedMarker, earth);
showBGPInfo(clickedMarker);
showStatusMessage(
`已选择BGP异常: ${clickedMarker.userData.collector}`,
`已选择BGP事件: ${clickedMarker.userData.collector}`,
"info",
);
return;

View File

@@ -86,6 +86,8 @@ export function updateEarthStats(stats) {
const cableCountEl = document.getElementById("cable-count");
const landingPointCountEl = document.getElementById("landing-point-count");
const bgpAnomalyCountEl = document.getElementById("bgp-anomaly-count");
const bgpCollectorCountEl = document.getElementById("bgp-collector-count");
const bgpStatusSummaryEl = document.getElementById("bgp-status-summary");
const terrainStatusEl = document.getElementById("terrain-status");
const textureQualityEl = document.getElementById("texture-quality");
@@ -94,6 +96,10 @@ export function updateEarthStats(stats) {
landingPointCountEl.textContent = stats.landingPointCount || 0;
if (bgpAnomalyCountEl)
bgpAnomalyCountEl.textContent = stats.bgpAnomalyCount || 0;
if (bgpCollectorCountEl)
bgpCollectorCountEl.textContent = stats.bgpCollectorCount || 0;
if (bgpStatusSummaryEl)
bgpStatusSummaryEl.textContent = stats.bgpStatusSummary || "-";
if (terrainStatusEl)
terrainStatusEl.textContent = stats.terrainOn ? "开启" : "关闭";
if (textureQualityEl)