Skip to content

Commit 86e1768

Browse files
authored
Merge pull request #22 from tinythings/isbm-ui-logview-formats
Extended log view
2 parents 1e3f162 + 528788c commit 86e1768

5 files changed

Lines changed: 88 additions & 13 deletions

File tree

ljx/src/commands/view/app.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ use logjet::{LogjetReader, LogjetWriter, WriterConfig};
1212
use ratatui::Terminal;
1313
use ratatui::backend::CrosstermBackend;
1414

15-
use super::detail::{export_ndjson_objects, format_summary, parse_export_selection, render_modal_message};
15+
use super::detail::{export_ndjson_objects, extract_otlp_log_severity, format_summary, parse_export_selection, render_modal_message};
1616
use super::scan::{
1717
follow_appended_matches, open_temp_spool_pair, read_spool_record, remember_summary, scan_field_catalog, scan_matches,
1818
write_export_selection_to_temp_logjet,
1919
};
2020
use super::text::{char_count, delete_char_at, delete_char_before, insert_char_at};
21-
use super::types::{ActiveScan, DedupUpdate, ExportField, Focus, ScanUpdate, ViewApp, discover_export_format_choices};
21+
use super::types::{ActiveScan, DedupUpdate, ExportField, Focus, ListRowSummary, ScanUpdate, ViewApp, discover_export_format_choices};
2222
use crate::cli::ViewArgs;
2323
use crate::dedup::{DedupMatchMode, DedupMode};
2424
use crate::error::{Error, Result};
@@ -691,16 +691,16 @@ impl ViewApp {
691691
Ok(())
692692
}
693693

694-
pub(super) fn summary_for(&mut self, index: usize) -> Result<String> {
694+
pub(super) fn summary_for(&mut self, index: usize) -> Result<ListRowSummary> {
695695
if let Some(summary) = self.summary_cache.get(&index) {
696696
return Ok(summary.clone());
697697
}
698698

699699
let Some(scan) = &mut self.current_scan else {
700-
return Ok(String::new());
700+
return Ok(ListRowSummary { message: String::new(), severity: None });
701701
};
702702
let detail = read_spool_record(&mut scan.spool_reader, self.entries[index])?;
703-
let summary = format_summary(&detail, self.hex_payload);
703+
let summary = ListRowSummary { message: format_summary(&detail, self.hex_payload), severity: extract_otlp_log_severity(&detail.payload) };
704704
remember_summary(&mut self.summary_cache, &mut self.summary_order, index, summary.clone());
705705
Ok(summary)
706706
}

ljx/src/commands/view/detail.rs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,23 @@ pub(crate) fn extract_otlp_log_message(payload: &[u8]) -> Option<String> {
117117
None
118118
}
119119

120+
pub(crate) fn extract_otlp_log_severity(payload: &[u8]) -> Option<String> {
121+
let batch = ExportLogsServiceRequest::decode(payload).ok()?;
122+
for resource_logs in &batch.resource_logs {
123+
for scope_logs in &resource_logs.scope_logs {
124+
for log_record in &scope_logs.log_records {
125+
if !log_record.severity_text.is_empty() {
126+
return Some(log_record.severity_text.clone());
127+
}
128+
if let Some(severity) = severity_number_label(log_record.severity_number) {
129+
return Some(severity.to_string());
130+
}
131+
}
132+
}
133+
}
134+
None
135+
}
136+
120137
pub(crate) fn render_modal_message(detail: &DetailRecord, hex_payload: bool) -> String {
121138
if let Some(message) = extract_otlp_log_message(&detail.payload) {
122139
return message;
@@ -597,7 +614,7 @@ fn key_value_line(label: &str, value: String, value_style: Style) -> Line<'stati
597614
Line::from(vec![Span::styled(format!("{label:<12} "), Style::default().fg(Color::Indexed(136))), Span::styled(value, value_style)])
598615
}
599616

600-
fn severity_style(value: &str) -> Style {
617+
pub(super) fn severity_style(value: &str) -> Style {
601618
let upper = value.to_ascii_uppercase();
602619
let color = if upper.contains("ERROR") || upper.contains("ERR") || upper.contains("FATAL") {
603620
Color::LightRed
@@ -615,6 +632,10 @@ fn severity_style(value: &str) -> Style {
615632
Style::default().fg(color).add_modifier(Modifier::BOLD)
616633
}
617634

635+
pub(super) fn severity_initial(value: &str) -> String {
636+
value.chars().find(|c| c.is_ascii_alphabetic()).map(|c| c.to_ascii_uppercase().to_string()).unwrap_or_else(|| " ".to_string())
637+
}
638+
618639
pub(super) fn format_timestamp(ts_unix_ns: u64) -> String {
619640
let secs = (ts_unix_ns / 1_000_000_000) as i64;
620641
let nanos = (ts_unix_ns % 1_000_000_000) as u32;
@@ -624,6 +645,27 @@ pub(super) fn format_timestamp(ts_unix_ns: u64) -> String {
624645
}
625646
}
626647

648+
pub(super) fn format_syslog_timestamp(ts_unix_ns: u64) -> String {
649+
let secs = (ts_unix_ns / 1_000_000_000) as i64;
650+
let nanos = (ts_unix_ns % 1_000_000_000) as u32;
651+
match Utc.timestamp_opt(secs, nanos).single() {
652+
Some(ts) => ts.format("%b %e %H:%M:%S").to_string(),
653+
None => format!("{ts_unix_ns:<15}").chars().take(15).collect(),
654+
}
655+
}
656+
657+
fn severity_number_label(value: i32) -> Option<&'static str> {
658+
match value {
659+
1..=4 => Some("TRACE"),
660+
5..=8 => Some("DEBUG"),
661+
9..=12 => Some("INFO"),
662+
13..=16 => Some("WARN"),
663+
17..=20 => Some("ERROR"),
664+
21..=24 => Some("FATAL"),
665+
_ => None,
666+
}
667+
}
668+
627669
pub(super) fn fit_modal_body(message: &str, width: usize) -> (String, u16) {
628670
let wrapped = smart_wrap(message, width);
629671
let line_count = wrapped.lines().count() as u16;

ljx/src/commands/view/render.rs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ use ratatui::text::{Line, Span, Text};
55
use ratatui::widgets::{Block, BorderType, Borders, Clear, Gauge, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, Wrap};
66

77
use super::detail::{
8-
fit_line, fit_modal_body, modal_info_line, render_detail_lines, render_modal_footer, render_modal_footer_placeholder, render_modal_info_entries,
8+
fit_line, fit_modal_body, format_syslog_timestamp, modal_info_line, render_detail_lines, render_modal_footer, render_modal_footer_placeholder,
9+
render_modal_info_entries, severity_initial, severity_style,
910
};
1011
use super::text::{fit_to_width, trim_single_line};
1112
use super::types::{ExportField, Focus, ViewApp};
@@ -23,6 +24,8 @@ const EXPORT_RANGE_HELP: &str = "Range: a / all | c / current / 0 | N | N
2324
const EXPORT_ORDER_HELP: &str = "Uses the current filtered view order.";
2425
const EXPORT_FORMAT_HELP_WIDTH: u16 = 55;
2526
const EXPORT_PROMPT_MIN_WIDTH: u16 = EXPORT_FORMAT_HELP_WIDTH + 2;
27+
const LIST_TIMESTAMP_WIDTH: usize = 15;
28+
const LIST_SEVERITY_WIDTH: usize = 1;
2629

2730
impl ViewApp {
2831
pub(super) fn render(&mut self, frame: &mut Frame<'_>) {
@@ -109,7 +112,7 @@ impl ViewApp {
109112
let end = (self.list_offset + visible_rows).min(self.entries.len());
110113
let row_width = area.width.saturating_sub(1) as usize;
111114
for index in self.list_offset..end {
112-
let style = if self.tail_mode && self.tail_marker_index == Some(index) {
115+
let row_style = if self.tail_mode && self.tail_marker_index == Some(index) {
113116
Style::default().fg(Color::White).bg(Color::Red).add_modifier(Modifier::BOLD)
114117
} else if index == self.selected {
115118
if self.focus == Focus::Search {
@@ -120,8 +123,14 @@ impl ViewApp {
120123
} else {
121124
Style::default().fg(Color::White)
122125
};
123-
let summary = self.summary_for(index).unwrap_or_else(|_| "<failed to render summary>".to_string());
124-
lines.push(Line::from(Span::styled(fit_line(&summary, row_width), style)));
126+
let summary = self
127+
.summary_for(index)
128+
.unwrap_or_else(|_| super::types::ListRowSummary { message: "<failed to render summary>".to_string(), severity: None });
129+
if self.details_visible {
130+
lines.push(Line::from(Span::styled(fit_line(&summary.message, row_width), row_style)));
131+
} else {
132+
lines.push(self.render_list_table_row(index, &summary, row_width, row_style));
133+
}
125134
}
126135
}
127136

@@ -134,6 +143,24 @@ impl ViewApp {
134143
}
135144
}
136145

146+
fn render_list_table_row(&self, index: usize, summary: &super::types::ListRowSummary, row_width: usize, row_style: Style) -> Line<'static> {
147+
let timestamp = format_syslog_timestamp(self.entries[index].ts_unix_ns);
148+
let timestamp = fit_to_width(&timestamp, LIST_TIMESTAMP_WIDTH);
149+
let severity = summary.severity.as_deref().unwrap_or("");
150+
let severity_style = severity_style(severity).bg(row_style.bg.unwrap_or(Color::Reset));
151+
let severity = fit_to_width(&severity_initial(severity), LIST_SEVERITY_WIDTH);
152+
let prefix_width = LIST_TIMESTAMP_WIDTH + 1 + LIST_SEVERITY_WIDTH + 2;
153+
let message_width = row_width.saturating_sub(prefix_width);
154+
155+
Line::from(vec![
156+
Span::styled(timestamp, row_style.fg(Color::LightBlue)),
157+
Span::styled(" ", row_style),
158+
Span::styled(severity, severity_style),
159+
Span::styled(" ", row_style),
160+
Span::styled(fit_line(&summary.message, message_width), row_style),
161+
])
162+
}
163+
137164
fn render_details(&self, frame: &mut Frame<'_>, area: Rect) {
138165
let block = pane_block(" Info ", false);
139166
let inner = block.inner(area);

ljx/src/commands/view/scan.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use opentelemetry_proto::tonic::common::v1::AnyValue;
1515
use opentelemetry_proto::tonic::common::v1::any_value::Value;
1616
use prost::Message;
1717

18-
use super::types::{ActiveScan, DetailRecord, EntryMeta, FieldCatalog, SCAN_BATCH_SIZE, ScanUpdate};
18+
use super::types::{ActiveScan, DetailRecord, EntryMeta, FieldCatalog, ListRowSummary, SCAN_BATCH_SIZE, ScanUpdate};
1919
use crate::error::{Error, Result};
2020
use crate::input::InputHandle;
2121

@@ -164,7 +164,7 @@ pub(crate) fn open_temp_spool_pair() -> Result<(PathBuf, File, File)> {
164164
Ok((spool_path, spool_reader, spool_writer))
165165
}
166166

167-
pub(super) fn remember_summary(cache: &mut HashMap<usize, String>, order: &mut VecDeque<usize>, index: usize, summary: String) {
167+
pub(super) fn remember_summary(cache: &mut HashMap<usize, ListRowSummary>, order: &mut VecDeque<usize>, index: usize, summary: ListRowSummary) {
168168
cache.insert(index, summary);
169169
order.push_back(index);
170170
while order.len() > super::types::SUMMARY_CACHE_LIMIT {

ljx/src/commands/view/types.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ pub(crate) struct DetailRecord {
5252
pub(crate) payload: Vec<u8>,
5353
}
5454

55+
#[derive(Debug, Clone)]
56+
pub(crate) struct ListRowSummary {
57+
pub(crate) message: String,
58+
pub(crate) severity: Option<String>,
59+
}
60+
5561
#[derive(Debug, Clone)]
5662
pub(crate) enum ScanUpdate {
5763
Batch(Vec<EntryMeta>),
@@ -217,7 +223,7 @@ pub(crate) struct ViewApp {
217223
pub(super) modal_info_visible: bool,
218224
pub(super) details_visible: bool,
219225
pub(super) detail_scroll: u16,
220-
pub(super) summary_cache: HashMap<usize, String>,
226+
pub(super) summary_cache: HashMap<usize, ListRowSummary>,
221227
pub(super) summary_order: VecDeque<usize>,
222228
pub(super) selected_detail: Option<DetailRecord>,
223229
pub(super) modal_text: Option<String>,

0 commit comments

Comments
 (0)