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

@@ -20,6 +20,32 @@ interface BGPAnomaly {
created_at: string | null
}
interface BGPEvent {
id: number
collector: string | null
event_type: string
prefix: string | null
origin_asn: number | null
peer_asn: number | null
observed_at: string | null
}
interface BGPIncident {
id: number
incident_type: string
title: string
summary: string
severity: string
status: string
confidence: number
affected_prefixes: string[]
affected_asns: number[]
affected_collectors: string[]
affected_regions: Array<{ country?: string; city?: string }>
created_at: string | null
started_at: string | null
}
interface Summary {
total: number
by_type: Record<string, number>
@@ -27,6 +53,13 @@ interface Summary {
by_status: Record<string, number>
}
interface EventSummary {
total: number
collector_count: number
prefix_count: number
by_type: Record<string, number>
}
function severityColor(severity: string) {
if (severity === 'critical') return 'red'
if (severity === 'high') return 'orange'
@@ -36,19 +69,31 @@ function severityColor(severity: string) {
function BGP() {
const [loading, setLoading] = useState(false)
const [incidents, setIncidents] = useState<BGPIncident[]>([])
const [anomalies, setAnomalies] = useState<BGPAnomaly[]>([])
const [summary, setSummary] = useState<Summary | null>(null)
const [events, setEvents] = useState<BGPEvent[]>([])
const [incidentSummary, setIncidentSummary] = useState<Summary | null>(null)
const [anomalySummary, setAnomalySummary] = useState<Summary | null>(null)
const [eventSummary, setEventSummary] = useState<EventSummary | null>(null)
useEffect(() => {
const load = async () => {
setLoading(true)
try {
const [anomaliesRes, summaryRes] = await Promise.all([
const [incidentsRes, incidentSummaryRes, anomaliesRes, anomalySummaryRes, eventsRes, eventSummaryRes] = await Promise.all([
axios.get('/api/v1/bgp/incidents', { params: { page_size: 50 } }),
axios.get('/api/v1/bgp/incidents/summary'),
axios.get('/api/v1/bgp/anomalies', { params: { page_size: 100 } }),
axios.get('/api/v1/bgp/anomalies/summary'),
axios.get('/api/v1/bgp/events', { params: { page_size: 20 } }),
axios.get('/api/v1/bgp/events/summary'),
])
setIncidents(incidentsRes.data.data || [])
setIncidentSummary(incidentSummaryRes.data)
setAnomalies(anomaliesRes.data.data || [])
setSummary(summaryRes.data)
setAnomalySummary(anomalySummaryRes.data)
setEvents(eventsRes.data.data || [])
setEventSummary(eventSummaryRes.data)
} finally {
setLoading(false)
}
@@ -62,7 +107,7 @@ function BGP() {
<Space direction="vertical" size={16} style={{ width: '100%' }}>
<div>
<Title level={3} style={{ marginBottom: 4 }}>BGP观测</Title>
<Text type="secondary"></Text>
<Text type="secondary"></Text>
</div>
<Alert
@@ -74,22 +119,102 @@ function BGP() {
<Row gutter={16}>
<Col xs={24} md={8}>
<Card>
<Statistic title="异常总数" value={summary?.total || 0} />
<Statistic title="观测事件" value={eventSummary?.total || 0} />
</Card>
</Col>
<Col xs={24} md={8}>
<Card>
<Statistic title="Critical" value={summary?.by_severity?.critical || 0} />
<Statistic title="观测站" value={eventSummary?.collector_count || 0} />
</Card>
</Col>
<Col xs={24} md={8}>
<Card>
<Statistic title="Active" value={summary?.by_status?.active || 0} />
<Statistic title="观测前缀" value={eventSummary?.prefix_count || 0} />
</Card>
</Col>
</Row>
<Card title="异常列表">
<Row gutter={16}>
<Col xs={24} md={8}>
<Card>
<Statistic title="事件总数" value={incidentSummary?.total || 0} />
</Card>
</Col>
<Col xs={24} md={8}>
<Card>
<Statistic title="活跃事件" value={incidentSummary?.by_status?.active || 0} />
</Card>
</Col>
<Col xs={24} md={8}>
<Card>
<Statistic title="严重事件" value={incidentSummary?.by_severity?.critical || 0} />
</Card>
</Col>
</Row>
<Card title="事件列表">
<Table<BGPIncident>
rowKey="id"
loading={loading}
dataSource={incidents}
pagination={{ pageSize: 8 }}
columns={[
{
title: '开始时间',
dataIndex: 'started_at',
width: 180,
render: (value: string | null) => formatDateTimeZhCN(value),
},
{
title: '类型',
dataIndex: 'incident_type',
width: 180,
},
{
title: '严重度',
dataIndex: 'severity',
width: 120,
render: (value: string) => <Tag color={severityColor(value)}>{value}</Tag>,
},
{
title: '影响前缀',
dataIndex: 'affected_prefixes',
width: 200,
render: (value: string[]) => (value && value.length > 0 ? value.join(', ') : '-'),
},
{
title: '观测站',
dataIndex: 'affected_collectors',
width: 180,
render: (value: string[]) => (value && value.length > 0 ? `${value.length}个 (${value.slice(0, 3).join(', ')})` : '-'),
},
{
title: '区域',
dataIndex: 'affected_regions',
width: 220,
render: (value: Array<{ country?: string; city?: string }>) => {
if (!value || value.length === 0) return '-'
return value
.slice(0, 3)
.map((item) => [item.city, item.country].filter(Boolean).join(', '))
.join(' / ')
},
},
{
title: '置信度',
dataIndex: 'confidence',
width: 120,
render: (value: number) => `${Math.round((value || 0) * 100)}%`,
},
{
title: '摘要',
dataIndex: 'summary',
},
]}
/>
</Card>
<Card title="异常明细">
<Table<BGPAnomaly>
rowKey="id"
loading={loading}
@@ -151,6 +276,52 @@ function BGP() {
]}
/>
</Card>
<Card title="最近观测事件">
<Table<BGPEvent>
rowKey="id"
loading={loading}
dataSource={events}
pagination={{ pageSize: 8 }}
columns={[
{
title: '时间',
dataIndex: 'observed_at',
width: 180,
render: (value: string | null) => formatDateTimeZhCN(value),
},
{
title: '观测站',
dataIndex: 'collector',
width: 140,
render: (value: string | null) => value || '-',
},
{
title: '类型',
dataIndex: 'event_type',
width: 120,
},
{
title: '前缀',
dataIndex: 'prefix',
width: 200,
render: (value: string | null) => value || '-',
},
{
title: 'Origin ASN',
dataIndex: 'origin_asn',
width: 140,
render: (value: number | null) => (value ? `AS${value}` : '-'),
},
{
title: 'Peer ASN',
dataIndex: 'peer_asn',
width: 140,
render: (value: number | null) => (value ? `AS${value}` : '-'),
},
]}
/>
</Card>
</Space>
</AppLayout>
)