This commit is contained in:
Robin Appelman 2024-07-24 23:22:04 +02:00
commit 6e0c662fb4
13 changed files with 263 additions and 45 deletions

11
Cargo.lock generated
View file

@ -207,6 +207,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"thiserror", "thiserror",
"tinystr",
"zip", "zip",
] ]
@ -1000,6 +1001,16 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "tinystr"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
dependencies = [
"displaydoc",
"serde",
]
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.17.0" version = "1.17.0"

View file

@ -16,6 +16,7 @@ cloud-log-analyser-data = { version = "0.1.0", path = "./data" }
zip = "2.1.5" zip = "2.1.5"
itertools = "0.13.0" itertools = "0.13.0"
ratatui = "0.27.0" ratatui = "0.27.0"
tinystr = { version = "0.7.6", features = ["serde"] }
[profile.dev.package."*"] [profile.dev.package."*"]
opt-level = 3 opt-level = 3

View file

@ -38,6 +38,21 @@ impl LogLevel {
}; };
matcher_level == *self || matcher_level == LogLevel::Exception || *self == LogLevel::Unknown matcher_level == *self || matcher_level == LogLevel::Exception || *self == LogLevel::Unknown
} }
pub fn as_str(&self) -> &'static str {
match self {
LogLevel::Debug => "debug",
LogLevel::Info => "info",
LogLevel::Notice => "notice",
LogLevel::Warn => "warn",
LogLevel::Error => "error",
LogLevel::Alert => "alert",
LogLevel::Critical => "critical",
LogLevel::Emergency => "emergency",
LogLevel::Exception => "exception",
LogLevel::Unknown => "log",
}
}
} }
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]

View file

@ -1,12 +1,33 @@
use crate::logline::LogLine;
use crate::matcher::MatchResult; use crate::matcher::MatchResult;
use cloud_log_analyser_data::StatementList; use cloud_log_analyser_data::StatementList;
pub struct App { pub struct App {
pub lines: Vec<LogLine>,
pub log_statements: StatementList, pub log_statements: StatementList,
pub matches: Vec<LogMatch>, pub matches: Vec<LogMatch>,
pub error_count: usize,
pub unmatched: Vec<UnMatched>,
} }
pub struct LogMatch { pub struct LogMatch {
pub result: MatchResult, pub result: MatchResult,
pub count: usize, pub lines: Vec<usize>,
}
impl LogMatch {
pub fn count(&self) -> usize {
self.lines.len()
}
}
pub struct UnMatched {
pub app: String,
pub lines: Vec<usize>,
}
impl UnMatched {
pub fn count(&self) -> usize {
self.lines.len()
}
} }

View file

@ -1,32 +1,32 @@
use cloud_log_analyser_data::LogLevel; use cloud_log_analyser_data::LogLevel;
use serde::Deserialize; use serde::Deserialize;
use std::borrow::Cow; use tinystr::TinyAsciiStr;
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct LogLine<'a> { pub struct LogLine {
pub version: &'a str, pub version: TinyAsciiStr<8>,
pub level: LogLevel, pub level: LogLevel,
pub message: Cow<'a, str>, pub message: String,
pub exception: Option<Exception<'a>>, pub exception: Option<Exception>,
pub app: Cow<'a, str>, pub app: TinyAsciiStr<16>,
} }
impl LogLine<'_> { impl LogLine {
pub fn major_version(&self) -> Option<u32> { pub fn major_version(&self) -> Option<u32> {
let major = self let major = self
.version .version
.split_once('.') .split_once('.')
.map(|(major, _)| major) .map(|(major, _)| major)
.unwrap_or(self.version); .unwrap_or(self.version.as_str());
major.parse().ok() major.parse().ok()
} }
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")] #[serde(rename_all = "PascalCase")]
pub struct Exception<'a> { pub struct Exception {
pub exception: Cow<'a, str>, pub exception: String,
pub file: Cow<'a, str>, pub file: String,
pub line: usize, pub line: usize,
pub previous: Option<Box<Exception<'a>>>, pub previous: Option<Box<Exception>>,
} }

View file

@ -1,4 +1,4 @@
use crate::app::{App, LogMatch}; use crate::app::{App, LogMatch, UnMatched};
use crate::error::LogError; use crate::error::LogError;
use crate::logfile::LogFile; use crate::logfile::LogFile;
use crate::logline::LogLine; use crate::logline::LogLine;
@ -9,7 +9,6 @@ use cloud_log_analyser_data::{get_statements, MAX_VERSION};
use main_error::MainResult; use main_error::MainResult;
use std::collections::HashMap; use std::collections::HashMap;
use std::iter::once; use std::iter::once;
use std::ops::AddAssign;
mod app; mod app;
mod error; mod error;
@ -34,7 +33,7 @@ fn main() -> MainResult {
})?; })?;
let mut lines = log_file.iter(); let mut lines = log_file.iter();
let mut counts: HashMap<MatchResult, usize> = HashMap::new(); let mut counts: HashMap<MatchResult, Vec<usize>> = HashMap::new();
let first = lines.next().unwrap(); let first = lines.next().unwrap();
let first_parsed: LogLine = serde_json::from_str(&first).unwrap(); let first_parsed: LogLine = serde_json::from_str(&first).unwrap();
@ -43,8 +42,9 @@ fn main() -> MainResult {
let lines = once(first).chain(lines); let lines = once(first).chain(lines);
let mut error_count = 0; let mut error_count = 0;
let mut unmatched_total = 0; let mut unmatched_counts: HashMap<String, Vec<usize>> = HashMap::new();
let mut unmatched_counts = HashMap::new(); let mut parsed_lines = Vec::new();
let mut i = 0;
for line in lines { for line in lines {
if line.starts_with('{') { if line.starts_with('{') {
let parsed = match serde_json::from_str::<LogLine>(&line) { let parsed = match serde_json::from_str::<LogLine>(&line) {
@ -55,31 +55,42 @@ fn main() -> MainResult {
} }
}; };
if let Some(index) = matcher.match_log(&parsed) { if let Some(index) = matcher.match_log(&parsed) {
counts.entry(index).or_default().add_assign(1); counts.entry(index).or_default().push(i);
} else { } else {
if args.unmatched && parsed.app != "PHP" { if args.unmatched && parsed.app != "PHP" {
println!("{} :{:?}", parsed.message, &parsed.exception); println!("{} :{:?}", parsed.message, &parsed.exception);
} }
unmatched_total += 1; if let Some(entry) = unmatched_counts.get_mut(parsed.app.as_str()) {
if let Some(entry) = unmatched_counts.get_mut(parsed.app.as_ref()) { entry.push(i)
*entry += 1;
} else { } else {
unmatched_counts.insert(parsed.app.to_string(), 1); unmatched_counts.insert(parsed.app.to_string(), vec![i]);
} }
} }
parsed_lines.push(parsed);
i += 1;
} }
} }
let mut counts: Vec<(_, _)> = counts.into_iter().collect(); let mut matched_lines: Vec<(_, _)> = counts.into_iter().collect();
counts.sort_by_key(|(_, count)| *count); matched_lines.sort_by_key(|(_, lines)| lines.len());
counts.reverse(); matched_lines.reverse();
let mut unmatched_lines: Vec<(_, _)> = unmatched_counts.into_iter().collect();
unmatched_lines.sort_by_key(|(_, lines)| lines.len());
unmatched_lines.reverse();
let app = App { let app = App {
lines: parsed_lines,
log_statements: statements, log_statements: statements,
matches: counts matches: matched_lines
.into_iter() .into_iter()
.map(|(result, count)| LogMatch { result, count }) .map(|(result, lines)| LogMatch { result, lines })
.collect(), .collect(),
unmatched: unmatched_lines
.into_iter()
.map(|(app, lines)| UnMatched { app, lines })
.collect(),
error_count,
}; };
run_ui(app)?; run_ui(app)?;

View file

@ -49,8 +49,8 @@ impl Matcher {
if let Some(exception) = &log.exception { if let Some(exception) = &log.exception {
for (i, log_match) in self.matches.iter().enumerate() { for (i, log_match) in self.matches.iter().enumerate() {
if log_match.line == exception.line if log_match.line == exception.line
&& log_match.exception.as_deref() == Some(exception.exception.as_ref()) && log_match.exception == Some(exception.exception.as_str())
&& exception.file.as_ref().ends_with(log_match.path) && exception.file.ends_with(log_match.path)
{ {
return Some(MatchResult::Single(i)); return Some(MatchResult::Single(i));
} }
@ -60,7 +60,7 @@ impl Matcher {
for (i, log_match) in self.matches.iter().enumerate() { for (i, log_match) in self.matches.iter().enumerate() {
if log_match.has_meaningful_message { if log_match.has_meaningful_message {
if log.level.matches(log_match.level) if log.level.matches(log_match.level)
&& log_match.pattern.is_match(log.message.as_ref()) && log_match.pattern.is_match(log.message.as_str())
&& log_match.pattern_length >= best_length && log_match.pattern_length >= best_length
{ {
if log_match.pattern_length > best_length { if log_match.pattern_length > best_length {

41
src/ui/footer.rs Normal file
View file

@ -0,0 +1,41 @@
use crate::app::App;
use crate::ui::state::UiPage;
use ratatui::layout::Constraint;
use ratatui::prelude::Style;
use ratatui::style::palette::tailwind;
use ratatui::widgets::{Row, Table};
pub fn footer(app: &App, page: UiPage) -> Table {
let footer_style = Style::default()
.bg(tailwind::BLACK)
.fg(tailwind::GREEN.c600);
let widths = [
Constraint::Percentage(100),
Constraint::Min(25),
Constraint::Min(20),
];
Table::new(
[Row::new([
help(page).to_string(),
format!(
"{} unmatched items",
app.unmatched
.iter()
.map(|unmatched| unmatched.count())
.sum::<usize>()
),
format!("{} parse errors", app.error_count),
])],
widths,
)
.style(footer_style)
}
fn help(page: UiPage) -> &'static str {
match page {
UiPage::MatchList => "«Q» Exit - «Enter» Select",
UiPage::Match => "«Q» Exit - «Esc» Back",
}
}

View file

@ -1,23 +1,15 @@
use crate::app::{App, LogMatch}; use crate::app::{App, LogMatch};
use crate::ui::style::{TABLE_HEADER_STYLE, TABLE_SELECTED_STYLE};
use ratatui::prelude::*; use ratatui::prelude::*;
use ratatui::style::palette::tailwind;
use ratatui::widgets::{Cell, HighlightSpacing, Row, Table}; use ratatui::widgets::{Cell, HighlightSpacing, Row, Table};
use std::fmt::Write; use std::fmt::Write;
pub fn match_list(app: &App) -> Table { pub fn match_list(app: &App) -> Table {
let header_style = Style::default()
.bg(tailwind::BLACK)
.fg(tailwind::GREEN.c600);
let selected_style = Style::default()
.add_modifier(Modifier::REVERSED)
.bg(tailwind::BLACK)
.fg(tailwind::GREEN.c600);
let header = ["Statement", "File", "Line", "Count"] let header = ["Statement", "File", "Line", "Count"]
.into_iter() .into_iter()
.map(Cell::from) .map(Cell::from)
.collect::<Row>() .collect::<Row>()
.style(header_style) .style(TABLE_HEADER_STYLE)
.height(1); .height(1);
let widths = [ let widths = [
@ -31,7 +23,7 @@ pub fn match_list(app: &App) -> Table {
widths, widths,
) )
.header(header) .header(header)
.highlight_style(selected_style) .highlight_style(TABLE_SELECTED_STYLE)
.highlight_spacing(HighlightSpacing::Always); .highlight_spacing(HighlightSpacing::Always);
table table
} }
@ -46,5 +38,5 @@ fn log_row<'a>(result: &LogMatch, app: &'a App) -> Row<'a> {
writeln!(&mut paths, "{}", statement.path()).unwrap(); writeln!(&mut paths, "{}", statement.path()).unwrap();
writeln!(&mut lines, "{}", statement.line).unwrap(); writeln!(&mut lines, "{}", statement.line).unwrap();
} }
Row::new([message, paths, lines, result.count.to_string()]).height(result.result.len() as u16) Row::new([message, paths, lines, result.count().to_string()]).height(result.result.len() as u16)
} }

View file

@ -1,6 +1,8 @@
use crate::app::App; use crate::app::App;
use crate::error::UiError; use crate::error::UiError;
use crate::ui::footer::footer;
use crate::ui::match_list::match_list; use crate::ui::match_list::match_list;
use crate::ui::single_match::single_match;
use crate::ui::state::{UiEvent, UiState}; use crate::ui::state::{UiEvent, UiState};
use ratatui::crossterm::event::{Event, KeyCode, KeyModifiers}; use ratatui::crossterm::event::{Event, KeyCode, KeyModifiers};
use ratatui::crossterm::terminal::{ use ratatui::crossterm::terminal::{
@ -12,8 +14,11 @@ use ratatui::Terminal;
use std::io; use std::io;
use std::io::stdout; use std::io::stdout;
mod footer;
mod match_list; mod match_list;
mod single_match;
mod state; mod state;
pub mod style;
pub fn run_ui(app: App) -> Result<(), UiError> { pub fn run_ui(app: App) -> Result<(), UiError> {
enable_raw_mode()?; enable_raw_mode()?;
@ -46,6 +51,7 @@ fn handle_events() -> io::Result<Option<UiEvent>> {
KeyCode::Esc => Some(UiEvent::Back), KeyCode::Esc => Some(UiEvent::Back),
KeyCode::Down => Some(UiEvent::Down), KeyCode::Down => Some(UiEvent::Down),
KeyCode::Up => Some(UiEvent::Up), KeyCode::Up => Some(UiEvent::Up),
KeyCode::Enter => Some(UiEvent::Select),
_ => None, _ => None,
}); });
} }
@ -55,10 +61,25 @@ fn handle_events() -> io::Result<Option<UiEvent>> {
} }
fn ui(frame: &mut Frame, app: &App, state: &mut UiState) { fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
let page = state.page();
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Percentage(100), Constraint::Length(1)])
.split(frame.size());
match state { match state {
UiState::Quit => {} UiState::Quit => {}
UiState::MatchList { table_state } => { UiState::MatchList { table_state } => {
frame.render_stateful_widget(match_list(app), frame.size(), table_state); frame.render_stateful_widget(match_list(app), layout[0], table_state);
frame.render_widget(footer(app, page), layout[1]);
}
UiState::Match {
selected: index,
table_state,
} => {
let log_match = &app.matches[*index];
frame.render_stateful_widget(single_match(app, log_match), layout[0], table_state);
frame.render_widget(footer(app, page), layout[1]);
} }
} }
} }

35
src/ui/single_match.rs Normal file
View file

@ -0,0 +1,35 @@
use crate::app::{App, LogMatch};
use crate::logline::LogLine;
use crate::ui::style::{TABLE_HEADER_STYLE, TABLE_SELECTED_STYLE};
use ratatui::layout::Constraint;
use ratatui::widgets::{Cell, HighlightSpacing, Row, Table};
pub fn single_match<'a>(app: &'a App, matches: &'a LogMatch) -> Table<'a> {
let lines = matches.lines.iter().map(|i| &app.lines[*i]);
let header = ["Level", "App", "Message"]
.into_iter()
.map(Cell::from)
.collect::<Row>()
.style(TABLE_HEADER_STYLE)
.height(1);
let widths = [
Constraint::Min(10),
Constraint::Min(20),
Constraint::Percentage(100),
];
let table = Table::new(lines.map(|line| log_row(line)), widths)
.header(header)
.highlight_style(TABLE_SELECTED_STYLE)
.highlight_spacing(HighlightSpacing::Always);
table
}
fn log_row(line: &LogLine) -> Row {
Row::new([
line.level.as_str(),
line.app.as_str(),
line.message.as_str(),
])
}

View file

@ -4,7 +4,13 @@ use table_state::TableStateExt;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum UiState { pub enum UiState {
MatchList { table_state: TableState }, MatchList {
table_state: TableState,
},
Match {
selected: usize,
table_state: TableState,
},
Quit, Quit,
} }
@ -17,6 +23,13 @@ impl Default for UiState {
} }
impl UiState { impl UiState {
pub fn page(&self) -> UiPage {
match self {
UiState::Quit | UiState::MatchList { .. } => UiPage::MatchList,
UiState::Match { .. } => UiPage::Match,
}
}
pub fn process(self, event: UiEvent, app: &App) -> UiState { pub fn process(self, event: UiEvent, app: &App) -> UiState {
match (self, event) { match (self, event) {
(UiState::Quit, _) => UiState::Quit, (UiState::Quit, _) => UiState::Quit,
@ -30,6 +43,52 @@ impl UiState {
table_state.up(app.matches.len()); table_state.up(app.matches.len());
UiState::MatchList { table_state } UiState::MatchList { table_state }
} }
(UiState::MatchList { table_state }, UiEvent::Select) => {
let selected = table_state.selected().unwrap_or(0);
let mut table_state = TableState::default();
table_state.select(Some(0));
UiState::Match {
selected,
table_state,
}
}
(
UiState::Match {
selected: index, ..
},
UiEvent::Back,
) => {
let mut table_state = TableState::default();
table_state.select(Some(index));
UiState::MatchList { table_state }
}
(
UiState::Match {
mut table_state,
selected,
},
UiEvent::Down,
) => {
table_state.down(app.matches[selected].count());
UiState::Match {
table_state,
selected,
}
}
(
UiState::Match {
mut table_state,
selected,
},
UiEvent::Up,
) => {
table_state.up(app.matches[selected].count());
UiState::Match {
table_state,
selected,
}
}
(state @ UiState::Match { .. }, _) => state,
} }
} }
} }
@ -39,6 +98,12 @@ pub enum UiEvent {
Back, Back,
Up, Up,
Down, Down,
Select,
}
pub enum UiPage {
MatchList,
Match,
} }
mod table_state { mod table_state {

5
src/ui/style.rs Normal file
View file

@ -0,0 +1,5 @@
use ratatui::prelude::Style;
use ratatui::style::palette::tailwind;
pub const TABLE_HEADER_STYLE: Style = Style::new().bg(tailwind::BLACK).fg(tailwind::GREEN.c600);
pub const TABLE_SELECTED_STYLE: Style = Style::new().fg(tailwind::BLACK).bg(tailwind::GREEN.c600);