mirror of
https://codeberg.org/icewind/logsmash.git
synced 2026-06-03 18:14:11 +02:00
log trace
This commit is contained in:
parent
04308e966b
commit
aff67c43dd
4 changed files with 229 additions and 11 deletions
104
src/logline.rs
104
src/logline.rs
|
|
@ -1,6 +1,8 @@
|
||||||
use ahash::AHasher;
|
use ahash::AHasher;
|
||||||
use logsmash_data::LogLevel;
|
use logsmash_data::LogLevel;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use tinystr::TinyAsciiStr;
|
use tinystr::TinyAsciiStr;
|
||||||
|
|
@ -73,3 +75,105 @@ impl Hash for Exception {
|
||||||
self.line.hash(state);
|
self.line.hash(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
pub struct FullLogLine {
|
||||||
|
#[serde(rename = "reqId")]
|
||||||
|
pub request_id: TinyAsciiStr<32>,
|
||||||
|
pub level: LogLevel,
|
||||||
|
#[serde(with = "time::serde::iso8601")]
|
||||||
|
pub time: OffsetDateTime,
|
||||||
|
#[serde(rename = "remoteAddr")]
|
||||||
|
pub remote_address: String,
|
||||||
|
pub user: String,
|
||||||
|
pub app: TinyAsciiStr<32>,
|
||||||
|
pub method: TinyAsciiStr<16>,
|
||||||
|
pub url: String,
|
||||||
|
pub message: String,
|
||||||
|
#[serde(rename = "userAgent")]
|
||||||
|
pub user_agent: String,
|
||||||
|
pub version: TinyAsciiStr<16>,
|
||||||
|
pub exception: Option<FullException>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
#[serde(rename_all = "PascalCase")]
|
||||||
|
pub struct FullException {
|
||||||
|
pub exception: String,
|
||||||
|
pub message: String,
|
||||||
|
pub code: ExceptionCode,
|
||||||
|
pub trace: Vec<Trace>,
|
||||||
|
pub file: String,
|
||||||
|
pub line: usize,
|
||||||
|
pub previous: Option<Box<FullException>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum ExceptionCode {
|
||||||
|
Num(isize),
|
||||||
|
String(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FullException {
|
||||||
|
pub fn stack(&self) -> impl Iterator<Item = &FullException> + '_ {
|
||||||
|
ExceptionStack {
|
||||||
|
exception: Some(self),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
pub struct Trace {
|
||||||
|
#[serde(default)]
|
||||||
|
pub file: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub line: usize,
|
||||||
|
#[serde(default)]
|
||||||
|
pub function: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub class: String,
|
||||||
|
#[serde(default, rename = "type")]
|
||||||
|
pub ty: Option<TinyAsciiStr<4>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub args: Vec<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Trace {
|
||||||
|
pub fn function(&self) -> impl Display + '_ {
|
||||||
|
TraceFunction {
|
||||||
|
function: self.function.as_str(),
|
||||||
|
class: self.class.as_str(),
|
||||||
|
ty: self.ty.as_deref(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TraceFunction<'a> {
|
||||||
|
function: &'a str,
|
||||||
|
class: &'a str,
|
||||||
|
ty: Option<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for TraceFunction<'_> {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self.ty {
|
||||||
|
Some(ty) => write!(f, "{}{}{}", self.class, ty, self.function),
|
||||||
|
_ => write!(f, "{}", self.function),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ExceptionStack<'a> {
|
||||||
|
exception: Option<&'a FullException>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for ExceptionStack<'a> {
|
||||||
|
type Item = &'a FullException;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
let current = self.exception?;
|
||||||
|
self.exception = current.previous.as_deref();
|
||||||
|
Some(current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -170,8 +170,14 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
|
||||||
);
|
);
|
||||||
frame.render_widget(footer(app, page), layout[2]);
|
frame.render_widget(footer(app, page), layout[2]);
|
||||||
}
|
}
|
||||||
UiState::Log(LogState { log, .. }) => {
|
UiState::Log(LogState {
|
||||||
frame.render_widget(single_log(app, log), layout[0].union(layout[1]));
|
log, table_state, ..
|
||||||
|
}) => {
|
||||||
|
frame.render_stateful_widget(
|
||||||
|
single_log(app, log),
|
||||||
|
layout[0].union(layout[1]),
|
||||||
|
table_state,
|
||||||
|
);
|
||||||
frame.render_widget(footer(app, page), layout[2]);
|
frame.render_widget(footer(app, page), layout[2]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,98 @@
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::logline::LogLine;
|
use crate::logline::{FullException, FullLogLine, LogLine, Trace};
|
||||||
use ratatui::widgets::{Paragraph, Wrap};
|
use crate::ui::style::{TABLE_HEADER_STYLE, TABLE_SELECTED_STYLE, TIME_FORMAT};
|
||||||
|
use ratatui::prelude::*;
|
||||||
|
use ratatui::widgets::{Cell, HighlightSpacing, Paragraph, Row, Table, TableState, Wrap};
|
||||||
|
use std::iter::once;
|
||||||
|
use time::format_description::well_known::Iso8601;
|
||||||
|
|
||||||
pub fn single_log<'a>(_app: &App, line: &'a LogLine) -> Paragraph<'a> {
|
pub fn single_log(app: &App, line: &LogLine) -> SingleLog {
|
||||||
Paragraph::new(line.display()).wrap(Wrap::default())
|
let raw_line = app.get_line(line.index).unwrap();
|
||||||
|
let line: FullLogLine = serde_json::from_str(raw_line).unwrap();
|
||||||
|
SingleLog { line }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SingleLog {
|
||||||
|
line: FullLogLine,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StatefulWidget for SingleLog {
|
||||||
|
type State = TableState;
|
||||||
|
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
let par = Paragraph::new(format!(
|
||||||
|
"{}\n\n {} {}\n {}\n\n from {} by {} at {}",
|
||||||
|
self.line.message,
|
||||||
|
self.line.method,
|
||||||
|
self.line.url,
|
||||||
|
self.line.user_agent,
|
||||||
|
self.line.remote_address,
|
||||||
|
self.line.user,
|
||||||
|
self.line.time.format(&Iso8601::<TIME_FORMAT>).unwrap()
|
||||||
|
))
|
||||||
|
.wrap(Wrap::default());
|
||||||
|
|
||||||
|
let layout = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints(vec![Constraint::Min(7), Constraint::Percentage(100)])
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
par.render(layout[0], buf);
|
||||||
|
|
||||||
|
if let Some(exception) = &self.line.exception {
|
||||||
|
StatefulWidget::render(render_exception(exception), layout[1], buf, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_exception(exception: &FullException) -> Table {
|
||||||
|
let header = [
|
||||||
|
Text::from("File"),
|
||||||
|
Text::from("Line").alignment(Alignment::Right),
|
||||||
|
Text::from("Function"),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(Cell::from)
|
||||||
|
.collect::<Row>()
|
||||||
|
.style(TABLE_HEADER_STYLE)
|
||||||
|
.height(1);
|
||||||
|
|
||||||
|
let widths = [
|
||||||
|
Constraint::Percentage(40),
|
||||||
|
Constraint::Min(10),
|
||||||
|
Constraint::Percentage(60),
|
||||||
|
];
|
||||||
|
let rows = exception.stack().flat_map(exception_trace);
|
||||||
|
let table = Table::new(rows, widths)
|
||||||
|
.header(header)
|
||||||
|
.highlight_style(TABLE_SELECTED_STYLE)
|
||||||
|
.highlight_spacing(HighlightSpacing::Always);
|
||||||
|
table
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exception_trace(exception: &FullException) -> impl Iterator<Item = Row> + '_ {
|
||||||
|
let exception_row = Row::new([
|
||||||
|
Text::from(""),
|
||||||
|
Text::from(exception.line.to_string()).alignment(Alignment::Right),
|
||||||
|
Text::from(exception.file.clone()),
|
||||||
|
])
|
||||||
|
.style(TABLE_HEADER_STYLE);
|
||||||
|
let trace_rows = exception.trace.iter().map(trace_line);
|
||||||
|
once(exception_row).chain(trace_rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trace_line(trace: &Trace) -> Row {
|
||||||
|
Row::new([
|
||||||
|
Text::from(trace.file.clone()),
|
||||||
|
Text::from(if trace.line > 0 {
|
||||||
|
trace.line.to_string()
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
})
|
||||||
|
.alignment(Alignment::Right),
|
||||||
|
Text::from(trace.function().to_string()),
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::app::{App, LogMatch};
|
use crate::app::{App, LogMatch};
|
||||||
use crate::copy_osc;
|
use crate::copy_osc;
|
||||||
use crate::logline::LogLine;
|
use crate::logline::{FullLogLine, LogLine};
|
||||||
use derive_more::From;
|
use derive_more::From;
|
||||||
use ratatui::widgets::{ScrollbarState, TableState};
|
use ratatui::widgets::{ScrollbarState, TableState};
|
||||||
use table_state::TableStateExt;
|
use table_state::TableStateExt;
|
||||||
|
|
@ -56,7 +56,10 @@ impl<'a> LogsState<'a> {
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct LogState<'a> {
|
pub struct LogState<'a> {
|
||||||
|
pub trace_len: usize,
|
||||||
pub log: &'a LogLine,
|
pub log: &'a LogLine,
|
||||||
|
pub full_line: FullLogLine,
|
||||||
|
pub table_state: TableState,
|
||||||
pub previous: Box<UiState<'a>>,
|
pub previous: Box<UiState<'a>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,6 +87,7 @@ impl<'a> UiState<'a> {
|
||||||
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::Logs(state) => Some(&mut state.table_state),
|
||||||
|
UiState::Log(state) => Some(&mut state.table_state),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -102,6 +106,7 @@ impl<'a> UiState<'a> {
|
||||||
UiState::MatchList(_) => app.match_lines(),
|
UiState::MatchList(_) => app.match_lines(),
|
||||||
UiState::Match(state) => state.result.grouped.len(),
|
UiState::Match(state) => state.result.grouped.len(),
|
||||||
UiState::Logs(state) => state.lines.len(),
|
UiState::Logs(state) => state.lines.len(),
|
||||||
|
UiState::Log(state) => state.trace_len,
|
||||||
_ => 0,
|
_ => 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -115,18 +120,20 @@ impl<'a> UiState<'a> {
|
||||||
let count = state.row_count(app);
|
let count = state.row_count(app);
|
||||||
if let Some(table_state) = state.table_state() {
|
if let Some(table_state) = state.table_state() {
|
||||||
let pos = table_state.down(count, step);
|
let pos = table_state.down(count, step);
|
||||||
let scroll_state = state.scroll_state().unwrap();
|
if let Some(scroll_state) = state.scroll_state() {
|
||||||
*scroll_state = scroll_state.position(pos);
|
*scroll_state = scroll_state.position(pos);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
(mut state, UiEvent::Up(step)) => {
|
(mut state, UiEvent::Up(step)) => {
|
||||||
let count = state.row_count(app);
|
let count = state.row_count(app);
|
||||||
if let Some(table_state) = state.table_state() {
|
if let Some(table_state) = state.table_state() {
|
||||||
let pos = table_state.up(count, step);
|
let pos = table_state.up(count, step);
|
||||||
let scroll_state = state.scroll_state().unwrap();
|
if let Some(scroll_state) = state.scroll_state() {
|
||||||
*scroll_state = scroll_state.position(pos);
|
*scroll_state = scroll_state.position(pos);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
(UiState::MatchList(state), UiEvent::Select) => {
|
(UiState::MatchList(state), UiEvent::Select) => {
|
||||||
|
|
@ -168,8 +175,18 @@ impl<'a> UiState<'a> {
|
||||||
|
|
||||||
let line = state.lines[selected];
|
let line = state.lines[selected];
|
||||||
let log = &app.lines[line];
|
let log = &app.lines[line];
|
||||||
|
let raw_line = app.get_line(log.index).unwrap();
|
||||||
|
let full_line: FullLogLine = serde_json::from_str(raw_line).unwrap();
|
||||||
|
let trace_len = if let Some(exception) = &full_line.exception {
|
||||||
|
exception.stack().map(|e| 1 + e.trace.len()).sum()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
UiState::Log(LogState {
|
UiState::Log(LogState {
|
||||||
log,
|
log,
|
||||||
|
full_line,
|
||||||
|
trace_len,
|
||||||
|
table_state,
|
||||||
previous: Box::new(state.into()),
|
previous: Box::new(state.into()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue