///|
pub struct ObservationGap {
from_time : UInt
from_micros : Int
to_time : UInt
to_micros : Int
} derive(ToJson)
///|
pub extend ObservationGap with ToJson::{to_json}
///|
pub fn ObservationGap::new(
from_time : UInt,
to_time : UInt,
from_micros? : Int = 0,
to_micros? : Int = 0,
) -> ObservationGap {
{ from_time, to_time, from_micros, to_micros, }
}
///|
pub struct TimelineEntry {
timestamp : UInt
microseconds : Int?
kind : String
prefix : String
path_id : UInt?
peer_asn : UInt?
peer_ip : String?
outbound : Bool?
accepted : Bool
source : Source?
before : RouteSnapshot?
after : RouteSnapshot?
changes : Array[Change]
} derive(ToJson)
///|
pub extend TimelineEntry with ToJson::{to_json}
///|
pub struct VisibilityInterval {
scope : String
prefix : String
path_id : UInt?
peer_asn : UInt
peer_ip : String
outbound : Bool
started_at : UInt64
measured_from : UInt64
observed_until : UInt64
recovery_time : UInt64?
known_invisible_microseconds : UInt64?
left_censored : Bool
right_censored : Bool
quality : String
closed_by : String
start_source : Source?
end_source : Source?
} derive(ToJson)
///|
pub extend VisibilityInterval with ToJson::{to_json}
///|
pub struct TimelinePath {
prefix : String
path_id : UInt?
peer_asn : UInt
peer_ip : String
outbound : Bool
visibility : String
observed_at : UInt64
source : Source?
} derive(ToJson)
///|
pub extend TimelinePath with ToJson::{to_json}
///|
pub struct TimelinePeer {
peer_asn : UInt
peer_ip : String
outbound : Bool
visibility : String
visible_paths : Int
not_visible_paths : Int
unobserved_paths : Int
baseline_present : Bool
} derive(ToJson)
///|
pub extend TimelinePeer with ToJson::{to_json}
///|
pub struct TimelineReport {
schema_version : String
prefix : String
interpretation : String
baseline_status : String
continuous_observation : Bool
last_input_time : UInt64?
observed_window_end : UInt64?
gaps : Array[ObservationGap]
entries : Array[TimelineEntry]
intervals : Array[VisibilityInterval]
peer_intervals : Array[VisibilityInterval]
paths : Array[TimelinePath]
peers : Array[TimelinePeer]
} derive(ToJson)
///|
pub extend TimelineReport with ToJson::{to_json}
///|
priv struct TracePath {
mut event : RouteEvent
mut visibility : String
mut last_time : UInt64
mut source : Source?
mut invisible_since : UInt64?
mut invisible_source : Source?
mut left_censored : Bool
}
///|
pub struct Timeline {
priv analyzer : Analyzer
priv prefix : String
priv continuous : Bool
priv gaps : Array[ObservationGap]
priv capacity : Int
priv peer_asn : UInt?
priv entries : Array[TimelineEntry]
priv intervals : Array[VisibilityInterval]
priv peer_intervals : Array[VisibilityInterval]
priv peer_paths : Map[String, TracePath]
priv paths : Map[String, TracePath]
priv baselines : Map[String, (Peer, UInt64)]
priv sessions : Map[String, (Peer, UInt64)]
priv mut last_input : UInt64?
}
///|
pub fn Timeline::new(
prefix : String,
analyzer : Analyzer,
continuous? : Bool = false,
gaps? : Array[ObservationGap] = [],
capacity? : Int = 1000000,
peer_asn? : UInt,
) -> Timeline raise ParseError {
if capacity < 1 {
raise Invalid(0, "时间线容量必须为正数")
}
let sorted = gaps.copy()
for gap in sorted {
if gap.from_micros < 0 ||
gap.from_micros > 999999 ||
gap.to_micros < 0 ||
gap.to_micros > 999999 ||
event_time(gap.from_time, Some(gap.from_micros)) >=
event_time(gap.to_time, Some(gap.to_micros)) {
raise Invalid(0, "观测中断时间范围无效")
}
}
sorted.sort_by_key(fn(g) { event_time(g.from_time, Some(g.from_micros)) })
{
analyzer,
prefix: parse_prefix_text(prefix).text(),
continuous,
gaps: sorted,
capacity,
peer_asn,
entries: [],
intervals: [],
peer_intervals: [],
peer_paths: Map([]),
paths: Map([]),
baselines: Map([]),
sessions: Map([]),
last_input: None,
}
}
///|
pub fn Timeline::seed_peers(
self : Timeline,
table : PeerTable,
timestamp : UInt,
) -> Unit raise ParseError {
if event_time(timestamp, None) > self.analyzer.from_time {
raise Invalid(0, "初始 peer 表晚于时间线窗口")
}
for peer in table.peers {
if self.peer_asn.map(fn(n) { n == peer.asn }).unwrap_or(true) {
self.baselines[peer_key(peer.asn, peer.address, false)] = (
peer,
event_time(timestamp, None),
)
let time = event_time(timestamp, None)
self.peer_paths[peer_key(peer.asn, peer.address, false)] = {
event: {
schema_version: "2.0",
source: None,
timestamp,
microseconds: None,
action: "baseline_absence",
prefix: self.prefix,
path_id: None,
peer_asn: peer.asn,
peer_ip: peer.address,
outbound: false,
originated: None,
attributes: empty_attributes(),
},
visibility: "not_visible",
last_time: time,
source: None,
invisible_since: Some(time),
invisible_source: None,
left_censored: true,
}
}
}
self.timeline_time(event_time(timestamp, None))
}
///|
fn Timeline::timeline_time(self : Timeline, time : UInt64) -> Unit {
if self.last_input.map(fn(t) { time > t }).unwrap_or(true) {
self.last_input = Some(time)
}
}
///|
pub fn Timeline::seed(
self : Timeline,
event : RouteEvent,
) -> Unit raise ParseError {
if event.prefix != self.prefix {
raise Invalid(0, "时间线要求精确匹配一个前缀")
}
if !self.peer_asn.map(fn(n) { n == event.peer_asn }).unwrap_or(true) {
return
}
if !self.paths.contains(snapshot_route_key(event)) &&
self.paths.length() >= self.capacity {
raise Invalid(0, "时间线路径数量超过容量")
}
self.analyzer.seed(event)
self.timeline_time(event_time(event.timestamp, event.microseconds))
self.paths[snapshot_route_key(event)] = {
event,
visibility: "visible",
last_time: event_time(event.timestamp, event.microseconds),
source: event.source,
invisible_since: None,
invisible_source: None,
left_censored: false,
}
self.update_peer(event, seeding=true)
}
///|
fn Timeline::append(
self : Timeline,
entry : TimelineEntry,
) -> Unit raise ParseError {
if self.entries.length() >= self.capacity {
raise Invalid(0, "时间线事件数量超过容量")
}
self.entries.push(entry)
}
///|
fn Timeline::interval(
self : Timeline,
trace : TracePath,
until : UInt64,
closed_by : String,
end_source : Source?,
scope? : String = "path",
) -> VisibilityInterval? {
let since = match trace.invisible_since {
Some(t) => t
None => return None
}
let start = if since > self.analyzer.from_time {
since
} else {
self.analyzer.from_time
}
let end = if until < self.analyzer.to_time {
until
} else {
self.analyzer.to_time
}
if end < start {
return None
}
let interrupted = self.gaps.any(fn(g) {
event_time(g.from_time, Some(g.from_micros)) < end &&
event_time(g.to_time, Some(g.to_micros)) > since
})
let recovered = closed_by == "announce" && until <= self.analyzer.to_time
let event = trace.event
Some({
scope,
prefix: event.prefix,
path_id: event.path_id,
peer_asn: event.peer_asn,
peer_ip: event.peer_ip,
outbound: event.outbound,
started_at: since,
measured_from: start,
observed_until: end,
recovery_time: if recovered {
Some(until)
} else {
None
},
known_invisible_microseconds: if self.continuous && !interrupted {
Some(end - start)
} else {
None
},
left_censored: trace.left_censored || since < self.analyzer.from_time,
right_censored: !recovered,
quality: if interrupted {
"observation_gap"
} else if !self.continuous {
"continuity_unverified"
} else {
"observed_route_visibility_only"
},
closed_by,
start_source: trace.invisible_source,
end_source,
})
}
///|
pub fn Timeline::observe(
self : Timeline,
event : RouteEvent,
) -> Unit raise ParseError {
if event.prefix != self.prefix {
raise Invalid(0, "时间线要求精确匹配一个前缀")
}
let time = event_time(event.timestamp, event.microseconds)
self.timeline_time(time)
if !self.peer_asn.map(fn(n) { n == event.peer_asn }).unwrap_or(true) {
return
}
if time > self.analyzer.to_time {
return
}
let key = snapshot_route_key(event)
let peer = peer_key(event.peer_asn, event.peer_ip, event.outbound)
let old = self.analyzer.routes.get(key)
let stale = old.map(fn(r) { time < r.time }).unwrap_or(false) ||
self.analyzer.peer_resets.get(peer).map(fn(t) { time < t }).unwrap_or(false)
let changes = self.analyzer.observe(event)
let measured = time >= self.analyzer.from_time
if stale {
if measured {
self.append({
timestamp: event.timestamp,
microseconds: event.microseconds,
kind: "out_of_order",
prefix: event.prefix,
path_id: event.path_id,
peer_asn: Some(event.peer_asn),
peer_ip: Some(event.peer_ip),
outbound: Some(event.outbound),
accepted: false,
source: event.source,
before: None,
after: None,
changes: [],
})
}
return
}
if !self.paths.contains(key) && self.paths.length() >= self.capacity {
raise Invalid(0, "时间线路径数量超过容量")
}
let baseline = if self.analyzer.peer_resets.contains(peer) {
None
} else {
self.baselines.get(peer)
}
let trace = self.paths.get_or_init(key, fn() {
{
event,
visibility: if baseline is Some(_) {
"not_visible"
} else {
"unobserved"
},
last_time: baseline.map(fn(b) { b.1 }).unwrap_or(time),
source: None,
invisible_since: baseline.map(fn(b) { b.1 }),
invisible_source: None,
left_censored: true,
}
})
if self.gaps.any(fn(g) {
event_time(g.from_time, Some(g.from_micros)) <= time &&
event_time(g.to_time, Some(g.to_micros)) > trace.last_time
}) {
if self.interval(trace, time, "observation_gap", event.source)
is Some(interval) {
self.intervals.push(interval)
}
trace.invisible_since = None
trace.visibility = "unobserved"
}
let kind = if event.action == "withdraw" {
"withdraw"
} else if event.action == "rib" {
"rib"
} else if old is Some(previous) && !previous.active {
"reannounce"
} else if changes.any(fn(c) { c.kind == "path_change" }) {
"path_change"
} else if changes.any(fn(c) { c.kind == "attribute_change" }) {
"attribute_change"
} else {
"announce"
}
if event.action != "withdraw" {
if self.interval(trace, time, "announce", event.source) is Some(interval) {
self.intervals.push(interval)
}
trace.invisible_since = None
trace.invisible_source = None
trace.visibility = "visible"
} else {
if trace.invisible_since is None {
trace.invisible_since = Some(time)
trace.invisible_source = event.source
trace.left_censored = trace.visibility != "visible"
}
trace.visibility = "not_visible"
}
trace.event = event
trace.last_time = time
trace.source = event.source
self.update_peer(event)
if measured {
let next = self.analyzer.routes.get(key).unwrap()
self.append({
timestamp: event.timestamp,
microseconds: event.microseconds,
kind,
prefix: event.prefix,
path_id: event.path_id,
peer_asn: Some(event.peer_asn),
peer_ip: Some(event.peer_ip),
outbound: Some(event.outbound),
accepted: true,
source: event.source,
before: old.map(fn(r) { route_snapshot(r.event, r.active, r.attributes) }),
after: Some(route_snapshot(next.event, next.active, next.attributes)),
changes,
})
}
}
///|
pub fn Timeline::record_batch(
self : Timeline,
batch : Batch,
) -> Unit raise ParseError {
let time = event_time(batch.header.timestamp, batch.microseconds)
self.timeline_time(time)
self.analyzer.record_batch({ ..batch, states: [], })
for state in batch.states {
let state_time = event_time(state.timestamp, state.microseconds)
let reset = state.old_state == 6 && state.new_state != 6
let stale = [false, true].any(fn(direction) {
self.analyzer.peer_latest
.get(peer_key(state.peer_asn, state.peer_ip, direction))
.map(fn(t) { t > state_time })
.unwrap_or(false)
})
self.analyzer.observe_state(state)
if !reset ||
state_time > self.analyzer.to_time ||
stale ||
!self.peer_asn.map(fn(n) { n == state.peer_asn }).unwrap_or(true) {
continue
}
self.sessions[peer_key(state.peer_asn, state.peer_ip, false)] = (
{ bgp_id: 0U, asn: state.peer_asn, address: state.peer_ip, },
state_time,
)
for _, trace in self.paths {
if trace.event.peer_asn == state.peer_asn &&
trace.event.peer_ip == state.peer_ip {
if self.interval(trace, state_time, "session_reset", state.source)
is Some(interval) {
self.intervals.push(interval)
}
trace.invisible_since = None
trace.visibility = "unobserved"
trace.last_time = state_time
trace.source = state.source
}
}
for _, trace in self.peer_paths {
if trace.event.peer_asn == state.peer_asn &&
trace.event.peer_ip == state.peer_ip {
if self.interval(
trace,
state_time,
"session_reset",
state.source,
scope="peer_prefix",
)
is Some(interval) {
self.peer_intervals.push(interval)
}
trace.invisible_since = None
trace.visibility = "unobserved"
trace.last_time = state_time
trace.source = state.source
}
}
if state_time >= self.analyzer.from_time {
self.append({
timestamp: state.timestamp,
microseconds: state.microseconds,
kind: "session_reset",
prefix: self.prefix,
path_id: None,
peer_asn: Some(state.peer_asn),
peer_ip: Some(state.peer_ip),
outbound: None,
accepted: true,
source: state.source,
before: None,
after: None,
changes: [],
})
}
}
}
///|
/// peer 可见性取所选路径的并集,撤回单条 ADD-PATH 不代表前缀消失。
fn Timeline::update_peer(
self : Timeline,
event : RouteEvent,
seeding? : Bool = false,
) -> Unit {
let peer = peer_key(event.peer_asn, event.peer_ip, event.outbound)
let time = event_time(event.timestamp, event.microseconds)
let mut visible = false
let mut unknown = false
let mut addpath = false
for _, path in self.paths {
if peer_key(path.event.peer_asn, path.event.peer_ip, path.event.outbound) !=
peer {
continue
}
let interrupted = self.gaps.any(fn(g) {
event_time(g.from_time, Some(g.from_micros)) <= time &&
event_time(g.to_time, Some(g.to_micros)) > path.last_time
})
let state = if interrupted { "unobserved" } else { path.visibility }
if state == "visible" {
visible = true
}
if state == "unobserved" {
unknown = true
}
if path.event.path_id is Some(_) {
addpath = true
}
}
let full_baseline = self.baselines.contains(peer) &&
!self.analyzer.peer_resets.contains(peer)
let next = if visible {
"visible"
} else if unknown || (addpath && !full_baseline) {
"unobserved"
} else {
"not_visible"
}
let trace = self.peer_paths.get_or_init(peer, fn() {
{
event: { ..event, path_id: None, },
visibility: "unobserved",
last_time: time,
source: None,
invisible_since: None,
invisible_source: None,
left_censored: true,
}
})
let interrupted = self.gaps.any(fn(g) {
event_time(g.from_time, Some(g.from_micros)) <= time &&
event_time(g.to_time, Some(g.to_micros)) > trace.last_time
})
if interrupted {
if !seeding &&
self.interval(
trace,
time,
"observation_gap",
event.source,
scope="peer_prefix",
)
is Some(interval) {
self.peer_intervals.push(interval)
}
trace.invisible_since = None
trace.visibility = "unobserved"
}
if next != "not_visible" {
if !seeding &&
self.interval(
trace,
time,
if next == "visible" {
"announce"
} else {
"observation_gap"
},
event.source,
scope="peer_prefix",
)
is Some(interval) {
self.peer_intervals.push(interval)
}
trace.invisible_since = None
} else if trace.invisible_since is None {
trace.invisible_since = Some(time)
trace.invisible_source = event.source
trace.left_censored = trace.visibility != "visible"
}
trace.event = { ..event, path_id: None, }
trace.visibility = next
trace.last_time = time
trace.source = event.source
}
///|
fn Timeline::visibility(
self : Timeline,
since : UInt64,
visibility : String,
until : UInt64,
) -> String {
if until < self.analyzer.from_time ||
self.gaps.any(fn(g) {
event_time(g.to_time, Some(g.to_micros)) > since &&
event_time(g.from_time, Some(g.from_micros)) <= until
}) {
"unobserved"
} else {
visibility
}
}
///|
pub fn Timeline::report(self : Timeline) -> TimelineReport raise ParseError {
let end = self.last_input.unwrap_or(0UL)
let until = if end < self.analyzer.to_time {
end
} else {
self.analyzer.to_time
}
let entries = self.entries.copy()
for gap in self.gaps {
let time = event_time(gap.from_time, Some(gap.from_micros))
if time >= self.analyzer.from_time && time <= until {
entries.push({
timestamp: gap.from_time,
microseconds: Some(gap.from_micros),
kind: "observation_gap",
prefix: self.prefix,
path_id: None,
peer_asn: None,
peer_ip: None,
outbound: None,
accepted: true,
source: None,
before: None,
after: None,
changes: [],
})
}
}
if entries.length() > self.capacity {
raise Invalid(0, "时间线事件数量超过容量")
}
entries.sort_by_key(fn(e) {
(
event_time(e.timestamp, e.microseconds),
e.source.map(fn(s) { s.file }).unwrap_or(""),
e.source.map(fn(s) { s.record }).unwrap_or(0L),
e.source.map(fn(s) { s.event_index }).unwrap_or(0),
e.kind,
)
})
let intervals = self.intervals.copy()
let peer_intervals = self.peer_intervals.copy()
for _, trace in self.peer_paths {
if self.interval(trace, until, "observation_end", None, scope="peer_prefix")
is Some(interval) {
peer_intervals.push(interval)
}
}
let paths = []
let peers : Map[String, TimelinePeer] = Map([])
for key, pair in self.baselines {
peers[key] = {
peer_asn: pair.0.asn,
peer_ip: pair.0.address,
outbound: false,
visibility: self.visibility(pair.1, "not_visible", until),
visible_paths: 0,
not_visible_paths: 0,
unobserved_paths: 0,
baseline_present: true,
}
}
for key, pair in self.sessions {
peers[key] = {
peer_asn: pair.0.asn,
peer_ip: pair.0.address,
outbound: false,
visibility: "unobserved",
visible_paths: 0,
not_visible_paths: 0,
unobserved_paths: 0,
baseline_present: self.baselines.contains(key),
}
}
let keys = self.paths.keys().to_array()
keys.sort()
for key in keys {
let trace = self.paths.get(key).unwrap()
if self.interval(trace, until, "observation_end", None) is Some(interval) {
intervals.push(interval)
}
let event = trace.event
let visibility = self.visibility(trace.last_time, trace.visibility, until)
paths.push({
prefix: event.prefix,
path_id: event.path_id,
peer_asn: event.peer_asn,
peer_ip: event.peer_ip,
outbound: event.outbound,
visibility,
observed_at: trace.last_time,
source: trace.source,
})
let peer = peer_key(event.peer_asn, event.peer_ip, event.outbound)
let old = peers
.get(peer)
.unwrap_or({
peer_asn: event.peer_asn,
peer_ip: event.peer_ip,
outbound: event.outbound,
visibility: "unobserved",
visible_paths: 0,
not_visible_paths: 0,
unobserved_paths: 0,
baseline_present: false,
})
let active = old.visible_paths +
(if visibility == "visible" { 1 } else { 0 })
let absent = old.not_visible_paths +
(if visibility == "not_visible" { 1 } else { 0 })
let unknown = old.unobserved_paths +
(if visibility == "unobserved" { 1 } else { 0 })
peers[peer] = {
..old,
visible_paths: active,
not_visible_paths: absent,
unobserved_paths: unknown,
visibility: if active > 0 {
"visible"
} else if unknown > 0 {
"unobserved"
} else {
"not_visible"
},
}
}
intervals.sort_by_key(fn(i) {
(i.measured_from, i.peer_asn, i.peer_ip, i.outbound, i.prefix, i.path_id)
})
let peer_keys = peers.keys().to_array()
peer_keys.sort()
peer_intervals.sort_by_key(fn(i) {
(i.measured_from, i.peer_asn, i.peer_ip, i.outbound)
})
{
schema_version: "2.0",
prefix: self.prefix,
interpretation: "collector_route_visibility_not_business_outage",
baseline_status: self.analyzer.summary().baseline_status,
continuous_observation: self.continuous,
last_input_time: self.last_input,
observed_window_end: if until >= self.analyzer.from_time {
Some(until)
} else {
None
},
gaps: self.gaps,
entries,
intervals,
peer_intervals,
paths,
peers: peer_keys.map(fn(k) {
let peer = peers.get(k).unwrap()
if self.peer_paths.get(k) is Some(trace) {
{
..peer,
visibility: self.visibility(trace.last_time, trace.visibility, until),
}
} else {
peer
}
}),
}
}