use std::collections::VecDeque;
pub const SYSTEM_LOG_LIMIT: usize = 1000;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SystemLogCategory {
Forge,
Git,
Session,
Sync,
System,
Update,
}
impl SystemLogCategory {
pub const fn label(self) -> &'static str {
match self {
Self::Forge => "forge",
Self::Git => "git",
Self::Session => "session",
Self::Sync => "sync",
Self::System => "system",
Self::Update => "update",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SystemLogLevel {
Info,
Success,
Warning,
Error,
}
impl SystemLogLevel {
pub const fn label(self) -> &'static str {
match self {
Self::Info => "INFO",
Self::Success => "OK",
Self::Warning => "WARN",
Self::Error => "ERR",
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SystemLogEvent {
pub category: SystemLogCategory,
pub detail: Option<String>,
pub level: SystemLogLevel,
pub message: String,
}
impl SystemLogEvent {
pub fn new(
level: SystemLogLevel,
category: SystemLogCategory,
message: impl Into<String>,
) -> Self {
Self {
category,
detail: None,
level,
message: message.into(),
}
}
#[must_use]
pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
self.detail = Some(detail.into());
self
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SystemLogEntry {
pub category: SystemLogCategory,
pub detail: Option<String>,
pub level: SystemLogLevel,
pub message: String,
pub sequence: u64,
pub timestamp_unix_seconds: i64,
}
pub struct SystemLogBuffer {
entries: VecDeque<SystemLogEntry>,
next_sequence: u64,
}
impl SystemLogBuffer {
pub fn entries(&self) -> &VecDeque<SystemLogEntry> {
&self.entries
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn push(&mut self, timestamp_unix_seconds: i64, event: SystemLogEvent) {
self.entries.push_back(SystemLogEntry {
category: event.category,
detail: event.detail,
level: event.level,
message: event.message,
sequence: self.next_sequence,
timestamp_unix_seconds,
});
self.next_sequence = self.next_sequence.saturating_add(1);
while self.entries.len() > SYSTEM_LOG_LIMIT {
self.entries.pop_front();
}
}
}
impl Default for SystemLogBuffer {
fn default() -> Self {
Self {
entries: VecDeque::with_capacity(SYSTEM_LOG_LIMIT),
next_sequence: 0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn push_records_timestamped_entries_in_order() {
let mut buffer = SystemLogBuffer::default();
buffer.push(
10,
SystemLogEvent::new(
SystemLogLevel::Info,
SystemLogCategory::System,
"Agentty started",
),
);
buffer.push(
11,
SystemLogEvent::new(
SystemLogLevel::Success,
SystemLogCategory::Sync,
"Sync complete",
)
.with_detail("main"),
);
let entries = buffer.entries().iter().collect::<Vec<_>>();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].sequence, 0);
assert_eq!(entries[0].timestamp_unix_seconds, 10);
assert_eq!(entries[0].message, "Agentty started");
assert_eq!(entries[1].sequence, 1);
assert_eq!(entries[1].detail.as_deref(), Some("main"));
}
#[test]
fn push_purges_oldest_entries_after_limit() {
let mut buffer = SystemLogBuffer::default();
for index in 0..=SYSTEM_LOG_LIMIT {
let timestamp = i64::try_from(index).expect("test index should fit in i64");
buffer.push(
timestamp,
SystemLogEvent::new(
SystemLogLevel::Info,
SystemLogCategory::System,
format!("event {index}"),
),
);
}
assert_eq!(buffer.len(), SYSTEM_LOG_LIMIT);
assert_eq!(
buffer.entries().front().map(|entry| entry.sequence),
Some(1)
);
assert_eq!(
buffer.entries().back().map(|entry| entry.sequence),
Some(SYSTEM_LOG_LIMIT as u64)
);
}
}