logviewer/src/tui/reader.rs
2026-04-03 18:07:49 +02:00

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::SpanDescriptor,
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: SpanDescriptor::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: SpanDescriptor::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: SpanDescriptor::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: SpanDescriptor::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("enter"));
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: SpanDescriptor::Main,
},
kind: FilterKind::Inline,
}));
f.push(Arc::new(Filter {
matcher: Matcher::Field {
name: "name".to_string(),
value: MatcherValue::Exact("nest2".to_string()),
span: SpanDescriptor::Main,
},
kind: FilterKind::Inline,
}));
c.update_with_parents(&f);
assert_eq!(c.curr().message_or_name(), Some("baz"));
}
}