add log list per request
Some checks failed
CI / matrix (push) Has been cancelled
CI / ${{ matrix.check }} (push) Has been cancelled
CI / build (push) Has been cancelled
CI / build-nixpkgs (push) Has been cancelled

This commit is contained in:
Robin Appelman 2024-12-18 19:52:26 +01:00
commit 763d4747a8
7 changed files with 155 additions and 46 deletions

View file

@ -31,6 +31,17 @@ impl<'a> App<'a> {
pub fn get_line(&self, index: usize) -> Option<&'a str> { pub fn get_line(&self, index: usize) -> Option<&'a str> {
self.log_file.nth(index) self.log_file.nth(index)
} }
pub fn line_indices_by_request<'b>(
&'b self,
request_id: &'b str,
) -> impl Iterator<Item = usize> + 'b {
self.lines
.iter()
.enumerate()
.filter(move |(_, line)| line.request_id == request_id)
.map(|(i, _)| i)
}
} }
pub struct LogMatch { pub struct LogMatch {

View file

@ -14,7 +14,7 @@ impl TimeGraph {
let histogram = Histogram::new_with_bounds( let histogram = Histogram::new_with_bounds(
1, 1,
max( max(
end.unix_timestamp() as u64 - start.unix_timestamp() as u64 + 1, (end.unix_timestamp() as u64).saturating_sub(start.unix_timestamp() as u64) + 1,
4, 4,
), ),
3, 3,

View file

@ -57,8 +57,12 @@ fn help(page: UiPage) -> &'static str {
match page { match page {
UiPage::MatchList => "«Q» Exit - «Enter» Select - «F» Filter - «E» Show parse errors", UiPage::MatchList => "«Q» Exit - «Enter» Select - «F» Filter - «E» Show parse errors",
UiPage::Match => "«Q» Exit - «Enter» Select - «F» Filter - «Esc» Back", UiPage::Match => "«Q» Exit - «Enter» Select - «F» Filter - «Esc» Back",
UiPage::Logs => "«Q» Exit - «F» Filter - «Esc» Back - «C» Copy log line", UiPage::Logs => {
UiPage::Log => "«Q» Exit - «Esc» Back - «R» Toggle raw - «C» Copy log line", "«Q» Exit - «F» Filter - «Esc» Back - «C» Copy log line - «R» Show logs for request"
}
UiPage::Log => {
"«Q» Exit - «Esc» Back - «R» Toggle raw - «C» Copy log line - «R» Show logs for request"
}
UiPage::Errors => "«Q» Exit - «Esc» Back - «C» Copy log line", UiPage::Errors => "«Q» Exit - «Esc» Back - «C» Copy log line",
} }
} }

View file

@ -1,5 +1,6 @@
use crate::app::{App, Filter}; use crate::app::{App, Filter};
use crate::logline::{format_time, LogLine}; use crate::logline::{format_time, LogLine};
use crate::ui::state::GroupedLogGrouping;
use crate::ui::style::TABLE_HEADER_STYLE; use crate::ui::style::TABLE_HEADER_STYLE;
use crate::ui::table::{ScrollbarTable, ScrollbarTableState}; use crate::ui::table::{ScrollbarTable, ScrollbarTableState};
use crate::ui::UI_HEADER_SIZE; use crate::ui::UI_HEADER_SIZE;
@ -13,14 +14,21 @@ pub struct GroupedLogs<'a> {
lines: &'a [usize], lines: &'a [usize],
app: &'a App<'a>, app: &'a App<'a>,
filter: &'a Filter, filter: &'a Filter,
grouping: GroupedLogGrouping,
} }
pub fn grouped_logs<'a>( pub fn grouped_logs<'a>(
app: &'a App<'a>, app: &'a App<'a>,
lines: &'a [usize], lines: &'a [usize],
filter: &'a Filter, filter: &'a Filter,
grouping: GroupedLogGrouping,
) -> GroupedLogs<'a> { ) -> GroupedLogs<'a> {
GroupedLogs { lines, app, filter } GroupedLogs {
lines,
app,
filter,
grouping,
}
} }
impl StatefulWidget for GroupedLogs<'_> { impl StatefulWidget for GroupedLogs<'_> {
@ -33,45 +41,70 @@ impl StatefulWidget for GroupedLogs<'_> {
let line = &self.app.lines[self.lines[state.selected()]]; let line = &self.app.lines[self.lines[state.selected()]];
let lines = self.lines.iter().copied().map(|i| &self.app.lines[i]); let lines = self.lines.iter().copied().map(|i| &self.app.lines[i]);
let par = Paragraph::new(format!( let par = match self.grouping {
"{}{}{}\n\n{} from {} - Nextcloud {}", GroupedLogGrouping::Message => Paragraph::new(format!(
line.exception "{}{}{}\n\n{} from {} - Nextcloud {}",
.as_ref() line.exception
.map(|e| e.exception.as_ref()) .as_ref()
.unwrap_or_default(), .map(|e| e.exception.as_ref())
if line.exception.is_some() { ":\n" } else { "" }, .unwrap_or_default(),
line.message, if line.exception.is_some() { ":\n" } else { "" },
line.level.as_str(), line.message,
line.app, line.level.as_str(),
line.version, line.app,
)) line.version,
.wrap(Wrap::default()); ))
.wrap(Wrap::default()),
GroupedLogGrouping::Request => Paragraph::new(format!(
"{} {}\n\n {} from {} by {} - Nextcloud {}",
line.method, line.url, line.request_id, line.remote, line.user, line.version,
))
.wrap(Wrap::default()),
};
let header = [ let header = match self.grouping {
Text::from("Remote"), GroupedLogGrouping::Message => [
Text::from("Method"), Text::from("Remote"),
Text::from("Url"), Text::from("Method"),
Text::from("Request Id"), Text::from("Url"),
Text::from("Time").alignment(Alignment::Right), Text::from("Request Id"),
] Text::from("Time").alignment(Alignment::Right),
],
GroupedLogGrouping::Request => [
Text::from("Level"),
Text::from("App"),
Text::from("Message"),
Text::from(""),
Text::from("Time").alignment(Alignment::Right),
],
}
.into_iter() .into_iter()
.map(Cell::from) .map(Cell::from)
.collect::<Row>() .collect::<Row>()
.style(TABLE_HEADER_STYLE) .style(TABLE_HEADER_STYLE)
.height(1); .height(1);
let widths = [ let widths = match self.grouping {
Constraint::Min(16), GroupedLogGrouping::Message => [
Constraint::Min(8), Constraint::Min(16),
Constraint::Percentage(100), Constraint::Min(8),
Constraint::Min(25), Constraint::Percentage(100),
Constraint::Length(27), Constraint::Min(25),
]; Constraint::Length(27),
],
GroupedLogGrouping::Request => [
Constraint::Min(16),
Constraint::Min(8),
Constraint::Percentage(100),
Constraint::Length(0),
Constraint::Length(27),
],
};
let table = ScrollbarTable::new( let table = ScrollbarTable::new(
lines lines
.filter(|line| line.matches(self.filter)) .filter(|line| line.matches(self.filter))
.enumerate() .enumerate()
.map(|(i, line)| log_row(line, i.abs_diff(state.selected()) < 100)), .map(|(i, line)| log_row(line, self.grouping, i.abs_diff(state.selected()) < 100)),
widths, widths,
) )
.header(header); .header(header);
@ -89,15 +122,24 @@ impl StatefulWidget for GroupedLogs<'_> {
} }
} }
fn log_row<'a>(line: &'a LogLine<'a>, is_in_view: bool) -> Row<'a> { fn log_row<'a>(line: &'a LogLine<'a>, grouping: GroupedLogGrouping, is_in_view: bool) -> Row<'a> {
if is_in_view { if is_in_view {
Row::new([ match grouping {
Text::from(line.remote.as_str()), GroupedLogGrouping::Message => Row::new([
Text::from(line.method.as_str()), Text::from(line.remote.as_str()),
Text::from(line.url.as_ref()), Text::from(line.method.as_str()),
Text::from(line.request_id.as_str()), Text::from(line.url.as_ref()),
Text::from(format_time(line.time)).alignment(Alignment::Right), Text::from(line.request_id.as_str()),
]) Text::from(format_time(line.time)).alignment(Alignment::Right),
]),
GroupedLogGrouping::Request => Row::new([
Text::from(line.level.as_str()),
Text::from(line.app.as_ref()),
Text::from(line.message.as_ref()),
Text::from(""),
Text::from(format_time(line.time)).alignment(Alignment::Right),
]),
}
} else { } else {
Row::default() Row::default()
} }

View file

@ -21,6 +21,7 @@ pub enum UiEvent {
ClearFilter, ClearFilter,
Text(char), Text(char),
PopText(PopMode), PopText(PopMode),
ByRequest,
} }
pub enum PopMode { pub enum PopMode {
@ -51,6 +52,7 @@ pub fn handle_events(page: UiPage, ui_state: &UiState) -> io::Result<Option<UiEv
(_, KeyCode::Home) => Some(UiEvent::Up(usize::MAX, false)), (_, KeyCode::Home) => Some(UiEvent::Up(usize::MAX, false)),
(_, KeyCode::Enter | KeyCode::Right) => Some(UiEvent::Select), (_, KeyCode::Enter | KeyCode::Right) => Some(UiEvent::Select),
(Mode::Normal, KeyCode::Char('c')) => Some(UiEvent::Copy), (Mode::Normal, KeyCode::Char('c')) => Some(UiEvent::Copy),
(Mode::Normal, KeyCode::Char('r')) => Some(UiEvent::ByRequest),
(Mode::Normal, KeyCode::F(4) | KeyCode::Char('f')) => { (Mode::Normal, KeyCode::F(4) | KeyCode::Char('f')) => {
Some(UiEvent::EnterFilterMode) Some(UiEvent::EnterFilterMode)
} }

View file

@ -149,10 +149,11 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
lines, lines,
table_state, table_state,
filter, filter,
grouping,
.. ..
}) => { }) => {
frame.render_stateful_widget( frame.render_stateful_widget(
grouped_logs(app, lines, filter), grouped_logs(app, lines, filter, *grouping),
layout[0].union(layout[1]), layout[0].union(layout[1]),
table_state, table_state,
); );

View file

@ -7,6 +7,7 @@ use crate::ui::UI_HEADER_SIZE;
use crate::{copy_osc, parse_line_full}; use crate::{copy_osc, parse_line_full};
use derive_more::From; use derive_more::From;
use ratatui::widgets::TableState; use ratatui::widgets::TableState;
use std::borrow::Cow;
use std::iter::once; use std::iter::once;
#[derive(Clone, From, PartialEq)] #[derive(Clone, From, PartialEq)]
@ -104,11 +105,12 @@ 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::GroupedLogs(GroupedLogsState { UiState::GroupedLogs(GroupedLogsState {
lines, lines: lines.into(),
table_state, table_state,
previous: Box::new(self.into()), previous: Box::new(self.into()),
filter: Filter::default(), filter: Filter::default(),
mode: Mode::Normal, mode: Mode::Normal,
grouping: GroupedLogGrouping::Message,
}) })
} }
} }
@ -119,13 +121,20 @@ impl PartialEq for MatchState<'_> {
} }
} }
#[derive(Clone, Copy)]
pub enum GroupedLogGrouping {
Message,
Request,
}
#[derive(Clone)] #[derive(Clone)]
pub struct GroupedLogsState<'a> { pub struct GroupedLogsState<'a> {
pub lines: &'a [usize], pub lines: Cow<'a, [usize]>,
pub table_state: ScrollbarTableState, pub table_state: ScrollbarTableState,
pub previous: Box<UiState<'a>>, pub previous: Box<UiState<'a>>,
pub filter: Filter, pub filter: Filter,
mode: Mode, mode: Mode,
pub grouping: GroupedLogGrouping,
} }
impl<'a> GroupedLogsState<'a> { impl<'a> GroupedLogsState<'a> {
@ -133,8 +142,8 @@ impl<'a> GroupedLogsState<'a> {
self.table_state.selected() self.table_state.selected()
} }
fn enter(self, selected: usize, app: &'a App<'a>) -> UiState<'a> { fn get_selected<'b>(&self, selected: usize, app: &'b App<'b>) -> &'b LogLine<'b> {
let log = if self.filter.is_empty() { if self.filter.is_empty() {
let line = self.lines[selected]; let line = self.lines[selected];
&app.lines[line] &app.lines[line]
} else { } else {
@ -144,7 +153,11 @@ impl<'a> GroupedLogsState<'a> {
.filter(|line| line.matches(&self.filter)) .filter(|line| line.matches(&self.filter))
.nth(selected) .nth(selected)
.expect("filtered select out of bounds") .expect("filtered select out of bounds")
}; }
}
fn enter(self, selected: usize, app: &'a App<'a>) -> UiState<'a> {
let log = self.get_selected(selected, app);
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,6 +174,21 @@ impl<'a> GroupedLogsState<'a> {
previous: Box::new(self.into()), previous: Box::new(self.into()),
}) })
} }
fn by_request(self, selected: usize, app: &'a App<'a>) -> UiState<'a> {
let log = self.get_selected(selected, app);
let lines: Vec<_> = app.line_indices_by_request(&log.request_id).collect();
let table_state = ScrollbarTableState::new(lines.len());
UiState::GroupedLogs(GroupedLogsState {
lines: lines.into(),
mode: Mode::Normal,
filter: Filter::default(),
table_state,
previous: Box::new(self.into()),
grouping: GroupedLogGrouping::Request,
})
}
} }
impl PartialEq for GroupedLogsState<'_> { impl PartialEq for GroupedLogsState<'_> {
@ -189,6 +217,22 @@ pub struct LogState<'a> {
pub previous: Box<UiState<'a>>, pub previous: Box<UiState<'a>>,
} }
impl<'a> LogState<'a> {
fn by_request(self, app: &'a App<'a>) -> UiState<'a> {
let lines: Vec<_> = app.line_indices_by_request(&self.log.request_id).collect();
let table_state = ScrollbarTableState::new(lines.len());
UiState::GroupedLogs(GroupedLogsState {
lines: lines.into(),
mode: Mode::Normal,
filter: Filter::default(),
table_state,
previous: Box::new(self.into()),
grouping: GroupedLogGrouping::Request,
})
}
}
impl PartialEq for LogState<'_> { impl PartialEq for LogState<'_> {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.log.index == other.log.index self.log.index == other.log.index
@ -411,6 +455,11 @@ impl<'a> UiState<'a> {
copy_osc(raw); copy_osc(raw);
(false, UiState::Log(state)) (false, UiState::Log(state))
} }
(UiState::GroupedLogs(state), UiEvent::ByRequest) => {
let selected = state.selected();
(true, state.by_request(selected, app))
}
(UiState::Log(state), UiEvent::ByRequest) => (true, state.by_request(app)),
(UiState::Errors(state), UiEvent::Copy) => { (UiState::Errors(state), UiEvent::Copy) => {
let raw = app let raw = app
.error_lines .error_lines