new streams

This commit is contained in:
Jana Dönszelmann 2026-02-24 12:41:00 +01:00
parent 3963fc50c3
commit c73be7166f
No known key found for this signature in database
13 changed files with 748 additions and 174 deletions

19
.direnv/bin/nix-direnv-reload Executable file
View file

@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -e
if [[ ! -d "/home/jana/src/projects/rustc-logviz" ]]; then
echo "Cannot find source directory; Did you move it?"
echo "(Looking for "/home/jana/src/projects/rustc-logviz")"
echo 'Cannot force reload with this script - use "direnv reload" manually and then try again'
exit 1
fi
# rebuild the cache forcefully
_nix_direnv_force_reload=1 direnv exec "/home/jana/src/projects/rustc-logviz" true
# Update the mtime for .envrc.
# This will cause direnv to reload again - but without re-building.
touch "/home/jana/src/projects/rustc-logviz/.envrc"
# Also update the timestamp of whatever profile_rc we have.
# This makes sure that we know we are up to date.
touch -r "/home/jana/src/projects/rustc-logviz/.envrc" "/home/jana/src/projects/rustc-logviz/.direnv"/*.rc

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake;

1
.gitignore vendored
View file

@ -1 +1,2 @@
/target /target
.direnv

153
flake.lock generated Normal file
View file

@ -0,0 +1,153 @@
{
"nodes": {
"fenix": {
"inputs": {
"nixpkgs": [
"naersk",
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1752475459,
"narHash": "sha256-z6QEu4ZFuHiqdOPbYss4/Q8B0BFhacR8ts6jO/F/aOU=",
"owner": "nix-community",
"repo": "fenix",
"rev": "bf0d6f70f4c9a9cf8845f992105652173f4b617f",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"naersk": {
"inputs": {
"fenix": "fenix",
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1769799857,
"narHash": "sha256-88IFXZ7Sa1vxbz5pty0Io5qEaMQMMUPMonLa3Ls/ss4=",
"owner": "nix-community",
"repo": "naersk",
"rev": "9d4ed44d8b8cecdceb1d6fd76e74123d90ae6339",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "naersk",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1752077645,
"narHash": "sha256-HM791ZQtXV93xtCY+ZxG1REzhQenSQO020cu6rHtAPk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "be9e214982e20b8310878ac2baa063a961c1bdf6",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-mozilla": {
"flake": false,
"locked": {
"lastModified": 1762256096,
"narHash": "sha256-Hzj/d8eRpfRfjHSRX6gGRf0jSg2zIJMXl6S5opuKsHc=",
"owner": "mozilla",
"repo": "nixpkgs-mozilla",
"rev": "80c058cf774c198fb838fc3549806b232dd3e320",
"type": "github"
},
"original": {
"owner": "mozilla",
"repo": "nixpkgs-mozilla",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1771848320,
"narHash": "sha256-0MAd+0mun3K/Ns8JATeHT1sX28faLII5hVLq0L3BdZU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2fc6539b481e1d2569f25f8799236694180c0993",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"naersk": "naersk",
"nixpkgs": "nixpkgs_2",
"nixpkgs-mozilla": "nixpkgs-mozilla"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1752428706,
"narHash": "sha256-EJcdxw3aXfP8Ex1Nm3s0awyH9egQvB2Gu+QEnJn2Sfg=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "591e3b7624be97e4443ea7b5542c191311aa141d",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

77
flake.nix Normal file
View file

@ -0,0 +1,77 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
# for building rust packages
naersk.url = "github:nix-community/naersk";
# for eary pre-built toolchains
nixpkgs-mozilla = {
url = "github:mozilla/nixpkgs-mozilla";
flake = false;
};
};
outputs =
{
self,
nixpkgs,
flake-utils,
nixpkgs-mozilla,
naersk,
}:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = (import nixpkgs) {
inherit system;
overlays = [
(import nixpkgs-mozilla)
];
};
toolchain = (
(pkgs.rustChannelOf {
rustToolchain = ./rust-toolchain.toml;
sha256 = "sha256-sqSWJDUxc+zaz1nBWMAJKTAGBuGWP25GCftIOlCEAtA=";
}).rust.override
{
extensions = [
"rust-src"
];
}
);
in
{
packages = {
backend = pkgs.callPackage ./packages/rawr-backend.nix {
inherit naersk toolchain;
};
};
devShells.default =
with pkgs;
mkShell {
nativeBuildInputs = [
openssl
];
buildInputs = [
pkg-config
clang
llvmPackages_latest.bintools
toolchain
];
packages = [
gdb
];
shellHook = ''
export LIBCLANG_PATH="${lib.makeLibraryPath [ llvmPackages_latest.libclang.lib ]}"
export LD_LIBRARY_PATH="'$LD_LIBRARY_PATH:${
lib.makeLibraryPath [
openssl
]
}"
PKG_CONFIG_PATH="${openssl.dev}/lib/pkgconfig";
'';
};
}
);
}

3
rust-toolchain.toml Normal file
View file

@ -0,0 +1,3 @@
[toolchain]
channel = "1.92"
components = ["rust-analyzer", "rust-src", "rustfmt"]

View file

@ -27,7 +27,15 @@ enum Preset {
} }
fn default_tempdir() -> PathBuf { fn default_tempdir() -> PathBuf {
temp_dir().join("rustc-logviz") let home = std::env::var("HOME").unwrap();
let t_rs_tempdirs = PathBuf::from(home).join("tempdirs");
let tempdir = if t_rs_tempdirs.exists() {
t_rs_tempdirs
} else {
temp_dir()
};
tempdir.join("rustc-logviz")
} }
#[derive(Parser, Debug)] #[derive(Parser, Debug)]

View file

@ -1,12 +1,13 @@
use crate::tui::model::LogEntry; use crate::tui::model::LogEntry;
#[derive(Clone)]
pub enum WipMatcher { pub enum WipMatcher {
Field { Field {
name: Option<String>, name: Option<String>,
value: Option<serde_json::Value>, value: Option<serde_json::Value>,
}, },
Specific { Specific {
path: Option<Vec<usize>>, hash: Option<u64>,
}, },
} }
@ -20,9 +21,7 @@ impl WipMatcher {
name: name.clone(), name: name.clone(),
value: value.clone(), value: value.clone(),
}), }),
WipMatcher::Specific { path: Some(path) } => { WipMatcher::Specific { hash } => Some(Matcher::Specific { hash: (*hash)? }),
Some(Matcher::Specific { path: path.clone() })
}
_ => None, _ => None,
} }
} }
@ -34,7 +33,7 @@ pub enum Matcher {
value: serde_json::Value, value: serde_json::Value,
}, },
Specific { Specific {
path: Vec<usize>, hash: u64,
}, },
} }
@ -46,7 +45,7 @@ impl Matcher {
.fields .fields
.get(name) .get(name)
.is_some_and(|v| v == value), .is_some_and(|v| v == value),
Matcher::Specific { path } => false, Matcher::Specific { hash } => entry.hash() == *hash,
} }
} }
} }
@ -62,6 +61,16 @@ pub struct Filter {
pub kind: FilterKind, pub kind: FilterKind,
} }
impl Filter {
pub fn removes(&self, elem: &LogEntry) -> bool {
match self.kind {
FilterKind::Inline => false,
FilterKind::Remove => self.matcher.matches(elem),
}
}
}
#[derive(Clone)]
pub struct WipFilter { pub struct WipFilter {
pub matcher: Option<WipMatcher>, pub matcher: Option<WipMatcher>,
pub kind: Option<FilterKind>, pub kind: Option<FilterKind>,

View file

@ -1,88 +1,208 @@
use std::{collections::HashMap, mem, rc::Rc}; use std::{collections::HashMap, iter, mem, rc::Rc};
use crate::tui::{ use crate::tui::{
filter::Filter,
model::LogEntry, model::LogEntry,
reader::{FilterAdapter, LogfileReader}, processing::{IntoLogStream, LogStream},
}; };
use tui_widget_list::ListState; use tui_widget_list::ListState;
#[derive(Default, Clone)]
pub struct LogView { pub struct LogView {
first_item: usize, iter: Box<dyn LogStream>,
selected: usize, selection_offset: usize,
}
impl LogView {
pub fn selected(&self) -> Option<Rc<LogEntry>> {
let mut temp_iter = self.iter.clone();
for _ in 0..self.selection_offset {
let _ = temp_iter.next()?;
}
temp_iter.next()
}
}
impl Clone for LogView {
fn clone(&self) -> Self {
Self {
iter: self.iter.clone(),
selection_offset: self.selection_offset.clone(),
}
}
} }
pub struct LogViewer { pub struct LogViewer {
stack: Vec<LogView>, stack: Vec<LogView>,
curr: LogView, curr: LogView,
cache: HashMap<Vec<usize>, LogView>, cache: HashMap<Vec<usize>, LogView>,
filters: Vec<Rc<Filter>>,
pub root_stream: Box<dyn LogStream>,
pub last_height: usize, pub last_height: usize,
pub footer_selected: bool, pub footer_selected: bool,
pub footer_list: ListState, pub footer_list: ListState,
} }
impl LogViewer { impl LogViewer {
pub fn new() -> Self { pub fn new(stream: impl LogStream) -> Self {
Self { Self {
stack: Vec::new(), stack: Vec::new(),
curr: LogView::default(), curr: LogView {
iter: stream.clone(),
selection_offset: 0,
},
root_stream: stream.clone(),
cache: HashMap::new(), cache: HashMap::new(),
footer_list: ListState::default(), footer_list: ListState::default(),
footer_selected: false, footer_selected: false,
last_height: 0, last_height: 0,
filters: Vec::new(),
} }
} }
pub fn filtered_root_stream(&self) -> Box<dyn LogStream> {
let mut curr = self.root_stream.clone();
for filter in &self.filters {
curr = Box::new(curr.filter(Rc::clone(filter)));
}
curr
}
pub fn add_filter(&mut self, filter: Rc<Filter>) {
self.filters.push(Rc::clone(&filter));
self.cache.clear();
let offsets_list: Vec<_> = self
.stack
.iter()
.map(|i| (i.selected(), i.selection_offset))
.chain(iter::once((
self.curr.selected(),
self.curr.selection_offset,
)))
.collect();
fn find_elem_in_stream(
stream: &dyn LogStream,
elem: &Rc<LogEntry>,
) -> Option<Box<dyn LogStream>> {
let mut temp_stream = stream.clone();
let mut max = 100usize;
while let Some(curr) = temp_stream.next() {
if Rc::ptr_eq(&curr, elem) {
return Some(temp_stream);
}
if max == 0 {
break;
}
max -= 1;
}
None
}
let mut current_stream = self.filtered_root_stream();
let mut new_stack = Vec::<LogView>::new();
'outer: for (elem, old_offset) in offsets_list {
let Some(elem) = elem else {
break;
};
// If the value we're looking for is removed by the filter,
// we'll have a hard time finding it so quit
if filter.removes(&elem) {
break;
}
// find the nearest stream in which this element can be found
let mut curr = current_stream.as_ref();
let mut parents = new_stack.iter().rev();
let mut found_in_toplevel = true;
let mut stream = loop {
if let Some(stream) = find_elem_in_stream(curr, &elem) {
break stream;
}
found_in_toplevel = false;
if let Some(parent) = parents.next() {
curr = parent.iter.as_ref();
continue;
}
break 'outer;
};
let offset = if found_in_toplevel {
let mut offset = 0;
for _ in 0..old_offset {
if stream.prev().is_none() {
break;
}
offset += 1;
}
offset
} else {
0
};
current_stream = stream.clone();
new_stack.push(LogView {
iter: stream,
selection_offset: offset,
});
}
// Take the top of the stack as `curr`, unless
// the new stack is empty, then just reset the current view.
if let Some(curr) = new_stack.pop() {
self.curr = curr;
} else {
self.curr = LogView {
iter: self.filtered_root_stream().clone(),
selection_offset: 0,
};
}
self.stack = new_stack;
}
pub fn update_num_items(&mut self, num_visible_items: usize) { pub fn update_num_items(&mut self, num_visible_items: usize) {
while self.curr.selected >= self.curr.first_item + num_visible_items { while self.curr.selection_offset >= num_visible_items {
self.curr.first_item += 1; if self.curr.selection_offset == 0 {
break;
}
self.curr.iter.next();
self.curr.selection_offset -= 1;
} }
self.last_height = num_visible_items; self.last_height = num_visible_items;
} }
pub fn footer_fields(&self, file: &mut LogfileReader) -> Vec<(String, serde_json::Value)> { pub fn footer_fields(&self) -> Vec<(String, serde_json::Value)> {
if let Some(selected) = self.selected(file) { if let Some(selected) = self.selected() {
selected.all_fields().fields.into_iter().collect::<Vec<_>>() selected.all_fields().fields.into_iter().collect::<Vec<_>>()
} else { } else {
Vec::new() Vec::new()
} }
} }
pub fn items( pub fn items(&self, max: usize) -> Option<(Vec<Rc<LogEntry>>, usize)> {
&self, let mut temp_iter = self.curr.iter.clone();
file: &mut LogfileReader, let mut res = Vec::new();
max: usize, for _ in 0..max {
) -> Option<(Vec<Rc<LogEntry>>, usize, usize)> { let Some(i) = temp_iter.next() else {
let items: Vec<_> = if self.stack.is_empty() { break;
file.iter_from(self.curr.first_item).take(max).collect()
} else {
let mut stack = self.stack.iter();
let first = stack.next().unwrap();
let mut curr_log_entry = file.iter_from(first.selected).next()?;
for elem in stack {
curr_log_entry = curr_log_entry.get(elem.selected)?;
}
match curr_log_entry.as_ref() {
LogEntry::Single { .. } => return None,
LogEntry::Sub { sub_entries, .. } => {
FilterAdapter::new(file, &sub_entries[self.curr.first_item..])
.take(max)
.cloned()
.collect()
}
}
}; };
res.push(i);
Some((items, self.curr.first_item, self.curr.selected))
} }
pub fn selected(&self, file: &mut LogfileReader) -> Option<Rc<LogEntry>> { Some((res, self.curr.selection_offset))
self.items(file, self.curr.selected - self.curr.first_item + 1)? }
.0
.get(self.curr.selected - self.curr.first_item) pub fn selected(&self) -> Option<Rc<LogEntry>> {
.cloned() self.curr.selected()
} }
fn update_footer_select(&mut self) { fn update_footer_select(&mut self) {
@ -93,8 +213,11 @@ impl LogViewer {
if self.footer_selected { if self.footer_selected {
self.footer_list.previous(); self.footer_list.previous();
} else { } else {
self.curr.selected = self.curr.selected.saturating_sub(1); if self.curr.selection_offset == 0 {
self.curr.first_item = self.curr.first_item.min(self.curr.selected); let _ = self.curr.iter.prev();
} else {
self.curr.selection_offset -= 1;
}
self.update_footer_select(); self.update_footer_select();
} }
} }
@ -103,20 +226,25 @@ impl LogViewer {
if self.footer_selected { if self.footer_selected {
self.footer_list.next(); self.footer_list.next();
} else { } else {
self.curr.selected += 1; self.curr.selection_offset += 1;
self.update_footer_select(); self.update_footer_select();
} }
} }
pub fn page_down(&mut self) { pub fn page_down(&mut self) {
self.curr.selected += self.last_height; self.curr.selection_offset += self.last_height;
self.footer_selected = false; self.footer_selected = false;
self.update_footer_select(); self.update_footer_select();
} }
pub fn page_up(&mut self) { pub fn page_up(&mut self) {
self.curr.selected = self.curr.selected.saturating_sub(self.last_height); for _ in 0..self.last_height {
self.curr.first_item = self.curr.first_item.min(self.curr.selected); if self.curr.selection_offset == 0 {
let _ = self.curr.iter.prev();
} else {
self.curr.selection_offset -= 1;
}
}
self.footer_selected = false; self.footer_selected = false;
self.update_footer_select(); self.update_footer_select();
} }
@ -125,19 +253,21 @@ impl LogViewer {
if self.footer_selected { if self.footer_selected {
self.footer_list.select(Some(0)); self.footer_list.select(Some(0));
} else { } else {
self.curr.selected = 0; self.curr.selection_offset = 0;
self.curr.first_item = 0; while self.curr.iter.prev().is_some() {}
self.update_footer_select(); self.update_footer_select();
} }
} }
pub fn path(&self) -> Vec<usize> { pub fn path(&self) -> Vec<usize> {
self.stack.iter().map(|i| i.selected).collect() self.stack.iter().map(|i| i.selection_offset).collect()
} }
pub fn back(&mut self) { pub fn back(&mut self) {
self.cache.insert(self.path(), self.curr); self.cache.insert(self.path(), self.curr.clone());
self.curr = self.stack.pop().unwrap_or_default(); if let Some(stack) = self.stack.pop() {
self.curr = stack;
}
self.footer_selected = false; self.footer_selected = false;
self.update_footer_select(); self.update_footer_select();
} }
@ -146,19 +276,24 @@ impl LogViewer {
self.footer_selected = !self.footer_selected; self.footer_selected = !self.footer_selected;
} }
pub fn enter(&mut self, file: &mut LogfileReader) { pub fn enter(&mut self) {
if !self.footer_selected { if !self.footer_selected {
let Some(s) = self.selected(file) else { let Some(s) = self.selected() else {
return; return;
}; };
if let LogEntry::Single { .. } = s.as_ref() { let Some(i) = s.from_start() else {
return; return;
} };
self.stack self.stack.push(mem::replace(
.push(mem::replace(&mut self.curr, LogView::default())); &mut self.curr,
LogView {
iter: Box::new(i),
selection_offset: 0,
},
));
if let Some(cached_view) = self.cache.get(&self.path()) { if let Some(cached_view) = self.cache.get(&self.path()) {
self.curr = *cached_view; self.curr = cached_view.clone();
} }
self.update_footer_select(); self.update_footer_select();
} }

View file

@ -4,6 +4,7 @@ use std::{
io, io,
path::{Path, PathBuf}, path::{Path, PathBuf},
process::exit, process::exit,
rc::Rc,
}; };
use tui_widget_list::{ListBuilder, ListView}; use tui_widget_list::{ListBuilder, ListView};
@ -21,6 +22,7 @@ use ratatui::{
crossterm::event::{self, Event, KeyCode, KeyModifiers}, crossterm::event::{self, Event, KeyCode, KeyModifiers},
layout::{Constraint, HorizontalAlignment, Layout, Rect}, layout::{Constraint, HorizontalAlignment, Layout, Rect},
style::Style, style::Style,
text::Span,
widgets::{ widgets::{
Block, Clear, List, ListItem, ListState, Padding, Paragraph, StatefulWidget, Widget, Wrap, Block, Clear, List, ListItem, ListState, Padding, Paragraph, StatefulWidget, Widget, Wrap,
}, },
@ -29,6 +31,7 @@ use ratatui::{
pub mod filter; pub mod filter;
pub mod log_viewer; pub mod log_viewer;
pub mod model; pub mod model;
pub mod processing;
pub mod reader; pub mod reader;
pub fn run(logs_dir: PathBuf) { pub fn run(logs_dir: PathBuf) {
@ -68,13 +71,9 @@ impl Tab {
} }
} }
fn initialize_filter( fn initialize_filter(lv: &mut LogViewer, kind: Option<FilterKind>) -> WipFilter {
lv: &mut LogViewer,
file: &mut LogfileReader,
kind: Option<FilterKind>,
) -> WipFilter {
let matcher = if lv.footer_selected { let matcher = if lv.footer_selected {
let footer_fields = lv.footer_fields(file); let footer_fields = lv.footer_fields();
let (key, value) = footer_fields let (key, value) = footer_fields
.get(lv.footer_list.selected.unwrap_or(0)) .get(lv.footer_list.selected.unwrap_or(0))
.map_or((None, None), |(k, v)| (Some(k), Some(v))); .map_or((None, None), |(k, v)| (Some(k), Some(v)));
@ -84,7 +83,7 @@ fn initialize_filter(
}) })
} else { } else {
Some(WipMatcher::Specific { Some(WipMatcher::Specific {
path: Some(lv.path().clone()), hash: lv.selected().map(|i| i.hash()),
}) })
}; };
@ -115,7 +114,7 @@ impl App {
} }
fn current_file_path(&self) -> Option<PathBuf> { fn current_file_path(&self) -> Option<PathBuf> {
self.current_file.as_ref().map(|i| i.path.to_path_buf()) self.current_file.as_ref().map(|i| i.path())
} }
fn replace_tab(&mut self, tab: Tab) { fn replace_tab(&mut self, tab: Tab) {
@ -244,23 +243,19 @@ impl App {
(KeyCode::Left, Tab::CreateFilter { filter }) => { (KeyCode::Left, Tab::CreateFilter { filter }) => {
filter.left(); filter.left();
} }
(KeyCode::Right, Tab::LogViewer(lv)) => { (KeyCode::Right, Tab::LogViewer(lv)) => lv.enter(),
if let Some(file) = &mut self.current_file {
lv.enter(file)
}
}
(KeyCode::Tab, Tab::LogViewer(lv)) => { (KeyCode::Tab, Tab::LogViewer(lv)) => {
lv.switch_focus(); lv.switch_focus();
} }
(KeyCode::Char('r'), Tab::LogViewer(lv)) => { (KeyCode::Char('r'), Tab::LogViewer(lv)) => {
if let Some(file) = &mut self.current_file { if let Some(file) = &mut self.current_file {
let filter = initialize_filter(lv, file, Some(FilterKind::Remove)); let filter = initialize_filter(lv, Some(FilterKind::Remove));
self.push_tab(Tab::CreateFilter { filter }); self.push_tab(Tab::CreateFilter { filter });
} }
} }
(KeyCode::Char('i'), Tab::LogViewer(lv)) => { (KeyCode::Char('i'), Tab::LogViewer(lv)) => {
if let Some(file) = &mut self.current_file { if let Some(file) = &mut self.current_file {
let filter = initialize_filter(lv, file, Some(FilterKind::Inline)); let filter = initialize_filter(lv, Some(FilterKind::Inline));
self.push_tab(Tab::CreateFilter { filter }); self.push_tab(Tab::CreateFilter { filter });
} }
} }
@ -271,8 +266,8 @@ impl App {
{ {
match LogfileReader::new(&selected.path()) { match LogfileReader::new(&selected.path()) {
Ok(i) => { Ok(i) => {
self.current_file = Some(i); self.current_file = Some(i.clone());
self.replace_tab(Tab::LogViewer(LogViewer::new())); self.replace_tab(Tab::LogViewer(LogViewer::new(i.iter())));
} }
Err(e) => { Err(e) => {
panic!() panic!()
@ -281,22 +276,26 @@ impl App {
} }
} }
Tab::LogViewer(lv) => { Tab::LogViewer(lv) => {
if let Some(file) = &mut self.current_file {
if lv.footer_selected { if lv.footer_selected {
let filter = initialize_filter(lv, file, None); let filter = initialize_filter(lv, None);
self.push_tab(Tab::CreateFilter { filter }); self.push_tab(Tab::CreateFilter { filter });
} else { } else {
lv.enter(file) lv.enter()
}
} }
} }
Tab::Empty => {} Tab::Empty => {}
Tab::CreateFilter { filter } => { Tab::CreateFilter { filter } => {
if let FilterSelection::Confirm = filter.selection if let FilterSelection::Confirm = filter.selection {
&& let Some(file) = &mut self.current_file let filter_clone = filter.clone();
if let Some(lv) = self.tabs.iter_mut().rev().find_map(|i| {
if let Tab::LogViewer(lv) = i {
Some(lv)
} else {
None
}
}) && let Some(filter) = filter_clone.validate()
{ {
if let Some(filter) = filter.validate() { lv.add_filter(Rc::new(filter));
file.add_filter(filter);
self.pop_tab(); self.pop_tab();
if let Tab::LogViewer(lv) = self.current_tab() { if let Tab::LogViewer(lv) = self.current_tab() {
@ -419,22 +418,20 @@ impl Widget for &mut App {
StatefulWidget::render(list, main_area, buf, state); StatefulWidget::render(list, main_area, buf, state);
} }
Tab::LogViewer(lv) => { Tab::LogViewer(lv) => {
let Some(file) = &mut self.current_file else {
continue;
};
lv.update_num_items(main_area.height as usize); lv.update_num_items(main_area.height as usize);
let (items, start, selected) = lv let (items, selected_offset) = lv
.items(file, main_area.height as usize) .items(main_area.height as usize)
.unwrap_or_else(|| (Vec::new(), 0, 0)); .unwrap_or_else(|| (Vec::new(), 0));
Paragraph::new(selected_offset.to_string()).render(right, buf);
let list = List::new(items.into_iter().enumerate().map(|(idx, i)| { let list = List::new(items.into_iter().enumerate().map(|(idx, i)| {
let line = i.line_text(false); let line = i.line_text(false);
let mut list_item = ListItem::new(line); let mut list_item = ListItem::new(line);
if idx + start == selected { if idx == selected_offset {
list_item = list_item.style(highlighted); list_item = list_item.style(highlighted);
} }
@ -442,7 +439,7 @@ impl Widget for &mut App {
})); }));
Widget::render(list, main_area, buf); Widget::render(list, main_area, buf);
let items = lv.footer_fields(file); let items = lv.footer_fields();
let width = 20; let width = 20;
let builder = ListBuilder::new(|cx| { let builder = ListBuilder::new(|cx| {
let Some((k, v)) = &items.get(cx.index) else { let Some((k, v)) = &items.get(cx.index) else {

View file

@ -1,10 +1,15 @@
use std::{collections::BTreeMap, rc::Rc, sync::OnceLock}; use std::{
collections::BTreeMap,
hash::{DefaultHasher, Hash, Hasher},
rc::Rc,
sync::OnceLock,
};
use jiff::Timestamp; use jiff::Timestamp;
use ratatui::text::Line; use ratatui::text::Line;
use serde::Deserialize; use serde::Deserialize;
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug, Hash)]
pub enum Level { pub enum Level {
#[serde(rename = "TRACE")] #[serde(rename = "TRACE")]
Trace, Trace,
@ -33,6 +38,20 @@ pub enum LogEntry {
} }
impl LogEntry { impl LogEntry {
pub fn hash(&self) -> u64 {
let mut hasher = DefaultHasher::new();
match self {
LogEntry::Single { raw } => {
raw.hash(&mut hasher);
hasher.finish()
}
LogEntry::Sub { enter, exit, .. } => {
(enter, exit).hash(&mut hasher);
hasher.finish()
}
}
}
pub fn get(&self, index: usize) -> Option<Rc<LogEntry>> { pub fn get(&self, index: usize) -> Option<Rc<LogEntry>> {
match self { match self {
LogEntry::Single { .. } => None, LogEntry::Single { .. } => None,
@ -89,7 +108,7 @@ impl LogEntry {
} }
} }
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone, Hash)]
pub struct LogFields { pub struct LogFields {
#[serde(flatten)] #[serde(flatten)]
pub fields: BTreeMap<String, serde_json::Value>, pub fields: BTreeMap<String, serde_json::Value>,
@ -112,7 +131,7 @@ impl LogFields {
} }
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug, Hash)]
pub struct RawLogEntry { pub struct RawLogEntry {
pub timestamp: Timestamp, pub timestamp: Timestamp,
pub level: Level, pub level: Level,

170
src/tui/processing.rs Normal file
View file

@ -0,0 +1,170 @@
use std::rc::Rc;
use crate::tui::{
filter::{Filter, FilterKind},
model::LogEntry,
};
pub trait IntoLogStream {
type Stream: LogStream;
fn from_end(self) -> Option<Self::Stream>;
fn from_start(self) -> Option<Self::Stream>;
}
pub struct LogEntryStream {
inner: Rc<LogEntry>,
curr: usize,
}
impl LogStream for LogEntryStream {
fn next(&mut self) -> Option<Rc<LogEntry>> {
let LogEntry::Sub { sub_entries, .. } = self.inner.as_ref() else {
return None;
};
let res = sub_entries.get(self.curr)?;
self.curr += 1;
Some(Rc::clone(res))
}
fn prev(&mut self) -> Option<Rc<LogEntry>> {
let LogEntry::Sub { sub_entries, .. } = self.inner.as_ref() else {
return None;
};
self.curr -= 1;
let res = sub_entries.get(self.curr)?;
Some(Rc::clone(res))
}
fn clone(&self) -> Box<dyn LogStream> {
Box::new(Self {
inner: Rc::clone(&self.inner),
curr: self.curr,
})
}
}
impl IntoLogStream for &Rc<LogEntry> {
type Stream = LogEntryStream;
fn from_end(self) -> Option<Self::Stream> {
if let LogEntry::Sub { sub_entries, .. } = self.as_ref() {
Some(LogEntryStream {
inner: Rc::clone(&self),
curr: sub_entries.len(),
})
} else {
None
}
}
fn from_start(self) -> Option<Self::Stream> {
if let LogEntry::Sub { .. } = self.as_ref() {
Some(LogEntryStream {
inner: Rc::clone(&self),
curr: 0,
})
} else {
None
}
}
}
pub trait LogStream {
fn next(&mut self) -> Option<Rc<LogEntry>>;
fn prev(&mut self) -> Option<Rc<LogEntry>>;
fn clone(&self) -> Box<dyn LogStream>;
fn filter(&self, filter: Rc<Filter>) -> FilteredLogStream {
FilteredLogStream {
filter: filter,
stack: vec![self.clone()],
}
}
}
pub struct FilteredLogStream {
filter: Rc<Filter>,
stack: Vec<Box<dyn LogStream>>,
}
macro_rules! generate_candidate {
($_self: tt, $iter_method: ident) => {
loop {
let top = $_self.stack.last_mut().unwrap();
if let Some(top) = top.$iter_method() {
// if we can find it in the top of stack iterator, neat!
return Some(top);
} else if $_self.stack.len() > 1 {
// Otherwise, try popping the stack once and try again
$_self.stack.pop();
} else {
// However, never pop the stack empty.
// If that'd happen, just return None.
return None;
}
}
};
}
macro_rules! generate_filter {
($_self: tt, $candidate: ident, $into_iter: ident) => {
loop {
let elem = $_self.$candidate()?;
let Filter { matcher, kind } = $_self.filter.as_ref();
if matcher.matches(&elem) {
match kind {
FilterKind::Inline => {
// When we inline, add this item to the stack
// so we continue iterating inside it.
if let Some(iter) = elem.$into_iter() {
$_self.stack.push(Box::new(iter));
}
// Continue so we actually return a nested item.
continue;
}
FilterKind::Remove => {
// continue past removed items
continue;
}
}
} else {
return Some(elem);
}
}
};
}
impl FilteredLogStream {
fn next_candidate(&mut self) -> Option<Rc<LogEntry>> {
generate_candidate!(self, next)
}
fn prev_candidate(&mut self) -> Option<Rc<LogEntry>> {
generate_candidate!(self, prev)
}
}
impl LogStream for FilteredLogStream {
fn next(&mut self) -> Option<Rc<LogEntry>> {
generate_filter!(self, next_candidate, from_start)
}
fn prev(&mut self) -> Option<Rc<LogEntry>> {
generate_filter!(self, prev_candidate, from_end)
}
fn clone(&self) -> Box<dyn LogStream> {
Box::new(Self {
filter: Rc::clone(&self.filter),
stack: self
.stack
.iter()
.map(|i| LogStream::clone(i.as_ref()))
.collect(),
})
}
}

View file

@ -1,4 +1,5 @@
use std::{ use std::{
cell::RefCell,
fs::File, fs::File,
io::{self, BufRead, BufReader}, io::{self, BufRead, BufReader},
mem, mem,
@ -8,35 +9,36 @@ use std::{
}; };
use crate::tui::{ use crate::tui::{
filter::{Filter, FilterKind},
model::{LogEntry, RawLogEntry}, model::{LogEntry, RawLogEntry},
processing::LogStream,
}; };
pub struct LogfileReader { struct Inner {
pub path: PathBuf, pub path: PathBuf,
file: BufReader<File>,
entries: Vec<Rc<LogEntry>>, file: BufReader<File>,
filters: Vec<Rc<Filter>>, cached_entries: Vec<Rc<LogEntry>>,
} }
#[derive(Clone)]
pub struct LogfileReader(Rc<RefCell<Inner>>);
impl LogfileReader { impl LogfileReader {
pub fn new(p: &Path) -> io::Result<Self> { pub fn new(p: &Path) -> io::Result<Self> {
Ok(Self { Ok(Self(Rc::new(RefCell::new(Inner {
file: BufReader::new(File::open(p)?), file: BufReader::new(File::open(p)?),
path: p.to_path_buf(), path: p.to_path_buf(),
entries: Vec::new(), cached_entries: Vec::new(),
filters: Vec::new(), }))))
})
} }
pub fn add_filter(&mut self, filter: Filter) { pub fn path(&self) -> PathBuf {
self.filters.push(Rc::new(filter)); self.0.borrow().path.clone()
} }
fn next_line(&mut self) -> Option<String> { fn next_line(&mut self) -> Option<String> {
let mut res = String::new(); let mut res = String::new();
match self.file.read_line(&mut res) { match self.0.borrow_mut().file.read_line(&mut res) {
Err(e) => { Err(e) => {
eprintln!("error: {e:?}"); eprintln!("error: {e:?}");
None None
@ -93,71 +95,51 @@ impl LogfileReader {
} }
} }
pub fn iter_from(&mut self, start: usize) -> FilterAdapter<EntryIterator<'_>> { fn fill_buf_to_access_index(&mut self, n: usize) -> Option<Rc<LogEntry>> {
FilterAdapter { while self.0.borrow().cached_entries.len() <= n {
filters: self.filters.clone(),
inner: EntryIterator {
curr: start,
reader: self,
},
}
}
fn add_next_entry(&mut self) -> Option<()> {
let entry = self.next_entry()?; let entry = self.next_entry()?;
self.entries.push(entry); self.0.borrow_mut().cached_entries.push(entry);
Some(()) }
Some(Rc::clone(&self.0.borrow().cached_entries[n]))
}
pub fn iter(&self) -> LogFileReaderStream {
LogFileReaderStream {
reader: self.clone(),
curr: 0,
}
} }
} }
pub struct EntryIterator<'a> { pub struct LogFileReaderStream {
reader: LogfileReader,
curr: usize, curr: usize,
reader: &'a mut LogfileReader,
} }
impl<'a> Iterator for EntryIterator<'a> { impl LogStream for LogFileReaderStream {
type Item = Rc<LogEntry>; fn next(&mut self) -> Option<Rc<LogEntry>> {
let entry = self.reader.fill_buf_to_access_index(self.curr)?;
fn next(&mut self) -> Option<Self::Item> {
while self.reader.entries.len() <= self.curr {
self.reader.add_next_entry()?;
}
let res = Rc::clone(&self.reader.entries[self.curr]);
self.curr += 1; self.curr += 1;
Some(res)
} Some(entry)
} }
pub struct FilterAdapter<I> { fn prev(&mut self) -> Option<Rc<LogEntry>> {
filters: Vec<Rc<Filter>>, if self.curr == 0 {
inner: I, return None;
} }
impl<I: IntoIterator> FilterAdapter<I> { let entry = self.reader.fill_buf_to_access_index(self.curr)?;
pub fn new(file: &LogfileReader, inner: I) -> FilterAdapter<I::IntoIter> { self.curr -= 1;
Self {
filters: file.filters.clone(), Some(entry)
inner: inner.into_iter(),
}
}
} }
impl<I: Iterator> Iterator for FilterAdapter<I> { fn clone(&self) -> Box<dyn LogStream> {
type Item = I::Item; Box::new(Self {
reader: self.reader.clone(),
fn next(&mut self) -> Option<Self::Item> { curr: self.curr,
'next_entry: loop { })
let res = self.inner.next()?;
for filter in &self.reader.filters {
if let FilterKind::Remove = filter.kind
&& filter.matcher.matches(&res)
{
continue 'next_entry;
}
}
break Some(res);
}
} }
} }