diff --git a/src/config/raw.rs b/src/config/raw.rs index d9bb953a..9a8ddb25 100644 --- a/src/config/raw.rs +++ b/src/config/raw.rs @@ -476,6 +476,9 @@ appenders: path: /tmp/baz.log encoder: pattern: "%m" + color_map: + info: Blue + trace: Black root: appenders: diff --git a/src/encode/mod.rs b/src/encode/mod.rs index aa290b3c..060d0056 100644 --- a/src/encode/mod.rs +++ b/src/encode/mod.rs @@ -13,6 +13,8 @@ use std::collections::BTreeMap; #[cfg(feature = "config_parsing")] use crate::config::Deserializable; +#[cfg(feature = "file")] +use crate::file::Deserializable; #[cfg(feature = "json_encoder")] pub mod json; @@ -77,6 +79,8 @@ impl<'de> de::Deserialize<'de> for EncoderConfig { } /// A text or background color. +#[cfg_attr(feature = "config_parsing", derive(serde::Deserialize))] +#[cfg_attr(feature = "config_parsing", derive(serde::Serialize))] #[allow(missing_docs)] #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] pub enum Color { @@ -89,6 +93,11 @@ pub enum Color { Cyan, White, } +impl Default for Color { + fn default() -> Self { + Color::Black + } +} /// The style applied to text output. /// diff --git a/src/encode/pattern/mod.rs b/src/encode/pattern/mod.rs index 61f236b5..78903c3a 100644 --- a/src/encode/pattern/mod.rs +++ b/src/encode/pattern/mod.rs @@ -135,6 +135,117 @@ use crate::config::{Deserialize, Deserializers}; mod parser; +const DEFAULT_PATTERN_ENCODER: &str = "{d} {l} {t} - {m}{n}"; + +#[allow(dead_code)] +fn default_pattern() -> Option { + Some(DEFAULT_PATTERN_ENCODER.to_owned()) +} + +/// A simple color map struct +/// +/// You can use this struct to define a custom color map. +/// This can be done using a serializer (i.e. config file) or pragmatically. +/// +/// It is part of the encoder +/// +/// ```not_rust +/// pattern: "%m" +/// color_map: +/// INFO: Blue +/// TRACE: Black +/// ``` +/// +/// Or progmatically in conjuction with PatternEncoder +///! ```no_run +///! # #[cfg(all(feature = "console_appender", +///! # feature = "file_appender", +///! # feature = "pattern_encoder"))] +///! # fn f() { +///! let mut log_color_map = ColorMap::default() +///! log_color_map.info_color = Some(log4rs::encode::Color::Cyan); +///! } +///! # } +///! # fn main() {} +/// ``` +#[cfg_attr(feature = "config_parsing", derive(serde::Deserialize))] +#[cfg_attr(feature = "config_parsing", serde(deny_unknown_fields))] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ColorMap { + // in future this represent other aspects of style, like bold/italics, etc. + trace: Option, + debug: Option, + info: Option, + warn: Option, + error: Option, +} +impl Default for ColorMap { + fn default() -> Self { + ColorMap { + trace: None, + debug: None, + info: Some(Color::Blue), + warn: Some(Color::Red), + error: Some(Color::Red), + } + } +} + +impl ColorMap { + /// Clear color styling for given log sevarity + pub fn unset(&mut self, level: &Level) { + match level { + Level::Trace => { + self.trace = None; + } + Level::Debug => { + self.debug = None; + } + Level::Info => { + self.info = None; + } + Level::Warn => { + self.warn = None; + } + Level::Error => { + self.error = None; + } + } + } + + /// Set color styling for given log sevarity + pub fn set(&mut self, level: &Level, color: Color) { + match level { + Level::Trace => { + self.trace = Some(color); + } + Level::Debug => { + self.debug = Some(color); + } + Level::Info => { + self.info = Some(color); + } + Level::Warn => { + self.warn = Some(color); + } + Level::Error => { + self.error = Some(color); + } + } + } + + /// Get color styling for given log sevarity + pub fn get(&self, level: &Level) -> Option { + match level { + Level::Trace => self.trace, + Level::Debug => self.debug, + Level::Info => self.info, + Level::Warn => self.warn, + Level::Error => self.error, + } + } +} + thread_local!( /// Thread-locally cached thread ID. static TID: usize = thread_id::get() @@ -142,10 +253,13 @@ thread_local!( /// The pattern encoder's configuration. #[cfg(feature = "config_parsing")] -#[derive(Clone, Eq, PartialEq, Hash, Debug, Default, serde::Deserialize)] +#[derive(Clone, Eq, PartialEq, Debug, Default, serde::Deserialize)] #[serde(deny_unknown_fields)] pub struct PatternEncoderConfig { + #[serde(default = "default_pattern")] pattern: Option, + #[serde(default = "ColorMap::default")] + color_map: ColorMap, } fn is_char_boundary(b: u8) -> bool { @@ -310,20 +424,25 @@ enum Chunk { } impl Chunk { - fn encode(&self, w: &mut dyn encode::Write, record: &Record) -> io::Result<()> { + fn encode( + &self, + w: &mut dyn encode::Write, + record: &Record, + color_map: &ColorMap, + ) -> io::Result<()> { match *self { Chunk::Text(ref s) => w.write_all(s.as_bytes()), Chunk::Formatted { ref chunk, ref params, } => match (params.min_width, params.max_width, params.align) { - (None, None, _) => chunk.encode(w, record), + (None, None, _) => chunk.encode(w, record, color_map), (None, Some(max_width), _) => { let mut w = MaxWidthWriter { remaining: max_width, w, }; - chunk.encode(&mut w, record) + chunk.encode(&mut w, record, color_map) } (Some(min_width), None, Alignment::Left) => { let mut w = LeftAlignWriter { @@ -331,7 +450,7 @@ impl Chunk { fill: params.fill, w, }; - chunk.encode(&mut w, record)?; + chunk.encode(&mut w, record, color_map)?; w.finish() } (Some(min_width), None, Alignment::Right) => { @@ -341,7 +460,7 @@ impl Chunk { w, buf: vec![], }; - chunk.encode(&mut w, record)?; + chunk.encode(&mut w, record, color_map)?; w.finish() } (Some(min_width), Some(max_width), Alignment::Left) => { @@ -353,7 +472,7 @@ impl Chunk { w, }, }; - chunk.encode(&mut w, record)?; + chunk.encode(&mut w, record, color_map)?; w.finish() } (Some(min_width), Some(max_width), Alignment::Right) => { @@ -366,7 +485,7 @@ impl Chunk { }, buf: vec![], }; - chunk.encode(&mut w, record)?; + chunk.encode(&mut w, record, color_map)?; w.finish() } }, @@ -558,7 +677,12 @@ enum FormattedChunk { } impl FormattedChunk { - fn encode(&self, w: &mut dyn encode::Write, record: &Record) -> io::Result<()> { + fn encode( + &self, + w: &mut dyn encode::Write, + record: &Record, + color_map: &ColorMap, + ) -> io::Result<()> { match *self { FormattedChunk::Time(ref fmt, Timezone::Utc) => write!(w, "{}", Utc::now().format(fmt)), FormattedChunk::Time(ref fmt, Timezone::Local) => { @@ -584,22 +708,24 @@ impl FormattedChunk { FormattedChunk::Newline => w.write_all(NEWLINE.as_bytes()), FormattedChunk::Align(ref chunks) => { for chunk in chunks { - chunk.encode(w, record)?; + chunk.encode(w, record, color_map)?; } Ok(()) } FormattedChunk::Highlight(ref chunks) => { - match record.level() { - Level::Error => { - w.set_style(Style::new().text(Color::Red).intense(true))?; + if let Some(color) = color_map.get(&record.level()) { + match record.level() { + Level::Error => { + w.set_style(Style::new().text(color).intense(true))?; + } + Level::Warn => w.set_style(Style::new().text(color))?, + Level::Info => w.set_style(Style::new().text(color))?, + Level::Debug => w.set_style(Style::new().text(color))?, + _ => {} } - Level::Warn => w.set_style(Style::new().text(Color::Yellow))?, - Level::Info => w.set_style(Style::new().text(Color::Green))?, - Level::Trace => w.set_style(Style::new().text(Color::Cyan))?, - _ => {} } for chunk in chunks { - chunk.encode(w, record)?; + chunk.encode(w, record, color_map)?; } match record.level() { Level::Error | Level::Warn | Level::Info | Level::Trace => { @@ -619,29 +745,37 @@ impl FormattedChunk { /// An `Encode`r configured via a format string. #[derive(Derivative)] #[derivative(Debug)] -#[derive(Clone, Eq, PartialEq, Hash)] +#[derive(Clone, Eq, PartialEq)] pub struct PatternEncoder { #[derivative(Debug = "ignore")] chunks: Vec, pattern: String, + color_map: ColorMap, } /// Returns a `PatternEncoder` using the default pattern of `{d} {l} {t} - {m}{n}`. impl Default for PatternEncoder { fn default() -> PatternEncoder { - PatternEncoder::new("{d} {l} {t} - {m}{n}") + PatternEncoder::new(DEFAULT_PATTERN_ENCODER) } } impl Encode for PatternEncoder { fn encode(&self, w: &mut dyn encode::Write, record: &Record) -> anyhow::Result<()> { for chunk in &self.chunks { - chunk.encode(w, record)?; + chunk.encode(w, record, &self.color_map)?; } Ok(()) } } +#[cfg(feature = "file")] +impl From for PatternEncoder { + fn from(pattern_config: PatternEncoderConfig) -> Self { + PatternEncoder::new_with_colormap(&pattern_config.pattern, color_map) + } +} + impl PatternEncoder { /// Creates a `PatternEncoder` from a pattern string. /// @@ -650,6 +784,34 @@ impl PatternEncoder { PatternEncoder { chunks: Parser::new(pattern).map(From::from).collect(), pattern: pattern.to_owned(), + color_map: ColorMap::default(), + } + } + + /// Creates a `PatternEncoder` from a pattern string and color hashmap. + /// + /// The pattern string syntax is documented in the `pattern` module. + /// + /// + ///! ```no_run + ///! # #[cfg(all(feature = "console_appender", + ///! # feature = "file_appender", + ///! # feature = "pattern_encoder"))] + ///! # fn f() { + ///! // Change color of Info level msgs, other msgs will be the default color + ///! let mut log_color_map = ColorMap::default() + ///! log_color_map.info_color = Some(log4rs::encode::Color::Cyan); + ///! let pattern_encoder = PatternEncoder::new_with_colormap("{d} - {m}{n}", log_color_map); + ///! + ///! } + ///! # } + ///! # fn main() {} + ///! ``` + pub fn new_with_colormap(pattern: &str, color_map: ColorMap) -> PatternEncoder { + PatternEncoder { + chunks: Parser::new(pattern).map(From::from).collect(), + pattern: pattern.to_owned(), + color_map, } } } @@ -976,4 +1138,42 @@ mod tests { assert_eq!(buf, b"missing value"); } + #[cfg(feature = "config_parsing")] + #[test] + fn check_deserialize_color_hash() { + use crate::encode::pattern::ColorMap; + let serialized = r" + info: Blue + trace: Black + "; + let _deserialized: ColorMap = serde_yaml::from_str(&serialized).unwrap(); + } + #[test] + fn check_color_hash() { + // purpose of this test is to specify a single custom color. + // - test the custom color + // - test that default colors were intact + use crate::encode::pattern::ColorMap; + use crate::encode::Color; + let mut log_color_map = ColorMap::default(); + + log_color_map.info = Some(Color::Cyan); + let encoder = Box::new(PatternEncoder::new_with_colormap( + "{d(%Y-%m-%d %H:%M:%S)(local)} {h({l} [{f}:{L} {T} {t}] {m})}{n}", + log_color_map, + )); + assert_eq!(encoder.color_map.get(&log::Level::Info), Some(Color::Cyan)); + assert_eq!( + encoder.color_map.get(&log::Level::Warn), + ColorMap::default().get(&log::Level::Warn) + ); + assert_eq!( + encoder.color_map.get(&log::Level::Error), + ColorMap::default().get(&log::Level::Error) + ); + assert_eq!( + encoder.color_map.get(&log::Level::Debug), + ColorMap::default().get(&log::Level::Debug) + ); + } } diff --git a/test/log.yml b/test/log.yml index b8c5aee3..3dee86cc 100644 --- a/test/log.yml +++ b/test/log.yml @@ -5,6 +5,9 @@ appenders: kind: console encoder: pattern: "{d(%+)(local)} [{t}] {h({l})} {M}:{m}{n}" + color_map: + Info: Cyan + Warn: Red filters: - kind: threshold level: error