binocular/preview/structured_log/
mod.rs1pub mod actions;
2mod detect;
3mod filter;
4mod format;
5mod parse;
6pub mod reducer;
7mod types;
8pub(crate) mod ui;
9pub mod watcher;
10
11use crate::app::{App, AppAction};
12use crate::preview::structured_log::actions::LogViewerOutcome;
13use crate::preview::{PreviewContent, PreviewSource};
14use crossterm::event::KeyEvent;
15
16pub const DEFAULT_MAX_ENTRIES: usize = 100_000;
17pub const STDIN_STREAM_PATH: &str = "<stdin>";
18pub use detect::detect_structured_log;
19pub use filter::{parse_epoch_secs, parse_filters};
20pub use format::format_entry_visible;
21pub use parse::{parse_initial, parse_line};
22pub use types::{
23 ColModal, ColumnConfig, FilterOp, LogEntry, LogFilter, LogFilterState, LogFormat, StructuredLog,
24};
25
26pub fn preview_content(log: StructuredLog) -> PreviewContent {
27 reducer::preview_content(log)
28}
29
30pub fn initialize_empty_stream(app: &mut App, path: String, format: LogFormat) {
31 app.preview_session.preview.source = Some(PreviewSource::LogStream(path));
32 app.preview_session.preview.content = Some(preview_content(StructuredLog {
33 entries: vec![],
34 total_lines: 0,
35 all_fields: vec![],
36 format,
37 }));
38}
39
40pub fn handle_input(app: &mut App, key: KeyEvent) {
41 let Some(PreviewContent::StructuredLog(lp)) = &mut app.preview_session.preview.content else {
42 return;
43 };
44
45 let Some(action) = actions::action_for_key(lp, key) else {
46 return;
47 };
48
49 match reducer::apply_action(lp, action, app.runtime.run.log) {
50 LogViewerOutcome::None => {}
51 LogViewerOutcome::ExitApp => app.apply_action(AppAction::Quit),
52 LogViewerOutcome::FocusSearch => {
53 app.ui.layout.preview_fullscreen = false;
54 app.apply_action(AppAction::FocusSearch);
55 }
56 }
57}
58
59pub fn apply_append(app: &mut App, path: &str, entries: Vec<LogEntry>, max_entries: usize) {
60 let Some(PreviewSource::LogStream(active_path)) = app.preview_session.preview.source.as_ref()
61 else {
62 return;
63 };
64
65 let is_valid_source = path == active_path
66 || app
67 .runtime
68 .run
69 .log_files
70 .iter()
71 .any(|f| f.to_str() == Some(path));
72 if !is_valid_source {
73 return;
74 }
75
76 let Some(PreviewContent::StructuredLog(lp)) = &mut app.preview_session.preview.content else {
77 return;
78 };
79
80 reducer::append_entries(lp, entries, max_entries);
81}
82
83pub fn init_visible_cols(fields: &[String], entries: &[LogEntry]) -> Vec<ColumnConfig> {
84 const WIDTH_SAMPLE: usize = 200;
85 const MAX_COL_WIDTH: usize = 40;
86 const MIN_COL_WIDTH: usize = 6;
87 const MSG_FIELDS: &[&str] = &["msg", "message", "text", "body", "log"];
88
89 let mut widths: Vec<usize> = fields
90 .iter()
91 .map(|f| f.chars().count().max(MIN_COL_WIDTH))
92 .collect();
93
94 for entry in entries.iter().take(WIDTH_SAMPLE) {
95 for (col_i, field) in fields.iter().enumerate() {
96 if let Some((_, v)) = entry.fields.iter().find(|(k, _)| k == field) {
97 let len = v.chars().count().min(MAX_COL_WIDTH);
98 if len > widths[col_i] {
99 widths[col_i] = len;
100 }
101 }
102 }
103 }
104
105 for w in &mut widths {
106 *w = (*w).min(MAX_COL_WIDTH);
107 }
108
109 for (i, field) in fields.iter().enumerate() {
110 let lower = field.to_ascii_lowercase();
111 if MSG_FIELDS.iter().any(|m| *m == lower.as_str()) {
112 widths[i] = widths[i].max(40).min(60);
113 }
114 }
115
116 fields
117 .iter()
118 .zip(widths.iter())
119 .map(|(f, &w)| ColumnConfig {
120 field: f.clone(),
121 width: w,
122 })
123 .collect()
124}
125
126pub fn update_fields(all_fields: &mut Vec<String>, entry: &LogEntry) {
127 for (k, _) in &entry.fields {
128 if !all_fields.iter().any(|f| f == k) {
129 all_fields.push(k.clone());
130 }
131 }
132}