refactor: simplify earth bgp fallback and geography helpers

This commit is contained in:
linkong
2026-04-02 16:26:31 +08:00
parent e5fec8ba3d
commit f01d24240f
9 changed files with 191 additions and 190 deletions

View File

@@ -14,3 +14,4 @@
- [ ] 接入 `OpenGeoFeed` 作为 prefix geography 的高质量覆盖/override 数据源
- [ ] 把 RIR delegated / `inetnum` / `inet6num` whois 设计成 prefix geography 的 fallback而不是主来源
- [x] 在 activity layer 之后继续补 `route leak``path instability / flap` detector
- [ ] 对 [frontend/public/earth/js/bgp.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/bgp.js) 做按职责拆分的小重构,拆成 data / markers / overlays / animation降低后续维护复杂度

View File

@@ -1 +1 @@
0.22.10
0.22.11

View File

@@ -19,7 +19,6 @@ from app.models.bgp_anomaly import BGPAnomaly
from app.models.bgp_incident import BGPIncident
from app.models.collected_data import CollectedData
from app.services.bgp_collectors import build_bgp_collector_coverage
from app.services.bgp_enrichment import _lookup_prefix_geography
from app.services.cable_graph import build_graph_from_data, CableGraph, haversine_distance
from app.services.collectors.bgp_common import RIPE_RIS_COLLECTOR_COORDS
@@ -437,44 +436,9 @@ async def build_anomaly_geography_hints(
) -> Dict[str, Dict[str, Any]]:
hints: Dict[str, Dict[str, Any]] = {}
for record in records:
evidence = record.evidence or {}
key = str(record.entity_key or record.id)
prefix_geo_regions = []
prefix_regions = []
asn_regions = []
evidence_prefix_geography = evidence.get("prefix_geography") or {}
prefix_geo_regions.extend(
_normalize_geo_regions(evidence_prefix_geography.get("regions") or [])
)
prefix_scope = evidence.get("prefix_scope") or {}
prefix_regions.extend(_normalize_geo_regions(prefix_scope.get("regions") or []))
for profile_key in ("origin_asn_profile", "new_origin_asn_profile"):
profile = evidence.get(profile_key) or {}
latitude = profile.get("latitude")
longitude = profile.get("longitude")
if isinstance(latitude, (int, float)) and isinstance(longitude, (int, float)):
asn_regions.append(
{
"country": profile.get("country"),
"city": profile.get("city"),
"latitude": float(latitude),
"longitude": float(longitude),
}
)
prefix_geo_regions = _normalize_geo_regions(prefix_geo_regions)
prefix_regions = _normalize_geo_regions(prefix_regions)
asn_regions = _normalize_geo_regions(asn_regions)
if prefix_geo_regions:
hints[key] = {"regions": prefix_geo_regions, "geography_mode": "prefix_geography"}
elif prefix_regions:
hints[key] = {"regions": prefix_regions, "geography_mode": "prefix_scope"}
elif asn_regions:
hints[key] = {"regions": asn_regions, "geography_mode": "asn_region"}
hint = _extract_evidence_geography_hint(record.evidence or {})
if hint:
hints[str(record.entity_key or record.id)] = hint
return hints
@@ -597,6 +561,46 @@ def _normalize_geo_regions(regions: List[Dict[str, Any]]) -> List[Dict[str, Any]
return normalized
def _extract_evidence_geography_hint(evidence: Dict[str, Any]) -> Dict[str, Any] | None:
prefix_geo_regions = []
prefix_regions = []
asn_regions = []
evidence_prefix_geography = evidence.get("prefix_geography") or {}
prefix_geo_regions.extend(
_normalize_geo_regions(evidence_prefix_geography.get("regions") or [])
)
prefix_scope = evidence.get("prefix_scope") or {}
prefix_regions.extend(_normalize_geo_regions(prefix_scope.get("regions") or []))
for profile_key in ("origin_asn_profile", "new_origin_asn_profile"):
profile = evidence.get(profile_key) or {}
latitude = profile.get("latitude")
longitude = profile.get("longitude")
if isinstance(latitude, (int, float)) and isinstance(longitude, (int, float)):
asn_regions.append(
{
"country": profile.get("country"),
"city": profile.get("city"),
"latitude": float(latitude),
"longitude": float(longitude),
}
)
prefix_geo_regions = _normalize_geo_regions(prefix_geo_regions)
prefix_regions = _normalize_geo_regions(prefix_regions)
asn_regions = _normalize_geo_regions(asn_regions)
if prefix_geo_regions:
return {"regions": prefix_geo_regions, "geography_mode": "prefix_geography"}
if prefix_regions:
return {"regions": prefix_regions, "geography_mode": "prefix_scope"}
if asn_regions:
return {"regions": asn_regions, "geography_mode": "asn_region"}
return None
async def build_incident_geography_hints(
db: AsyncSession,
records: List[BGPIncident],
@@ -624,53 +628,33 @@ async def build_incident_geography_hints(
hints: Dict[str, Dict[str, Any]] = {}
for record in records:
prefix_geo_regions: list[dict[str, Any]] = []
prefix_regions: list[dict[str, Any]] = []
asn_regions: list[dict[str, Any]] = []
merged_hint: Dict[str, Any] | None = None
priority = {"prefix_geography": 3, "prefix_scope": 2, "asn_region": 1}
for ref in record.evidence_refs or []:
anomaly = anomaly_by_key.get(str(ref))
if anomaly is None:
continue
evidence = anomaly.evidence or {}
if not prefix_geo_regions:
prefix_geography = evidence.get("prefix_geography") or {}
prefix_geo_regions.extend(_normalize_geo_regions(prefix_geography.get("regions") or []))
prefix_scope = evidence.get("prefix_scope") or {}
prefix_regions.extend(_normalize_geo_regions(prefix_scope.get("regions") or []))
hint = _extract_evidence_geography_hint(anomaly.evidence or {})
if hint is None:
continue
if merged_hint is None:
merged_hint = {
"regions": list(hint["regions"]),
"geography_mode": hint["geography_mode"],
}
continue
if priority[hint["geography_mode"]] > priority[merged_hint["geography_mode"]]:
merged_hint = {
"regions": list(hint["regions"]),
"geography_mode": hint["geography_mode"],
}
elif priority[hint["geography_mode"]] == priority[merged_hint["geography_mode"]]:
merged_hint["regions"].extend(hint["regions"])
for key in ("origin_asn_profile", "new_origin_asn_profile"):
profile = evidence.get(key) or {}
latitude = profile.get("latitude")
longitude = profile.get("longitude")
if isinstance(latitude, (int, float)) and isinstance(longitude, (int, float)):
asn_regions.append(
{
"country": profile.get("country"),
"city": profile.get("city"),
"latitude": float(latitude),
"longitude": float(longitude),
}
)
prefix_geo_regions = _normalize_geo_regions(prefix_geo_regions)
prefix_regions = _normalize_geo_regions(prefix_regions)
asn_regions = _normalize_geo_regions(asn_regions)
if prefix_geo_regions:
hints[record.incident_key] = {
"regions": prefix_geo_regions,
"geography_mode": "prefix_geography",
}
elif prefix_regions:
hints[record.incident_key] = {
"regions": prefix_regions,
"geography_mode": "prefix_scope",
}
elif asn_regions:
hints[record.incident_key] = {
"regions": asn_regions,
"geography_mode": "asn_region",
}
if merged_hint:
merged_hint["regions"] = _normalize_geo_regions(merged_hint["regions"])
hints[record.incident_key] = merged_hint
return hints

View File

@@ -7,6 +7,31 @@ This project follows the repository versioning rule:
- `feature` -> `+0.1.0`
- `bugfix` -> `+0.0.1`
## 0.22.11
Released: 2026-04-02
### Highlights
- Cleaned up the most obvious BGP/Earth cleanup leftovers from the recent stabilization work without changing the current user-facing interaction model.
### Improved
- Improved [bgp.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/bgp.js) by factoring the BGP Earth loading flow into smaller helpers, separating timed GeoJSON fetch fallback from `incident vs anomaly` render-mode selection.
- Improved [bgp.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/bgp.js) by extracting shared feature parsing helpers so anomaly and incident marker preparation no longer duplicate coordinate, severity, and timestamp parsing logic.
- Improved [visualization.py](/home/ray/dev/linkong/planet/backend/app/api/v1/visualization.py) by consolidating repeated BGP evidence geography parsing into a shared helper used by both anomaly and incident geography-hint builders.
- Improved planning hygiene in [TODO.md](/home/ray/dev/linkong/planet/TODO.md) by recording the deferred `bgp.js` responsibility split as an explicit follow-up task instead of leaving the idea only in conversation context.
### Refined
- Refined [constants.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/constants.js) by removing stale Earth BGP configuration entries that were left behind after the floating event hub design was removed.
- Refined [bgp.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/bgp.js) by removing no-longer-used arc helpers that only served the deleted off-surface hub/link route.
- Refined [visualization.py](/home/ray/dev/linkong/planet/backend/app/api/v1/visualization.py) by dropping an unused `_lookup_prefix_geography` import after the earlier rollback from expensive live prefix-geography visualization lookups.
### Fixed
- Fixed the codebase drift where Earth BGP cleanup patches had left behind dead constants, duplicate parsing branches, and fallback-loading glue that was harder to reason about than the now-stable runtime required.
## 0.22.10
Released: 2026-04-02

View File

@@ -1,12 +1,12 @@
{
"name": "planet-frontend",
"version": "0.22.10",
"version": "0.22.11",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "planet-frontend",
"version": "0.22.10",
"version": "0.22.11",
"dependencies": {
"@ant-design/icons": "^5.2.6",
"antd": "^5.12.5",

View File

@@ -1,6 +1,6 @@
{
"name": "planet-frontend",
"version": "0.22.10",
"version": "0.22.11",
"private": true,
"dependencies": {
"@ant-design/icons": "^5.2.6",

View File

@@ -636,26 +636,17 @@ function spreadCollectorPositions(markers) {
}
function buildAnomalyFeatureData(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 point = extractFeaturePoint(feature);
if (!point) return null;
const { latitude, longitude } = point;
const properties = feature?.properties || {};
const severity = normalizeSeverity(properties.severity);
const createdAt = properties.created_at || null;
const meta = extractFeatureMeta(properties, properties.created_at || null);
return {
latitude,
longitude,
rawSeverity: properties.severity || severity,
severity,
rawSeverity: meta.rawSeverity,
severity: meta.severity,
collector: properties.collector || "-",
city: properties.city || "-",
country: properties.country || "-",
@@ -673,8 +664,8 @@ function buildAnomalyFeatureData(feature) {
: [],
confidence: properties.confidence ?? "-",
summary: properties.summary || "-",
created_at: formatLocalDateTime(createdAt),
created_at_raw: createdAt,
created_at: meta.createdAt,
created_at_raw: meta.createdAtRaw,
id:
properties.id ||
`${properties.collector || "unknown"}-${latitude}-${longitude}`,
@@ -682,20 +673,12 @@ 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 point = extractFeaturePoint(feature);
if (!point) return null;
const { latitude, longitude } = point;
const properties = feature?.properties || {};
const severity = normalizeSeverity(properties.severity);
const startedAt = properties.started_at || properties.created_at || null;
const meta = extractFeatureMeta(properties, startedAt);
const affectedPrefixes = Array.isArray(properties.affected_prefixes)
? properties.affected_prefixes
: [];
@@ -714,8 +697,8 @@ function buildIncidentFeatureData(feature) {
return {
latitude,
longitude,
rawSeverity: properties.severity || severity,
severity,
rawSeverity: meta.rawSeverity,
severity: meta.severity,
collector: affectedCollectors[0] || primaryRegion.collector || "-",
city: primaryRegion.city || "-",
country: primaryRegion.country || "-",
@@ -741,8 +724,8 @@ function buildIncidentFeatureData(feature) {
: [],
confidence: properties.confidence ?? "-",
summary: properties.summary || properties.title || "-",
created_at: formatLocalDateTime(startedAt),
created_at_raw: startedAt,
created_at: meta.createdAt,
created_at_raw: meta.createdAtRaw,
route_change:
affectedAsns.length > 1
? affectedAsns.slice(0, 2).map((asn) => `AS${asn}`).join(" -> ")
@@ -759,6 +742,30 @@ function buildIncidentFeatureData(feature) {
};
}
function extractFeaturePoint(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;
}
return { latitude, longitude };
}
function extractFeatureMeta(properties, createdAtRaw) {
const severity = normalizeSeverity(properties.severity);
return {
rawSeverity: properties.severity || severity,
severity,
createdAt: formatLocalDateTime(createdAtRaw),
createdAtRaw: createdAtRaw,
};
}
function clearMarkerArray(markers) {
while (markers.length > 0) {
const marker = markers.pop();
@@ -798,32 +805,6 @@ function createOverlaySprite({ color, opacity, scale }) {
return sprite;
}
function createArcLine(start, end, color, opacity = 0.82) {
const points = createArcPoints(start, end);
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial({
color,
transparent: true,
opacity,
depthWrite: false,
blending: THREE.AdditiveBlending,
});
return new THREE.Line(geometry, material);
}
function createArcPoints(start, end, heightOffset = BGP_CONFIG.eventHubAltitudeOffset * 0.8, segments = 32) {
const midpoint = start
.clone()
.add(end)
.multiplyScalar(0.5)
.normalize()
.multiplyScalar(CONFIG.earthRadius + heightOffset);
const curve = new THREE.QuadraticBezierCurve3(start, midpoint, end);
return curve.getPoints(segments);
}
function projectLatLon(lat, lon, bearingDeg, distanceDeg) {
const latRad = (lat * Math.PI) / 180;
const lonRad = (lon * Math.PI) / 180;
@@ -1237,6 +1218,43 @@ function applyCollectorCounts() {
});
}
async function fetchGeoJSONWithTimeout(url, timeoutMs, warningMessage, fallbackPayload) {
try {
const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.warn(warningMessage, error);
return fallbackPayload;
}
}
function selectBGPEventFeatures(incidentPayload, anomalyPayload) {
const incidentFeatures = Array.isArray(incidentPayload?.features)
? incidentPayload.features
: [];
if (incidentFeatures.length > 0) {
return {
features: incidentFeatures,
totalIncidentCount: incidentPayload?.count ?? incidentFeatures.length,
totalAnomalyCount: anomalyPayload?.count ?? 0,
mode: "incident",
};
}
const anomalyFeatures = Array.isArray(anomalyPayload?.features)
? anomalyPayload.features
: [];
return {
features: anomalyFeatures,
totalIncidentCount: 0,
totalAnomalyCount: anomalyPayload?.count ?? anomalyFeatures.length,
mode: "anomaly",
};
}
export async function loadBGPAnomalies(scene, earth) {
clearBGPData(earth);
@@ -1245,47 +1263,27 @@ export async function loadBGPAnomalies(scene, earth) {
throw new Error(`BGP collectors HTTP ${collectorsResponse.status}`);
}
let anomaliesPayload = { type: "FeatureCollection", features: [], count: 0 };
try {
const anomaliesResponse = await fetch(
`${PATHS.bgpApi}?limit=${BGP_CONFIG.defaultFetchLimit}`,
{ signal: AbortSignal.timeout(5000) },
);
if (!anomaliesResponse.ok) {
throw new Error(`BGP anomalies HTTP ${anomaliesResponse.status}`);
}
anomaliesPayload = await anomaliesResponse.json();
} catch (error) {
console.warn("BGP anomalies unavailable, falling back to collectors only:", error);
}
let incidentsPayload = { type: "FeatureCollection", features: [], count: 0 };
try {
const incidentsResponse = await fetch(
`${PATHS.bgpIncidentsApi}?limit=${BGP_CONFIG.defaultFetchLimit}`,
{ signal: AbortSignal.timeout(5000) },
);
if (!incidentsResponse.ok) {
throw new Error(`BGP incidents HTTP ${incidentsResponse.status}`);
}
incidentsPayload = await incidentsResponse.json();
} catch (error) {
console.warn("BGP incidents unavailable, falling back to anomalies:", error);
}
const emptyPayload = { type: "FeatureCollection", features: [], count: 0 };
const anomaliesPayload = await fetchGeoJSONWithTimeout(
`${PATHS.bgpApi}?limit=${BGP_CONFIG.defaultFetchLimit}`,
5000,
"BGP anomalies unavailable, falling back to collectors only:",
emptyPayload,
);
const incidentsPayload = await fetchGeoJSONWithTimeout(
`${PATHS.bgpIncidentsApi}?limit=${BGP_CONFIG.defaultFetchLimit}`,
5000,
"BGP incidents unavailable, falling back to anomalies:",
emptyPayload,
);
const collectorsPayload = await collectorsResponse.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;
totalIncidentCount = incidentsPayload?.count ?? incidentFeatures.length;
const selectedEventData = selectBGPEventFeatures(incidentsPayload, anomaliesPayload);
totalAnomalyCount = selectedEventData.totalAnomalyCount;
totalIncidentCount = selectedEventData.totalIncidentCount;
activeEventCountByCollector.clear();
spreadCollectorPositions(
@@ -1294,11 +1292,10 @@ export async function loadBGPAnomalies(scene, earth) {
.filter(Boolean),
).forEach(createCollectorMarker);
const incidentMarkers = dedupeIncidents(incidentFeatures);
if (incidentMarkers.length > 0) {
incidentMarkers.forEach(createAnomalyMarker);
if (selectedEventData.mode === "incident") {
dedupeIncidents(selectedEventData.features).forEach(createAnomalyMarker);
} else {
dedupeAnomalies(anomalyFeatures).forEach(createAnomalyMarker);
dedupeAnomalies(selectedEventData.features).forEach(createAnomalyMarker);
}
applyCollectorCounts();

View File

@@ -29,8 +29,6 @@ export const PATHS = {
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',
};
// Cable colors mapping
@@ -83,8 +81,6 @@ export const BGP_CONFIG = {
dimmedScale: 0.92,
pulseSpeed: 0.0045,
collectorPulseSpeed: 0.0024,
eventHubAltitudeOffset: 7.2,
eventHubScale: 4.8,
regionScale: 11.5,
normalPulseAmplitude: 0.08,
lockedPulseAmplitude: 0.28,
@@ -135,8 +131,6 @@ export const BGP_CONFIG = {
lockedNeutralColor: 0xd8e4ed,
dimmedColor: 0x7d8ca3,
},
eventHubColor: 0x8af5ff,
linkColor: 0x54d2ff,
regionColor: 0x2dd4bf,
eventRingScaleA: 2.5,
eventRingScaleB: 3.4,

View File

@@ -1,6 +1,6 @@
[project]
name = "planet"
version = "0.22.10"
version = "0.22.11"
description = "智能星球计划 - 态势感知系统"
requires-python = ">=3.14"
dependencies = [