mirror of
https://codeberg.org/icewind/logsmash.git
synced 2026-06-03 18:14:11 +02:00
work towards generic grouping
This commit is contained in:
parent
4f9e5df792
commit
3943407b9b
13 changed files with 309 additions and 202 deletions
5
Cargo.lock
generated
5
Cargo.lock
generated
|
|
@ -570,9 +570,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.13.0"
|
version = "1.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encode_unicode"
|
name = "encode_unicode"
|
||||||
|
|
@ -907,6 +907,7 @@ dependencies = [
|
||||||
"csv",
|
"csv",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"dialoguer",
|
"dialoguer",
|
||||||
|
"either",
|
||||||
"flate2",
|
"flate2",
|
||||||
"hdrhistogram",
|
"hdrhistogram",
|
||||||
"indicatif",
|
"indicatif",
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ dialoguer = "0.11.0"
|
||||||
indicatif = { version = "0.17.11", features = ["rayon"] }
|
indicatif = { version = "0.17.11", features = ["rayon"] }
|
||||||
csv = "1.3.1"
|
csv = "1.3.1"
|
||||||
osc94 = "0.1.1"
|
osc94 = "0.1.1"
|
||||||
|
either = "1.15.0"
|
||||||
|
|
||||||
[target.'cfg(target_env = "musl")'.dependencies]
|
[target.'cfg(target_env = "musl")'.dependencies]
|
||||||
tikv-jemallocator = "0.6.0"
|
tikv-jemallocator = "0.6.0"
|
||||||
|
|
|
||||||
94
src/app.rs
94
src/app.rs
|
|
@ -1,28 +1,26 @@
|
||||||
use crate::grouping::group_lines_by;
|
use crate::grouping::LogGrouping;
|
||||||
use crate::logfile::{LogFile, LogLine, LogLineNumber};
|
use crate::logfile::{LogFile, LogLine, LogLineNumber};
|
||||||
use crate::logs::ParsedLogs;
|
use crate::logs::ParsedLogs;
|
||||||
use crate::matcher::MatchResult;
|
use crate::matcher::MatchResult;
|
||||||
use crate::timegraph::{SparkLine, TimeGraph};
|
use crate::timegraph::{SparkLine, TimeGraph};
|
||||||
use logsmash_data::{LoggingStatementWithPathPrefix, StatementList};
|
|
||||||
use regex::{escape, Regex, RegexBuilder};
|
use regex::{escape, Regex, RegexBuilder};
|
||||||
use serde_json::Error as JsonError;
|
use serde_json::Error as JsonError;
|
||||||
use std::cell::OnceCell;
|
use std::cell::OnceCell;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
use std::ops::RangeInclusive;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
pub struct App<'logs> {
|
pub struct App<'logs> {
|
||||||
pub lines: &'logs ParsedLogs<'logs>,
|
pub lines: &'logs ParsedLogs<'logs>,
|
||||||
pub log_statements: StatementList,
|
pub matches: Vec<LogGrouping<'logs, MatchResult>>,
|
||||||
pub matches: Vec<LogMatch<'logs>>,
|
pub all: LogGrouping<'logs, MatchResult>,
|
||||||
pub all: LogMatch<'logs>,
|
|
||||||
pub unmatched: LogMatch<'logs>,
|
|
||||||
pub log_file: &'logs LogFile,
|
pub log_file: &'logs LogFile,
|
||||||
|
pub unmatched_count: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'logs> App<'logs> {
|
impl<'logs> App<'logs> {
|
||||||
pub fn match_lines(&self) -> usize {
|
pub fn match_lines(&self) -> usize {
|
||||||
let unmatched_line_count = if self.unmatched.count == 0 { 0 } else { 1 };
|
self.matches.len() + 1
|
||||||
self.matches.len() + 1 + unmatched_line_count
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_source_line(&self, index: LogLineNumber) -> Option<&'logs str> {
|
pub fn get_source_line(&self, index: LogLineNumber) -> Option<&'logs str> {
|
||||||
|
|
@ -47,75 +45,8 @@ impl<'logs> App<'logs> {
|
||||||
self.lines.errors().len()
|
self.lines.errors().len()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn time_range(&self) -> (OffsetDateTime, OffsetDateTime) {
|
pub fn time_range(&self) -> RangeInclusive<OffsetDateTime> {
|
||||||
(self.lines.first().time, self.lines.last().time)
|
self.lines.first().time..=self.lines.last().time
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct LogMatch<'logs> {
|
|
||||||
pub result: Option<MatchResult>,
|
|
||||||
pub count: usize,
|
|
||||||
pub all: LineSet<'logs>,
|
|
||||||
pub grouped: Vec<LineSet<'logs>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'logs> LogMatch<'logs> {
|
|
||||||
pub fn new(result: Option<MatchResult>, lines: Vec<&'logs LogLine<'logs>>) -> Self {
|
|
||||||
let count = lines.len();
|
|
||||||
let grouped = group_lines_by(lines.iter().copied(), LogLine::identity);
|
|
||||||
let all = LineSet::new(lines);
|
|
||||||
|
|
||||||
LogMatch {
|
|
||||||
result,
|
|
||||||
count,
|
|
||||||
grouped,
|
|
||||||
all,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sparkline(&self, app: &App) -> &SparkLine {
|
|
||||||
self.all.sparkline(app)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn histogram(&self, app: &App) -> &TimeGraph {
|
|
||||||
self.all.histogram(app)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn row_count(&self) -> usize {
|
|
||||||
self.result.as_ref().map(|res| res.count()).unwrap_or(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn statements<'a>(
|
|
||||||
&'a self,
|
|
||||||
) -> impl Iterator<Item = &'a LoggingStatementWithPathPrefix> + use<'a> {
|
|
||||||
self.result.iter().flat_map(|res| res.iter())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn matches(&self, filter: &Filter) -> bool {
|
|
||||||
if filter.is_empty() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
self.statements().any(|statement| {
|
|
||||||
filter.parts().all(|filter_part| {
|
|
||||||
filter_part.is_match(statement.statement.pattern)
|
|
||||||
|| filter_part.is_match(statement.statement.path)
|
|
||||||
|| filter_part.is_match(statement.path_prefix)
|
|
||||||
|| statement
|
|
||||||
.statement
|
|
||||||
.placeholders
|
|
||||||
.iter()
|
|
||||||
.any(|placeholder| filter_part.is_match(placeholder))
|
|
||||||
|| statement
|
|
||||||
.statement
|
|
||||||
.exception
|
|
||||||
.filter(|exception| filter_part.is_match(exception))
|
|
||||||
.is_some()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn count(&self) -> usize {
|
|
||||||
self.count
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -134,15 +65,14 @@ impl<'logs> LineSet<'logs> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sparkline(&self, app: &App) -> &SparkLine {
|
pub fn sparkline(&self, time_range: RangeInclusive<OffsetDateTime>) -> &SparkLine {
|
||||||
self.sparkline
|
self.sparkline
|
||||||
.get_or_init(|| self.histogram(app).sparkline())
|
.get_or_init(|| self.histogram(time_range).sparkline())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn histogram(&self, app: &App) -> &TimeGraph {
|
pub fn histogram(&self, time_range: RangeInclusive<OffsetDateTime>) -> &TimeGraph {
|
||||||
self.histogram.get_or_init(|| {
|
self.histogram.get_or_init(|| {
|
||||||
let (min_time, max_time) = app.time_range();
|
let mut histogram = TimeGraph::new(*time_range.start(), *time_range.end());
|
||||||
let mut histogram = TimeGraph::new(min_time, max_time);
|
|
||||||
for line in self.lines.iter() {
|
for line in self.lines.iter() {
|
||||||
histogram.add(line.time);
|
histogram.add(line.time);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,99 @@
|
||||||
use crate::app::LineSet;
|
use crate::app::{Filter, LineSet};
|
||||||
use crate::logfile::LogLine;
|
use crate::logfile::LogLine;
|
||||||
|
use crate::timegraph::{SparkLine, TimeGraph};
|
||||||
|
use ratatui::layout::{Alignment, Constraint};
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
use std::ops::RangeInclusive;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
|
pub trait GroupedLine {
|
||||||
|
const HEADER: &'static [(&'static str, Alignment)];
|
||||||
|
|
||||||
|
const WIDTHS: &'static [Constraint];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::len_without_is_empty)]
|
||||||
|
pub trait GroupingResult {
|
||||||
|
type Lines: GroupedLine;
|
||||||
|
|
||||||
|
fn len(&self) -> usize;
|
||||||
|
fn lines<'a>(&'a self) -> impl Iterator<Item = &'a Self::Lines> + use<'a, Self>
|
||||||
|
where
|
||||||
|
<Self as GroupingResult>::Lines: 'a;
|
||||||
|
|
||||||
|
fn matches(&self, filter: &Filter) -> bool;
|
||||||
|
|
||||||
|
fn render(&self) -> impl Iterator<Item = Cow<str>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LogGrouping<'logs, G> {
|
||||||
|
pub name: Option<&'static str>,
|
||||||
|
pub result: Option<G>,
|
||||||
|
pub count: usize,
|
||||||
|
pub all: LineSet<'logs>,
|
||||||
|
pub grouped: Vec<LineSet<'logs>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'logs, G: GroupingResult> LogGrouping<'logs, G> {
|
||||||
|
pub fn new(result: G, lines: Vec<&'logs LogLine<'logs>>) -> Self {
|
||||||
|
let count = lines.len();
|
||||||
|
let grouped = group_lines_by(lines.iter().copied(), LogLine::identity);
|
||||||
|
let all = LineSet::new(lines);
|
||||||
|
|
||||||
|
LogGrouping {
|
||||||
|
name: None,
|
||||||
|
result: Some(result),
|
||||||
|
count,
|
||||||
|
grouped,
|
||||||
|
all,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn named(name: &'static str, lines: Vec<&'logs LogLine<'logs>>) -> Self {
|
||||||
|
let count = lines.len();
|
||||||
|
let grouped = group_lines_by(lines.iter().copied(), LogLine::identity);
|
||||||
|
let all = LineSet::new(lines);
|
||||||
|
|
||||||
|
LogGrouping {
|
||||||
|
name: Some(name),
|
||||||
|
result: None,
|
||||||
|
count,
|
||||||
|
grouped,
|
||||||
|
all,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sparkline(&self, time_range: RangeInclusive<OffsetDateTime>) -> &SparkLine {
|
||||||
|
self.all.sparkline(time_range)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn histogram(&self, time_range: RangeInclusive<OffsetDateTime>) -> &TimeGraph {
|
||||||
|
self.all.histogram(time_range)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn row_count(&self) -> usize {
|
||||||
|
self.result.as_ref().map(|res| res.len()).unwrap_or(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = &LineSet<'logs>> {
|
||||||
|
self.grouped.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn matches(&self, filter: &Filter) -> bool {
|
||||||
|
if filter.is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
match &self.result {
|
||||||
|
Some(result) => result.matches(filter),
|
||||||
|
_ => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn count(&self) -> usize {
|
||||||
|
self.count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn group_lines_by<'logs, I, F, K>(indices: I, f: F) -> Vec<LineSet<'logs>>
|
pub fn group_lines_by<'logs, I, F, K>(indices: I, f: F) -> Vec<LineSet<'logs>>
|
||||||
where
|
where
|
||||||
|
|
|
||||||
19
src/main.rs
19
src/main.rs
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::app::{App, LogMatch};
|
use crate::app::App;
|
||||||
use crate::error::LogError;
|
use crate::error::LogError;
|
||||||
use crate::logfile::{LogFile, LogLineNumber};
|
use crate::logfile::{LogFile, LogLineNumber};
|
||||||
use crate::logs::ParsedLogs;
|
use crate::logs::ParsedLogs;
|
||||||
|
|
@ -29,6 +29,7 @@ mod matcher;
|
||||||
mod timegraph;
|
mod timegraph;
|
||||||
mod ui;
|
mod ui;
|
||||||
|
|
||||||
|
use crate::grouping::LogGrouping;
|
||||||
#[cfg(target_env = "musl")]
|
#[cfg(target_env = "musl")]
|
||||||
use tikv_jemallocator::Jemalloc;
|
use tikv_jemallocator::Jemalloc;
|
||||||
use time::format_description::{parse_owned, parse_strftime_owned};
|
use time::format_description::{parse_owned, parse_strftime_owned};
|
||||||
|
|
@ -151,21 +152,25 @@ fn main() -> MainResult {
|
||||||
matched_lines.sort_by_key(|(_, lines)| lines.len());
|
matched_lines.sort_by_key(|(_, lines)| lines.len());
|
||||||
matched_lines.reverse();
|
matched_lines.reverse();
|
||||||
|
|
||||||
let all = LogMatch::new(None, parsed_log.find_lines(|_| true).collect());
|
let all = LogGrouping::<MatchResult>::named("All", parsed_log.find_lines(|_| true).collect());
|
||||||
let unmatched = LogMatch::new(None, unmatched_lines);
|
let unmatched = LogGrouping::<MatchResult>::named("Unmatched", unmatched_lines);
|
||||||
|
|
||||||
let matches = matched_lines
|
let mut matches: Vec<_> = matched_lines
|
||||||
.into_par_iter()
|
.into_par_iter()
|
||||||
.map(|(result, lines)| LogMatch::new(Some(result), lines))
|
.map(|(result, lines)| LogGrouping::new(result, lines))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
let unmatched_count = unmatched.count();
|
||||||
|
if unmatched_count > 0 {
|
||||||
|
matches.push(unmatched);
|
||||||
|
}
|
||||||
|
|
||||||
let app = App {
|
let app = App {
|
||||||
lines: &parsed_log,
|
lines: &parsed_log,
|
||||||
log_statements: statements,
|
|
||||||
matches,
|
matches,
|
||||||
unmatched,
|
|
||||||
all,
|
all,
|
||||||
log_file: &log_file,
|
log_file: &log_file,
|
||||||
|
unmatched_count,
|
||||||
};
|
};
|
||||||
|
|
||||||
if args.profile {
|
if args.profile {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,12 @@
|
||||||
|
use crate::app::Filter;
|
||||||
|
use crate::grouping::{GroupedLine, GroupingResult};
|
||||||
use crate::logfile::logline::{Exception, LogLine};
|
use crate::logfile::logline::{Exception, LogLine};
|
||||||
use crate::logfile::LineNumber;
|
use crate::logfile::LineNumber;
|
||||||
use itertools::Either;
|
use itertools::Either;
|
||||||
use logsmash_data::{LogLevel, LoggingStatementWithPathPrefix, StatementList};
|
use logsmash_data::{LogLevel, LoggingStatementWithPathPrefix, StatementList};
|
||||||
|
use ratatui::layout::{Alignment, Constraint};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::fmt::Write;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::iter::once;
|
use std::iter::once;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
@ -206,6 +211,73 @@ impl From<Vec<LoggingStatementWithPathPrefix>> for MatchResult {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl GroupedLine for LoggingStatementWithPathPrefix {
|
||||||
|
const HEADER: &'static [(&'static str, Alignment)] = &[
|
||||||
|
("Statement", Alignment::Left),
|
||||||
|
("File", Alignment::Left),
|
||||||
|
("Line", Alignment::Right),
|
||||||
|
];
|
||||||
|
|
||||||
|
const WIDTHS: &'static [Constraint] = &[
|
||||||
|
Constraint::Percentage(70),
|
||||||
|
Constraint::Percentage(30),
|
||||||
|
Constraint::Min(6),
|
||||||
|
Constraint::Length(10),
|
||||||
|
Constraint::Min(10),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GroupingResult for MatchResult {
|
||||||
|
type Lines = LoggingStatementWithPathPrefix;
|
||||||
|
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
self.count()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lines<'a>(&'a self) -> impl Iterator<Item = &'a Self::Lines> + use<'a>
|
||||||
|
where
|
||||||
|
<Self as GroupingResult>::Lines: 'a,
|
||||||
|
{
|
||||||
|
self.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matches(&self, filter: &Filter) -> bool {
|
||||||
|
if filter.is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
self.iter().any(|statement| {
|
||||||
|
filter.parts().all(|filter_part| {
|
||||||
|
filter_part.is_match(statement.statement.pattern)
|
||||||
|
|| filter_part.is_match(statement.statement.path)
|
||||||
|
|| filter_part.is_match(statement.path_prefix)
|
||||||
|
|| statement
|
||||||
|
.statement
|
||||||
|
.placeholders
|
||||||
|
.iter()
|
||||||
|
.any(|placeholder| filter_part.is_match(placeholder))
|
||||||
|
|| statement
|
||||||
|
.statement
|
||||||
|
.exception
|
||||||
|
.filter(|exception| filter_part.is_match(exception))
|
||||||
|
.is_some()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&self) -> impl Iterator<Item = Cow<str>> {
|
||||||
|
let mut message = String::new();
|
||||||
|
let mut paths = String::new();
|
||||||
|
let mut lines = String::new();
|
||||||
|
|
||||||
|
for statement in self.lines() {
|
||||||
|
writeln!(&mut message, "{}", statement.message()).ok();
|
||||||
|
writeln!(&mut paths, "{}", statement.path()).ok();
|
||||||
|
writeln!(&mut lines, "{}", statement.line()).ok();
|
||||||
|
}
|
||||||
|
[message.into(), paths.into(), lines.into()].into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default, Copy, Clone)]
|
#[derive(Default, Copy, Clone)]
|
||||||
pub struct SingleMatchState<'a> {
|
pub struct SingleMatchState<'a> {
|
||||||
pattern: &'a [u8],
|
pattern: &'a [u8],
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use hdrhistogram::Histogram;
|
use hdrhistogram::Histogram;
|
||||||
use ratatui::text::Text;
|
use ratatui::text::Text;
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::cmp::max;
|
use std::cmp::max;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
|
|
@ -83,4 +84,12 @@ impl<'a> From<&'a SparkLine> for Text<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a SparkLine> for Cow<'a, str> {
|
||||||
|
fn from(value: &'a SparkLine) -> Self {
|
||||||
|
// SAFETY: we only put bytes into the buffer from encode_utf8
|
||||||
|
let str = unsafe { str::from_utf8_unchecked(&value.bytes).trim_end_matches(char::from(0)) };
|
||||||
|
str.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const SPARKS: &[char] = &[' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
|
const SPARKS: &[char] = &[' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ pub fn footer<'a>(app: &App<'a>, params: FooterParams<'a>) -> Table<'a> {
|
||||||
Table::new(
|
Table::new(
|
||||||
[Row::new([
|
[Row::new([
|
||||||
Text::from(help(page)),
|
Text::from(help(page)),
|
||||||
Text::from(format!("{} unmatched items", app.unmatched.count())),
|
Text::from(format!("{} unmatched items", app.unmatched_count)),
|
||||||
Text::from(format!("{} parse errors", app.error_count())),
|
Text::from(format!("{} parse errors", app.error_count())),
|
||||||
])],
|
])],
|
||||||
widths,
|
widths,
|
||||||
|
|
|
||||||
59
src/ui/grouping_list.rs
Normal file
59
src/ui/grouping_list.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
use crate::app::Filter;
|
||||||
|
use crate::grouping::{GroupedLine, GroupingResult, LogGrouping};
|
||||||
|
use crate::ui::style::TABLE_HEADER_STYLE;
|
||||||
|
use crate::ui::table::ScrollbarTable;
|
||||||
|
use ratatui::prelude::*;
|
||||||
|
use ratatui::widgets::{Cell, Row};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::iter::once;
|
||||||
|
use std::ops::RangeInclusive;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
|
pub fn grouping_list<'a, G: GroupingResult>(
|
||||||
|
all: &'a LogGrouping<'a, G>,
|
||||||
|
items: &'a [LogGrouping<'a, G>],
|
||||||
|
time_range: RangeInclusive<OffsetDateTime>,
|
||||||
|
filter: &Filter,
|
||||||
|
) -> ScrollbarTable<'a> {
|
||||||
|
let header = G::Lines::HEADER
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.chain([("Time", Alignment::Left), ("Count", Alignment::Left)]);
|
||||||
|
|
||||||
|
let header = header
|
||||||
|
.map(|(text, align)| Text::from(text).alignment(align))
|
||||||
|
.map(Cell::from)
|
||||||
|
.collect::<Row>()
|
||||||
|
.style(TABLE_HEADER_STYLE)
|
||||||
|
.height(1);
|
||||||
|
|
||||||
|
ScrollbarTable::new(
|
||||||
|
once(all)
|
||||||
|
.chain(items.iter().filter(|result| result.matches(filter)))
|
||||||
|
.map(|result| grouped_row(result, time_range.clone())),
|
||||||
|
G::Lines::WIDTHS,
|
||||||
|
)
|
||||||
|
.header(header)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn grouped_row<'a, G: GroupingResult>(
|
||||||
|
grouping: &'a LogGrouping<'a, G>,
|
||||||
|
time_range: RangeInclusive<OffsetDateTime>,
|
||||||
|
) -> Row<'a> {
|
||||||
|
if let Some(match_result) = &grouping.result {
|
||||||
|
let grouping_columns = match_result.render();
|
||||||
|
let columns = grouping_columns.chain([
|
||||||
|
Cow::from(grouping.sparkline(time_range)),
|
||||||
|
Cow::from(grouping.count().to_string()),
|
||||||
|
]);
|
||||||
|
Row::new(columns).height(match_result.len() as u16)
|
||||||
|
} else {
|
||||||
|
Row::new([
|
||||||
|
Text::from(grouping.name.unwrap_or_default()),
|
||||||
|
Text::from(""),
|
||||||
|
Text::from(""),
|
||||||
|
Text::from(grouping.sparkline(time_range)),
|
||||||
|
Text::from(grouping.count().to_string()),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
use crate::app::{App, Filter, LogMatch};
|
|
||||||
use crate::ui::style::TABLE_HEADER_STYLE;
|
|
||||||
use crate::ui::table::ScrollbarTable;
|
|
||||||
use itertools::Either;
|
|
||||||
use ratatui::prelude::*;
|
|
||||||
use ratatui::widgets::{Cell, Row};
|
|
||||||
use std::fmt::Write;
|
|
||||||
use std::iter::{empty, once};
|
|
||||||
|
|
||||||
pub fn match_list<'a>(app: &'a App<'a>, filter: &Filter) -> ScrollbarTable<'a> {
|
|
||||||
let header = [
|
|
||||||
Text::from("Statement"),
|
|
||||||
Text::from("File"),
|
|
||||||
Text::from("Line").alignment(Alignment::Right),
|
|
||||||
Text::from("Time"),
|
|
||||||
Text::from("Count"),
|
|
||||||
]
|
|
||||||
.into_iter()
|
|
||||||
.map(Cell::from)
|
|
||||||
.collect::<Row>()
|
|
||||||
.style(TABLE_HEADER_STYLE)
|
|
||||||
.height(1);
|
|
||||||
|
|
||||||
let widths = [
|
|
||||||
Constraint::Percentage(70),
|
|
||||||
Constraint::Percentage(30),
|
|
||||||
Constraint::Min(6),
|
|
||||||
Constraint::Length(10),
|
|
||||||
Constraint::Min(10),
|
|
||||||
];
|
|
||||||
|
|
||||||
let all = log_row(&app.all, app, "All lines");
|
|
||||||
let unmatched = if app.unmatched.count() == 0 {
|
|
||||||
Either::Right(empty())
|
|
||||||
} else {
|
|
||||||
Either::Left(once(log_row(&app.unmatched, app, "Unmatched lines")))
|
|
||||||
};
|
|
||||||
|
|
||||||
ScrollbarTable::new(
|
|
||||||
once(all)
|
|
||||||
.chain(
|
|
||||||
app.matches
|
|
||||||
.iter()
|
|
||||||
.filter(|result| result.matches(filter))
|
|
||||||
.map(|result| log_row(result, app, "")),
|
|
||||||
)
|
|
||||||
.chain(unmatched),
|
|
||||||
widths,
|
|
||||||
)
|
|
||||||
.header(header)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn log_row<'a>(result: &'a LogMatch, app: &'a App, name: &'static str) -> Row<'a> {
|
|
||||||
if let Some(match_result) = &result.result {
|
|
||||||
let mut message = String::new();
|
|
||||||
let mut paths = String::new();
|
|
||||||
let mut lines = String::new();
|
|
||||||
for statement in match_result.iter() {
|
|
||||||
writeln!(&mut message, "{}", statement.message()).ok();
|
|
||||||
writeln!(&mut paths, "{}", statement.path()).ok();
|
|
||||||
writeln!(&mut lines, "{}", statement.line()).ok();
|
|
||||||
}
|
|
||||||
Row::new([
|
|
||||||
Text::from(message),
|
|
||||||
Text::from(paths),
|
|
||||||
Text::from(lines).alignment(Alignment::Right),
|
|
||||||
Text::from(result.sparkline(app)),
|
|
||||||
Text::from(result.count().to_string()),
|
|
||||||
])
|
|
||||||
.height(match_result.count() as u16)
|
|
||||||
} else {
|
|
||||||
Row::new([
|
|
||||||
Text::from(name),
|
|
||||||
Text::from(""),
|
|
||||||
Text::from(""),
|
|
||||||
Text::from(result.sparkline(app)),
|
|
||||||
Text::from(result.count().to_string()),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::error::UiError;
|
use crate::error::UiError;
|
||||||
|
use crate::matcher::MatchResult;
|
||||||
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::grouped_logs::grouped_logs;
|
||||||
|
use crate::ui::grouping_list::grouping_list;
|
||||||
use crate::ui::histogram::UiHistogram;
|
use crate::ui::histogram::UiHistogram;
|
||||||
use crate::ui::input::handle_events;
|
use crate::ui::input::handle_events;
|
||||||
use crate::ui::match_list::match_list;
|
|
||||||
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::{
|
||||||
|
|
@ -27,9 +28,9 @@ use std::panic::{set_hook, take_hook};
|
||||||
mod error_list;
|
mod error_list;
|
||||||
mod footer;
|
mod footer;
|
||||||
mod grouped_logs;
|
mod grouped_logs;
|
||||||
|
mod grouping_list;
|
||||||
mod histogram;
|
mod histogram;
|
||||||
mod input;
|
mod input;
|
||||||
mod match_list;
|
|
||||||
mod single_log;
|
mod single_log;
|
||||||
mod single_match;
|
mod single_match;
|
||||||
mod state;
|
mod state;
|
||||||
|
|
@ -116,16 +117,18 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
|
||||||
}) => {
|
}) => {
|
||||||
let selected = table_state.selected();
|
let selected = table_state.selected();
|
||||||
let histogram = if selected == 0 {
|
let histogram = if selected == 0 {
|
||||||
app.all.histogram(app)
|
app.all.histogram(app.time_range())
|
||||||
} else if selected < app.matches.len() + 1 {
|
|
||||||
let log_match = &app.matches[selected - 1];
|
|
||||||
log_match.histogram(app)
|
|
||||||
} else {
|
} else {
|
||||||
app.unmatched.histogram(app)
|
let log_match = &app.matches[selected - 1];
|
||||||
|
log_match.histogram(app.time_range())
|
||||||
};
|
};
|
||||||
|
|
||||||
frame.render_widget(UiHistogram::new(histogram), layout[0]);
|
frame.render_widget(UiHistogram::new(histogram), layout[0]);
|
||||||
frame.render_stateful_widget(match_list(app, filter), layout[1], table_state);
|
frame.render_stateful_widget(
|
||||||
|
grouping_list::<MatchResult>(&app.all, &app.matches, app.time_range(), filter),
|
||||||
|
layout[1],
|
||||||
|
table_state,
|
||||||
|
);
|
||||||
frame.render_widget(footer(app, state.footer_params()), layout[2]);
|
frame.render_widget(footer(app, state.footer_params()), layout[2]);
|
||||||
}
|
}
|
||||||
UiState::Match(MatchState {
|
UiState::Match(MatchState {
|
||||||
|
|
@ -141,7 +144,10 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
|
||||||
&result.grouped[selected - 1]
|
&result.grouped[selected - 1]
|
||||||
};
|
};
|
||||||
|
|
||||||
frame.render_widget(UiHistogram::new(selected_group.histogram(app)), layout[0]);
|
frame.render_widget(
|
||||||
|
UiHistogram::new(selected_group.histogram(app.time_range())),
|
||||||
|
layout[0],
|
||||||
|
);
|
||||||
frame.render_stateful_widget(
|
frame.render_stateful_widget(
|
||||||
grouped_lines(app, result, filter),
|
grouped_lines(app, result, filter),
|
||||||
layout[1],
|
layout[1],
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
use crate::app::{App, Filter, LineSet, LogMatch};
|
use crate::app::{App, Filter, LineSet};
|
||||||
|
use crate::grouping::LogGrouping;
|
||||||
|
use crate::matcher::MatchResult;
|
||||||
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 ratatui::buffer::Buffer;
|
use ratatui::buffer::Buffer;
|
||||||
|
|
@ -7,10 +9,12 @@ use ratatui::prelude::StatefulWidget;
|
||||||
use ratatui::text::Text;
|
use ratatui::text::Text;
|
||||||
use ratatui::widgets::{Cell, Row};
|
use ratatui::widgets::{Cell, Row};
|
||||||
use std::iter::once;
|
use std::iter::once;
|
||||||
|
use std::ops::RangeInclusive;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
pub fn grouped_lines<'a>(
|
pub fn grouped_lines<'a>(
|
||||||
app: &'a App<'a>,
|
app: &'a App<'a>,
|
||||||
log_match: &'a LogMatch,
|
log_match: &'a LogGrouping<'a, MatchResult>,
|
||||||
filter: &'a Filter,
|
filter: &'a Filter,
|
||||||
) -> SingleMatchTable<'a> {
|
) -> SingleMatchTable<'a> {
|
||||||
SingleMatchTable {
|
SingleMatchTable {
|
||||||
|
|
@ -22,7 +26,7 @@ pub fn grouped_lines<'a>(
|
||||||
|
|
||||||
pub struct SingleMatchTable<'a> {
|
pub struct SingleMatchTable<'a> {
|
||||||
app: &'a App<'a>,
|
app: &'a App<'a>,
|
||||||
log_match: &'a LogMatch<'a>,
|
log_match: &'a LogGrouping<'a, MatchResult>,
|
||||||
filter: &'a Filter,
|
filter: &'a Filter,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,7 +63,7 @@ impl StatefulWidget for SingleMatchTable<'_> {
|
||||||
Text::from("All lines"),
|
Text::from("All lines"),
|
||||||
Text::from(""),
|
Text::from(""),
|
||||||
Text::from(""),
|
Text::from(""),
|
||||||
Text::from(self.log_match.sparkline(self.app)),
|
Text::from(self.log_match.sparkline(self.app.time_range())),
|
||||||
Text::from(self.log_match.count().to_string()),
|
Text::from(self.log_match.count().to_string()),
|
||||||
]))
|
]))
|
||||||
.chain(
|
.chain(
|
||||||
|
|
@ -68,7 +72,11 @@ impl StatefulWidget for SingleMatchTable<'_> {
|
||||||
.filter(|group| group.matches(self.filter))
|
.filter(|group| group.matches(self.filter))
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, group)| {
|
.map(|(i, group)| {
|
||||||
group_row(self.app, group, i.abs_diff(state.selected()) < 100)
|
group_row(
|
||||||
|
self.app.time_range(),
|
||||||
|
group,
|
||||||
|
i.abs_diff(state.selected()) < 100,
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
widths,
|
widths,
|
||||||
|
|
@ -78,7 +86,11 @@ impl StatefulWidget for SingleMatchTable<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn group_row<'a>(app: &'a App, group: &'a LineSet, is_in_view: bool) -> Row<'a> {
|
fn group_row<'a>(
|
||||||
|
time_range: RangeInclusive<OffsetDateTime>,
|
||||||
|
group: &'a LineSet,
|
||||||
|
is_in_view: bool,
|
||||||
|
) -> Row<'a> {
|
||||||
if is_in_view {
|
if is_in_view {
|
||||||
let line = group.lines[0];
|
let line = group.lines[0];
|
||||||
|
|
||||||
|
|
@ -86,7 +98,7 @@ fn group_row<'a>(app: &'a App, group: &'a LineSet, is_in_view: bool) -> Row<'a>
|
||||||
Text::from(line.level.as_str()),
|
Text::from(line.level.as_str()),
|
||||||
Text::from(line.app.as_ref()),
|
Text::from(line.app.as_ref()),
|
||||||
Text::from(line.display()),
|
Text::from(line.display()),
|
||||||
Text::from(group.sparkline(app)),
|
Text::from(group.sparkline(time_range)),
|
||||||
Text::from(group.len().to_string()),
|
Text::from(group.len().to_string()),
|
||||||
])
|
])
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
use crate::app::{App, Filter, LogMatch, EMPTY_FILTER};
|
use crate::app::{App, Filter, EMPTY_FILTER};
|
||||||
use crate::error::ParseError;
|
use crate::error::ParseError;
|
||||||
|
use crate::grouping::LogGrouping;
|
||||||
use crate::logfile::logline::{FullLogLine, LogLine};
|
use crate::logfile::logline::{FullLogLine, LogLine};
|
||||||
|
use crate::matcher::MatchResult;
|
||||||
use crate::ui::footer::FooterParams;
|
use crate::ui::footer::FooterParams;
|
||||||
use crate::ui::input::{PopMode, UiEvent};
|
use crate::ui::input::{PopMode, UiEvent};
|
||||||
use crate::ui::table::ScrollbarTableState;
|
use crate::ui::table::ScrollbarTableState;
|
||||||
|
|
@ -45,19 +47,16 @@ impl<'a> MatchListState<'a> {
|
||||||
fn enter(self, selected: usize, app: &'a App) -> UiState<'a> {
|
fn enter(self, selected: usize, app: &'a App) -> UiState<'a> {
|
||||||
let result = if selected == 0 {
|
let result = if selected == 0 {
|
||||||
&app.all
|
&app.all
|
||||||
} else if selected <= app.matches.len() {
|
} else if self.filter.is_empty() {
|
||||||
if self.filter.is_empty() {
|
&app.matches[selected - 1]
|
||||||
&app.matches[selected - 1]
|
|
||||||
} else {
|
|
||||||
app.matches
|
|
||||||
.iter()
|
|
||||||
.filter(|log_match| log_match.matches(&self.filter))
|
|
||||||
.nth(selected - 1)
|
|
||||||
.unwrap_or(&app.unmatched)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
&app.unmatched
|
app.matches
|
||||||
|
.iter()
|
||||||
|
.filter(|log_match| log_match.matches(&self.filter))
|
||||||
|
.nth(selected - 1)
|
||||||
|
.unwrap_or(app.matches.last().unwrap())
|
||||||
};
|
};
|
||||||
|
|
||||||
let table_state = ScrollbarTableState::new(result.grouped.len() + 1);
|
let table_state = ScrollbarTableState::new(result.grouped.len() + 1);
|
||||||
UiState::Match(MatchState {
|
UiState::Match(MatchState {
|
||||||
result,
|
result,
|
||||||
|
|
@ -77,7 +76,7 @@ impl PartialEq for MatchListState<'_> {
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct MatchState<'a> {
|
pub struct MatchState<'a> {
|
||||||
pub result: &'a LogMatch<'a>,
|
pub result: &'a LogGrouping<'a, MatchResult>,
|
||||||
pub table_state: ScrollbarTableState,
|
pub table_state: ScrollbarTableState,
|
||||||
pub previous: Box<UiState<'a>>,
|
pub previous: Box<UiState<'a>>,
|
||||||
pub filter: Filter,
|
pub filter: Filter,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue