log trace

This commit is contained in:
Robin Appelman 2024-07-28 18:32:22 +02:00
commit aff67c43dd
4 changed files with 229 additions and 11 deletions

View file

@ -1,6 +1,8 @@
use ahash::AHasher;
use logsmash_data::LogLevel;
use serde::Deserialize;
use serde_json::Value;
use std::fmt::{Display, Formatter};
use std::hash::{Hash, Hasher};
use time::OffsetDateTime;
use tinystr::TinyAsciiStr;
@ -73,3 +75,105 @@ impl Hash for Exception {
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)
}
}

View file

@ -170,8 +170,14 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
);
frame.render_widget(footer(app, page), layout[2]);
}
UiState::Log(LogState { log, .. }) => {
frame.render_widget(single_log(app, log), layout[0].union(layout[1]));
UiState::Log(LogState {
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]);
}
}

View file

@ -1,7 +1,98 @@
use crate::app::App;
use crate::logline::LogLine;
use ratatui::widgets::{Paragraph, Wrap};
use crate::logline::{FullException, FullLogLine, LogLine, Trace};
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> {
Paragraph::new(line.display()).wrap(Wrap::default())
pub fn single_log(app: &App, line: &LogLine) -> SingleLog {
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()),
])
}

View file

@ -1,6 +1,6 @@
use crate::app::{App, LogMatch};
use crate::copy_osc;
use crate::logline::LogLine;
use crate::logline::{FullLogLine, LogLine};
use derive_more::From;
use ratatui::widgets::{ScrollbarState, TableState};
use table_state::TableStateExt;
@ -56,7 +56,10 @@ impl<'a> LogsState<'a> {
#[derive(Clone)]
pub struct LogState<'a> {
pub trace_len: usize,
pub log: &'a LogLine,
pub full_line: FullLogLine,
pub table_state: TableState,
pub previous: Box<UiState<'a>>,
}
@ -84,6 +87,7 @@ impl<'a> UiState<'a> {
UiState::MatchList(state) => Some(&mut state.table_state),
UiState::Match(state) => Some(&mut state.table_state),
UiState::Logs(state) => Some(&mut state.table_state),
UiState::Log(state) => Some(&mut state.table_state),
_ => None,
}
}
@ -102,6 +106,7 @@ impl<'a> UiState<'a> {
UiState::MatchList(_) => app.match_lines(),
UiState::Match(state) => state.result.grouped.len(),
UiState::Logs(state) => state.lines.len(),
UiState::Log(state) => state.trace_len,
_ => 0,
}
}
@ -115,18 +120,20 @@ impl<'a> UiState<'a> {
let count = state.row_count(app);
if let Some(table_state) = state.table_state() {
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);
}
}
state
}
(mut state, UiEvent::Up(step)) => {
let count = state.row_count(app);
if let Some(table_state) = state.table_state() {
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);
}
}
state
}
(UiState::MatchList(state), UiEvent::Select) => {
@ -168,8 +175,18 @@ impl<'a> UiState<'a> {
let line = state.lines[selected];
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 {
log,
full_line,
trace_len,
table_state,
previous: Box::new(state.into()),
})
}