fix: stabilize earth bgp geography and rendering
This commit is contained in:
@@ -5,6 +5,7 @@ Returns GeoJSON format compatible with Three.js, CesiumJS, and Unreal Cesium.
|
||||
"""
|
||||
|
||||
from datetime import UTC, datetime
|
||||
import math
|
||||
from fastapi import APIRouter, HTTPException, Depends, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func
|
||||
@@ -18,7 +19,8 @@ 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.cable_graph import build_graph_from_data, CableGraph
|
||||
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
|
||||
|
||||
router = APIRouter()
|
||||
@@ -316,11 +318,16 @@ def convert_gpu_cluster_to_geojson(records: List[CollectedData]) -> Dict[str, An
|
||||
return {"type": "FeatureCollection", "features": features}
|
||||
|
||||
|
||||
def convert_bgp_anomalies_to_geojson(records: List[BGPAnomaly]) -> Dict[str, Any]:
|
||||
def convert_bgp_anomalies_to_geojson(
|
||||
records: List[BGPAnomaly],
|
||||
geography_hints: Optional[Dict[str, Dict[str, Any]]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
features = []
|
||||
geography_hints = geography_hints or {}
|
||||
|
||||
for record in records:
|
||||
evidence = record.evidence or {}
|
||||
hint = geography_hints.get(str(record.entity_key or record.id), {})
|
||||
collectors = evidence.get("collectors") or record.peer_scope or []
|
||||
if not collectors:
|
||||
nested = evidence.get("events") or []
|
||||
@@ -334,23 +341,6 @@ def convert_bgp_anomalies_to_geojson(records: List[BGPAnomaly]) -> Dict[str, Any
|
||||
if not collectors:
|
||||
collectors = []
|
||||
|
||||
collector = collectors[0] if collectors else None
|
||||
location = None
|
||||
if collector:
|
||||
location = RIPE_RIS_COLLECTOR_COORDS.get(str(collector))
|
||||
|
||||
if location is None:
|
||||
nested = evidence.get("events") or []
|
||||
for item in nested:
|
||||
collector_name = (item or {}).get("collector")
|
||||
if collector_name and collector_name in RIPE_RIS_COLLECTOR_COORDS:
|
||||
location = RIPE_RIS_COLLECTOR_COORDS[collector_name]
|
||||
collector = collector_name
|
||||
break
|
||||
|
||||
if location is None:
|
||||
continue
|
||||
|
||||
as_path = []
|
||||
if isinstance(evidence.get("as_path"), list):
|
||||
as_path = evidence.get("as_path") or []
|
||||
@@ -385,6 +375,28 @@ def convert_bgp_anomalies_to_geojson(records: List[BGPAnomaly]) -> Dict[str, Any
|
||||
}
|
||||
)
|
||||
|
||||
geography_regions = _normalize_geo_regions(hint.get("regions") or [])
|
||||
geography_mode = hint.get("geography_mode") or "collector_centroid"
|
||||
|
||||
collector = collectors[0] if collectors else None
|
||||
location = geography_regions[0] if geography_regions else None
|
||||
|
||||
if location is None and collector:
|
||||
location = RIPE_RIS_COLLECTOR_COORDS.get(str(collector))
|
||||
|
||||
if location is None:
|
||||
nested = evidence.get("events") or []
|
||||
for item in nested:
|
||||
collector_name = (item or {}).get("collector")
|
||||
if collector_name and collector_name in RIPE_RIS_COLLECTOR_COORDS:
|
||||
location = RIPE_RIS_COLLECTOR_COORDS[collector_name]
|
||||
collector = collector_name
|
||||
geography_mode = "collector_centroid"
|
||||
break
|
||||
|
||||
if location is None:
|
||||
continue
|
||||
|
||||
features.append(
|
||||
{
|
||||
"type": "Feature",
|
||||
@@ -408,6 +420,7 @@ def convert_bgp_anomalies_to_geojson(records: List[BGPAnomaly]) -> Dict[str, Any
|
||||
"collector_count": len(collectors) or 1,
|
||||
"as_path": as_path,
|
||||
"impacted_regions": impacted_regions,
|
||||
"geography_mode": geography_mode,
|
||||
"confidence": record.confidence,
|
||||
"summary": record.summary,
|
||||
"created_at": to_iso8601_utc(record.created_at),
|
||||
@@ -418,6 +431,54 @@ def convert_bgp_anomalies_to_geojson(records: List[BGPAnomaly]) -> Dict[str, Any
|
||||
return {"type": "FeatureCollection", "features": features}
|
||||
|
||||
|
||||
async def build_anomaly_geography_hints(
|
||||
db: AsyncSession,
|
||||
records: List[BGPAnomaly],
|
||||
) -> 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"}
|
||||
|
||||
return hints
|
||||
|
||||
|
||||
def convert_bgp_collectors_to_geojson(
|
||||
coverage_by_collector: Dict[str, Dict[str, Any]] | None = None,
|
||||
) -> Dict[str, Any]:
|
||||
@@ -442,8 +503,10 @@ def convert_bgp_collectors_to_geojson(
|
||||
"prefix_count": coverage.get("prefix_count", 0),
|
||||
"origin_asn_count": coverage.get("origin_asn_count", 0),
|
||||
"peer_asn_count": coverage.get("peer_asn_count", 0),
|
||||
"recent_15m_observation_count": coverage.get("recent_15m_observation_count", 0),
|
||||
"recent_24h_observation_count": coverage.get("recent_24h_observation_count", 0),
|
||||
"recent_7d_observation_count": coverage.get("recent_7d_observation_count", 0),
|
||||
"recent_15m_prefix_count": coverage.get("recent_15m_prefix_count", 0),
|
||||
"recent_24h_prefix_count": coverage.get("recent_24h_prefix_count", 0),
|
||||
"recent_7d_prefix_count": coverage.get("recent_7d_prefix_count", 0),
|
||||
"top_event_types": coverage.get("top_event_types", []),
|
||||
@@ -463,33 +526,183 @@ def convert_bgp_collectors_to_geojson(
|
||||
return {"type": "FeatureCollection", "features": features}
|
||||
|
||||
|
||||
def convert_bgp_incidents_to_geojson(records: List[BGPIncident]) -> Dict[str, Any]:
|
||||
def _incident_estimated_center(valid_regions: List[Dict[str, Any]]) -> Dict[str, float]:
|
||||
x = 0.0
|
||||
y = 0.0
|
||||
z = 0.0
|
||||
for region in valid_regions:
|
||||
lat_rad = math.radians(float(region["latitude"]))
|
||||
lon_rad = math.radians(float(region["longitude"]))
|
||||
x += math.cos(lat_rad) * math.cos(lon_rad)
|
||||
y += math.cos(lat_rad) * math.sin(lon_rad)
|
||||
z += math.sin(lat_rad)
|
||||
|
||||
total = float(len(valid_regions))
|
||||
if total <= 0:
|
||||
return {"latitude": 0.0, "longitude": 0.0}
|
||||
|
||||
x /= total
|
||||
y /= total
|
||||
z /= total
|
||||
hyp = math.sqrt((x * x) + (y * y))
|
||||
if hyp == 0:
|
||||
return {"latitude": 0.0, "longitude": 0.0}
|
||||
|
||||
return {
|
||||
"latitude": math.degrees(math.atan2(z, hyp)),
|
||||
"longitude": math.degrees(math.atan2(y, x)),
|
||||
}
|
||||
|
||||
|
||||
def _incident_estimated_radius_km(center: Dict[str, float], valid_regions: List[Dict[str, Any]]) -> float:
|
||||
center_coords = (float(center["longitude"]), float(center["latitude"]))
|
||||
distances = [
|
||||
haversine_distance(
|
||||
center_coords,
|
||||
(float(region["longitude"]), float(region["latitude"])),
|
||||
)
|
||||
for region in valid_regions
|
||||
]
|
||||
return round(max(distances) if distances else 0.0, 1)
|
||||
|
||||
|
||||
def _normalize_geo_regions(regions: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
normalized: list[dict[str, Any]] = []
|
||||
seen: set[tuple[Any, ...]] = set()
|
||||
for region in regions:
|
||||
if not isinstance(region, dict):
|
||||
continue
|
||||
latitude = region.get("latitude")
|
||||
longitude = region.get("longitude")
|
||||
if not isinstance(latitude, (int, float)) or not isinstance(longitude, (int, float)):
|
||||
continue
|
||||
item = {
|
||||
"collector": region.get("collector"),
|
||||
"country": region.get("country"),
|
||||
"city": region.get("city"),
|
||||
"latitude": float(latitude),
|
||||
"longitude": float(longitude),
|
||||
}
|
||||
key = (
|
||||
item["collector"],
|
||||
item["country"],
|
||||
item["city"],
|
||||
item["latitude"],
|
||||
item["longitude"],
|
||||
)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
normalized.append(item)
|
||||
return normalized
|
||||
|
||||
|
||||
async def build_incident_geography_hints(
|
||||
db: AsyncSession,
|
||||
records: List[BGPIncident],
|
||||
) -> Dict[str, Dict[str, Any]]:
|
||||
evidence_refs = sorted(
|
||||
{
|
||||
str(ref)
|
||||
for record in records
|
||||
for ref in (record.evidence_refs or [])
|
||||
if ref
|
||||
}
|
||||
)
|
||||
|
||||
anomalies = []
|
||||
if evidence_refs:
|
||||
result = await db.execute(
|
||||
select(BGPAnomaly).where(BGPAnomaly.entity_key.in_(evidence_refs))
|
||||
)
|
||||
anomalies = result.scalars().all()
|
||||
anomaly_by_key = {
|
||||
str(anomaly.entity_key): anomaly
|
||||
for anomaly in anomalies
|
||||
if anomaly.entity_key
|
||||
}
|
||||
|
||||
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]] = []
|
||||
|
||||
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 []))
|
||||
|
||||
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",
|
||||
}
|
||||
|
||||
return hints
|
||||
|
||||
|
||||
def convert_bgp_incidents_to_geojson(
|
||||
records: List[BGPIncident],
|
||||
geography_hints: Optional[Dict[str, Dict[str, Any]]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
features = []
|
||||
|
||||
for record in records:
|
||||
regions = record.affected_regions or []
|
||||
hint = (geography_hints or {}).get(record.incident_key, {})
|
||||
regions = hint.get("regions") or (record.affected_regions or [])
|
||||
if not regions:
|
||||
continue
|
||||
|
||||
valid_regions = [
|
||||
region
|
||||
for region in regions
|
||||
if isinstance(region, dict)
|
||||
and isinstance(region.get("latitude"), (int, float))
|
||||
and isinstance(region.get("longitude"), (int, float))
|
||||
]
|
||||
valid_regions = _normalize_geo_regions(regions)
|
||||
if not valid_regions:
|
||||
continue
|
||||
|
||||
avg_lat = sum(float(region["latitude"]) for region in valid_regions) / len(valid_regions)
|
||||
avg_lon = sum(float(region["longitude"]) for region in valid_regions) / len(valid_regions)
|
||||
estimated_center = _incident_estimated_center(valid_regions)
|
||||
estimated_radius_km = _incident_estimated_radius_km(estimated_center, valid_regions)
|
||||
|
||||
features.append(
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [avg_lon, avg_lat],
|
||||
"coordinates": [
|
||||
estimated_center["longitude"],
|
||||
estimated_center["latitude"],
|
||||
],
|
||||
},
|
||||
"properties": {
|
||||
"id": record.id,
|
||||
@@ -504,6 +717,9 @@ def convert_bgp_incidents_to_geojson(records: List[BGPIncident]) -> Dict[str, An
|
||||
"affected_asns": record.affected_asns or [],
|
||||
"affected_collectors": record.affected_collectors or [],
|
||||
"affected_regions": valid_regions,
|
||||
"estimated_center": estimated_center,
|
||||
"estimated_radius_km": estimated_radius_km,
|
||||
"geography_mode": hint.get("geography_mode") or "collector_centroid",
|
||||
"related_cables": record.related_cables or [],
|
||||
"related_ixps": record.related_ixps or [],
|
||||
"created_at": to_iso8601_utc(record.created_at),
|
||||
@@ -739,7 +955,8 @@ async def get_bgp_anomalies_geojson(
|
||||
|
||||
result = await db.execute(stmt)
|
||||
records = list(result.scalars().all())
|
||||
geojson = convert_bgp_anomalies_to_geojson(records)
|
||||
geography_hints = await build_anomaly_geography_hints(db, records)
|
||||
geojson = convert_bgp_anomalies_to_geojson(records, geography_hints)
|
||||
return {**geojson, "count": len(geojson.get("features", []))}
|
||||
|
||||
|
||||
@@ -758,7 +975,8 @@ async def get_bgp_incidents_geojson(
|
||||
|
||||
result = await db.execute(stmt)
|
||||
records = list(result.scalars().all())
|
||||
geojson = convert_bgp_incidents_to_geojson(records)
|
||||
geography_hints = await build_incident_geography_hints(db, records)
|
||||
geojson = convert_bgp_incidents_to_geojson(records, geography_hints)
|
||||
return {**geojson, "count": len(geojson.get("features", []))}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user