484 lines
15 KiB
Rust
484 lines
15 KiB
Rust
use std::{
|
|
cell::RefCell,
|
|
fs::File,
|
|
io::{self, BufRead, BufReader, Read},
|
|
mem,
|
|
path::{Path, PathBuf},
|
|
rc::Rc,
|
|
sync::OnceLock,
|
|
thread::{self, JoinHandle},
|
|
};
|
|
|
|
use dumpster::sync::Gc;
|
|
|
|
use crate::tui::model::{ChildInfo, LogEntry, RawLogEntry};
|
|
|
|
struct LogFileEntryGenerator {
|
|
file: BufReader<Box<dyn Read + Send + Sync + 'static>>,
|
|
}
|
|
|
|
impl LogFileEntryGenerator {
|
|
fn next_line(&mut self) -> Option<String> {
|
|
let mut res = String::new();
|
|
match self.file.read_line(&mut res) {
|
|
Err(e) => {
|
|
eprintln!("error: {e:?}");
|
|
None
|
|
}
|
|
Ok(0) => None,
|
|
Ok(_) => Some(res),
|
|
}
|
|
}
|
|
|
|
fn next_raw_entry(&mut self) -> Option<RawLogEntry> {
|
|
let line = self.next_line()?;
|
|
match serde_json::from_str(&line) {
|
|
Ok(i) => Some(i),
|
|
Err(e) => {
|
|
#[cfg(test)]
|
|
panic!("deserializing: {e:?} in {line}");
|
|
#[cfg(not(test))]
|
|
eprintln!("deserializing: {e:?} in {line}");
|
|
#[cfg(not(test))]
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
fn next_entry(&mut self, prev: Option<Gc<LogEntry>>) -> Option<Gc<LogEntry>> {
|
|
let mut stack = Vec::<(RawLogEntry, Option<Gc<LogEntry>>, Option<Gc<LogEntry>>)>::new();
|
|
let mut curr_first = None;
|
|
let mut curr_last = prev;
|
|
|
|
loop {
|
|
let entry = self.next_raw_entry()?;
|
|
|
|
let new_entry = Gc::new(match entry.fields.message() {
|
|
Some("enter") => {
|
|
stack.push((entry, curr_first.take(), curr_last.take()));
|
|
continue;
|
|
}
|
|
Some("exit") => {
|
|
// TODO: does it match?
|
|
let Some((enter, prev_first, prev_last)) = stack.pop() else {
|
|
panic!("exit before entry");
|
|
};
|
|
let first_child = mem::replace(&mut curr_first, prev_first);
|
|
let last_child = mem::replace(&mut curr_last, prev_last);
|
|
|
|
let prev = curr_last.clone();
|
|
let next = OnceLock::new();
|
|
|
|
LogEntry::Sub {
|
|
enter,
|
|
exit: entry,
|
|
children: Gc::new(ChildInfo {
|
|
first_child,
|
|
last_child,
|
|
prev,
|
|
next,
|
|
}),
|
|
}
|
|
}
|
|
_ => {
|
|
let prev = curr_last.clone();
|
|
let next = OnceLock::new();
|
|
|
|
LogEntry::Single { entry, prev, next }
|
|
}
|
|
});
|
|
|
|
if stack.is_empty() {
|
|
return Some(new_entry);
|
|
} else if let Some(last) = &mut curr_last {
|
|
last.initialize_next().get_or_init({
|
|
let new_entry = new_entry.clone();
|
|
move || new_entry
|
|
});
|
|
*last = new_entry;
|
|
} else {
|
|
assert!(curr_first.is_none());
|
|
curr_first = Some(new_entry.clone());
|
|
curr_last = Some(new_entry);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Inner {
|
|
pub path: PathBuf,
|
|
#[allow(unused)]
|
|
pub jh: Option<JoinHandle<()>>,
|
|
first: Option<Gc<LogEntry>>,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct LogfileReader(Rc<RefCell<Inner>>);
|
|
|
|
impl LogfileReader {
|
|
pub fn new(p: impl AsRef<Path>) -> io::Result<Self> {
|
|
Ok(Self::new_from_read(File::open(p.as_ref())?, p))
|
|
}
|
|
|
|
pub fn new_from_read(r: impl Read + Send + Sync + 'static, p: impl AsRef<Path>) -> Self {
|
|
let mut generator = LogFileEntryGenerator {
|
|
file: BufReader::new(Box::new(r)),
|
|
};
|
|
|
|
let first = generator.next_entry(None);
|
|
|
|
let jh = thread::spawn({
|
|
let first = first.clone();
|
|
move || {
|
|
if let Some(mut curr_last) = first.clone() {
|
|
while let Some(new) = generator.next_entry(Some(curr_last.clone())) {
|
|
assert!(new.prev().is_some());
|
|
curr_last.initialize_next().get_or_init({
|
|
let new = new.clone();
|
|
|| new
|
|
});
|
|
curr_last = new;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
Self(Rc::new(RefCell::new(Inner {
|
|
path: p.as_ref().to_path_buf(),
|
|
first,
|
|
jh: Some(jh),
|
|
})))
|
|
}
|
|
|
|
pub fn path(&self) -> PathBuf {
|
|
self.0.borrow().path.clone()
|
|
}
|
|
|
|
pub fn first(&self) -> Option<Gc<LogEntry>> {
|
|
self.0.borrow().first.clone()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::sync::Arc;
|
|
|
|
use super::*;
|
|
use crate::tui::{
|
|
filter::{Filter, FilterKind, Matcher, MatcherValue},
|
|
log_viewer::filters::Filters,
|
|
model::FieldsName,
|
|
processing::Cursor,
|
|
widgets::last_error::LastError,
|
|
};
|
|
|
|
fn parse(data: &str) -> Cursor {
|
|
let r = LogfileReader::new_from_read(
|
|
std::io::Cursor::new(data.trim().as_bytes().to_vec()),
|
|
"test",
|
|
);
|
|
r.0.borrow_mut().jh.take().unwrap().join().unwrap();
|
|
Cursor::new(r.first().unwrap())
|
|
}
|
|
|
|
fn with_fields(fields: &str) -> String {
|
|
format!(
|
|
r#"{{"timestamp": "2026-03-30 23:47Z", "level": "DEBUG", "filename": "foo", "line_number": 30, "fields": {fields}}}"#
|
|
)
|
|
}
|
|
|
|
fn filters() -> Filters {
|
|
let le = LastError::new();
|
|
Filters::new(None, le)
|
|
}
|
|
|
|
#[test]
|
|
fn get_message() {
|
|
let c = parse(&[with_fields(r#"{"message": "foo"}"#)].join("\n"));
|
|
assert_eq!(c.curr().message_or_name(), Some("foo"));
|
|
}
|
|
|
|
#[test]
|
|
fn bounds() {
|
|
let mut c = parse(&[with_fields(r#"{"message": "foo"}"#)].join("\n"));
|
|
let f = filters();
|
|
|
|
assert_eq!(c.curr().message_or_name(), Some("foo"));
|
|
assert!(!c.next(&f));
|
|
assert!(!c.prev(&f));
|
|
assert!(!c.enter(&f));
|
|
assert!(!c.exit(&f));
|
|
assert_eq!(c.curr().message_or_name(), Some("foo"));
|
|
}
|
|
|
|
#[test]
|
|
fn two_singles() {
|
|
let mut c = parse(
|
|
&[
|
|
with_fields(r#"{"message": "foo"}"#),
|
|
with_fields(r#"{"message": "bar"}"#),
|
|
]
|
|
.join("\n"),
|
|
);
|
|
let f = filters();
|
|
|
|
assert_eq!(c.curr().message_or_name(), Some("foo"));
|
|
assert!(c.next(&f));
|
|
assert_eq!(c.curr().message_or_name(), Some("bar"));
|
|
assert!(!c.next(&f));
|
|
assert_eq!(c.curr().message_or_name(), Some("bar"));
|
|
assert!(c.prev(&f));
|
|
assert_eq!(c.curr().message_or_name(), Some("foo"));
|
|
assert!(!c.prev(&f));
|
|
assert!(!c.enter(&f));
|
|
assert!(!c.exit(&f));
|
|
assert_eq!(c.curr().message_or_name(), Some("foo"));
|
|
}
|
|
|
|
#[test]
|
|
fn enter_exit() {
|
|
let mut c = parse(
|
|
&[
|
|
with_fields(r#"{"message": "foo"}"#),
|
|
with_fields(r#"{"message": "enter"}"#),
|
|
with_fields(r#"{"message": "baz"}"#),
|
|
with_fields(r#"{"message": "meow"}"#),
|
|
with_fields(r#"{"message": "exit"}"#),
|
|
with_fields(r#"{"message": "bar"}"#),
|
|
]
|
|
.join("\n"),
|
|
);
|
|
let f = filters();
|
|
|
|
assert_eq!(c.curr().message_or_name(), Some("foo"));
|
|
assert!(c.next(&f));
|
|
assert!(matches!(Gc::as_ref(&c.curr()), LogEntry::Sub { .. }));
|
|
assert!(c.next(&f));
|
|
assert_eq!(c.curr().message_or_name(), Some("bar"));
|
|
assert!(c.prev(&f));
|
|
assert!(c.enter(&f));
|
|
assert_eq!(c.curr().message_or_name(), Some("baz"));
|
|
assert!(!c.enter(&f));
|
|
assert!(!c.prev(&f));
|
|
assert!(c.exit(&f));
|
|
assert!(matches!(Gc::as_ref(&c.curr()), LogEntry::Sub { .. }));
|
|
assert!(c.enter(&f));
|
|
assert!(c.next(&f));
|
|
assert!(!c.enter(&f));
|
|
assert!(!c.next(&f));
|
|
assert_eq!(c.curr().message_or_name(), Some("meow"));
|
|
assert!(c.exit(&f));
|
|
}
|
|
|
|
#[test]
|
|
fn empty_enter() {
|
|
let mut c = parse(
|
|
&[
|
|
with_fields(r#"{"message": "foo"}"#),
|
|
with_fields(r#"{"message": "enter"}"#),
|
|
with_fields(r#"{"message": "exit"}"#),
|
|
with_fields(r#"{"message": "bar"}"#),
|
|
]
|
|
.join("\n"),
|
|
);
|
|
let f = filters();
|
|
|
|
assert_eq!(c.curr().message_or_name(), Some("foo"));
|
|
assert!(c.next(&f));
|
|
assert!(
|
|
matches!(Gc::as_ref(&c.curr()), LogEntry::Sub { children, .. } if children.first_child.is_none() )
|
|
);
|
|
assert!(!c.enter(&f));
|
|
assert!(!c.exit(&f));
|
|
assert!(matches!(Gc::as_ref(&c.curr()), LogEntry::Sub { .. }));
|
|
assert!(c.next(&f));
|
|
assert_eq!(c.curr().message_or_name(), Some("bar"));
|
|
}
|
|
|
|
#[test]
|
|
fn remove_first() {
|
|
let mut c = parse(
|
|
&[
|
|
with_fields(r#"{"message": "foo"}"#),
|
|
with_fields(r#"{"message": "baz"}"#),
|
|
with_fields(r#"{"message": "meow"}"#),
|
|
with_fields(r#"{"message": "bar"}"#),
|
|
]
|
|
.join("\n"),
|
|
);
|
|
let mut f = filters();
|
|
f.push(Arc::new(Filter {
|
|
matcher: Matcher::Field {
|
|
name: "message".to_string(),
|
|
value: MatcherValue::Exact("foo".to_string()),
|
|
span: FieldsName::Main,
|
|
},
|
|
kind: FilterKind::Remove,
|
|
}));
|
|
|
|
assert!(c.next(&f));
|
|
assert_eq!(c.curr().message_or_name(), Some("baz"));
|
|
assert!(!c.prev(&f));
|
|
}
|
|
|
|
#[test]
|
|
fn remove_middle() {
|
|
let mut c = parse(
|
|
&[
|
|
with_fields(r#"{"message": "foo"}"#),
|
|
with_fields(r#"{"message": "baz"}"#),
|
|
with_fields(r#"{"message": "meow"}"#),
|
|
with_fields(r#"{"message": "bar"}"#),
|
|
]
|
|
.join("\n"),
|
|
);
|
|
let mut f = filters();
|
|
f.push(Arc::new(Filter {
|
|
matcher: Matcher::Field {
|
|
name: "message".to_string(),
|
|
value: MatcherValue::Exact("baz".to_string()),
|
|
span: FieldsName::Main,
|
|
},
|
|
kind: FilterKind::Remove,
|
|
}));
|
|
|
|
assert_eq!(c.curr().message_or_name(), Some("foo"));
|
|
assert!(c.next(&f));
|
|
assert_eq!(c.curr().message_or_name(), Some("meow"));
|
|
assert!(c.prev(&f));
|
|
assert_eq!(c.curr().message_or_name(), Some("foo"));
|
|
assert!(!c.prev(&f));
|
|
}
|
|
|
|
#[test]
|
|
fn inline() {
|
|
let mut c = parse(
|
|
&[
|
|
with_fields(r#"{"message": "foo"}"#),
|
|
with_fields(r#"{"message": "enter", "name": "nest"}"#),
|
|
with_fields(r#"{"message": "baz"}"#),
|
|
with_fields(r#"{"message": "meow"}"#),
|
|
with_fields(r#"{"message": "exit"}"#),
|
|
with_fields(r#"{"message": "bar"}"#),
|
|
]
|
|
.join("\n"),
|
|
);
|
|
let mut f = filters();
|
|
f.push(Arc::new(Filter {
|
|
matcher: Matcher::Field {
|
|
name: "name".to_string(),
|
|
value: MatcherValue::Exact("nest".to_string()),
|
|
span: FieldsName::Main,
|
|
},
|
|
kind: FilterKind::Inline,
|
|
}));
|
|
|
|
assert_eq!(c.curr().message_or_name(), Some("foo"));
|
|
assert!(c.next(&f));
|
|
assert_eq!(c.curr().message_or_name(), Some("baz"));
|
|
assert!(c.next(&f));
|
|
assert!(!c.exit(&f));
|
|
assert_eq!(c.curr().message_or_name(), Some("meow"));
|
|
assert!(c.next(&f));
|
|
assert_eq!(c.curr().message_or_name(), Some("bar"));
|
|
assert!(c.prev(&f));
|
|
assert_eq!(c.curr().message_or_name(), Some("meow"));
|
|
assert!(!c.exit(&f));
|
|
assert!(c.prev(&f));
|
|
assert_eq!(c.curr().message_or_name(), Some("baz"));
|
|
assert!(c.prev(&f));
|
|
assert_eq!(c.curr().message_or_name(), Some("foo"));
|
|
assert!(!c.prev(&f));
|
|
}
|
|
|
|
#[test]
|
|
fn inline_while_inside() {
|
|
let mut c = parse(
|
|
&[
|
|
with_fields(r#"{"message": "foo"}"#),
|
|
with_fields(r#"{"message": "enter", "name": "nest"}"#),
|
|
with_fields(r#"{"message": "baz"}"#),
|
|
with_fields(r#"{"message": "meow"}"#),
|
|
with_fields(r#"{"message": "exit"}"#),
|
|
with_fields(r#"{"message": "bar"}"#),
|
|
]
|
|
.join("\n"),
|
|
);
|
|
let mut f = filters();
|
|
|
|
assert_eq!(c.curr().message_or_name(), Some("foo"));
|
|
assert!(c.next(&f));
|
|
assert!(c.enter(&f));
|
|
assert_eq!(c.curr().message_or_name(), Some("baz"));
|
|
// inline the current item
|
|
f.push(Arc::new(Filter {
|
|
matcher: Matcher::Field {
|
|
name: "name".to_string(),
|
|
value: MatcherValue::Exact("nest".to_string()),
|
|
span: FieldsName::Main,
|
|
},
|
|
kind: FilterKind::Inline,
|
|
}));
|
|
c.update_with_parents(&f);
|
|
assert_eq!(c.curr().message_or_name(), Some("baz"));
|
|
println!("undo");
|
|
f.undo();
|
|
c.update_with_parents(&f);
|
|
assert_eq!(c.curr().message_or_name(), Some("nest"));
|
|
assert!(c.next(&f));
|
|
assert_eq!(c.curr().message_or_name(), Some("bar"));
|
|
assert!(!c.next(&f));
|
|
assert!(c.prev(&f));
|
|
assert!(c.enter(&f));
|
|
assert_eq!(c.curr().message_or_name(), Some("baz"));
|
|
assert!(c.next(&f));
|
|
assert_eq!(c.curr().message_or_name(), Some("meow"));
|
|
f.redo();
|
|
c.update_with_parents(&f);
|
|
// redo inlines, and goes to start of inlined part
|
|
assert_eq!(c.curr().message_or_name(), Some("baz"));
|
|
assert!(c.prev(&f));
|
|
assert_eq!(c.curr().message_or_name(), Some("foo"));
|
|
assert!(!c.prev(&f));
|
|
}
|
|
|
|
#[test]
|
|
fn inline_depth() {
|
|
let mut c = parse(
|
|
&[
|
|
with_fields(r#"{"message": "enter", "name": "nest1"}"#),
|
|
with_fields(r#"{"message": "enter", "name": "nest2"}"#),
|
|
with_fields(r#"{"message": "baz"}"#),
|
|
with_fields(r#"{"message": "exit"}"#),
|
|
with_fields(r#"{"message": "exit"}"#),
|
|
]
|
|
.join("\n"),
|
|
);
|
|
let mut f = filters();
|
|
c.enter(&f);
|
|
c.enter(&f);
|
|
assert_eq!(c.curr().message_or_name(), Some("baz"));
|
|
c.exit(&f);
|
|
c.exit(&f);
|
|
|
|
f.push(Arc::new(Filter {
|
|
matcher: Matcher::Field {
|
|
name: "name".to_string(),
|
|
value: MatcherValue::Exact("nest1".to_string()),
|
|
span: FieldsName::Main,
|
|
},
|
|
kind: FilterKind::Inline,
|
|
}));
|
|
f.push(Arc::new(Filter {
|
|
matcher: Matcher::Field {
|
|
name: "name".to_string(),
|
|
value: MatcherValue::Exact("nest2".to_string()),
|
|
span: FieldsName::Main,
|
|
},
|
|
kind: FilterKind::Inline,
|
|
}));
|
|
c.update_with_parents(&f);
|
|
|
|
assert_eq!(c.curr().message_or_name(), Some("baz"));
|
|
}
|
|
}
|