make grouped logs view more meaningfull

This commit is contained in:
Robin Appelman 2024-12-17 18:01:02 +01:00
commit 64cea44dbe
5 changed files with 146 additions and 70 deletions

View file

@ -23,6 +23,11 @@ pub struct LogLine<'a> {
pub index: usize, pub index: usize,
#[serde(rename = "reqId")] #[serde(rename = "reqId")]
pub request_id: TinyAsciiStr<32>, pub request_id: TinyAsciiStr<32>,
pub user: TinyAsciiStr<64>,
pub method: TinyAsciiStr<12>,
pub url: Cow<'a, str>,
#[serde(rename = "remoteAddr")]
pub remote: TinyAsciiStr<40>,
pub version: &'a str, pub version: &'a str,
pub level: LogLevel, pub level: LogLevel,
pub message: Cow<'a, str>, pub message: Cow<'a, str>,
@ -137,6 +142,10 @@ impl<'a> LogLine<'a> {
filter.matches(&self.app) filter.matches(&self.app)
|| filter.matches(&self.message) || filter.matches(&self.message)
|| filter.matches(self.request_id.as_str()) || filter.matches(self.request_id.as_str())
|| filter.matches(&self.url)
|| filter.matches(&self.method)
|| filter.matches(&self.remote)
|| filter.matches(&self.user)
} }
} }

108
src/ui/grouped_logs.rs Normal file
View file

@ -0,0 +1,108 @@
use crate::app::{App, Filter};
use crate::logline::{format_time, LogLine};
use crate::ui::style::TABLE_HEADER_STYLE;
use crate::ui::table::{ScrollbarTable, ScrollbarTableState};
use crate::ui::UI_HEADER_SIZE;
use ratatui::buffer::Buffer;
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
use ratatui::prelude::{StatefulWidget, Widget};
use ratatui::text::Text;
use ratatui::widgets::{Cell, Paragraph, Row, Wrap};
pub struct GroupedLogs<'a> {
line: &'a LogLine<'a>,
lines: &'a [usize],
app: &'a App<'a>,
filter: &'a Filter,
}
pub fn grouped_logs<'a>(
app: &'a App<'a>,
lines: &'a [usize],
filter: &'a Filter,
) -> GroupedLogs<'a> {
let line = &app.lines[lines[0]];
GroupedLogs {
line,
lines,
app,
filter,
}
}
impl StatefulWidget for GroupedLogs<'_> {
type State = ScrollbarTableState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
where
Self: Sized,
{
let lines = self.lines.iter().copied().map(|i| &self.app.lines[i]);
let par = Paragraph::new(format!(
"{}{}{}\n\n{} from {} - Nextcloud {}",
self.line
.exception
.as_ref()
.map(|e| e.exception.as_ref())
.unwrap_or_default(),
if self.line.exception.is_some() {
":\n"
} else {
""
},
self.line.message,
self.line.level.as_str(),
self.line.app,
self.line.version,
))
.wrap(Wrap::default());
let header = [
Text::from("Remote"),
Text::from("Method"),
Text::from("Url"),
Text::from("Request Id"),
Text::from("Time").alignment(Alignment::Right),
]
.into_iter()
.map(Cell::from)
.collect::<Row>()
.style(TABLE_HEADER_STYLE)
.height(1);
let widths = [
Constraint::Min(16),
Constraint::Min(8),
Constraint::Percentage(100),
Constraint::Min(25),
Constraint::Length(27),
];
let table = ScrollbarTable::new(
lines.filter(|line| line.matches(self.filter)).map(log_row),
widths,
)
.header(header);
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
Constraint::Min(UI_HEADER_SIZE),
Constraint::Percentage(100),
])
.split(area);
par.render(layout[0], buf);
table.render(layout[1], buf, state);
}
}
fn log_row<'a>(line: &'a LogLine<'a>) -> Row<'a> {
Row::new([
Text::from(line.remote.as_str()),
Text::from(line.method.as_str()),
Text::from(line.url.as_ref()),
Text::from(line.request_id.as_str()),
Text::from(format_time(line.time)).alignment(Alignment::Right),
])
}

View file

@ -2,13 +2,14 @@ use crate::app::App;
use crate::error::UiError; use crate::error::UiError;
use crate::ui::error_list::error_list; use crate::ui::error_list::error_list;
use crate::ui::footer::footer; use crate::ui::footer::footer;
use crate::ui::grouped_logs::grouped_logs;
use crate::ui::histogram::UiHistogram; use crate::ui::histogram::UiHistogram;
use crate::ui::match_list::match_list; use crate::ui::match_list::match_list;
use crate::ui::raw_logs::raw_logs;
use crate::ui::single_log::single_log; use crate::ui::single_log::single_log;
use crate::ui::single_match::grouped_lines; use crate::ui::single_match::grouped_lines;
use crate::ui::state::{ use crate::ui::state::{
ErrorState, LogState, LogsState, MatchListState, MatchState, Mode, UiEvent, UiPage, UiState, ErrorState, GroupedLogsState, LogState, MatchListState, MatchState, Mode, UiEvent, UiPage,
UiState,
}; };
use ratatui::crossterm::event::{ use ratatui::crossterm::event::{
DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers, MouseButton, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers, MouseButton,
@ -27,9 +28,9 @@ use std::time::Duration;
mod error_list; mod error_list;
mod footer; mod footer;
mod grouped_logs;
mod histogram; mod histogram;
mod match_list; mod match_list;
mod raw_logs;
mod single_log; mod single_log;
mod single_match; mod single_match;
mod state; mod state;
@ -192,14 +193,14 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
); );
frame.render_widget(footer(app, state.footer_params()), layout[2]); frame.render_widget(footer(app, state.footer_params()), layout[2]);
} }
UiState::Logs(LogsState { UiState::GroupedLogs(GroupedLogsState {
lines, lines,
table_state, table_state,
filter, filter,
.. ..
}) => { }) => {
frame.render_stateful_widget( frame.render_stateful_widget(
raw_logs(app, lines, filter), grouped_logs(app, lines, filter),
layout[0].union(layout[1]), layout[0].union(layout[1]),
table_state, table_state,
); );

View file

@ -1,43 +0,0 @@
use crate::app::{App, Filter};
use crate::logline::{format_time, LogLine};
use crate::ui::style::TABLE_HEADER_STYLE;
use crate::ui::table::ScrollbarTable;
use ratatui::layout::{Alignment, Constraint};
use ratatui::text::Text;
use ratatui::widgets::{Cell, Row};
pub fn raw_logs<'a>(app: &'a App<'a>, lines: &[usize], filter: &Filter) -> ScrollbarTable<'a> {
let lines = lines.iter().copied().map(|i| &app.lines[i]);
let header = [
Text::from("Level"),
Text::from("App"),
Text::from("Message"),
Text::from("Time").alignment(Alignment::Right),
]
.into_iter()
.map(Cell::from)
.collect::<Row>()
.style(TABLE_HEADER_STYLE)
.height(1);
let widths = [
Constraint::Min(10),
Constraint::Min(20),
Constraint::Percentage(100),
Constraint::Length(27),
];
ScrollbarTable::new(
lines.filter(|line| line.matches(filter)).map(log_row),
widths,
)
.header(header)
}
fn log_row<'a>(line: &'a LogLine<'a>) -> Row<'a> {
Row::new([
Text::from(line.level.as_str()),
Text::from(line.app.as_ref()),
Text::from(line.display()),
Text::from(format_time(line.time)).alignment(Alignment::Right),
])
}

View file

@ -12,7 +12,7 @@ use std::iter::once;
pub enum UiState<'a> { pub enum UiState<'a> {
MatchList(MatchListState<'a>), MatchList(MatchListState<'a>),
Match(MatchState<'a>), Match(MatchState<'a>),
Logs(LogsState<'a>), GroupedLogs(GroupedLogsState<'a>),
Log(LogState<'a>), Log(LogState<'a>),
Errors(ErrorState<'a>), Errors(ErrorState<'a>),
Quit, Quit,
@ -100,7 +100,7 @@ impl<'a> MatchState<'a> {
}; };
let lines = selected_line.lines.as_slice(); let lines = selected_line.lines.as_slice();
let table_state = ScrollbarTableState::new(lines.len()); let table_state = ScrollbarTableState::new(lines.len());
UiState::Logs(LogsState { UiState::GroupedLogs(GroupedLogsState {
lines, lines,
table_state, table_state,
previous: Box::new(self.into()), previous: Box::new(self.into()),
@ -117,7 +117,7 @@ impl PartialEq for MatchState<'_> {
} }
#[derive(Clone)] #[derive(Clone)]
pub struct LogsState<'a> { pub struct GroupedLogsState<'a> {
pub lines: &'a [usize], pub lines: &'a [usize],
pub table_state: ScrollbarTableState, pub table_state: ScrollbarTableState,
pub previous: Box<UiState<'a>>, pub previous: Box<UiState<'a>>,
@ -125,24 +125,23 @@ pub struct LogsState<'a> {
mode: Mode, mode: Mode,
} }
impl<'a> LogsState<'a> { impl<'a> GroupedLogsState<'a> {
fn selected(&self) -> usize { fn selected(&self) -> usize {
self.table_state.selected() self.table_state.selected()
} }
fn enter(self, selected: usize, app: &'a App<'a>) -> UiState<'a> { fn enter(self, selected: usize, app: &'a App<'a>) -> UiState<'a> {
let line = if self.filter.is_empty() { let log = if self.filter.is_empty() {
self.lines[selected] let line = self.lines[selected];
&app.lines[line]
} else { } else {
self.lines self.lines
.iter() .iter()
.map(|index| &app.lines[*index]) .map(|index| &app.lines[*index])
.filter(|line| line.matches(&self.filter)) .filter(|line| line.matches(&self.filter))
.nth(selected) .nth(selected)
.map(|line| line.index)
.expect("filtered select out of bounds") .expect("filtered select out of bounds")
}; };
let log = &app.lines[line];
let raw_line = app.get_line(log.index).unwrap(); let raw_line = app.get_line(log.index).unwrap();
let full_line = parse_line_full(raw_line).unwrap(); let full_line = parse_line_full(raw_line).unwrap();
let trace_len = if let Some(exception) = &full_line.exception { let trace_len = if let Some(exception) = &full_line.exception {
@ -161,7 +160,7 @@ impl<'a> LogsState<'a> {
} }
} }
impl PartialEq for LogsState<'_> { impl PartialEq for GroupedLogsState<'_> {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.lines == other.lines self.lines == other.lines
} }
@ -209,7 +208,7 @@ impl<'a> UiState<'a> {
match self { match self {
UiState::Quit | UiState::MatchList(_) => UiPage::MatchList, UiState::Quit | UiState::MatchList(_) => UiPage::MatchList,
UiState::Match(_) => UiPage::Match, UiState::Match(_) => UiPage::Match,
UiState::Logs(_) => UiPage::Logs, UiState::GroupedLogs(_) => UiPage::Logs,
UiState::Log(_) => UiPage::Log, UiState::Log(_) => UiPage::Log,
UiState::Errors(_) => UiPage::Errors, UiState::Errors(_) => UiPage::Errors,
} }
@ -219,7 +218,7 @@ impl<'a> UiState<'a> {
match self { match self {
UiState::MatchList(state) => state.mode, UiState::MatchList(state) => state.mode,
UiState::Match(state) => state.mode, UiState::Match(state) => state.mode,
UiState::Logs(state) => state.mode, UiState::GroupedLogs(state) => state.mode,
_ => Mode::Normal, _ => Mode::Normal,
} }
} }
@ -228,7 +227,7 @@ impl<'a> UiState<'a> {
match self { match self {
UiState::MatchList(state) => state.mode = mode, UiState::MatchList(state) => state.mode = mode,
UiState::Match(state) => state.mode = mode, UiState::Match(state) => state.mode = mode,
UiState::Logs(state) => state.mode = mode, UiState::GroupedLogs(state) => state.mode = mode,
_ => {} _ => {}
} }
} }
@ -237,7 +236,7 @@ impl<'a> UiState<'a> {
match self { match self {
UiState::MatchList(state) => Some(&state.filter), UiState::MatchList(state) => Some(&state.filter),
UiState::Match(state) => Some(&state.filter), UiState::Match(state) => Some(&state.filter),
UiState::Logs(state) => Some(&state.filter), UiState::GroupedLogs(state) => Some(&state.filter),
_ => None, _ => None,
} }
} }
@ -246,7 +245,7 @@ impl<'a> UiState<'a> {
match self { match self {
UiState::MatchList(state) => Some(&mut state.filter), UiState::MatchList(state) => Some(&mut state.filter),
UiState::Match(state) => Some(&mut state.filter), UiState::Match(state) => Some(&mut state.filter),
UiState::Logs(state) => Some(&mut state.filter), UiState::GroupedLogs(state) => Some(&mut state.filter),
_ => None, _ => None,
} }
} }
@ -255,7 +254,7 @@ impl<'a> UiState<'a> {
match self { match self {
UiState::MatchList(state) => Some(&state.table_state), UiState::MatchList(state) => Some(&state.table_state),
UiState::Match(state) => Some(&state.table_state), UiState::Match(state) => Some(&state.table_state),
UiState::Logs(state) => Some(&state.table_state), UiState::GroupedLogs(state) => Some(&state.table_state),
UiState::Log(state) => Some(&state.table_state), UiState::Log(state) => Some(&state.table_state),
UiState::Errors(state) => Some(&state.table_state), UiState::Errors(state) => Some(&state.table_state),
_ => None, _ => None,
@ -266,7 +265,7 @@ impl<'a> UiState<'a> {
match self { match self {
UiState::MatchList(state) => Some(&mut state.table_state), UiState::MatchList(state) => Some(&mut state.table_state),
UiState::Match(state) => Some(&mut state.table_state), UiState::Match(state) => Some(&mut state.table_state),
UiState::Logs(state) => Some(&mut state.table_state), UiState::GroupedLogs(state) => Some(&mut state.table_state),
UiState::Log(state) => Some(&mut state.table_state), UiState::Log(state) => Some(&mut state.table_state),
UiState::Errors(state) => Some(&mut state.table_state), UiState::Errors(state) => Some(&mut state.table_state),
_ => None, _ => None,
@ -324,7 +323,7 @@ impl<'a> UiState<'a> {
match self { match self {
UiState::MatchList(_) => UI_HEADER_SIZE + 1, UiState::MatchList(_) => UI_HEADER_SIZE + 1,
UiState::Match(_) => UI_HEADER_SIZE + 1, UiState::Match(_) => UI_HEADER_SIZE + 1,
UiState::Logs(_) => 0, UiState::GroupedLogs(_) => UI_HEADER_SIZE + 1,
UiState::Log(_) => 0, UiState::Log(_) => 0,
UiState::Errors(_) => 0, UiState::Errors(_) => 0,
UiState::Quit => 0, UiState::Quit => 0,
@ -387,12 +386,14 @@ impl<'a> UiState<'a> {
(true, state.enter(selected, app)) (true, state.enter(selected, app))
} }
(UiState::Match(state), UiEvent::Enter(selected)) => (true, state.enter(selected, app)), (UiState::Match(state), UiEvent::Enter(selected)) => (true, state.enter(selected, app)),
(UiState::Logs(state), UiEvent::Select) => { (UiState::GroupedLogs(state), UiEvent::Select) => {
let selected = state.selected(); let selected = state.selected();
(true, state.enter(selected, app)) (true, state.enter(selected, app))
} }
(UiState::Logs(state), UiEvent::Enter(selected)) => (true, state.enter(selected, app)), (UiState::GroupedLogs(state), UiEvent::Enter(selected)) => {
(UiState::Logs(state), UiEvent::Copy) => { (true, state.enter(selected, app))
}
(UiState::GroupedLogs(state), UiEvent::Copy) => {
let selected = state.selected(); let selected = state.selected();
let mut table_state = TableState::default(); let mut table_state = TableState::default();
table_state.select(Some(0)); table_state.select(Some(0));
@ -400,7 +401,7 @@ impl<'a> UiState<'a> {
let line = &app.lines[state.lines[selected]]; let line = &app.lines[state.lines[selected]];
let raw = app.get_line(line.index).unwrap_or_default(); let raw = app.get_line(line.index).unwrap_or_default();
copy_osc(raw); copy_osc(raw);
(false, UiState::Logs(state)) (false, UiState::GroupedLogs(state))
} }
(UiState::Log(state), UiEvent::Copy) => { (UiState::Log(state), UiEvent::Copy) => {
let raw = app.get_line(state.log.index).unwrap_or_default(); let raw = app.get_line(state.log.index).unwrap_or_default();
@ -455,7 +456,7 @@ impl<'a> UiState<'a> {
( (
UiState::Match(MatchState { previous, .. }) UiState::Match(MatchState { previous, .. })
| UiState::Logs(LogsState { previous, .. }) | UiState::GroupedLogs(GroupedLogsState { previous, .. })
| UiState::Log(LogState { previous, .. }) | UiState::Log(LogState { previous, .. })
| UiState::Errors(ErrorState { previous, .. }), | UiState::Errors(ErrorState { previous, .. }),
UiEvent::Back, UiEvent::Back,