diff --git a/Cargo.lock b/Cargo.lock index 003eadf4..05e4937a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -601,7 +601,7 @@ checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" [[package]] name = "encstr" -version = "0.29.0-alpha.12" +version = "0.29.0-alpha.13" [[package]] name = "enum-map" @@ -758,7 +758,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hl" -version = "0.29.0-alpha.12" +version = "0.29.0-alpha.13" dependencies = [ "atoi", "bincode", diff --git a/Cargo.toml b/Cargo.toml index 6df5c058..17327051 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ members = [".", "crate/encstr"] [workspace.package] repository = "https://github.com/pamburus/hl" authors = ["Pavel Ivanov "] -version = "0.29.0-alpha.12" +version = "0.29.0-alpha.13" edition = "2021" license = "MIT" diff --git a/etc/defaults/config.yaml b/etc/defaults/config.yaml index 81936e4f..2d60ce5b 100644 --- a/etc/defaults/config.yaml +++ b/etc/defaults/config.yaml @@ -10,6 +10,7 @@ fields: # Configuration of the predefined set of fields. predefined: time: + show: auto names: [ ts, @@ -24,6 +25,7 @@ fields: logger: names: [logger, LOGGER, Logger] level: + show: auto variants: - names: [level, LEVEL, Level] values: diff --git a/src/app.rs b/src/app.rs index ec3907aa..aa69f7de 100644 --- a/src/app.rs +++ b/src/app.rs @@ -37,7 +37,7 @@ use crate::{ model::{Filter, Parser, ParserSettings, RawRecord, Record, RecordFilter, RecordWithSourceConstructor}, query::Query, scanning::{BufFactory, Delimit, Delimiter, Scanner, SearchExt, Segment, SegmentBuf, SegmentBufFactory}, - settings::{Fields, Formatting}, + settings::{FieldShowOption, Fields, Formatting}, theme::{Element, StylingPush, Theme}, timezone::Tz, IncludeExcludeKeyFilter, @@ -670,7 +670,9 @@ impl App { self.options.formatting.clone(), ) .with_field_unescaping(!self.options.raw_fields) - .with_flatten(self.options.flatten), + .with_flatten(self.options.flatten) + .with_always_show_time(self.options.fields.settings.predefined.time.show == FieldShowOption::Always) + .with_always_show_level(self.options.fields.settings.predefined.level.show == FieldShowOption::Always), ) } } diff --git a/src/formatting.rs b/src/formatting.rs index 17ef7a27..b999ab27 100644 --- a/src/formatting.rs +++ b/src/formatting.rs @@ -58,6 +58,8 @@ pub struct RecordFormatter { ts_width: usize, hide_empty_fields: bool, flatten: bool, + always_show_time: bool, + always_show_level: bool, fields: Arc, cfg: Formatting, } @@ -78,6 +80,8 @@ impl RecordFormatter { ts_width, hide_empty_fields, flatten: false, + always_show_time: false, + always_show_level: false, fields, cfg, } @@ -93,6 +97,16 @@ impl RecordFormatter { self } + pub fn with_always_show_time(mut self, value: bool) -> Self { + self.always_show_time = value; + self + } + + pub fn with_always_show_level(mut self, value: bool) -> Self { + self.always_show_level = value; + self + } + pub fn format_record(&self, buf: &mut Buf, rec: &model::Record) { let mut fs = FormattingState::new(self.flatten && self.unescape_fields); @@ -100,9 +114,10 @@ impl RecordFormatter { // // time // - s.element(Element::Time, |s| { - s.batch(|buf| { - if let Some(ts) = &rec.ts { + if let Some(ts) = &rec.ts { + fs.add_element(|| {}); + s.element(Element::Time, |s| { + s.batch(|buf| { aligned_left(buf, self.ts_width, b' ', |mut buf| { if ts .as_rfc3339() @@ -116,39 +131,46 @@ impl RecordFormatter { } } }); - } else { - centered(buf, self.ts_width, b' ', |mut buf| { - buf.extend_from_slice(b"---"); + }) + }); + } else if self.always_show_time { + fs.add_element(|| {}); + s.element(Element::Time, |s| { + s.batch(|buf| { + centered(buf, self.ts_width, b'-', |mut buf| { + buf.extend_from_slice(b"-"); }); - } - }) - }); + }) + }); + } + // // level // - s.space(); - s.element(Element::Level, |s| { - s.batch(|buf| { - buf.extend_from_slice(self.cfg.punctuation.level_left_separator.as_bytes()); - }); - s.element(Element::LevelInner, |s| { + let level = match rec.level { + Some(Level::Debug) => Some(b"DBG"), + Some(Level::Info) => Some(b"INF"), + Some(Level::Warning) => Some(b"WRN"), + Some(Level::Error) => Some(b"ERR"), + _ => None, + }; + let level = level.or_else(|| self.always_show_level.then(|| b"(?)")); + if let Some(level) = level { + fs.add_element(|| s.space()); + s.element(Element::Level, |s| { s.batch(|buf| { - buf.extend_from_slice(match rec.level { - Some(Level::Debug) => b"DBG", - Some(Level::Info) => b"INF", - Some(Level::Warning) => b"WRN", - Some(Level::Error) => b"ERR", - _ => b"(?)", - }) - }) + buf.extend_from_slice(self.cfg.punctuation.level_left_separator.as_bytes()); + }); + s.element(Element::LevelInner, |s| s.batch(|buf| buf.extend_from_slice(level))); + s.batch(|buf| buf.extend_from_slice(self.cfg.punctuation.level_right_separator.as_bytes())); }); - s.batch(|buf| buf.extend_from_slice(self.cfg.punctuation.level_right_separator.as_bytes())); - }); + } + // // logger // if let Some(logger) = rec.logger { - s.batch(|buf| buf.push(b' ')); + fs.add_element(|| s.batch(|buf| buf.push(b' '))); s.element(Element::Logger, |s| { s.element(Element::LoggerInner, |s| { s.batch(|buf| buf.extend_from_slice(logger.as_bytes())) @@ -226,8 +248,10 @@ impl RecordFormatter { match value { RawValue::String(value) => { if !value.is_empty() { - s.reset(); - s.space(); + fs.add_element(|| { + s.reset(); + s.space(); + }); s.element(Element::Message, |s| { s.batch(|buf| buf.with_auto_trim(|buf| MessageFormatAuto::new(value).format(buf).unwrap())) }); @@ -251,6 +275,7 @@ impl RecordWithSourceFormatter for RecordFormatter { struct FormattingState { key_prefix: KeyPrefix, flatten: bool, + empty: bool, } impl FormattingState { @@ -259,6 +284,15 @@ impl FormattingState { Self { key_prefix: KeyPrefix::default(), flatten, + empty: true, + } + } + + fn add_element(&mut self, add_space: impl FnOnce()) { + if self.empty { + self.empty = false; + } else { + add_space(); } } } @@ -438,7 +472,7 @@ impl<'a> FieldFormatter<'a> { let variant = FormattedFieldVariant::Normal { flatten: fs.flatten }; - s.space(); + fs.add_element(|| s.space()); s.element(Element::Key, |s| { s.batch(|buf| { if fs.flatten { @@ -922,4 +956,51 @@ mod tests { "\u{1b}[0;2;3m00-01-02 03:04:05.123 \u{1b}[0;36m|\u{1b}[0;95mDBG\u{1b}[0;36m|\u{1b}[0;2;3m \u{1b}[0;2;4mtl:\u{1b}[0m \u{1b}[0;1;39mtm \u{1b}[0;32mk-a.va.kb\u{1b}[0;2m=\u{1b}[0;94m42 \u{1b}[0;32mk-a.va.kc\u{1b}[0;2m=\u{1b}[0;94m43\u{1b}[0;2;3m @ tc\u{1b}[0m", ); } + + #[test] + fn test_timestamp_none() { + let rec = Record { + message: Some(RawValue::String(EncodedString::json(r#""tm""#))), + level: Some(Level::Error), + ..Default::default() + }; + + assert_eq!(&format(&rec), "\u{1b}[0;7;91m|ERR|\u{1b}[0m \u{1b}[0;1;39mtm\u{1b}[0m"); + } + + #[test] + fn test_timestamp_none_always_show() { + let rec = Record { + message: Some(RawValue::String(EncodedString::json(r#""tm""#))), + ..Default::default() + }; + + assert_eq!( + &formatter().with_always_show_time(true).format_to_string(&rec), + "\u{1b}[0;2;3m---------------------\u{1b}[0m \u{1b}[0;1;39mtm\u{1b}[0m", + ); + } + + #[test] + fn test_level_none() { + let rec = Record { + message: Some(RawValue::String(EncodedString::json(r#""tm""#))), + ..Default::default() + }; + + assert_eq!(&format(&rec), "\u{1b}[0;1;39mtm\u{1b}[0m",); + } + + #[test] + fn test_level_none_always_show() { + let rec = Record { + message: Some(RawValue::String(EncodedString::json(r#""tm""#))), + ..Default::default() + }; + + assert_eq!( + &formatter().with_always_show_level(true).format_to_string(&rec), + "\u{1b}[0;36m|(?)|\u{1b}[0m \u{1b}[0;1;39mtm\u{1b}[0m", + ); + } } diff --git a/src/model.rs b/src/model.rs index 5cfb2c9c..23cb151f 100644 --- a/src/model.rs +++ b/src/model.rs @@ -289,6 +289,7 @@ impl Eq for RawArray<'_> {} // --- +#[derive(Default)] pub struct Record<'a> { pub ts: Option>, pub message: Option>, @@ -335,6 +336,7 @@ impl<'a> Record<'a> { } } +#[derive(Default)] pub struct RecordFields<'a> { pub(crate) head: heapless::Vec<(&'a str, RawValue<'a>), RECORD_EXTRA_CAPACITY>, pub(crate) tail: Vec<(&'a str, RawValue<'a>)>, diff --git a/src/settings.rs b/src/settings.rs index 5cdd284f..8b9c1c35 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -94,9 +94,7 @@ pub struct TimeField(pub Field); impl Default for TimeField { fn default() -> Self { - Self(Field { - names: vec!["time".into(), "ts".into()], - }) + Self(Field::new(vec!["time".into(), "ts".into()])) } } @@ -104,12 +102,14 @@ impl Default for TimeField { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct LevelField { + pub show: FieldShowOption, pub variants: Vec, } impl Default for LevelField { fn default() -> Self { Self { + show: FieldShowOption::default(), variants: vec![LevelFieldVariant { names: vec!["level".into()], values: Level::iter() @@ -138,9 +138,7 @@ pub struct MessageField(Field); impl Default for MessageField { fn default() -> Self { - Self(Field { - names: vec!["msg".into()], - }) + Self(Field::new(vec!["msg".into()])) } } @@ -151,9 +149,7 @@ pub struct LoggerField(Field); impl Default for LoggerField { fn default() -> Self { - Self(Field { - names: vec!["logger".into()], - }) + Self(Field::new(vec!["logger".into()])) } } @@ -164,9 +160,7 @@ pub struct CallerField(Field); impl Default for CallerField { fn default() -> Self { - Self(Field { - names: vec!["caller".into()], - }) + Self(Field::new(vec!["caller".into()])) } } @@ -177,9 +171,7 @@ pub struct CallerFileField(Field); impl Default for CallerFileField { fn default() -> Self { - Self(Field { - names: vec!["file".into()], - }) + Self(Field::new(vec!["file".into()])) } } @@ -190,9 +182,7 @@ pub struct CallerLineField(Field); impl Default for CallerLineField { fn default() -> Self { - Self(Field { - names: vec!["line".into()], - }) + Self(Field::new(vec!["line".into()])) } } @@ -201,6 +191,17 @@ impl Default for CallerLineField { #[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] pub struct Field { pub names: Vec, + #[serde(default)] + pub show: FieldShowOption, +} + +impl Field { + pub fn new(names: Vec) -> Self { + Self { + names, + show: FieldShowOption::Auto, + } + } } // --- @@ -223,6 +224,21 @@ pub enum FlattenOption { // --- +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +#[serde(rename_all = "kebab-case")] +pub enum FieldShowOption { + Auto, + Always, +} + +impl Default for FieldShowOption { + fn default() -> Self { + Self::Auto + } +} + +// --- + #[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "kebab-case")] pub struct Punctuation { @@ -326,7 +342,8 @@ mod tests { assert_eq!( settings.fields.predefined.time, TimeField(Field { - names: vec!["ts".into()] + names: vec!["ts".into()], + show: FieldShowOption::Auto, }) ); assert_eq!(settings.time_format, "%b %d %T.%3N");