148 lines
6.2 KiB
Python
148 lines
6.2 KiB
Python
from __future__ import annotations
|
||
|
||
from collections import Counter
|
||
|
||
from sqlalchemy import func, select
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.api.v1.bgp import BGP_SOURCES
|
||
from app.models.bgp_anomaly import BGPAnomaly
|
||
from app.models.bgp_incident import BGPIncident
|
||
from app.models.bgp_observation import BGPObservation
|
||
from app.schemas.ai import SituationalAnalysisRequest
|
||
from app.services.bgp_collectors import build_bgp_collector_coverage
|
||
|
||
|
||
def _format_counter(counter: dict[str, int], empty_text: str = "无") -> str:
|
||
if not counter:
|
||
return empty_text
|
||
return ",".join(f"{key} {value}" for key, value in counter.items())
|
||
|
||
|
||
def _severity_rank(value: str | None) -> int:
|
||
order = {
|
||
"critical": 0,
|
||
"high": 1,
|
||
"medium": 2,
|
||
"low": 3,
|
||
"info": 4,
|
||
}
|
||
return order.get((value or "").lower(), 99)
|
||
|
||
|
||
async def build_bgp_brief_request(
|
||
db: AsyncSession,
|
||
*,
|
||
incident_limit: int = 5,
|
||
anomaly_limit: int = 6,
|
||
collector_limit: int = 5,
|
||
) -> SituationalAnalysisRequest:
|
||
incidents_result = await db.execute(
|
||
select(BGPIncident)
|
||
.order_by(BGPIncident.created_at.desc(), BGPIncident.id.desc())
|
||
.limit(max(incident_limit, 1))
|
||
)
|
||
anomalies_result = await db.execute(
|
||
select(BGPAnomaly)
|
||
.order_by(BGPAnomaly.created_at.desc(), BGPAnomaly.id.desc())
|
||
.limit(max(anomaly_limit, 1))
|
||
)
|
||
observations_result = await db.execute(
|
||
select(BGPObservation).where(BGPObservation.source.in_(BGP_SOURCES))
|
||
)
|
||
incident_count_result = await db.execute(select(func.count(BGPIncident.id)))
|
||
anomaly_count_result = await db.execute(select(func.count(BGPAnomaly.id)))
|
||
|
||
incidents = incidents_result.scalars().all()
|
||
anomalies = anomalies_result.scalars().all()
|
||
observations = observations_result.scalars().all()
|
||
collectors = await build_bgp_collector_coverage(db, source_filter=BGP_SOURCES)
|
||
|
||
total_incidents = incident_count_result.scalar() or 0
|
||
total_anomalies = anomaly_count_result.scalar() or 0
|
||
total_observations = len(observations)
|
||
active_collectors = [item for item in collectors if item["observation_count"] > 0]
|
||
|
||
incident_status_counts = Counter((item.status or "unknown") for item in incidents)
|
||
incident_severity_counts = Counter((item.severity or "unknown") for item in incidents)
|
||
incident_type_counts = Counter((item.incident_type or "unknown") for item in incidents)
|
||
anomaly_type_counts = Counter((item.anomaly_type or "unknown") for item in anomalies)
|
||
event_type_counts = Counter((item.event_type or "unknown") for item in observations)
|
||
|
||
top_collectors = sorted(
|
||
active_collectors,
|
||
key=lambda item: (
|
||
-int(item["recent_24h_observation_count"]),
|
||
-int(item["observation_count"]),
|
||
str(item["collector"]),
|
||
),
|
||
)[: max(collector_limit, 1)]
|
||
|
||
observations_lines: list[str] = [
|
||
f"当前共有 {total_incidents} 起 BGP incidents、{total_anomalies} 条 anomalies、{total_observations} 条原始观测事件。",
|
||
f"活跃观测站 {len(active_collectors)} 个;近 24 小时事件数合计 {sum(int(item['recent_24h_observation_count']) for item in active_collectors)}。",
|
||
f"最近 incidents 严重度分布:{_format_counter(dict(sorted(incident_severity_counts.items(), key=lambda item: _severity_rank(item[0]))))}。",
|
||
f"最近 incidents 状态分布:{_format_counter(dict(incident_status_counts))}。",
|
||
f"最近 incidents 类型分布:{_format_counter(dict(incident_type_counts.most_common(5)))}。",
|
||
f"最近 anomalies 类型分布:{_format_counter(dict(anomaly_type_counts.most_common(6)))}。",
|
||
f"观测事件类型分布:{_format_counter(dict(event_type_counts.most_common(6)))}。",
|
||
]
|
||
|
||
if incidents:
|
||
observations_lines.append(
|
||
"最近 incident 摘要:" + ";".join(
|
||
[
|
||
f"{item.incident_type} / {item.severity} / {item.status}"
|
||
f" / 前缀 {', '.join(item.affected_prefixes[:2]) if item.affected_prefixes else '-'}"
|
||
f" / 观测站 {len(item.affected_collectors or [])} 个"
|
||
for item in incidents
|
||
]
|
||
)
|
||
)
|
||
|
||
if anomalies:
|
||
observations_lines.append(
|
||
"最近 anomaly 摘要:" + ";".join(
|
||
[
|
||
f"{item.anomaly_type} / {item.severity}"
|
||
f" / 前缀 {item.prefix or '-'}"
|
||
f" / ASN {item.new_origin_asn or item.origin_asn or '-'}"
|
||
for item in anomalies
|
||
]
|
||
)
|
||
)
|
||
|
||
if top_collectors:
|
||
observations_lines.append(
|
||
"重点观测站:" + ";".join(
|
||
[
|
||
f"{item['collector']} ({', '.join([part for part in [item.get('city'), item.get('country')] if part]) or '未知位置'})"
|
||
f" / 近24h {item['recent_24h_observation_count']} 条"
|
||
f" / 前缀 {item['prefix_count']} 个"
|
||
for item in top_collectors
|
||
]
|
||
)
|
||
)
|
||
|
||
return SituationalAnalysisRequest(
|
||
title="BGP 态势 AI 简报",
|
||
objective="基于当前 BGP incidents、anomalies、原始观测事件与观测站覆盖情况,生成一份面向操作员的简明态势简报,突出当前风险、证据和优先动作。",
|
||
observations=observations_lines,
|
||
constraints=[
|
||
"明确区分事实、推断与建议。",
|
||
"优先指出需要立即关注的高严重度 incident 或异常模式。",
|
||
"结论应服务值班排障,不要写成泛泛的模型演示文案。",
|
||
"如果证据不足,要明确指出缺失数据。",
|
||
],
|
||
context={
|
||
"source": "bgp-overview",
|
||
"incident_total": total_incidents,
|
||
"anomaly_total": total_anomalies,
|
||
"observation_total": total_observations,
|
||
"active_collectors": len(active_collectors),
|
||
"top_incident_types": dict(incident_type_counts.most_common(5)),
|
||
"top_anomaly_types": dict(anomaly_type_counts.most_common(6)),
|
||
"top_event_types": dict(event_type_counts.most_common(6)),
|
||
},
|
||
)
|