show additional log data
Some checks failed
CI / build (push) Successful in 44s
CI / checks (push) Failing after 58s
CI / build-nixpkgs (push) Has been skipped

This commit is contained in:
Robin Appelman 2025-09-30 18:00:35 +02:00
commit a7a8bf60da
4 changed files with 101 additions and 9 deletions

View file

@ -1,7 +1,7 @@
use crate::app::Filter; use crate::app::Filter;
use crate::logfile::LogLineNumber; use crate::logfile::LogLineNumber;
use crate::logs::LogIndex; use crate::logs::LogIndex;
use ahash::AHasher; use ahash::{AHasher, HashMap};
use derive_more::{Display, From}; use derive_more::{Display, From};
use logsmash_data::LogLevel; use logsmash_data::LogLevel;
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
@ -247,6 +247,19 @@ pub struct FullLogLine {
pub user_agent: String, pub user_agent: String,
pub version: TinyAsciiStr<16>, pub version: TinyAsciiStr<16>,
pub exception: Option<FullException>, pub exception: Option<FullException>,
#[serde(default)]
pub data: HashMap<String, String>,
}
impl FullLogLine {
pub fn has_data(&self) -> bool {
self.data.keys().any(|key| key.as_str() != "app")
}
pub fn data(&self) -> impl Iterator<Item = (&str, &str)> {
self.data.iter().map(|(key, value)| (key.as_str(), value.as_str()))
.filter(|(key, _)| *key != "app")
}
} }
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]

View file

@ -2,7 +2,7 @@ use crate::logfile::logline::{format_time, FullException, FullLogLine, Trace};
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::prelude::*; use ratatui::prelude::*;
use ratatui::widgets::{Cell, Paragraph, Row, Wrap}; use ratatui::widgets::{Block, BorderType, Borders, Cell, Paragraph, Row, Wrap};
use std::fmt::Write; use std::fmt::Write;
use std::iter::once; use std::iter::once;
@ -13,6 +13,7 @@ pub fn single_log(line: &FullLogLine) -> SingleLog {
pub struct SingleLog<'a> { pub struct SingleLog<'a> {
line: &'a FullLogLine, line: &'a FullLogLine,
path_prefix_length: usize, path_prefix_length: usize,
data_length: usize,
} }
impl<'a> SingleLog<'a> { impl<'a> SingleLog<'a> {
@ -22,9 +23,11 @@ impl<'a> SingleLog<'a> {
.as_ref() .as_ref()
.map(|ex| find_path_prefix_length(ex.trace.iter().map(|t| t.file.as_str()))) .map(|ex| find_path_prefix_length(ex.trace.iter().map(|t| t.file.as_str())))
.unwrap_or_default(); .unwrap_or_default();
let data_length = line.data().count();
SingleLog { SingleLog {
line, line,
path_prefix_length, path_prefix_length,
data_length,
} }
} }
} }
@ -58,11 +61,31 @@ impl StatefulWidget for SingleLog<'_> {
par.render(layout[0], buf); par.render(layout[0], buf);
let (exception_area, mut exception_state) = if line.has_data() {
let (data_table, height) = render_data(line);
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Min(height + 2), Constraint::Percentage(100)])
.split(layout[1]);
let block = Block::new()
.borders(Borders::BOTTOM.union(Borders::TOP))
.border_type(BorderType::Thick)
.border_style(TABLE_HEADER_STYLE)
.title("Data");
let (mut data_state, exception_state) = state.split(self.data_length);
StatefulWidget::render(data_table.block(block), layout[0], buf, &mut data_state);
(layout[1], exception_state)
} else {
(layout[1], state.clone())
};
if let Some(exception) = &line.exception { if let Some(exception) = &line.exception {
if line.message.contains(&exception.message) { if line.message.contains(&exception.message) {
StatefulWidget::render( StatefulWidget::render(
render_exception(exception, self.path_prefix_length), render_exception(exception, self.path_prefix_length),
layout[1], exception_area,
buf, buf,
state, state,
); );
@ -83,17 +106,17 @@ impl StatefulWidget for SingleLog<'_> {
let ex_layout = Layout::default() let ex_layout = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints(vec![ .constraints(vec![
Constraint::Min(ex_par.line_count(layout[1].width) as u16 + 1), Constraint::Min(ex_par.line_count(exception_area.width) as u16 + 1),
Constraint::Percentage(100), Constraint::Percentage(100),
]) ])
.split(layout[1]); .split(exception_area);
ex_par.render(ex_layout[0], buf); ex_par.render(ex_layout[0], buf);
StatefulWidget::render( StatefulWidget::render(
render_exception(exception, self.path_prefix_length), render_exception(exception, self.path_prefix_length),
ex_layout[1], ex_layout[1],
buf, buf,
state, &mut exception_state,
); );
} }
} }
@ -181,3 +204,29 @@ fn find_path_prefix_length<'a, I: Iterator<Item = &'a str>>(paths: I) -> usize {
} }
0 0
} }
pub fn render_data(log: &FullLogLine) -> (ScrollbarTable, u16) {
let header = [
Text::from("Key"),
Text::from("Value"),
]
.into_iter()
.map(Cell::from)
.collect::<Row>()
.style(TABLE_HEADER_STYLE)
.height(1);
let max_key_width = log.data().map(|(key, _)| key).map(str::len).max().unwrap_or_default();
let lines =log.data().count();
let rows = log.data()
.map(|(key, value)| Row::new([Cell::new(Text::from(key)), Cell::new(Text::from(value))]));
let widths = [
Constraint::Min(max_key_width as u16),
Constraint::Percentage(100),
];
(ScrollbarTable::new(rows, widths).header(header), lines as u16 + 1)
}

View file

@ -284,8 +284,10 @@ impl<'a> DistinctLogsState<'a> {
} else { } else {
0 0
}; };
let data_len = full_line.data().count();
let table_state = ScrollbarTableState::new(trace_len); let table_state = ScrollbarTableState::new(trace_len + data_len);
UiState::Log(LogState { UiState::Log(LogState {
log, log,
full_line: Box::new(full_line), full_line: Box::new(full_line),

View file

@ -35,13 +35,22 @@ impl<'a> ScrollbarTable<'a> {
self.table = self.table.header(header); self.table = self.table.header(header);
self self
} }
#[must_use = "method moves the value of self and returns the modified value"]
pub fn block(self, block: Block<'a>) -> Self {
ScrollbarTable {
table: self.table.block(block),
scrollbar: self.scrollbar,
}
}
} }
#[derive(Clone)] #[derive(Clone, Debug)]
pub struct ScrollbarTableState { pub struct ScrollbarTableState {
count: usize, count: usize,
table: TableState, table: TableState,
scrollbar: ScrollbarState, scrollbar: ScrollbarState,
active: bool,
} }
impl ScrollbarTableState { impl ScrollbarTableState {
@ -52,6 +61,7 @@ impl ScrollbarTableState {
count, count,
table, table,
scrollbar: ScrollbarState::new(count), scrollbar: ScrollbarState::new(count),
active: true,
} }
} }
@ -113,13 +123,31 @@ impl ScrollbarTableState {
self.table.select(Some(selected)); self.table.select(Some(selected));
self.scrollbar = self.scrollbar.position(selected); self.scrollbar = self.scrollbar.position(selected);
} }
pub fn split(&self, len: usize) -> (Self, Self) {
let mut a = ScrollbarTableState::new(len);
let mut b = ScrollbarTableState::new(self.count - len);
if self.selected() < len {
a.select(self.selected());
b.active = false;
} else {
a.active = false;
b.select(self.selected() - len);
}
(a, b)
}
} }
impl StatefulWidget for ScrollbarTable<'_> { impl StatefulWidget for ScrollbarTable<'_> {
type State = ScrollbarTableState; type State = ScrollbarTableState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
StatefulWidget::render(self.table, area, buf, &mut state.table); if state.active {
StatefulWidget::render(self.table, area, buf, &mut state.table);
} else {
let mut table_state = TableState::default();
StatefulWidget::render(self.table, area, buf, &mut table_state);
}
StatefulWidget::render( StatefulWidget::render(
self.scrollbar, self.scrollbar,
area.inner(Margin { area.inner(Margin {