"""Collector baseline and coverage helpers for BGP observations.""" from __future__ import annotations from collections import defaultdict from datetime import UTC, datetime, timedelta from typing import Any from sqlalchemy import case, distinct, func, select from sqlalchemy.ext.asyncio import AsyncSession from app.core.time import to_iso8601_utc from app.models.bgp_observation import BGPObservation from app.services.collectors.bgp_common import RIPE_RIS_COLLECTOR_COORDS def _collector_base_filters(source_filter: tuple[str, ...] | None) -> list[Any]: filters: list[Any] = [ BGPObservation.collector.isnot(None), func.length(func.btrim(BGPObservation.collector)) > 0, ] if source_filter: filters.append(BGPObservation.source.in_(source_filter)) return filters async def build_bgp_collector_coverage( db: AsyncSession, *, source_filter: tuple[str, ...] | None = None, ) -> list[dict[str, Any]]: now = datetime.now(UTC) recent_15m_threshold = now - timedelta(minutes=15) recent_24h_threshold = now - timedelta(hours=24) recent_7d_threshold = now - timedelta(days=7) filters = _collector_base_filters(source_filter) country_expr = func.nullif(BGPObservation.collector_geo["country"].as_string(), "") city_expr = func.nullif(BGPObservation.collector_geo["city"].as_string(), "") aggregate_stmt = ( select( BGPObservation.collector.label("collector"), func.count(BGPObservation.id).label("observation_count"), func.count(distinct(BGPObservation.prefix)).label("prefix_count"), func.count(distinct(BGPObservation.origin_asn)).label("origin_asn_count"), func.count(distinct(BGPObservation.peer_asn)).label("peer_asn_count"), func.sum(case((BGPObservation.observed_at >= recent_15m_threshold, 1), else_=0)).label("recent_15m_observation_count"), func.sum(case((BGPObservation.observed_at >= recent_24h_threshold, 1), else_=0)).label("recent_24h_observation_count"), func.sum(case((BGPObservation.observed_at >= recent_7d_threshold, 1), else_=0)).label("recent_7d_observation_count"), func.count(distinct(case((BGPObservation.observed_at >= recent_15m_threshold, BGPObservation.prefix), else_=None))).label("recent_15m_prefix_count"), func.count(distinct(case((BGPObservation.observed_at >= recent_24h_threshold, BGPObservation.prefix), else_=None))).label("recent_24h_prefix_count"), func.count(distinct(case((BGPObservation.observed_at >= recent_7d_threshold, BGPObservation.prefix), else_=None))).label("recent_7d_prefix_count"), func.max(BGPObservation.observed_at).label("latest_observed_at"), ) .where(*filters) .group_by(BGPObservation.collector) ) aggregate_rows = (await db.execute(aggregate_stmt)).all() latest_subquery = ( select( BGPObservation.collector.label("collector"), BGPObservation.event_type.label("latest_event_type"), country_expr.label("country"), city_expr.label("city"), func.row_number() .over( partition_by=BGPObservation.collector, order_by=(BGPObservation.observed_at.desc(), BGPObservation.id.desc()), ) .label("rn"), ) .where(*filters) .subquery() ) latest_rows = ( await db.execute( select( latest_subquery.c.collector, latest_subquery.c.latest_event_type, latest_subquery.c.country, latest_subquery.c.city, ).where(latest_subquery.c.rn == 1) ) ).all() event_counts_subquery = ( select( BGPObservation.collector.label("collector"), BGPObservation.event_type.label("event_type"), func.count(BGPObservation.id).label("count"), func.row_number() .over( partition_by=BGPObservation.collector, order_by=(func.count(BGPObservation.id).desc(), BGPObservation.event_type.asc()), ) .label("rn"), ) .where(*filters) .group_by(BGPObservation.collector, BGPObservation.event_type) .subquery() ) top_event_rows = ( await db.execute( select( event_counts_subquery.c.collector, event_counts_subquery.c.event_type, event_counts_subquery.c.count, ).where(event_counts_subquery.c.rn <= 3) ) ).all() scope_rows = ( await db.execute( select( BGPObservation.collector.label("collector"), country_expr.label("country"), city_expr.label("city"), ) .where(*filters) .distinct() ) ).all() latest_by_collector = { row.collector: { "latest_event_type": row.latest_event_type, "country": row.country, "city": row.city, } for row in latest_rows } scope_by_collector: dict[str, dict[str, set[str]]] = defaultdict(lambda: {"countries": set(), "cities": set()}) for row in scope_rows: if row.country: scope_by_collector[row.collector]["countries"].add(row.country) if row.city: scope_by_collector[row.collector]["cities"].add(row.city) top_events_by_collector: dict[str, list[dict[str, Any]]] = defaultdict(list) for row in top_event_rows: top_events_by_collector[row.collector].append( {"event_type": row.event_type, "count": row.count} ) by_collector: dict[str, dict[str, Any]] = {} for row in aggregate_rows: collector = row.collector latest = latest_by_collector.get(collector, {}) fallback_location = RIPE_RIS_COLLECTOR_COORDS.get(collector, {}) scope = scope_by_collector.get(collector, {"countries": set(), "cities": set()}) by_collector[collector] = { "collector": collector, "city": latest.get("city") or fallback_location.get("city"), "country": latest.get("country") or fallback_location.get("country"), "latitude": fallback_location.get("latitude"), "longitude": fallback_location.get("longitude"), "observation_count": row.observation_count or 0, "prefix_count": row.prefix_count or 0, "origin_asn_count": row.origin_asn_count or 0, "peer_asn_count": row.peer_asn_count or 0, "recent_15m_observation_count": row.recent_15m_observation_count or 0, "recent_24h_observation_count": row.recent_24h_observation_count or 0, "recent_7d_observation_count": row.recent_7d_observation_count or 0, "recent_15m_prefix_count": row.recent_15m_prefix_count or 0, "recent_24h_prefix_count": row.recent_24h_prefix_count or 0, "recent_7d_prefix_count": row.recent_7d_prefix_count or 0, "top_event_types": top_events_by_collector.get(collector, []), "latest_observed_at": to_iso8601_utc(row.latest_observed_at), "latest_event_type": latest.get("latest_event_type"), "baseline_scope": { "countries": sorted(scope["countries"]), "cities": sorted(scope["cities"]), }, } for collector, location in RIPE_RIS_COLLECTOR_COORDS.items(): if collector in by_collector: continue by_collector[collector] = { "collector": collector, "city": location.get("city"), "country": location.get("country"), "latitude": location.get("latitude"), "longitude": location.get("longitude"), "observation_count": 0, "prefix_count": 0, "origin_asn_count": 0, "peer_asn_count": 0, "recent_15m_observation_count": 0, "recent_24h_observation_count": 0, "recent_7d_observation_count": 0, "recent_15m_prefix_count": 0, "recent_24h_prefix_count": 0, "recent_7d_prefix_count": 0, "top_event_types": [], "latest_observed_at": None, "latest_event_type": None, "baseline_scope": { "countries": [location["country"]] if location.get("country") else [], "cities": [location["city"]] if location.get("city") else [], }, } return [by_collector[collector] for collector in sorted(by_collector.keys())]