///|
pub struct Snapshot {
priv collector : String
priv decoder : Decoder
priv mut table : PeerTable?
priv mut table_source : Source?
priv peers : Map[String, Peer]
priv routes : Map[String, RouteEvent]
priv capacity : Int
priv mut first_time : UInt?
priv mut last_time : UInt?
}
///|
pub fn Snapshot::new(
collector : String,
capacity? : Int = 1000000,
) -> Snapshot raise ParseError {
if collector == "" || capacity < 1 {
raise Invalid(0, "快照需要采集器名称和正数容量")
}
{
collector,
capacity,
decoder: Decoder::new(),
table: None,
table_source: None,
peers: Map([]),
routes: Map([]),
first_time: None,
last_time: None,
}
}
///|
/// 快照保留 peer 表,避免把整个 peer 未采集误判为单条路由消失。
pub fn Snapshot::ingest(
self : Snapshot,
data : Bytes,
file : String,
record : Int64,
offset : Int64,
query? : Query,
peer_asn? : UInt,
) -> Unit raise ParseError {
let frame = read_frame(data)
if frame.header.kind != 13 || ![1, 2, 4, 8, 10].contains(frame.header.subtype) {
raise Invalid(0, "快照仅接受支持的 TABLE_DUMP_V2 单播记录")
}
if frame.header.subtype == 1 {
if self.table is Some(_) {
raise Invalid(0, "单个快照只能包含一个 peer 表")
}
let table = parse_peers(frame.payload)
self.table = Some(table)
self.table_source = Some({ file, record, offset, event_index: 0, })
for peer in table.peers {
if peer_asn.map(fn(asn) { asn == peer.asn }).unwrap_or(true) {
self.peers[peer_key(peer.asn, peer.address, false)] = peer
}
}
}
let batch = self.decoder.decode_at(data, file, record, offset)
let time = frame.header.timestamp
if self.first_time.map(fn(t) { time < t }).unwrap_or(true) {
self.first_time = Some(time)
}
if self.last_time.map(fn(t) { time > t }).unwrap_or(true) {
self.last_time = Some(time)
}
for event in batch.events {
if !self.peers.contains(
peer_key(event.peer_asn, event.peer_ip, event.outbound),
) ||
!query.map(fn(q) { q.matches(event) }).unwrap_or(true) {
continue
}
let key = snapshot_route_key(event)
if self.routes.contains(key) {
raise Invalid(0, "快照含重复路由键,不能按条目顺序覆盖")
}
if self.routes.length() >= self.capacity {
raise Invalid(0, "快照路由数量超过容量")
}
self.routes[key] = event
}
}
///|
fn snapshot_route_key(event : RouteEvent) -> String {
peer_key(event.peer_asn, event.peer_ip, event.outbound) +
"|" +
event.prefix +
"|" +
event.path_id.map(fn(n) { n.to_string() }).unwrap_or("none")
}
///|
pub struct SnapshotChange {
schema_version : String
collector : String
kind : String
peer_asn : UInt
peer_ip : String
outbound : Bool
prefix : String?
path_id : UInt?
before : RouteEvent?
after : RouteEvent?
diff : RouteDiff?
before_source : Source?
after_source : Source?
} derive(ToJson)
///|
pub extend SnapshotChange with ToJson::{to_json}
///|
pub struct SnapshotReport {
schema_version : String
collector : String
interpretation : String
before_source : Source?
after_source : Source?
before_time : UInt?
after_time : UInt?
before_peers : Int
after_peers : Int
before_routes : Int
after_routes : Int
changes : Array[SnapshotChange]
} derive(ToJson)
///|
pub extend SnapshotReport with ToJson::{to_json}
///|
/// 比较显式同源、同视图的两个快照;缺失不等同于观测到撤回。
pub fn Snapshot::diff(
self : Snapshot,
after : Snapshot,
) -> SnapshotReport raise ParseError {
let before_table = match self.table {
Some(t) => t
None => raise Invalid(0, "前快照为空或缺少 peer 表")
}
let after_table = match after.table {
Some(t) => t
None => raise Invalid(0, "后快照为空或缺少 peer 表")
}
if self.collector != after.collector ||
before_table.collector_id != after_table.collector_id ||
before_table.view_name != after_table.view_name {
raise Invalid(
0, "采集器名称、Collector BGP ID 或视图名称不同,快照不可比较",
)
}
if self.first_time.unwrap() > after.first_time.unwrap() {
raise Invalid(0, "前快照时间晚于后快照")
}
let changes = []
let peer_keys = self.peers.keys().to_array()
for key in after.peers.keys() {
if !self.peers.contains(key) {
peer_keys.push(key)
}
}
peer_keys.sort()
for key in peer_keys {
let a = self.peers.get(key)
let b = after.peers.get(key)
if a is None || b is None {
let peer = match a {
Some(p) => p
None => b.unwrap()
}
changes.push({
schema_version: "2.0",
collector: self.collector,
kind: if a is None {
"peer_added"
} else {
"peer_missing"
},
peer_asn: peer.asn,
peer_ip: peer.address,
outbound: false,
prefix: None,
path_id: None,
before: None,
after: None,
diff: None,
before_source: self.table_source,
after_source: after.table_source,
})
}
}
let route_keys = self.routes.keys().to_array()
for key in after.routes.keys() {
if !self.routes.contains(key) {
route_keys.push(key)
}
}
route_keys.sort()
for key in route_keys {
let a = self.routes.get(key)
let b = after.routes.get(key)
let event = match a {
Some(e) => e
None => b.unwrap()
}
let peer = peer_key(event.peer_asn, event.peer_ip, event.outbound)
if !self.peers.contains(peer) || !after.peers.contains(peer) {
continue
}
let kinds = []
match (a, b) {
(None, Some(_)) => kinds.push("route_added")
(Some(_), None) => kinds.push("route_missing")
(Some(before), Some(next)) => {
if !same_path(before.attributes.path, next.attributes.path) {
kinds.push("path_change")
}
if origin_as(before.attributes.path) != origin_as(next.attributes.path) {
kinds.push("origin_change")
}
if !route_attributes_equal({ ..before.attributes, path: [], }, {
..next.attributes,
path: [],
}) {
kinds.push("attribute_change")
}
}
_ => ()
}
for kind in kinds {
changes.push({
schema_version: "2.0",
collector: self.collector,
kind,
peer_asn: event.peer_asn,
peer_ip: event.peer_ip,
outbound: event.outbound,
prefix: Some(event.prefix),
path_id: event.path_id,
before: a,
after: b,
diff: match (a, b) {
(Some(x), Some(y)) => Some(route_diff(x.attributes, y.attributes))
_ => None
},
before_source: match a {
Some(e) => e.source
None => None
},
after_source: match b {
Some(e) => e.source
None => None
},
})
}
}
{
schema_version: "2.0",
collector: self.collector,
interpretation: "snapshot_presence_difference_not_observed_bgp_withdrawal",
before_source: self.table_source,
after_source: after.table_source,
before_time: self.first_time,
after_time: after.first_time,
before_peers: self.peers.length(),
after_peers: after.peers.length(),
before_routes: self.routes.length(),
after_routes: after.routes.length(),
changes,
}
}