mirror of
https://codeberg.org/icewind/logsmash.git
synced 2026-06-03 18:14:11 +02:00
add log list per request
This commit is contained in:
parent
8cc3180c54
commit
763d4747a8
7 changed files with 155 additions and 46 deletions
11
src/app.rs
11
src/app.rs
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue