///|
pub struct Change {
schema_version : String
timestamp : UInt
microseconds : Int?
prefix : String
path_id : UInt?
peer_asn : UInt
peer_ip : String
outbound : Bool
kind : String
source : Source?
before : RouteSnapshot?
after : RouteSnapshot
diff : RouteDiff
churn_count : Int?
} derive(ToJson)
///|
pub extend Change with ToJson::{to_json}
///|
pub struct Summary {
schema_version : String
baseline_status : String
warmup_records : Int64
records : Int64
skipped_records : Int64
ignored_families : Int64
rib_routes : Int64
announcements : Int64
withdrawals : Int64
path_changes : Int64
attribute_changes : Int64
origin_changes : Int64
duplicate_announcements : Int64
churn_alerts : Int64
out_of_order : Int64
session_resets : Int64
tracked_routes : Int
active_routes : Int
mean_path_length : Double
max_path_length : Int
} derive(ToJson)
///|
pub extend Summary with ToJson::{to_json}
///|
priv struct TrackedRoute {
event : RouteEvent
active : Bool
path : Array[AsSegment]
attributes : Attributes
semantic : Attributes
time : UInt64
window_start : UInt64
window_count : Int
}
///|
pub struct Analyzer {
priv routes : Map[String, TrackedRoute]
priv window : UInt64
priv threshold : Int
priv capacity : Int
priv mut stats : Summary
priv mut path_sum : Int64
priv mut path_samples : Int64
priv from_time : UInt64
priv to_time : UInt64
priv mut seeding : Bool
priv peer_latest : Map[String, UInt64]
priv peer_resets : Map[String, UInt64]
priv peer_routes : Map[String, Map[String, Unit]]
}
///|
pub fn Analyzer::new(
window_seconds? : Int = 60,
threshold? : Int = 10,
capacity? : Int = 1000000,
from_time? : UInt = 0U,
to_time? : UInt = 4294967295U,
from_micros? : Int = 0,
to_micros? : Int = 999999,
) -> Analyzer raise ParseError {
if window_seconds <= 0 || threshold < 2 || capacity <= 0 {
raise Invalid(0, "分析窗口、阈值或容量无效")
}
let lower = event_time(from_time, Some(from_micros))
let upper = event_time(to_time, Some(to_micros))
if lower > upper ||
from_micros < 0 ||
from_micros > 999999 ||
to_micros < 0 ||
to_micros > 999999 {
raise Invalid(0, "分析时间范围无效")
}
{
from_time: lower,
to_time: upper,
seeding: false,
peer_latest: Map([]),
peer_resets: Map([]),
peer_routes: Map([]),
routes: Map([]),
window: window_seconds.to_uint64() * 1000000UL,
threshold,
capacity,
stats: {
schema_version: "2.0",
baseline_status: "missing",
warmup_records: 0L,
records: 0L,
skipped_records: 0L,
ignored_families: 0L,
rib_routes: 0L,
announcements: 0L,
withdrawals: 0L,
path_changes: 0L,
attribute_changes: 0L,
origin_changes: 0L,
duplicate_announcements: 0L,
churn_alerts: 0L,
out_of_order: 0L,
session_resets: 0L,
tracked_routes: 0,
active_routes: 0,
mean_path_length: 0.0,
max_path_length: 0,
},
path_sum: 0L,
path_samples: 0L,
}
}
///|
fn event_time(seconds : UInt, micros : Int?) -> UInt64 {
let fraction = match micros {
Some(n) => n.to_uint64()
None => 0UL
}
seconds.to_uint64() * 1000000UL + fraction
}
///|
pub fn Analyzer::set_baseline(self : Analyzer) -> Unit {
self.stats = { ..self.stats, baseline_status: "explicit_rib", }
}
///|
/// 基线记录仅建立状态,不参与统计或波动计数。
pub fn Analyzer::seed(
self : Analyzer,
event : RouteEvent,
) -> Unit raise ParseError {
if event.action != "rib" ||
event_time(event.timestamp, event.microseconds) > self.from_time {
raise Invalid(0, "初始 RIB 必须不晚于分析开始时间")
}
self.set_baseline()
self.seeding = true
errdefer {
self.seeding = false
}
ignore(self.observe(event))
self.seeding = false
}
///|
fn peer_key(asn : UInt, ip : String, outbound : Bool) -> String {
"\{asn}|\{ip}|\{outbound}"
}
///|
fn change(
event : RouteEvent,
kind : String,
previous : TrackedRoute?,
attributes : Attributes,
churn_count? : Int,
) -> Change {
{
schema_version: "2.0",
timestamp: event.timestamp,
microseconds: event.microseconds,
prefix: event.prefix,
path_id: event.path_id,
peer_asn: event.peer_asn,
peer_ip: event.peer_ip,
outbound: event.outbound,
kind,
source: event.source,
before: previous.map(fn(old) {
route_snapshot(old.event, old.active, old.attributes)
}),
after: route_snapshot(event, event.action != "withdraw", attributes),
diff: route_diff(
previous.map(fn(old) { old.attributes }).unwrap_or(empty_attributes()),
attributes,
),
churn_count,
}
}
///|
/// 每个 peer、方向和前缀单独跟踪,固定窗口内仅提醒一次波动。
pub fn Analyzer::observe(
self : Analyzer,
event : RouteEvent,
) -> Array[Change] raise ParseError {
let peer = peer_key(event.peer_asn, event.peer_ip, event.outbound)
let key = peer +
"|" +
event.prefix +
"|" +
event.path_id.map(fn(n) { n.to_string() }).unwrap_or("none")
let time = event_time(event.timestamp, event.microseconds)
if time > self.to_time {
return []
}
let measured = !self.seeding && time >= self.from_time
let old = self.routes.get(key)
let reset = self.peer_resets.get(peer)
let stale = (old is Some(previous) && time < previous.time) ||
(reset is Some(t) && time < t)
if !stale && old is None && self.routes.length() >= self.capacity {
raise Invalid(0, "分析路由数量超过容量,请缩小筛选范围")
}
let mut stats = self.stats
match event.action {
"rib" => stats = { ..stats, rib_routes: stats.rib_routes + 1L, }
"announce" => stats = { ..stats, announcements: stats.announcements + 1L, }
"withdraw" => stats = { ..stats, withdrawals: stats.withdrawals + 1L, }
_ => raise Invalid(0, "未知路由事件")
}
if measured && event.action != "withdraw" {
let length = path_length(event.attributes.path)
self.path_sum += length.to_int64()
self.path_samples += 1L
if length > stats.max_path_length {
stats = { ..stats, max_path_length: length, }
}
}
if stale {
if measured {
self.stats = { ..stats, out_of_order: stats.out_of_order + 1L, }
}
return []
}
if self.peer_latest.get(peer).map(fn(t) { time > t }).unwrap_or(true) {
self.peer_latest[peer] = time
}
let notices = []
let active = event.action != "withdraw"
let semantic = if !active && old is Some(previous) {
previous.semantic
} else {
semantic_attributes(event.attributes)
}
let mut changed = event.action == "announce"
let mut start = time
let mut count = 0
let mut path = event.attributes.path
let mut attributes = event.attributes
if old is Some(previous) {
path = if active { event.attributes.path } else { previous.path }
attributes = if active { event.attributes } else { previous.attributes }
if event.action == "announce" && previous.active {
changed = previous.semantic != semantic
if previous.semantic.path != semantic.path {
stats = { ..stats, path_changes: stats.path_changes + 1L, }
notices.push(change(event, "path_change", old, attributes))
match (origin_as(previous.path), origin_as(path)) {
(Some(a), Some(b)) if a != b => {
stats = { ..stats, origin_changes: stats.origin_changes + 1L, }
notices.push(change(event, "origin_change", old, attributes))
}
_ => ()
}
}
let attribute_changed = { ..previous.semantic, path: [], } !=
{ ..semantic, path: [], }
if attribute_changed {
stats = { ..stats, attribute_changes: stats.attribute_changes + 1L, }
notices.push(change(event, "attribute_change", old, attributes))
}
if !changed {
stats = {
..stats,
duplicate_announcements: stats.duplicate_announcements + 1L,
}
}
}
if event.action == "withdraw" {
changed = previous.active
}
if time >= previous.window_start &&
time - previous.window_start < self.window {
start = previous.window_start
count = previous.window_count
}
} else if event.action == "withdraw" {
changed = false
}
if !measured {
start = self.from_time
count = 0
}
if measured && changed {
count += 1
}
if measured && count == self.threshold && changed {
stats = { ..stats, churn_alerts: stats.churn_alerts + 1L, }
notices.push(change(event, "churn", old, attributes, churn_count=count))
}
self.routes[key] = {
event: { ..event, attributes, },
active,
path,
attributes,
semantic,
time,
window_start: start,
window_count: count,
}
if old is None {
self.peer_routes.get_or_init(peer, fn() { Map([]) })[key] = ()
}
if measured {
self.stats = stats
} else if !self.seeding && self.stats.baseline_status == "missing" {
self.stats = { ..self.stats, baseline_status: "updates_only", }
}
if measured {
notices
} else {
[]
}
}
///|
/// 返回可审计的结束状态,顺序按状态键排序。
pub fn Analyzer::route_states(self : Analyzer) -> Array[RouteEvent] {
let keys = self.routes.keys().to_array()
keys.sort()
keys.map(fn(key) {
let state = self.routes.get(key).unwrap()
{
..state.event,
action: if state.active {
"active"
} else {
"withdrawn"
},
}
})
}
///|
/// 会话离开 Established 时清除该 peer 的缓存,避免跨会话误报。
pub fn Analyzer::observe_state(self : Analyzer, state : StateChange) -> Unit {
let time = event_time(state.timestamp, state.microseconds)
if time > self.to_time {
return
}
let measured = !self.seeding && time >= self.from_time
if state.old_state == 6 && state.new_state != 6 {
let inbound = peer_key(state.peer_asn, state.peer_ip, false)
let outbound = peer_key(state.peer_asn, state.peer_ip, true)
if [inbound, outbound].any(fn(peer) {
self.peer_latest.get(peer).map(fn(t) { t > time }).unwrap_or(false)
}) {
if measured {
self.stats = {
..self.stats,
out_of_order: self.stats.out_of_order + 1L,
}
}
return
}
for peer in [inbound, outbound] {
if self.peer_routes.get(peer) is Some(keys) {
for key in keys.keys() {
self.routes.remove(key)
}
self.peer_routes.remove(peer)
}
self.peer_resets[peer] = time
self.peer_latest[peer] = time
}
if measured {
self.stats = {
..self.stats,
session_resets: self.stats.session_resets + 1L,
}
}
}
}
///|
pub fn Analyzer::record_batch(self : Analyzer, batch : Batch) -> Unit {
let time = event_time(batch.header.timestamp, batch.microseconds)
if time > self.to_time {
return
}
if time < self.from_time {
self.stats = {
..self.stats,
warmup_records: self.stats.warmup_records + 1L,
}
} else {
let skipped = if batch.skipped { 1L } else { 0L }
self.stats = {
..self.stats,
records: self.stats.records + 1L,
skipped_records: self.stats.skipped_records + skipped,
ignored_families: self.stats.ignored_families +
batch.ignored_families.to_int64(),
}
}
for state in batch.states {
self.observe_state(state)
}
}
///|
pub fn Analyzer::summary(self : Analyzer) -> Summary {
let mut active = 0
for _, route in self.routes {
if route.active {
active += 1
}
}
let mean = if self.path_samples == 0L {
0.0
} else {
self.path_sum.to_double() / self.path_samples.to_double()
}
{
..self.stats,
tracked_routes: self.routes.length(),
active_routes: active,
mean_path_length: mean,
}
}