Skip to content

Commit 8e41588

Browse files
committed
feat: add log level conversion
1 parent d4b3817 commit 8e41588

File tree

3 files changed

+95
-8
lines changed

3 files changed

+95
-8
lines changed

README.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
Simple log viewer.
44
Works with log that have json on each line.
55
Works in both Web (WASM) and on native.
6+
Defaults are configured for [bunyan formatted logs](https://github.com/trentm/node-bunyan) but these are setup as setting and are able to be changed (UI not implemented to change at present due to no demand).
7+
For rust programs one can use the crate [tracing_bunyan_formatter](https://docs.rs/tracing-bunyan-formatter/latest/tracing_bunyan_formatter/) to generate logs as done in the book [Zero To Production In Rust](https://www.zero2prod.com/index.html).
68

79
You can use the deployed version without install at <https://c-git.github.io/log-viewer/>
810

911
# Description of expected log file format
1012

1113
It is expected that the file will contain multiple json objects separated by new lines.
14+
If a line is not valid json it can be converted into a Log record with the entire line being one json field.
1215
See [samples](tests/sample_logs/).
1316
A toy example would be:
1417

@@ -20,7 +23,8 @@ A toy example would be:
2023

2124
# How to run
2225

23-
Make sure you are using the latest version of stable rust by running `rustup update`.
26+
We do not test on older version of rust so it is possible you may get compilation errors if you are not on the latest version of stable rust.
27+
To get the most recent stable version use `rustup update`.
2428

2529
## Native
2630

src/app/data.rs

+43-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use serde_json::Value;
1111

1212
use super::{
1313
calculate_hash,
14-
data_display_options::{DataDisplayOptions, RowParseErrorHandling},
14+
data_display_options::{DataDisplayOptions, LevelConversion, RowParseErrorHandling},
1515
};
1616
mod data_iter;
1717
pub mod filter;
@@ -393,10 +393,52 @@ impl TryFrom<(&DataDisplayOptions, usize, &str)> for LogRow {
393393
if let Some(key) = data_display_options.row_idx_field_name.as_ref() {
394394
result.or_insert(key.to_string(), row_idx_val.into());
395395
}
396+
if let Some(settings) = data_display_options.level_conversion.as_ref() {
397+
if let Some((key, value)) = level_conversion_to_display(&result, settings) {
398+
result.or_insert(key, value);
399+
}
400+
}
396401
Ok(result)
397402
}
398403
}
399404

405+
fn level_conversion_to_display(
406+
row: &LogRow,
407+
settings: &LevelConversion,
408+
) -> Option<(String, Value)> {
409+
let FieldContent::Present(raw_value) = row.field_value(&settings.source_field_name) else {
410+
return None;
411+
};
412+
let raw_value = match raw_value.as_i64() {
413+
Some(x) => x,
414+
None => {
415+
warn!(
416+
"Failed to convert raw for {:?} to i64: {raw_value:?}",
417+
settings.source_field_name
418+
);
419+
debug_assert!(
420+
false,
421+
"This is not expected to happen. Unable to convert level to string slice"
422+
);
423+
return None;
424+
}
425+
};
426+
match settings.convert_map.get(&raw_value) {
427+
Some(converted_value) => Some((
428+
settings.display_field_name.clone(),
429+
converted_value.clone().into(),
430+
)),
431+
None => {
432+
warn!("Failed to convert raw_value to a displayable log level: {raw_value:?}");
433+
debug_assert!(
434+
false,
435+
"This is not expected to happen. Unable to convert level to a corresponding display value"
436+
);
437+
None
438+
}
439+
}
440+
}
441+
400442
impl TryFrom<(&DataDisplayOptions, &str)> for Data {
401443
type Error = anyhow::Error;
402444

src/app/data_display_options.rs

+47-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::collections::BTreeSet;
1+
use std::collections::{BTreeMap, BTreeSet};
22

33
#[derive(serde::Deserialize, serde::Serialize, Debug, PartialEq, Eq)]
44
#[serde(default)] // if we add new fields, give them default values when deserializing old state
@@ -13,11 +13,14 @@ pub struct DataDisplayOptions {
1313
/// WARNING: This must be a valid index into the list as this is assumed in method implementations
1414
emphasize_if_matching_field_idx: Option<usize>,
1515

16-
/// When set adds a field with this name and populates it with the row numbers
16+
/// When set adds a field with this name and populates it with the row numbers (Skips record if field name already exists)
1717
pub row_idx_field_name: Option<String>,
1818

1919
/// Controls how errors during file loading are treated
2020
pub row_parse_error_handling: RowParseErrorHandling,
21+
22+
/// Used for optionally converting message levels to strings
23+
pub level_conversion: Option<LevelConversion>,
2124
}
2225

2326
#[derive(serde::Deserialize, serde::Serialize, Debug, PartialEq, Eq)]
@@ -30,6 +33,15 @@ pub enum RowParseErrorHandling {
3033
},
3134
}
3235

36+
#[derive(serde::Deserialize, serde::Serialize, Debug, PartialEq, Eq)]
37+
pub struct LevelConversion {
38+
/// Skips record if field name already exists
39+
pub display_field_name: String,
40+
/// Skips conversion if source field cannot be found
41+
pub source_field_name: String,
42+
pub convert_map: BTreeMap<i64, String>,
43+
}
44+
3345
impl DataDisplayOptions {
3446
pub fn main_list_fields(&self) -> &[String] {
3547
&self.main_list_fields
@@ -46,10 +58,17 @@ impl Default for DataDisplayOptions {
4658
fn default() -> Self {
4759
Self {
4860
// TODO 3: Add ability to show, select and reorder selected fields
49-
main_list_fields: ["row#", "time", "request_id", "otel.name", "msg"]
50-
.into_iter()
51-
.map(String::from)
52-
.collect(),
61+
main_list_fields: [
62+
"row#",
63+
"level_str",
64+
"time",
65+
"request_id",
66+
"otel.name",
67+
"msg",
68+
]
69+
.into_iter()
70+
.map(String::from)
71+
.collect(),
5372
common_fields: [
5473
"elapsed_milliseconds",
5574
"file",
@@ -79,6 +98,7 @@ impl Default for DataDisplayOptions {
7998
emphasize_if_matching_field_idx: Some(2),
8099
row_idx_field_name: Some("row#".to_string()),
81100
row_parse_error_handling: Default::default(),
101+
level_conversion: Some(Default::default()),
82102
}
83103
}
84104
}
@@ -91,3 +111,24 @@ impl Default for RowParseErrorHandling {
91111
}
92112
}
93113
}
114+
115+
impl Default for LevelConversion {
116+
fn default() -> Self {
117+
// See bunyan levels https://github.com/trentm/node-bunyan?tab=readme-ov-file#levels and note rust only goes up to Error
118+
let convert_map = vec![
119+
(60, "Fatal".to_string()),
120+
(50, "Error".to_string()),
121+
(40, "Warn".to_string()),
122+
(30, "Info".to_string()),
123+
(20, "Debug".to_string()),
124+
(10, "Trace".to_string()),
125+
]
126+
.into_iter()
127+
.collect();
128+
Self {
129+
display_field_name: "level_str".into(),
130+
source_field_name: "level".into(),
131+
convert_map,
132+
}
133+
}
134+
}

0 commit comments

Comments
 (0)