Skip to main content

journal_engine/logs/
table.rs

1use super::query::LogEntryData;
2use journal_core::Result;
3use std::collections::HashMap;
4use std::fmt;
5
6/// A cell value with both raw and display representations
7#[derive(Debug, Clone)]
8pub struct CellValue {
9    pub raw: Option<String>,
10    pub display: Option<String>,
11}
12
13impl CellValue {
14    /// Create a new cell value with no transformation
15    pub fn new(value: Option<String>) -> Self {
16        Self {
17            raw: value.clone(),
18            display: value,
19        }
20    }
21
22    /// Create a new cell value with separate raw and display representations
23    pub fn with_display(raw: Option<String>, display: Option<String>) -> Self {
24        Self { raw, display }
25    }
26}
27
28/// Column metadata for a table, compatible with the JSON response format
29#[derive(Debug, Clone)]
30pub struct ColumnInfo {
31    pub name: String,
32    pub index: usize,
33}
34
35impl ColumnInfo {
36    pub fn new(name: String, index: usize) -> Self {
37        Self { name, index }
38    }
39}
40
41/// A table representation of log entries with extracted field values
42#[derive(Debug, Clone)]
43pub struct Table {
44    pub columns: Vec<ColumnInfo>,
45    pub data: Vec<Vec<CellValue>>,
46}
47
48impl Table {
49    /// Create a new empty table with the given column names
50    pub fn new(column_names: Vec<String>) -> Self {
51        let columns = column_names
52            .into_iter()
53            .enumerate()
54            .map(|(index, name)| ColumnInfo::new(name, index))
55            .collect();
56
57        Self {
58            columns,
59            data: Vec::new(),
60        }
61    }
62
63    /// Add a row to the table
64    pub fn add_row(&mut self, row: Vec<CellValue>) {
65        self.data.push(row);
66    }
67
68    /// Get the number of rows in the table
69    pub fn row_count(&self) -> usize {
70        self.data.len()
71    }
72
73    /// Get the number of columns in the table
74    pub fn column_count(&self) -> usize {
75        self.columns.len()
76    }
77
78    /// Get the column metadata
79    pub fn columns(&self) -> &[ColumnInfo] {
80        &self.columns
81    }
82
83    /// Get the table rows
84    pub fn rows(&self) -> &[Vec<CellValue>] {
85        &self.data
86    }
87
88    /// Calculate the optimal column widths for display
89    fn calculate_column_widths(&self) -> Vec<usize> {
90        const MESSAGE_MAX_WIDTH: usize = 80;
91
92        let mut widths: Vec<usize> = self.columns.iter().map(|col| col.name.len()).collect();
93
94        // Check each row to find the maximum width needed for each column
95        for row in &self.data {
96            for (col_idx, cell) in row.iter().enumerate() {
97                let display_len = cell.display.as_deref().unwrap_or("-").len();
98                if display_len > widths[col_idx] {
99                    widths[col_idx] = display_len;
100                }
101            }
102        }
103
104        // Cap the MESSAGE column width at MESSAGE_MAX_WIDTH
105        for (col_idx, col) in self.columns.iter().enumerate() {
106            if col.name == "MESSAGE" && widths[col_idx] > MESSAGE_MAX_WIDTH {
107                widths[col_idx] = MESSAGE_MAX_WIDTH;
108            }
109        }
110
111        widths
112    }
113}
114
115impl fmt::Display for Table {
116    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117        if self.columns.is_empty() {
118            return writeln!(f, "(empty table)");
119        }
120
121        let widths = self.calculate_column_widths();
122        write_table_border(f, &widths)?;
123        write_table_header(f, &self.columns, &widths)?;
124        write_table_border(f, &widths)?;
125        write_table_rows(f, &self.data, &widths)?;
126        write_table_border(f, &widths)?;
127        Ok(())
128    }
129}
130
131fn table_total_width(widths: &[usize]) -> usize {
132    widths.iter().sum::<usize>() + (widths.len() - 1) * 3 + 2
133}
134
135fn write_table_border(f: &mut fmt::Formatter<'_>, widths: &[usize]) -> fmt::Result {
136    writeln!(f, "{}", "=".repeat(table_total_width(widths)))
137}
138
139fn write_table_header(
140    f: &mut fmt::Formatter<'_>,
141    columns: &[ColumnInfo],
142    widths: &[usize],
143) -> fmt::Result {
144    write!(f, "|")?;
145    for (col, width) in columns.iter().zip(widths) {
146        write!(f, " {:<width$} |", col.name, width = width)?;
147    }
148    writeln!(f)
149}
150
151fn write_table_rows(
152    f: &mut fmt::Formatter<'_>,
153    rows: &[Vec<CellValue>],
154    widths: &[usize],
155) -> fmt::Result {
156    for row in rows {
157        write!(f, "|")?;
158        for (cell, width) in row.iter().zip(widths) {
159            write_table_cell(f, cell, *width)?;
160        }
161        writeln!(f)?;
162    }
163    Ok(())
164}
165
166fn write_table_cell(f: &mut fmt::Formatter<'_>, cell: &CellValue, width: usize) -> fmt::Result {
167    let display = cell.display.as_deref().unwrap_or("-");
168    if display.len() > width {
169        write!(f, " {:<width$} |", &display[..width], width = width)
170    } else {
171        write!(f, " {:<width$} |", display, width = width)
172    }
173}
174
175/// Converts extracted entry data into a table with specified columns.
176///
177/// This function takes raw field data and builds a table structure.
178/// It only extracts fields that are in the requested columns list.
179///
180/// # Arguments
181///
182/// * `entry_data` - Vector of extracted entry data
183/// * `column_names` - Names of fields to include (timestamp is always prepended)
184///
185/// # Returns
186///
187/// A `Table` containing the raw field values
188pub fn entry_data_to_table(
189    entry_data: &[LogEntryData],
190    column_names: Vec<String>,
191) -> Result<Table> {
192    // Always prepend "timestamp" as the first column
193    let mut all_columns = vec!["timestamp".to_string()];
194    all_columns.extend(column_names.clone());
195
196    let mut table = Table::new(all_columns);
197
198    // Create a mapping from column name to index for fast lookup
199    let column_map: HashMap<&str, usize> = column_names
200        .iter()
201        .enumerate()
202        .map(|(idx, name)| (name.as_str(), idx + 1)) // +1 because timestamp is at index 0
203        .collect();
204
205    // Process each entry
206    for data in entry_data {
207        let num_cols = column_names.len() + 1;
208        let mut row = vec![CellValue::new(None); num_cols];
209
210        // First column: timestamp
211        row[0] = CellValue::new(Some(data.timestamp.to_string()));
212
213        // Extract requested fields
214        for pair in &data.fields {
215            if let Some(&col_idx) = column_map.get(pair.field()) {
216                row[col_idx] = CellValue::new(Some(pair.value().to_string()));
217            }
218        }
219
220        table.add_row(row);
221    }
222
223    Ok(table)
224}