This commit is contained in:
Jana Dönszelmann 2026-04-03 18:07:35 +02:00
parent bb8f6bedf6
commit 4a6c1020f4
No known key found for this signature in database
10 changed files with 113 additions and 34 deletions

17
Cargo.lock generated
View file

@ -228,6 +228,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
[[package]]
name = "color-ansi"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e56460573dab12b4bb6dfdb0abbb021e35d6a2bbd925acd2f5a9accff5654a8e"
[[package]]
name = "colorchoice"
version = "1.0.4"
@ -1163,6 +1169,16 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "pretty-print"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5ad516586f2191e7ce412b9b164e61ee6403638aa70e98672b978c1f448e63f"
dependencies = [
"color-ansi",
"unicode-segmentation",
]
[[package]]
name = "prettyplease"
version = "0.2.37"
@ -1430,6 +1446,7 @@ dependencies = [
"jiff",
"logparse",
"nix 0.31.1",
"pretty-print",
"ratatui",
"ratatui-themes",
"regex",

View file

@ -26,3 +26,4 @@ regex = "1"
crossterm = "*"
dumpster = "2.1"
logparse = {path = "./logparse/", version="0.2.0"}
pretty-print = "0.1"

View file

@ -46,9 +46,13 @@ pub struct Span<'a> {
}
/// Configuration options for [`into_spans`]
#[derive(Default)]
#[non_exhaustive]
pub struct Config {
/// Turn sequences of more than 1 space into exactly 1 space.
pub collapse_space: bool,
/// Pretty print: wrap at braces etc
pub pretty_print: bool,
}
pub trait IntoSpans<'a>: private::IntoSpansImpl<'a> {}

View file

@ -6,7 +6,7 @@ use crate::tui::{
LogViewer,
input::{FieldMatcher, InputTarget},
},
model::{FieldsName, LogEntry},
model::{LogEntry, SpanDescriptor},
};
mod serialize_regex {
@ -56,7 +56,7 @@ impl MatcherValue {
#[derive(Serialize, Deserialize, Debug)]
pub enum Matcher {
Field {
span: FieldsName,
span: SpanDescriptor,
name: String,
value: MatcherValue,
},

View file

@ -7,7 +7,7 @@ use crate::tui::{
input::{FieldMatcher, InputState, InputTarget},
view::LogView,
},
model::{FieldsName, LogEntry, id},
model::{LogEntry, SpanDescriptor, id},
processing::Cursor,
widgets::{fieldtree, last_error::LastError},
};
@ -54,7 +54,7 @@ impl LogViewer {
}
}
pub fn get_selected_field(&self) -> Option<(FieldsName, String, String)> {
pub fn get_selected_field(&self) -> Option<(SpanDescriptor, String, String)> {
let (span_idx, name_idx) = self.field_state.get()?;
let entry = self.selected().map(|(s, _)| s)?;

View file

@ -5,6 +5,8 @@ use crossterm::{
},
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
};
use itertools::Itertools;
use logparse::{self as lp, Config, into_spans, parse_input};
use ratatui_themes::{Color, Theme, ThemeName};
use std::{
borrow::Cow,
@ -23,10 +25,11 @@ use crate::tui::{
LogViewer,
input::{FieldMatcher, InputState, InputTarget},
},
model::SpanDescriptor,
reader::LogfileReader,
widgets::{
fieldtree::FieldTree, hyperlink::Hyperlink, items::Items, last_error::LastError,
styled::IntoStyled,
line_text::style_span, styled::IntoStyled,
},
};
use ratatui::{
@ -37,7 +40,7 @@ use ratatui::{
prelude::CrosstermBackend,
style::Style,
symbols::merge::MergeStrategy,
text::Line,
text::{Line, Span},
widgets::{
Block, Clear, List, ListItem, ListState, Padding, Paragraph, StatefulWidget, Widget, Wrap,
},
@ -64,6 +67,7 @@ const HELP_TEXT: &str = "Generic:
-> / enter / l enter nested view
f toggle show active filters
z zoom in on a selected field
targeting logs:
@ -127,6 +131,11 @@ enum Tab {
LogViewer(LogViewer),
Empty,
Help,
Zoom {
span: SpanDescriptor,
name: String,
value: String,
},
}
impl Tab {
@ -137,6 +146,11 @@ impl Tab {
(Tab::LogViewer(_), Some(path)) => format!("logs of {}", path.display()),
(Tab::LogViewer(_), None) => "logs".to_string(),
(Tab::Help, _) => "help".to_string(),
(Tab::Zoom { span, name, .. }, _) => match span {
SpanDescriptor::Main => format!("{name}"),
SpanDescriptor::Span(s) => format!("{name} in {s}"),
SpanDescriptor::Numbered(n) => format!("{name} in span #{n}"),
},
}
}
}
@ -244,6 +258,7 @@ impl App {
match self.tabs.last_mut().unwrap() {
Tab::Help => {}
Tab::Empty => {}
Tab::Zoom { .. } => {}
Tab::FileChooser {
files,
state,
@ -327,6 +342,11 @@ impl App {
KeyCode::Char('u') => lv.undo(),
KeyCode::Char('r') => lv.redo(),
KeyCode::Char('z') => {
if let Some((span, name, value)) = lv.get_selected_field() {
self.push_tab(Tab::Zoom { span, name, value });
}
}
KeyCode::Tab => {
lv.input_state.target(InputTarget::Fields(None));
@ -592,6 +612,7 @@ impl Widget for &mut App {
let (footer_focused, header_focused) = match self.current_tab() {
Tab::Help => (false, false),
Tab::Zoom { .. } => (false, false),
Tab::FileChooser { .. } => (false, true),
Tab::LogViewer(lv) => {
let target_fields =
@ -716,6 +737,42 @@ impl Widget for &mut App {
Paragraph::new(HELP_TEXT).render(popup_area, buf);
}
Tab::Zoom { value, .. } => {
Clear.render(popup_area, buf);
let popup_area = {
let block = Block::bordered()
.title_top("zoom")
.style(styles.default)
.padding(Padding::symmetric(3, 1))
.border_style(styles.border_highlighted);
let inner = block.inner(popup_area);
block.render(popup_area, buf);
inner
};
if let Ok(parsed) = parse_input(&value) {
let spans = into_spans(
parsed,
Config {
collapse_space: true,
},
);
let spans = spans
.into_iter()
.map(|lp::Span { text, kind }| {
let span = Span::from(text.into_owned());
let style = style_span(kind, styles.default, &styles);
span.style(style)
})
.collect_vec();
Paragraph::new(Line::from(spans)).render(popup_area, buf);
} else {
Paragraph::new(value.clone()).render(popup_area, buf);
}
}
}
self.last_error.clone().styled(&styles).render(error, buf);

View file

@ -396,7 +396,7 @@ unsafe impl<V: Visitor> TraceWith<V> for RawLogEntry {
}
#[derive(PartialEq, Eq, Debug, Hash, Clone, PartialOrd, Ord, Serialize, Deserialize)]
pub enum FieldsName {
pub enum SpanDescriptor {
/// The main fields (not of a span) of a log entry
Main,
Span(String),
@ -409,23 +409,23 @@ pub struct SpansRef<'a> {
}
impl<'a> SpansRef<'a> {
pub fn find(&self, span: &FieldsName, name: &str) -> Option<&String> {
pub fn find(&self, span: &SpanDescriptor, name: &str) -> Option<&String> {
self.named()
.find(|(name, _)| name == span)
.and_then(|(_, fields)| fields.get(name))
}
pub fn named(&self) -> impl Iterator<Item = (FieldsName, &'a LogFields)> {
iter::once((FieldsName::Main, self.main)).chain(self.spans.iter().rev().enumerate().map(
|(idx, fields)| {
pub fn named(&self) -> impl Iterator<Item = (SpanDescriptor, &'a LogFields)> {
iter::once((SpanDescriptor::Main, self.main)).chain(
self.spans.iter().rev().enumerate().map(|(idx, fields)| {
(
fields
.name()
.map(|i| FieldsName::Span(i.to_string()))
.unwrap_or_else(|| FieldsName::Numbered(idx)),
.map(|i| SpanDescriptor::Span(i.to_string()))
.unwrap_or_else(|| SpanDescriptor::Numbered(idx)),
fields,
)
},
))
}),
)
}
}

View file

@ -167,7 +167,7 @@ mod tests {
use crate::tui::{
filter::{Filter, FilterKind, Matcher, MatcherValue},
log_viewer::filters::Filters,
model::FieldsName,
model::SpanDescriptor,
processing::Cursor,
widgets::last_error::LastError,
};
@ -311,7 +311,7 @@ mod tests {
matcher: Matcher::Field {
name: "message".to_string(),
value: MatcherValue::Exact("foo".to_string()),
span: FieldsName::Main,
span: SpanDescriptor::Main,
},
kind: FilterKind::Remove,
}));
@ -337,7 +337,7 @@ mod tests {
matcher: Matcher::Field {
name: "message".to_string(),
value: MatcherValue::Exact("baz".to_string()),
span: FieldsName::Main,
span: SpanDescriptor::Main,
},
kind: FilterKind::Remove,
}));
@ -368,7 +368,7 @@ mod tests {
matcher: Matcher::Field {
name: "name".to_string(),
value: MatcherValue::Exact("nest".to_string()),
span: FieldsName::Main,
span: SpanDescriptor::Main,
},
kind: FilterKind::Inline,
}));
@ -415,7 +415,7 @@ mod tests {
matcher: Matcher::Field {
name: "name".to_string(),
value: MatcherValue::Exact("nest".to_string()),
span: FieldsName::Main,
span: SpanDescriptor::Main,
},
kind: FilterKind::Inline,
}));
@ -465,7 +465,7 @@ mod tests {
matcher: Matcher::Field {
name: "name".to_string(),
value: MatcherValue::Exact("nest1".to_string()),
span: FieldsName::Main,
span: SpanDescriptor::Main,
},
kind: FilterKind::Inline,
}));
@ -473,7 +473,7 @@ mod tests {
matcher: Matcher::Field {
name: "name".to_string(),
value: MatcherValue::Exact("nest2".to_string()),
span: FieldsName::Main,
span: SpanDescriptor::Main,
},
kind: FilterKind::Inline,
}));

View file

@ -15,7 +15,7 @@ use tui_widget_list::{ListBuilder, ListState, ListView};
use crate::tui::{
block_around,
log_viewer::LogViewer,
model::{FieldsName, LogEntry, LogFields},
model::{SpanDescriptor, LogEntry, LogFields},
widgets::{line_text::style_span, styled::Styled},
};
@ -91,7 +91,7 @@ impl<'a> FieldTree<'a> {
self.lv.selected().map(|(s, _)| s)
}
fn current_span_fields(&mut self, spans: &[(FieldsName, &LogFields)]) -> Vec<(String, String)> {
fn current_span_fields(&mut self, spans: &[(SpanDescriptor, &LogFields)]) -> Vec<(String, String)> {
let mut selected = self.state().current_span.selected.unwrap_or(0);
if selected > spans.len() {
selected = spans.len().saturating_sub(1);
@ -120,7 +120,7 @@ impl<'a> FieldTree<'a> {
}
impl Styled<'_, &mut FieldTree<'_>> {
fn areas(&self, area: Rect, buf: &mut Buffer, name: Option<&FieldsName>) -> (Rect, Rect, Rect) {
fn areas(&self, area: Rect, buf: &mut Buffer, name: Option<&SpanDescriptor>) -> (Rect, Rect, Rect) {
let [spans_area, fields_area] = Layout::horizontal([
Constraint::Length(if self.spans_focussed() || !self.focussed {
25
@ -146,9 +146,9 @@ impl Styled<'_, &mut FieldTree<'_>> {
self.styles,
Some(if let Some(i) = name {
match i {
FieldsName::Main => "own fields".to_string(),
FieldsName::Span(s) => format!("fields of {s}"),
FieldsName::Numbered(idx) => format!("fields of span #{idx}"),
SpanDescriptor::Main => "own fields".to_string(),
SpanDescriptor::Span(s) => format!("fields of {s}"),
SpanDescriptor::Numbered(idx) => format!("fields of span #{idx}"),
}
} else {
"fields".to_string()
@ -191,9 +191,9 @@ impl Widget for Styled<'_, &mut FieldTree<'_>> {
};
let mut item = Line::from(match span_name {
FieldsName::Main => "own fields".to_string(),
FieldsName::Span(s) => s.to_string(),
FieldsName::Numbered(idx) => format!("span #{idx}"),
SpanDescriptor::Main => "own fields".to_string(),
SpanDescriptor::Span(s) => s.to_string(),
SpanDescriptor::Numbered(idx) => format!("span #{idx}"),
});
if cx.is_selected && self_focussed {
item = item.style(self.styles.highlighted);

View file

@ -9,7 +9,7 @@ use crate::tui::{
filters::Filters,
input::{FieldMatcher, InputState, InputTarget},
},
model::{FieldsName, LogEntry},
model::{SpanDescriptor, LogEntry},
widgets::{
last_error::LastError,
line_text::Highlighted,
@ -23,7 +23,7 @@ pub struct Items<'a> {
input_state: &'a InputState,
filters: &'a Filters,
selected_footer_field: Option<(FieldsName, String, String)>,
selected_footer_field: Option<(SpanDescriptor, String, String)>,
last_error: LastError,
}
@ -33,7 +33,7 @@ impl<'a> Items<'a> {
filters: &'a Filters,
selected_offset: usize,
input_state: &'a InputState,
selected_footer_field: Option<(FieldsName, String, String)>,
selected_footer_field: Option<(SpanDescriptor, String, String)>,
last_error: LastError,
) -> Self {
Self {