Skip to main content

binocular/preview/structured_log/
mod.rs

1pub 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}