Files
kaptan/firmware/tools/configurator/src/log_monitor.rs
T

236 lines
7.3 KiB
Rust

//! Bounded diagnostic-event history for the browser's live log view.
use std::{collections::VecDeque, time::SystemTime};
use kaptan_log::Level;
use kaptan_mavlink::generated::kaptan::LogLevel;
use serde::{Serialize, Serializer};
const MAX_LOG_ENTRIES: usize = 512;
fn serialize_level<S>(level: &Level, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(level.as_str())
}
/// Decodes a wire event level, rejecting the filter-only Off value.
pub const fn event_level_from_wire(level: LogLevel) -> Option<Level> {
match level {
LogLevel::LOG_LEVEL_OFF => None,
LogLevel::LOG_LEVEL_TRACE => Some(Level::Trace),
LogLevel::LOG_LEVEL_DEBUG => Some(Level::Debug),
LogLevel::LOG_LEVEL_INFO => Some(Level::Info),
LogLevel::LOG_LEVEL_WARN => Some(Level::Warn),
LogLevel::LOG_LEVEL_ERROR => Some(Level::Error),
}
}
/// Human-readable packet-view representation.
pub const fn wire_level_label(level: LogLevel) -> &'static str {
match event_level_from_wire(level) {
Some(level) => level.as_str(),
None => "OFF",
}
}
/// One diagnostic event decoded from a CRC-valid Kaptan MAVLink frame.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LogObservation {
pub system_id: u8,
pub component_id: u8,
pub level: Level,
pub truncated: bool,
pub text: String,
}
/// Read-only browser projection of the current link session's log history.
#[derive(Clone, Debug, Serialize)]
pub struct LogSnapshot {
pub session: u32,
pub discarded: u64,
pub entries: Vec<LogView>,
}
/// One retained diagnostic event, ordered by `id` within a link session.
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct LogView {
pub id: u32,
pub received_at_ms: u64,
pub system_id: u8,
pub component_id: u8,
#[serde(serialize_with = "serialize_level")]
pub level: Level,
pub truncated: bool,
pub text: String,
}
#[derive(Default)]
pub struct LogMonitor {
entries: VecDeque<LogView>,
session: u32,
next_id: u32,
discarded: u64,
}
impl LogMonitor {
/// Retains every event in receive order up to the fixed host-side bound.
pub fn observe(
&mut self,
observations: impl IntoIterator<Item = LogObservation>,
now: SystemTime,
) {
let received_at_ms = now
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_millis()
.min(u128::from(u64::MAX)) as u64;
for observation in observations {
if self.next_id == u32::MAX {
self.clear();
}
if self.entries.len() == MAX_LOG_ENTRIES {
self.entries.pop_front();
self.discarded = self.discarded.saturating_add(1);
}
self.entries.push_back(LogView {
id: self.next_id,
received_at_ms,
system_id: observation.system_id,
component_id: observation.component_id,
level: observation.level,
truncated: observation.truncated,
text: observation.text,
});
self.next_id += 1;
}
}
pub fn snapshot(&self) -> LogSnapshot {
LogSnapshot {
session: self.session,
discarded: self.discarded,
entries: self.entries.iter().cloned().collect(),
}
}
/// Starts a fresh browser history for a new serial-link session.
pub fn clear(&mut self) {
self.entries.clear();
self.session = self.session.wrapping_add(1);
self.next_id = 0;
self.discarded = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
fn observation(index: usize) -> LogObservation {
LogObservation {
system_id: 1,
component_id: 2,
level: Level::Info,
truncated: false,
text: format!("event {index}"),
}
}
#[test]
fn retains_distinct_events_in_receive_order() {
let mut monitor = LogMonitor::default();
let now = SystemTime::UNIX_EPOCH + Duration::from_millis(1_234);
monitor.observe([observation(0), observation(1)], now);
let snapshot = monitor.snapshot();
assert_eq!(snapshot.entries.len(), 2);
assert_eq!(snapshot.entries[0].id, 0);
assert_eq!(snapshot.entries[0].received_at_ms, 1_234);
assert!(!snapshot.entries[0].truncated);
assert_eq!(snapshot.entries[0].text, "event 0");
assert_eq!(snapshot.entries[1].id, 1);
assert_eq!(snapshot.entries[1].text, "event 1");
assert_eq!(snapshot.discarded, 0);
assert_eq!(
serde_json::to_value(&snapshot).unwrap()["entries"][0]["level"],
"INFO"
);
}
#[test]
fn drops_only_the_oldest_entries_at_the_fixed_bound() {
let mut monitor = LogMonitor::default();
monitor.observe(
(0..=MAX_LOG_ENTRIES).map(observation),
SystemTime::UNIX_EPOCH,
);
let snapshot = monitor.snapshot();
assert_eq!(snapshot.entries.len(), MAX_LOG_ENTRIES);
assert_eq!(snapshot.entries[0].text, "event 1");
assert_eq!(snapshot.entries[MAX_LOG_ENTRIES - 1].text, "event 512");
assert_eq!(snapshot.discarded, 1);
}
#[test]
fn clear_starts_a_new_link_session() {
let mut monitor = LogMonitor::default();
monitor.observe([observation(0)], SystemTime::UNIX_EPOCH);
monitor.clear();
let snapshot = monitor.snapshot();
assert_eq!(snapshot.session, 1);
assert!(snapshot.entries.is_empty());
assert_eq!(snapshot.discarded, 0);
}
#[test]
fn wire_level_accepts_only_event_values() {
assert_eq!(event_level_from_wire(LogLevel::LOG_LEVEL_OFF), None);
for (wire, expected) in [
(LogLevel::LOG_LEVEL_TRACE, Level::Trace),
(LogLevel::LOG_LEVEL_DEBUG, Level::Debug),
(LogLevel::LOG_LEVEL_INFO, Level::Info),
(LogLevel::LOG_LEVEL_WARN, Level::Warn),
(LogLevel::LOG_LEVEL_ERROR, Level::Error),
] {
assert_eq!(event_level_from_wire(wire), Some(expected));
}
assert_eq!(wire_level_label(LogLevel::LOG_LEVEL_TRACE), "TRACE");
assert_eq!(wire_level_label(LogLevel::LOG_LEVEL_OFF), "OFF");
}
#[test]
fn truncation_state_survives_the_browser_projection() {
let mut event = observation(0);
event.truncated = true;
let mut monitor = LogMonitor::default();
monitor.observe([event], SystemTime::UNIX_EPOCH);
let snapshot = monitor.snapshot();
assert!(snapshot.entries[0].truncated);
assert_eq!(
serde_json::to_value(&snapshot).unwrap()["entries"][0]["truncated"],
true
);
}
#[test]
fn identifier_exhaustion_starts_a_fresh_rendering_session() {
let mut monitor = LogMonitor {
next_id: u32::MAX,
..LogMonitor::default()
};
monitor.observe([observation(0)], SystemTime::UNIX_EPOCH);
let snapshot = monitor.snapshot();
assert_eq!(snapshot.session, 1);
assert_eq!(snapshot.entries.len(), 1);
assert_eq!(snapshot.entries[0].id, 0);
assert_eq!(snapshot.entries[0].text, "event 0");
}
}