diff --git a/Cargo.toml b/Cargo.toml index 332e64c..5ad0d49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "flexi_logger" -version = "0.15.13" +version = "0.16.0-alpha" authors = ["emabee "] edition = "2018" license = "MIT OR Apache-2.0" diff --git a/README.md b/README.md index c7d4cdb..e0e7e19 100644 --- a/README.md +++ b/README.md @@ -137,19 +137,22 @@ Make use of any of these features by specifying them in your `Cargo.toml` ### **`colors`** -Getting colored output was also possible without this feature, by adding -colors to the logged message, -and/or implementing and using your own coloring format function. +Getting colored output is also possible without this feature, +by implementing and using your own coloring format function. -The new default feature `colors` simplifies this by doing three things: +The default feature `colors` simplifies this by doing three things: * it activates the optional dependency to `yansi` and * provides additional colored pendants to the existing uncolored format functions * it uses `colored_default_format()` for the output to stderr, and the non-colored `default_format()` for the output to files +* it activates the optional dependency to `atty` to being able to switch off + coloring if the output is not sent to a terminal but e.g. piped to another program. **Colors**, -or styles in general, are a matter of taste, and no choice will fit every need. So you can override the default formatting for stderr, using `Logger::format_for_stderr()`, and for the files using `Logger::format_for_files()`, or for both in one shot using `Logger::format()`. +or styles in general, are a matter of taste, and no choice will fit every need. So you can override the default formatting and coloring in various ways. + +With `--no-default-features --features="atty"` you can remove the yansi-based coloring but keep the capability to switch off your own coloring. ### **`specfile`** diff --git a/examples/colors.rs b/examples/colors.rs index 481a0dc..cf1e5cf 100644 --- a/examples/colors.rs +++ b/examples/colors.rs @@ -1,6 +1,75 @@ fn main() { + #[cfg(not(feature = "colors"))] + println!("Feature color is switched off"); + #[cfg(feature = "colors")] - for i in 0..=255 { - println!("{}: {}", i, yansi::Paint::fixed(i, i)); + { + use atty::Stream::{Stderr, Stdout}; + use yansi::{Color, Paint, Style}; + + for i in 0..=255 { + println!("{}: {}", i, Paint::fixed(i, i)); + } + + println!(""); + + if atty::is(Stdout) { + println!( + "Stdout is considered a tty - \ + flexi_logger::AdaptiveFormat will use colors", + ); + } else { + println!( + "Stdout is not considered a tty - \ + flexi_logger::AdaptiveFormat will NOT use colors" + ); + } + + if atty::is(Stderr) { + println!( + "Stderr is considered a tty - \ + flexi_logger::AdaptiveFormat will use colors", + ); + } else { + println!( + "Stderr is not considered a tty - \ + flexi_logger::AdaptiveFormat will NOT use colors!" + ); + } + + // Enable ASCII escape sequence support on Windows consoles, + // but disable coloring on unsupported Windows consoles + if cfg!(windows) { + if !Paint::enable_windows_ascii() { + println!("unsupported windows console detected => coloring disabled"); + Paint::disable(); + return; + } + } + + println!( + "\n{}", + Style::new(Color::Fixed(196)) + .bold() + .paint("This is red output like by default with err!") + ); + println!( + "{}", + Style::new(Color::Fixed(208)) + .bold() + .paint("This is yellow output like by default with warn!") + ); + println!( + "{}", + Style::new(Color::Unset).paint("This is normal output like by default with info!") + ); + println!( + "{}", + Style::new(Color::Fixed(7)).paint("This is output like by default with debug!") + ); + println!( + "{}", + Style::new(Color::Fixed(8)).paint("This is grey output like by default with trace!") + ); } } diff --git a/scripts/qualify.rs b/scripts/qualify.rs index 7fd6f2c..38f330d 100644 --- a/scripts/qualify.rs +++ b/scripts/qualify.rs @@ -46,6 +46,7 @@ fn main() { // Build in important variants run_command!("cargo", "build"); run_command!("cargo", "build", "--no-default-features"); + run_command!("cargo", "build", "--no-default-features", "--features=atty"); run_command!("cargo", "build", "--all-features"); run_command!("cargo", "+1.37.0", "build", "--no-default-features"); run_command!("cargo", "+1.37.0", "build", "--all-features"); diff --git a/src/flexi_error.rs b/src/flexi_error.rs index 36ae166..273773a 100644 --- a/src/flexi_error.rs +++ b/src/flexi_error.rs @@ -56,4 +56,8 @@ pub enum FlexiLoggerError { /// Some synchronization object is poisoned. #[error("Some synchronization object is poisoned")] Poison, + + /// Palette parsing failed + #[error("Palette parsing failed")] + Palette(#[from] std::num::ParseIntError), } diff --git a/src/formats.rs b/src/formats.rs index 6903b7a..ec58f07 100644 --- a/src/formats.rs +++ b/src/formats.rs @@ -4,6 +4,31 @@ use std::thread; #[cfg(feature = "colors")] use yansi::{Color, Paint, Style}; +/// Function type for Format functions. +/// +/// If you want to write the log lines in your own format, +/// implement a function with this signature and provide it to one of the methods +/// [`Logger::format()`](struct.Logger.html#method.format), +/// [`Logger::format_for_files()`](struct.Logger.html#method.format_for_files), +/// or [`Logger::format_for_stderr()`](struct.Logger.html#method.format_for_stderr). +/// +/// Checkout the code of the provided [format functions](index.html#functions) +/// if you want to start with a template. +/// +/// ## Parameters +/// +/// - `write`: the output stream +/// +/// - `now`: the timestamp that you should use if you want a timestamp to appear in the log line +/// +/// - `record`: the log line's content and metadata, as provided by the log crate's macros. +/// +pub type FormatFunction = fn( + write: &mut dyn std::io::Write, + now: &mut DeferredNow, + record: &Record, +) -> Result<(), std::io::Error>; + /// A logline-formatter that produces log lines like
/// ```INFO [my_prog::some_submodule] Task successfully read from conf.json``` /// @@ -212,43 +237,48 @@ pub fn colored_with_thread( ) } -/// Helper function that is used in the provided colored format functions. -/// -/// The palette that is used by `style` can be overridden by setting the environment variable -/// `FLEXI_LOGGER_PALETTE` to a semicolon-separated list of numbers (0..=255) and/or dashes (´-´). -/// The first five values denote the fixed color that is used for coloring error, warning, info, -/// debug, and trace messages. -/// -/// `FLEXI_LOGGER_PALETTE = "196;208;-;7;8"` -/// reflects the default palette; color 196 is used for error messages, and so on. +/// Helper function that is used in the provided coloring format functions to apply +/// colors based on the log level and the effective color palette. /// -/// The '-' means that no coloring is done, i.e., with "-;-;-;-;-" all coloring is switched off. -/// -/// For your convenience, if you want to specify your own palette, -/// you can produce a colored list of all 255 colors with `cargo run --example colors` -/// to see the available colors. +/// See [`Logger::set_palette`](struct.Logger.html#method.set_palette) if you want to +/// modify the color palette. /// /// Only available with feature `colors`. #[cfg(feature = "colors")] pub fn style(level: log::Level, item: T) -> Paint { + let palette = &*(PALETTE.read().unwrap()); match level { - log::Level::Error => Paint::new(item).with_style(PALETTE.error), - log::Level::Warn => Paint::new(item).with_style(PALETTE.warn), - log::Level::Info => Paint::new(item).with_style(PALETTE.info), - log::Level::Debug => Paint::new(item).with_style(PALETTE.debug), - log::Level::Trace => Paint::new(item).with_style(PALETTE.trace), + log::Level::Error => palette.error, + log::Level::Warn => palette.warn, + log::Level::Info => palette.info, + log::Level::Debug => palette.debug, + log::Level::Trace => palette.trace, } + .paint(item) } #[cfg(feature = "colors")] lazy_static::lazy_static! { - static ref PALETTE: Palette = { - match std::env::var("FLEXI_LOGGER_PALETTE") { - Ok(palette) => Palette::parse(&palette).unwrap_or_else(|_| Palette::default()), - Err(..) => Palette::default(), - } + static ref PALETTE: std::sync::RwLock = std::sync::RwLock::new(Palette::default()); +} - }; +// Overwrites the default PALETTE value either from the environment, if set, +// or from the parameter, if filled. +// Returns an error if parsing failed. +#[cfg(feature = "colors")] +pub(crate) fn set_palette(input: &Option) -> Result<(), std::num::ParseIntError> { + match std::env::var_os("FLEXI_LOGGER_PALETTE") { + Some(ref env_osstring) => { + *(PALETTE.write().unwrap()) = Palette::from(env_osstring.to_string_lossy().as_ref())?; + } + None => match input { + Some(ref input_string) => { + *(PALETTE.write().unwrap()) = Palette::from(input_string)?; + } + None => {} + }, + } + Ok(()) } #[cfg(feature = "colors")] @@ -271,7 +301,7 @@ impl Palette { } } - fn parse(palette: &str) -> Result { + fn from(palette: &str) -> Result { let mut items = palette.split(';'); Ok(Palette { error: parse_style(items.next().unwrap_or("196").trim())?, @@ -291,3 +321,95 @@ fn parse_style(input: &str) -> Result { Style::new(Color::Fixed(input.parse()?)) }) } + +/// Specifies the `FormatFunction` and decides if coloring should be used. +/// +/// Is used in +/// [`Logger::adaptive_format_for_stderr`](struct.Logger.html#method.adaptive_format_for_stderr) and +/// [`Logger::adaptive_format_for_stdout`](struct.Logger.html#method.adaptive_format_for_stdout). +/// The coloring format functions are used if the output channel is a tty. +/// +/// Only available with feature `atty`. +#[cfg(feature = "atty")] +#[derive(Clone, Copy)] +pub enum AdaptiveFormat { + /// Chooses between [`default_format`](fn.default_format.html) + /// and [`colored_default_format`](fn.colored_default_format.html). + /// + /// Only available with feature `colors`. + #[cfg(feature = "colors")] + Default, + /// Chooses between [`detailed_format`](fn.detailed_format.html) + /// and [`colored_detailed_format`](fn.colored_detailed_format.html). + /// + /// Only available with feature `colors`. + #[cfg(feature = "colors")] + Detailed, + /// Chooses between [`opt_format`](fn.opt_format.html) + /// and [`colored_opt_format`](fn.colored_opt_format.html). + /// + /// Only available with feature `colors`. + #[cfg(feature = "colors")] + Opt, + /// Chooses between [`with_thread`](fn.with_thread.html) + /// and [`colored_with_thread`](fn.colored_with_thread.html). + /// + /// Only available with feature `colors`. + #[cfg(feature = "colors")] + WithThread, + /// Chooses between the first format function (which is supposed to be uncolored) + /// and the second (which is supposed to be colored). + /// + /// Allows providing own format functions, with freely choosable coloring technique, + /// _and_ making use of the tty detection. + Custom(FormatFunction, FormatFunction), +} + +#[cfg(feature = "atty")] +impl AdaptiveFormat { + #[must_use] + pub(crate) fn format_function(self, stream: Stream) -> FormatFunction { + if stream.is_tty() { + match self { + #[cfg(feature = "colors")] + Self::Default => colored_default_format, + #[cfg(feature = "colors")] + Self::Detailed => colored_detailed_format, + #[cfg(feature = "colors")] + Self::Opt => colored_opt_format, + #[cfg(feature = "colors")] + Self::WithThread => colored_with_thread, + Self::Custom(_, colored) => colored, + } + } else { + match self { + #[cfg(feature = "colors")] + Self::Default => default_format, + #[cfg(feature = "colors")] + Self::Detailed => detailed_format, + #[cfg(feature = "colors")] + Self::Opt => opt_format, + #[cfg(feature = "colors")] + Self::WithThread => with_thread, + Self::Custom(uncolored, _) => uncolored, + } + } + } +} + +#[cfg(feature = "atty")] +#[derive(Clone, Copy)] +pub(crate) enum Stream { + StdOut, + StdErr, +} +#[cfg(feature = "atty")] +impl Stream { + #[must_use] + pub fn is_tty(self) -> bool { + match self { + Self::StdOut => atty::is(atty::Stream::Stdout), + Self::StdErr => atty::is(atty::Stream::Stderr), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index c926884..ea544a9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,28 +60,3 @@ pub use crate::log_specification::{LogSpecBuilder, LogSpecification, ModuleFilte pub use crate::logger::{Duplicate, LogTarget, Logger}; pub use crate::parameters::{Age, Cleanup, Criterion, Naming}; pub use crate::reconfiguration_handle::ReconfigurationHandle; - -/// Function type for Format functions. -/// -/// If you want to write the log lines in your own format, -/// implement a function with this signature and provide it to one of the methods -/// [`Logger::format()`](struct.Logger.html#method.format), -/// [`Logger::format_for_files()`](struct.Logger.html#method.format_for_files), -/// or [`Logger::format_for_stderr()`](struct.Logger.html#method.format_for_stderr). -/// -/// Checkout the code of the provided [format functions](index.html#functions) -/// if you want to start with a template. -/// -/// ## Parameters -/// -/// - `write`: the output stream -/// -/// - `now`: the timestamp that you should use if you want a timestamp to appear in the log line -/// -/// - `record`: the log line's content and metadata, as provided by the log crate's macros. -/// -pub type FormatFunction = fn( - write: &mut dyn std::io::Write, - now: &mut DeferredNow, - record: &Record, -) -> Result<(), std::io::Error>; diff --git a/src/logger.rs b/src/logger.rs index dddcfc7..14e5bab 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -1,8 +1,11 @@ use crate::flexi_logger::FlexiLogger; +use crate::formats::default_format; +#[cfg(feature = "atty")] +use crate::formats::{AdaptiveFormat, Stream}; use crate::primary_writer::PrimaryWriter; use crate::writers::{FileLogWriter, FileLogWriterBuilder, LogWriter}; use crate::{ - formats, Cleanup, Criterion, FlexiLoggerError, FormatFunction, LogSpecification, Naming, + Cleanup, Criterion, FlexiLoggerError, FormatFunction, LogSpecification, Naming, ReconfigurationHandle, }; @@ -45,7 +48,6 @@ use std::sync::{Arc, RwLock}; /// /// * [`start()`](struct.Logger.html#method.start), /// * or [`start_with_specfile()`](struct.Logger.html#method.start_with_specfile). -/// pub struct Logger { spec: LogSpecification, parse_errs: Option, @@ -56,6 +58,8 @@ pub struct Logger { format_for_stderr: FormatFunction, format_for_stdout: FormatFunction, format_for_writer: FormatFunction, + #[cfg(feature = "colors")] + o_palette: Option, flwb: FileLogWriterBuilder, other_writers: HashMap>, } @@ -144,7 +148,13 @@ impl Logger { fn from_spec_and_errs(spec: LogSpecification, parse_errs: Option) -> Self { #[cfg(feature = "colors")] - yansi::Paint::enable_windows_ascii(); + { + // Enable ASCII escape sequence support on Windows consoles, + // but disable coloring on unsupported Windows consoles + if cfg!(windows) && !yansi::Paint::enable_windows_ascii() { + yansi::Paint::disable(); + } + } Self { spec, @@ -152,26 +162,25 @@ impl Logger { log_target: LogTarget::StdErr, duplicate_err: Duplicate::None, duplicate_out: Duplicate::None, - format_for_file: formats::default_format, - format_for_stdout: Self::tty_format(true), - format_for_stderr: Self::tty_format(false), - format_for_writer: formats::default_format, + format_for_file: default_format, + + #[cfg(feature = "colors")] + format_for_stdout: AdaptiveFormat::Default.format_function(Stream::StdOut), + #[cfg(feature = "colors")] + format_for_stderr: AdaptiveFormat::Default.format_function(Stream::StdErr), + + #[cfg(not(feature = "colors"))] + format_for_stdout: default_format, + #[cfg(not(feature = "colors"))] + format_for_stderr: default_format, + + format_for_writer: default_format, + #[cfg(feature = "colors")] + o_palette: None, flwb: FileLogWriter::builder(), other_writers: HashMap::>::new(), } } - - fn tty_format(_out: bool) -> FormatFunction { - #[cfg(feature = "colors")] - #[allow(clippy::used_underscore_binding)] - { - if (_out && atty::is(atty::Stream::Stdout)) || (!_out && atty::is(atty::Stream::Stderr)) - { - return formats::colored_default_format; - } - } - formats::default_format - } } /// Simple methods for influencing the behavior of the Logger. @@ -223,21 +232,6 @@ impl Logger { self } - /// Makes the logger write no logs at all. - /// - /// This can be useful when you want to run tests of your programs with all log-levels active. - /// Such tests can ensure that those parts of your code, which are only executed - /// within normally unused log calls (like `std::fmt::Display` implementations), - /// will not cause undesired side-effects when activated (note that the log macros prevent - /// arguments of inactive log-calls from being evaluated). - #[deprecated( - since = "0.15.6", - note = "use `log_target()` with `LogTarget::DevNull`" - )] - pub fn do_not_log(self) -> Self { - self.log_target(LogTarget::DevNull) - } - /// Makes the logger print an info message to stdout with the name of the logfile /// when a logfile is opened for writing. pub fn print_message(mut self) -> Self { @@ -269,9 +263,9 @@ impl Logger { /// ```fn(&Record) -> String```. /// /// By default, - /// [`default_format()`](fn.default_format.html) is used for output to files, - /// and [`colored_default_format()`](fn.colored_default_format.html) is used for output - /// to stdout or stderr. + /// [`default_format()`](fn.default_format.html) is used for output to files + /// and to custom writers, and [`AdaptiveFormat::Default`](enum.AdaptiveFormat.html#variant.Default) + /// is used for output to `stderr` and `stdout`. /// /// If the feature `colors` is switched off, /// `default_format()` is used for all outputs. @@ -292,6 +286,30 @@ impl Logger { self } + /// Makes the logger use the specified format for messages that are written to `stderr`. + /// Coloring is used if `stderr` is a tty. + /// + /// Regarding the default, see [`Logger::format`](struct.Logger.html#method.format). + /// + /// Only available with feature `colors`. + #[cfg(feature = "atty")] + pub fn adaptive_format_for_stderr(mut self, adaptive_format: AdaptiveFormat) -> Self { + self.format_for_stderr = adaptive_format.format_function(Stream::StdErr); + self + } + + /// Makes the logger use the specified format for messages that are written to `stdout`. + /// Coloring is used if `stdout` is a tty. + /// + /// Regarding the default, see [`Logger::format`](struct.Logger.html#method.format). + /// + /// Only available with feature `colors`. + #[cfg(feature = "atty")] + pub fn adaptive_format_for_stdout(mut self, adaptive_format: AdaptiveFormat) -> Self { + self.format_for_stdout = adaptive_format.format_function(Stream::StdOut); + self + } + /// Makes the logger use the provided format function for messages /// that are written to stderr. /// @@ -301,7 +319,7 @@ impl Logger { self } - /// Makes the logger use the provided format function for messages + /// Makes the logger use the provided format function to format messages /// that are written to stdout. /// /// Regarding the default, see [`Logger::format`](struct.Logger.html#method.format). @@ -320,6 +338,34 @@ impl Logger { self } + /// Sets the color palette for function [`style`](fn.style.html), which is used in the + /// provided coloring format functions. + /// + /// The palette given here overrides the default palette. + /// + /// The palette is specified in form of a String that contains a semicolon-separated list + /// of numbers (0..=255) and/or dashes (´-´). + /// The first five values denote the fixed color that is + /// used for coloring `error`, `warn`, `info`, `debug`, and `trace` messages. + /// + /// The String `"196;208;-;7;8"` describes the default palette, where color 196 is + /// used for error messages, and so on. The `-` means that no coloring is done, + /// i.e., with `"-;-;-;-;-"` all coloring is switched off. + /// + /// The palette can further be overridden at runtime by setting the environment variable + /// `FLEXI_LOGGER_PALETTE` to a palette String. This allows adapting the used text colors to + /// differently colored terminal backgrounds. + /// + /// For your convenience, if you want to specify your own palette, + /// you can produce a colored list with all 255 colors with `cargo run --example colors`. + /// + /// Only available with feature `colors`. + #[cfg(feature = "colors")] + pub fn set_palette(mut self, palette: String) -> Self { + self.o_palette = Some(palette); + self + } + /// Specifies a folder for the log files. /// /// This parameter only has an effect if `log_to_file()` is used, too. @@ -470,17 +516,6 @@ impl Logger { /// Use these methods when you want to control the settings flexibly, /// e.g. with commandline arguments via `docopts` or `clap`. impl Logger { - /// With true, makes the logger write all logs to a file, otherwise to stderr. - #[deprecated(since = "0.13.3", note = "please use `log_target` instead")] - pub fn o_log_to_file(mut self, log_to_file: bool) -> Self { - if log_to_file { - self.log_target = LogTarget::File; - } else { - self.log_target = LogTarget::StdErr; - } - self - } - /// With true, makes the logger print an info message to stdout, each time /// when a new file is used for log-output. pub fn o_print_message(mut self, print_message: bool) -> Self { @@ -588,6 +623,9 @@ impl Logger { let spec = Arc::new(RwLock::new(self.spec)); let other_writers = Arc::new(self.other_writers); + #[cfg(feature = "colors")] + crate::formats::set_palette(&self.o_palette)?; + let primary_writer = Arc::new(match self.log_target { LogTarget::File => { self.flwb = self.flwb.format(self.format_for_file);