diff --git a/examples/cmd-program.rs b/examples/cmd-program.rs index d4df8a5..52594eb 100644 --- a/examples/cmd-program.rs +++ b/examples/cmd-program.rs @@ -1,5 +1,3 @@ -use std::io; - use log::{debug, info, trace, warn}; fn setup_logging(verbosity: u64) -> Result<(), fern::InitError> { @@ -32,7 +30,7 @@ fn setup_logging(verbosity: u64) -> Result<(), fern::InitError> { message )) }) - .chain(fern::log_file("program.log")?); + .chain(fern::logger::file("program.log")?); let stdout_config = fern::Dispatch::new() .format(|out, message, record| { @@ -53,7 +51,7 @@ fn setup_logging(verbosity: u64) -> Result<(), fern::InitError> { )) } }) - .chain(io::stdout()); + .chain(fern::logger::stdout()); base_config .chain(file_config) diff --git a/examples/colored.rs b/examples/colored.rs index cd50e61..a7a45bd 100644 --- a/examples/colored.rs +++ b/examples/colored.rs @@ -5,7 +5,7 @@ fn main() { let colors = ColoredLevelConfig::new().debug(Color::Magenta); fern::Dispatch::new() - .chain(std::io::stdout()) + .chain(fern::logger::stdout()) .format(move |out, message, record| { out.finish(format_args!( "[{}]{} {}", diff --git a/examples/date-based-file-log.rs b/examples/date-based-file-log.rs index 2b2c899..bd33a02 100644 --- a/examples/date-based-file-log.rs +++ b/examples/date-based-file-log.rs @@ -3,7 +3,7 @@ use log::{debug, info, warn}; fn setup_logging() -> Result<(), Box> { fern::Dispatch::new() .level(log::LevelFilter::Debug) - .chain(fern::DateBased::new("program.log.", "%Y-%m-%d")) + .chain(fern::logger::DateBased::new("program.log.", "%Y-%m-%d")) .apply()?; Ok(()) diff --git a/examples/meta-logging.rs b/examples/meta-logging.rs index cdf0fc9..3153a64 100644 --- a/examples/meta-logging.rs +++ b/examples/meta-logging.rs @@ -8,9 +8,9 @@ use log::{debug, info}; fn main() { fern::Dispatch::new() - .chain(std::io::stdout()) - .chain(std::io::stderr()) - .chain(fern::log_file("hello.txt").unwrap()) + .chain(fern::logger::stdout()) + .chain(fern::logger::stderr()) + .chain(fern::logger::file("hello.txt").unwrap()) .format(move |out, message, record| { out.finish(format_args!("[{}] {}", record.level(), message)) }) diff --git a/examples/pretty-colored.rs b/examples/pretty-colored.rs index cbf2675..8578c87 100644 --- a/examples/pretty-colored.rs +++ b/examples/pretty-colored.rs @@ -78,7 +78,7 @@ fn set_up_logging() { // `info!(target="special_target", "This log message is about special_target");` .level_for("pretty_colored", log::LevelFilter::Trace) // output to stdout - .chain(std::io::stdout()) + .chain(fern::logger::stdout()) .apply() .unwrap(); diff --git a/examples/syslog.rs b/examples/syslog.rs index 3d7e8c5..c68d52a 100644 --- a/examples/syslog.rs +++ b/examples/syslog.rs @@ -17,7 +17,7 @@ fn setup_logging() -> Result<(), Box> { .level(log::LevelFilter::Warn) // but accept Info if we explicitly mention it .level_for("explicit-syslog", log::LevelFilter::Info) - .chain(syslog::unix(syslog_fmt)?) + .chain(fern::syslog::syslog4_3164(syslog::unix(syslog_fmt)?)) .apply()?; Ok(()) diff --git a/examples/syslog3.rs b/examples/syslog3.rs index 2e45279..7ce1b49 100644 --- a/examples/syslog3.rs +++ b/examples/syslog3.rs @@ -11,7 +11,7 @@ fn setup_logging() -> Result<(), fern::InitError> { .level(log::LevelFilter::Warn) // but accept Info if we explicitly mention it .level_for("explicit-syslog", log::LevelFilter::Info) - .chain(syslog::unix(syslog::Facility::LOG_USER)?) + .chain(fern::syslog::syslog3(*syslog::unix(syslog::Facility::LOG_USER)?)) .apply()?; Ok(()) diff --git a/src/builders.rs b/src/builders.rs deleted file mode 100644 index 54f96da..0000000 --- a/src/builders.rs +++ /dev/null @@ -1,1456 +0,0 @@ -use std::{ - borrow::Cow, - cmp, fmt, fs, io, - io::Write, - sync::{mpsc::Sender, Arc, Mutex}, -}; - -#[cfg(feature = "date-based")] -use std::path::{Path, PathBuf}; - -#[cfg(all(not(windows), feature = "syslog-4"))] -use std::collections::HashMap; - -use log::Log; - -use crate::{log_impl, Filter, FormatCallback, Formatter}; - -#[cfg(feature = "date-based")] -use crate::log_impl::DateBasedState; - -#[cfg(all(not(windows), feature = "syslog-4"))] -use crate::{Syslog4Rfc3164Logger, Syslog4Rfc5424Logger}; - -/// The base dispatch logger. -/// -/// This allows for formatting log records, limiting what records can be passed -/// through, and then dispatching records to other dispatch loggers or output -/// loggers. -/// -/// Note that all methods are position-insensitive. -/// `Dispatch::new().format(a).chain(b)` produces the exact same result -/// as `Dispatch::new().chain(b).format(a)`. Given this, it is preferred to put -/// 'format' and other modifiers before 'chain' for the sake of clarity. -/// -/// Example usage demonstrating all features: -/// -/// ```no_run -/// # // no_run because this creates log files. -/// use std::{fs, io}; -/// -/// # fn setup_logger() -> Result<(), fern::InitError> { -/// fern::Dispatch::new() -/// .format(|out, message, record| { -/// out.finish(format_args!( -/// "[{}][{}] {}", -/// record.level(), -/// record.target(), -/// message, -/// )) -/// }) -/// .chain( -/// fern::Dispatch::new() -/// // by default only accept warn messages -/// .level(log::LevelFilter::Warn) -/// // accept info messages from the current crate too -/// .level_for("my_crate", log::LevelFilter::Info) -/// // `io::Stdout`, `io::Stderr` and `io::File` can be directly passed in. -/// .chain(io::stdout()), -/// ) -/// .chain( -/// fern::Dispatch::new() -/// // output all messages -/// .level(log::LevelFilter::Trace) -/// // except for hyper, in that case only show info messages -/// .level_for("hyper", log::LevelFilter::Info) -/// // `log_file(x)` equates to -/// // `OpenOptions::new().write(true).append(true).create(true).open(x)` -/// .chain(fern::log_file("persistent-log.log")?) -/// .chain( -/// fs::OpenOptions::new() -/// .write(true) -/// .create(true) -/// .truncate(true) -/// .create(true) -/// .open("/tmp/temp.log")?, -/// ), -/// ) -/// .chain( -/// fern::Dispatch::new() -/// .level(log::LevelFilter::Error) -/// .filter(|_meta_data| { -/// // as an example, randomly reject half of the messages -/// # /* -/// rand::random() -/// # */ -/// # true -/// }) -/// .chain(io::stderr()), -/// ) -/// // and finally, set as the global logger! -/// .apply()?; -/// # Ok(()) -/// # } -/// # -/// # fn main() { setup_logger().expect("failed to set up logger") } -/// ``` -#[must_use = "this is only a logger configuration and must be consumed with into_log() or apply()"] -pub struct Dispatch { - format: Option>, - children: Vec, - default_level: log::LevelFilter, - levels: Vec<(Cow<'static, str>, log::LevelFilter)>, - filters: Vec>, -} - -/// Logger which is usable as an output for multiple other loggers. -/// -/// This struct contains a built logger stored in an [`Arc`], and can be -/// safely cloned. -/// -/// See [`Dispatch::into_shared`]. -/// -/// [`Arc`]: https://doc.rust-lang.org/std/sync/struct.Arc.html -/// [`Dispatch::into_shared`]: struct.Dispatch.html#method.into_shared -#[derive(Clone)] -pub struct SharedDispatch { - inner: Arc, - min_level: log::LevelFilter, -} - -impl Dispatch { - /// Creates a dispatch, which will initially do nothing. - #[inline] - pub fn new() -> Self { - Dispatch { - format: None, - children: Vec::new(), - default_level: log::LevelFilter::Trace, - levels: Vec::new(), - filters: Vec::new(), - } - } - - /// Sets the formatter of this dispatch. The closure should accept a - /// callback, a message and a log record, and write the resulting - /// format to the writer. - /// - /// The log record is passed for completeness, but the `args()` method of - /// the record should be ignored, and the [`fmt::Arguments`] given - /// should be used instead. `record.args()` may be used to retrieve the - /// _original_ log message, but in order to allow for true log - /// chaining, formatters should use the given message instead whenever - /// including the message in the output. - /// - /// To avoid all allocation of intermediate results, the formatter is - /// "completed" by calling a callback, which then calls the rest of the - /// logging chain with the new formatted message. The callback object keeps - /// track of if it was called or not via a stack boolean as well, so if - /// you don't use `out.finish` the log message will continue down - /// the logger chain unformatted. - /// - /// [`fmt::Arguments`]: https://doc.rust-lang.org/std/fmt/struct.Arguments.html - /// - /// Example usage: - /// - /// ``` - /// fern::Dispatch::new().format(|out, message, record| { - /// out.finish(format_args!( - /// "[{}][{}] {}", - /// record.level(), - /// record.target(), - /// message - /// )) - /// }) - /// # .into_log(); - /// ``` - #[inline] - pub fn format(mut self, formatter: F) -> Self - where - F: Fn(FormatCallback, &fmt::Arguments, &log::Record) + Sync + Send + 'static, - { - self.format = Some(Box::new(formatter)); - self - } - - /// Adds a child to this dispatch. - /// - /// All log records which pass all filters will be formatted and then sent - /// to all child loggers in sequence. - /// - /// Note: If the child logger is also a Dispatch, and cannot accept any log - /// records, it will be dropped. This only happens if the child either - /// has no children itself, or has a minimum log level of - /// [`LevelFilter::Off`]. - /// - /// [`LevelFilter::Off`]: https://docs.rs/log/0.4/log/enum.LevelFilter.html#variant.Off - /// - /// Example usage: - /// - /// ``` - /// fern::Dispatch::new().chain(fern::Dispatch::new().chain(std::io::stdout())) - /// # .into_log(); - /// ``` - #[inline] - pub fn chain>(mut self, logger: T) -> Self { - self.children.push(logger.into().0); - self - } - - /// Sets the overarching level filter for this logger. All messages not - /// already filtered by something set by [`Dispatch::level_for`] will - /// be affected. - /// - /// All messages filtered will be discarded if less severe than the given - /// level. - /// - /// Default level is [`LevelFilter::Trace`]. - /// - /// [`Dispatch::level_for`]: #method.level_for - /// [`LevelFilter::Trace`]: https://docs.rs/log/0.4/log/enum.LevelFilter.html#variant.Trace - /// - /// Example usage: - /// - /// ``` - /// # fn main() { - /// fern::Dispatch::new().level(log::LevelFilter::Info) - /// # .into_log(); - /// # } - /// ``` - #[inline] - pub fn level(mut self, level: log::LevelFilter) -> Self { - self.default_level = level; - self - } - - /// Sets a per-target log level filter. Default target for log messages is - /// `crate_name::module_name` or - /// `crate_name` for logs in the crate root. Targets can also be set with - /// `info!(target: "target-name", ...)`. - /// - /// For each log record fern will first try to match the most specific - /// level_for, and then progressively more general ones until either a - /// matching level is found, or the default level is used. - /// - /// For example, a log for the target `hyper::http::h1` will first test a - /// level_for for `hyper::http::h1`, then for `hyper::http`, then for - /// `hyper`, then use the default level. - /// - /// Examples: - /// - /// A program wants to include a lot of debugging output, but the library - /// "hyper" is known to work well, so debug output from it should be - /// excluded: - /// - /// ``` - /// # fn main() { - /// fern::Dispatch::new() - /// .level(log::LevelFilter::Trace) - /// .level_for("hyper", log::LevelFilter::Info) - /// # .into_log(); - /// # } - /// ``` - /// - /// A program has a ton of debug output per-module, but there is so much - /// that debugging more than one module at a time is not very useful. - /// The command line accepts a list of modules to debug, while keeping the - /// rest of the program at info level: - /// - /// ``` - /// fn setup_logging(verbose_modules: T) -> Result<(), fern::InitError> - /// where - /// I: AsRef, - /// T: IntoIterator, - /// { - /// let mut config = fern::Dispatch::new().level(log::LevelFilter::Info); - /// - /// for module_name in verbose_modules { - /// config = config.level_for( - /// format!("my_crate_name::{}", module_name.as_ref()), - /// log::LevelFilter::Debug, - /// ); - /// } - /// - /// config.chain(std::io::stdout()).apply()?; - /// - /// Ok(()) - /// } - /// # - /// # // we're ok with apply() failing. - /// # fn main() { let _ = setup_logging(&["hi"]); } - /// ``` - #[inline] - pub fn level_for>>( - mut self, - module: T, - level: log::LevelFilter, - ) -> Self { - let module = module.into(); - - if let Some((index, _)) = self - .levels - .iter() - .enumerate() - .find(|&(_, &(ref name, _))| name == &module) - { - self.levels.remove(index); - } - - self.levels.push((module, level)); - self - } - - /// Adds a custom filter which can reject messages passing through this - /// logger. - /// - /// The logger will continue to process log records only if all filters - /// return `true`. - /// - /// [`Dispatch::level`] and [`Dispatch::level_for`] are preferred if - /// applicable. - /// - /// [`Dispatch::level`]: #method.level - /// [`Dispatch::level_for`]: #method.level_for - /// - /// Example usage: - /// - /// This sends error level messages to stderr and others to stdout. - /// - /// ``` - /// # fn main() { - /// fern::Dispatch::new() - /// .level(log::LevelFilter::Info) - /// .chain( - /// fern::Dispatch::new() - /// .filter(|metadata| { - /// // Reject messages with the `Error` log level. - /// metadata.level() != log::LevelFilter::Error - /// }) - /// .chain(std::io::stderr()), - /// ) - /// .chain( - /// fern::Dispatch::new() - /// .level(log::LevelFilter::Error) - /// .chain(std::io::stdout()), - /// ) - /// # .into_log(); - /// # } - #[inline] - pub fn filter(mut self, filter: F) -> Self - where - F: Fn(&log::Metadata) -> bool + Send + Sync + 'static, - { - self.filters.push(Box::new(filter)); - self - } - - /// Builds this dispatch and stores it in a clonable structure containing - /// an [`Arc`]. - /// - /// Once "shared", the dispatch can be used as an output for multiple other - /// dispatch loggers. - /// - /// Example usage: - /// - /// This separates info and warn messages, sending info to stdout + a log - /// file, and warn to stderr + the same log file. Shared is used so the - /// program only opens "file.log" once. - /// - /// ```no_run - /// # fn setup_logger() -> Result<(), fern::InitError> { - /// - /// let file_out = fern::Dispatch::new() - /// .chain(fern::log_file("file.log")?) - /// .into_shared(); - /// - /// let info_out = fern::Dispatch::new() - /// .level(log::LevelFilter::Debug) - /// .filter(|metadata| - /// // keep only info and debug (reject warn and error) - /// metadata.level() <= log::Level::Info) - /// .chain(std::io::stdout()) - /// .chain(file_out.clone()); - /// - /// let warn_out = fern::Dispatch::new() - /// .level(log::LevelFilter::Warn) - /// .chain(std::io::stderr()) - /// .chain(file_out); - /// - /// fern::Dispatch::new() - /// .chain(info_out) - /// .chain(warn_out) - /// .apply(); - /// - /// # Ok(()) - /// # } - /// # - /// # fn main() { setup_logger().expect("failed to set up logger"); } - /// ``` - /// - /// [`Arc`]: https://doc.rust-lang.org/std/sync/struct.Arc.html - pub fn into_shared(self) -> SharedDispatch { - let (min_level, dispatch) = self.into_dispatch(); - - SharedDispatch { - inner: Arc::new(dispatch), - min_level, - } - } - - /// Builds this into the actual logger implementation. - /// - /// This could probably be refactored, but having everything in one place - /// is also nice. - fn into_dispatch(self) -> (log::LevelFilter, log_impl::Dispatch) { - let Dispatch { - format, - children, - default_level, - levels, - mut filters, - } = self; - - let mut max_child_level = log::LevelFilter::Off; - - let output = children - .into_iter() - .flat_map(|child| match child { - OutputInner::Stdout { stream, line_sep } => { - max_child_level = log::LevelFilter::Trace; - Some(log_impl::Output::Stdout(log_impl::Stdout { - stream, - line_sep, - })) - } - OutputInner::Stderr { stream, line_sep } => { - max_child_level = log::LevelFilter::Trace; - Some(log_impl::Output::Stderr(log_impl::Stderr { - stream, - line_sep, - })) - } - OutputInner::File { stream, line_sep } => { - max_child_level = log::LevelFilter::Trace; - Some(log_impl::Output::File(log_impl::File { - stream: Mutex::new(io::BufWriter::new(stream)), - line_sep, - })) - } - OutputInner::Writer { stream, line_sep } => { - max_child_level = log::LevelFilter::Trace; - Some(log_impl::Output::Writer(log_impl::Writer { - stream: Mutex::new(stream), - line_sep, - })) - } - #[cfg(all(not(windows), feature = "reopen-03"))] - OutputInner::Reopen { stream, line_sep } => { - max_child_level = log::LevelFilter::Trace; - Some(log_impl::Output::Reopen(log_impl::Reopen { - stream: Mutex::new(stream), - line_sep, - })) - } - OutputInner::Sender { stream, line_sep } => { - max_child_level = log::LevelFilter::Trace; - Some(log_impl::Output::Sender(log_impl::Sender { - stream: Mutex::new(stream), - line_sep, - })) - } - #[cfg(all(not(windows), feature = "syslog-3"))] - OutputInner::Syslog3(log) => { - max_child_level = log::LevelFilter::Trace; - Some(log_impl::Output::Syslog3(log_impl::Syslog3 { inner: log })) - } - #[cfg(all(not(windows), feature = "syslog-4"))] - OutputInner::Syslog4Rfc3164(logger) => { - max_child_level = log::LevelFilter::Trace; - Some(log_impl::Output::Syslog4Rfc3164(log_impl::Syslog4Rfc3164 { - inner: Mutex::new(logger), - })) - } - #[cfg(all(not(windows), feature = "syslog-4"))] - OutputInner::Syslog4Rfc5424 { logger, transform } => { - max_child_level = log::LevelFilter::Trace; - Some(log_impl::Output::Syslog4Rfc5424(log_impl::Syslog4Rfc5424 { - inner: Mutex::new(logger), - transform, - })) - } - OutputInner::Panic => { - max_child_level = log::LevelFilter::Trace; - Some(log_impl::Output::Panic(log_impl::Panic)) - } - OutputInner::Dispatch(child_dispatch) => { - let (child_level, child) = child_dispatch.into_dispatch(); - if child_level > log::LevelFilter::Off { - max_child_level = cmp::max(max_child_level, child_level); - Some(log_impl::Output::Dispatch(child)) - } else { - None - } - } - OutputInner::SharedDispatch(child_dispatch) => { - let SharedDispatch { - inner: child, - min_level: child_level, - } = child_dispatch; - - if child_level > log::LevelFilter::Off { - max_child_level = cmp::max(max_child_level, child_level); - Some(log_impl::Output::SharedDispatch(child)) - } else { - None - } - } - OutputInner::OtherBoxed(child_log) => { - max_child_level = log::LevelFilter::Trace; - Some(log_impl::Output::OtherBoxed(child_log)) - } - OutputInner::OtherStatic(child_log) => { - max_child_level = log::LevelFilter::Trace; - Some(log_impl::Output::OtherStatic(child_log)) - } - #[cfg(feature = "date-based")] - OutputInner::DateBased { config } => { - max_child_level = log::LevelFilter::Trace; - - let config = log_impl::DateBasedConfig::new( - config.line_sep, - config.file_prefix, - config.file_suffix, - if config.utc_time { - log_impl::ConfiguredTimezone::Utc - } else { - log_impl::ConfiguredTimezone::Local - }, - ); - - let computed_suffix = config.compute_current_suffix(); - - // ignore errors - we'll just retry later. - let initial_file = config.open_current_log_file(&computed_suffix).ok(); - - Some(log_impl::Output::DateBased(log_impl::DateBased { - config, - state: Mutex::new(DateBasedState::new(computed_suffix, initial_file)), - })) - } - }) - .collect(); - - let min_level = levels - .iter() - .map(|t| t.1) - .max() - .map_or(default_level, |lvl| cmp::max(lvl, default_level)); - let real_min = cmp::min(min_level, max_child_level); - - filters.shrink_to_fit(); - - let dispatch = log_impl::Dispatch { - output: output, - default_level: default_level, - levels: levels.into(), - format: format, - filters: filters, - }; - - (real_min, dispatch) - } - - /// Builds this logger into a `Box` and calculates the minimum - /// log level needed to have any effect. - /// - /// While this method is exposed publicly, [`Dispatch::apply`] is typically - /// used instead. - /// - /// The returned LevelFilter is a calculation for all level filters of this - /// logger and child loggers, and is the minimum log level needed to - /// for a record to have any chance of passing through this logger. - /// - /// [`Dispatch::apply`]: #method.apply - /// - /// Example usage: - /// - /// ``` - /// # fn main() { - /// let (min_level, log) = fern::Dispatch::new() - /// .level(log::LevelFilter::Info) - /// .chain(std::io::stdout()) - /// .into_log(); - /// - /// assert_eq!(min_level, log::LevelFilter::Info); - /// # } - /// ``` - pub fn into_log(self) -> (log::LevelFilter, Box) { - let (level, logger) = self.into_dispatch(); - if level == log::LevelFilter::Off { - (level, Box::new(log_impl::Null)) - } else { - (level, Box::new(logger)) - } - } - - /// Builds this logger and instantiates it as the global [`log`] logger. - /// - /// # Errors: - /// - /// This function will return an error if a global logger has already been - /// set to a previous logger. - /// - /// [`log`]: https://github.com/rust-lang-nursery/log - pub fn apply(self) -> Result<(), log::SetLoggerError> { - let (max_level, log) = self.into_log(); - - log::set_boxed_logger(log)?; - log::set_max_level(max_level); - - Ok(()) - } -} - -/// This enum contains various outputs that you can send messages to. -enum OutputInner { - /// Prints all messages to stdout with `line_sep` separator. - Stdout { - stream: io::Stdout, - line_sep: Cow<'static, str>, - }, - /// Prints all messages to stderr with `line_sep` separator. - Stderr { - stream: io::Stderr, - line_sep: Cow<'static, str>, - }, - /// Writes all messages to file with `line_sep` separator. - File { - stream: fs::File, - line_sep: Cow<'static, str>, - }, - /// Writes all messages to the writer with `line_sep` separator. - Writer { - stream: Box, - line_sep: Cow<'static, str>, - }, - /// Writes all messages to the reopen::Reopen file with `line_sep` - /// separator. - #[cfg(all(not(windows), feature = "reopen-03"))] - Reopen { - stream: reopen::Reopen, - line_sep: Cow<'static, str>, - }, - /// Writes all messages to mpst::Sender with `line_sep` separator. - Sender { - stream: Sender, - line_sep: Cow<'static, str>, - }, - /// Passes all messages to other dispatch. - Dispatch(Dispatch), - /// Passes all messages to other dispatch that's shared. - SharedDispatch(SharedDispatch), - /// Passes all messages to other logger. - OtherBoxed(Box), - /// Passes all messages to other logger. - OtherStatic(&'static dyn Log), - /// Passes all messages to the syslog. - #[cfg(all(not(windows), feature = "syslog-3"))] - Syslog3(syslog3::Logger), - /// Passes all messages to the syslog. - #[cfg(all(not(windows), feature = "syslog-4"))] - Syslog4Rfc3164(Syslog4Rfc3164Logger), - /// Sends all messages through the transform then passes to the syslog. - #[cfg(all(not(windows), feature = "syslog-4"))] - Syslog4Rfc5424 { - logger: Syslog4Rfc5424Logger, - transform: Box< - dyn Fn(&log::Record) -> (i32, HashMap>, String) - + Sync - + Send, - >, - }, - /// Panics with messages text for all messages. - Panic, - /// File logger with custom date and timestamp suffix in file name. - #[cfg(feature = "date-based")] - DateBased { config: DateBased }, -} - -/// Logger which will panic whenever anything is logged. The panic -/// will be exactly the message of the log. -/// -/// `Panic` is useful primarily as a secondary logger, filtered by warning or -/// error. -/// -/// # Examples -/// -/// This configuration will output all messages to stdout and panic if an Error -/// message is sent. -/// -/// ``` -/// fern::Dispatch::new() -/// // format, etc. -/// .chain(std::io::stdout()) -/// .chain( -/// fern::Dispatch::new() -/// .level(log::LevelFilter::Error) -/// .chain(fern::Panic), -/// ) -/// # /* -/// .apply()?; -/// # */ .into_log(); -/// ``` -/// -/// This sets up a "panic on warn+" logger, and ignores errors so it can be -/// called multiple times. -/// -/// This might be useful in test setup, for example, to disallow warn-level -/// messages. -/// -/// ```no_run -/// fn setup_panic_logging() { -/// fern::Dispatch::new() -/// .level(log::LevelFilter::Warn) -/// .chain(fern::Panic) -/// .apply() -/// // ignore errors from setting up logging twice -/// .ok(); -/// } -/// ``` -pub struct Panic; - -/// Configuration for a logger output. -pub struct Output(OutputInner); - -impl From for Output { - /// Creates an output logger forwarding all messages to the dispatch. - fn from(log: Dispatch) -> Self { - Output(OutputInner::Dispatch(log)) - } -} - -impl From for Output { - /// Creates an output logger forwarding all messages to the dispatch. - fn from(log: SharedDispatch) -> Self { - Output(OutputInner::SharedDispatch(log)) - } -} - -impl From> for Output { - /// Creates an output logger forwarding all messages to the custom logger. - fn from(log: Box) -> Self { - Output(OutputInner::OtherBoxed(log)) - } -} - -impl From<&'static dyn Log> for Output { - /// Creates an output logger forwarding all messages to the custom logger. - fn from(log: &'static dyn Log) -> Self { - Output(OutputInner::OtherStatic(log)) - } -} - -impl From for Output { - /// Creates an output logger which writes all messages to the file with - /// `\n` as the separator. - /// - /// File writes are buffered and flushed once per log record. - fn from(file: fs::File) -> Self { - Output(OutputInner::File { - stream: file, - line_sep: "\n".into(), - }) - } -} - -impl From> for Output { - /// Creates an output logger which writes all messages to the writer with - /// `\n` as the separator. - /// - /// This does no buffering and it is up to the writer to do buffering as - /// needed (eg. wrap it in `BufWriter`). However, flush is called after - /// each log record. - fn from(writer: Box) -> Self { - Output(OutputInner::Writer { - stream: writer, - line_sep: "\n".into(), - }) - } -} - -#[cfg(all(not(windows), feature = "reopen-03"))] -impl From> for Output { - /// Creates an output logger which writes all messages to the file contained - /// in the Reopen struct, using `\n` as the separator. - fn from(reopen: reopen::Reopen) -> Self { - Output(OutputInner::Reopen { - stream: reopen, - line_sep: "\n".into(), - }) - } -} - -impl From for Output { - /// Creates an output logger which writes all messages to stdout with the - /// given handle and `\n` as the separator. - fn from(stream: io::Stdout) -> Self { - Output(OutputInner::Stdout { - stream, - line_sep: "\n".into(), - }) - } -} - -impl From for Output { - /// Creates an output logger which writes all messages to stderr with the - /// given handle and `\n` as the separator. - fn from(stream: io::Stderr) -> Self { - Output(OutputInner::Stderr { - stream, - line_sep: "\n".into(), - }) - } -} - -impl From> for Output { - /// Creates an output logger which writes all messages to the given - /// mpsc::Sender with '\n' as the separator. - /// - /// All messages sent to the mpsc channel are suffixed with '\n'. - fn from(stream: Sender) -> Self { - Output(OutputInner::Sender { - stream, - line_sep: "\n".into(), - }) - } -} - -#[cfg(all(not(windows), feature = "syslog-3"))] -impl From for Output { - /// Creates an output logger which writes all messages to the given syslog - /// output. - /// - /// Log levels are translated trace => debug, debug => debug, info => - /// informational, warn => warning, and error => error. - /// - /// This requires the `"syslog-3"` feature. - fn from(log: syslog3::Logger) -> Self { - Output(OutputInner::Syslog3(log)) - } -} - -#[cfg(all(not(windows), feature = "syslog-3"))] -impl From> for Output { - /// Creates an output logger which writes all messages to the given syslog - /// output. - /// - /// Log levels are translated trace => debug, debug => debug, info => - /// informational, warn => warning, and error => error. - /// - /// Note that while this takes a Box for convenience (syslog - /// methods return Boxes), it will be immediately unboxed upon storage - /// in the configuration structure. This will create a configuration - /// identical to that created by passing a raw `syslog::Logger`. - /// - /// This requires the `"syslog-3"` feature. - fn from(log: Box) -> Self { - Output(OutputInner::Syslog3(*log)) - } -} - -#[cfg(all(not(windows), feature = "syslog-4"))] -impl From for Output { - /// Creates an output logger which writes all messages to the given syslog. - /// - /// Log levels are translated trace => debug, debug => debug, info => - /// informational, warn => warning, and error => error. - /// - /// Note that due to https://github.com/Geal/rust-syslog/issues/41, - /// logging to this backend requires one allocation per log call. - /// - /// This is for RFC 3164 loggers. To use an RFC 5424 logger, use the - /// [`Output::syslog_5424`] helper method. - /// - /// This requires the `"syslog-4"` feature. - fn from(log: Syslog4Rfc3164Logger) -> Self { - Output(OutputInner::Syslog4Rfc3164(log)) - } -} - -impl From for Output { - /// Creates an output logger which will panic with message text for all - /// messages. - fn from(_: Panic) -> Self { - Output(OutputInner::Panic) - } -} - -impl Output { - /// Returns a file logger using a custom separator. - /// - /// If the default separator of `\n` is acceptable, an [`fs::File`] - /// instance can be passed into [`Dispatch::chain`] directly. - /// - /// ```no_run - /// # fn setup_logger() -> Result<(), fern::InitError> { - /// fern::Dispatch::new().chain(std::fs::File::create("log")?) - /// # .into_log(); - /// # Ok(()) - /// # } - /// # - /// # fn main() { setup_logger().expect("failed to set up logger"); } - /// ``` - /// - /// ```no_run - /// # fn setup_logger() -> Result<(), fern::InitError> { - /// fern::Dispatch::new().chain(fern::log_file("log")?) - /// # .into_log(); - /// # Ok(()) - /// # } - /// # - /// # fn main() { setup_logger().expect("failed to set up logger"); } - /// ``` - /// - /// Example usage (using [`fern::log_file`]): - /// - /// ```no_run - /// # fn setup_logger() -> Result<(), fern::InitError> { - /// fern::Dispatch::new().chain(fern::Output::file(fern::log_file("log")?, "\r\n")) - /// # .into_log(); - /// # Ok(()) - /// # } - /// # - /// # fn main() { setup_logger().expect("failed to set up logger"); } - /// ``` - /// - /// [`fs::File`]: https://doc.rust-lang.org/std/fs/struct.File.html - /// [`Dispatch::chain`]: struct.Dispatch.html#method.chain - /// [`fern::log_file`]: fn.log_file.html - pub fn file>>(file: fs::File, line_sep: T) -> Self { - Output(OutputInner::File { - stream: file, - line_sep: line_sep.into(), - }) - } - - /// Returns a logger using arbitrary write object and custom separator. - /// - /// If the default separator of `\n` is acceptable, an `Box` - /// instance can be passed into [`Dispatch::chain`] directly. - /// - /// ```no_run - /// # fn setup_logger() -> Result<(), fern::InitError> { - /// // Anything implementing 'Write' works. - /// let mut writer = std::io::Cursor::new(Vec::::new()); - /// - /// fern::Dispatch::new() - /// // as long as we explicitly cast into a type-erased Box - /// .chain(Box::new(writer) as Box) - /// # .into_log(); - /// # Ok(()) - /// # } - /// # - /// # fn main() { setup_logger().expect("failed to set up logger"); } - /// ``` - /// - /// Example usage: - /// - /// ```no_run - /// # fn setup_logger() -> Result<(), fern::InitError> { - /// let writer = Box::new(std::io::Cursor::new(Vec::::new())); - /// - /// fern::Dispatch::new().chain(fern::Output::writer(writer, "\r\n")) - /// # .into_log(); - /// # Ok(()) - /// # } - /// # - /// # fn main() { setup_logger().expect("failed to set up logger"); } - /// ``` - /// - /// [`Dispatch::chain`]: struct.Dispatch.html#method.chain - pub fn writer>>(writer: Box, line_sep: T) -> Self { - Output(OutputInner::Writer { - stream: writer, - line_sep: line_sep.into(), - }) - } - - /// Returns a reopenable logger, i.e., handling SIGHUP. - /// - /// If the default separator of `\n` is acceptable, a `Reopen` - /// instance can be passed into [`Dispatch::chain`] directly. - /// - /// This function is not available on Windows, and it requires the `reopen-03` - /// feature to be enabled. - /// - /// ```no_run - /// use std::fs::OpenOptions; - /// # fn setup_logger() -> Result<(), fern::InitError> { - /// let reopenable = reopen::Reopen::new(Box::new(|| { - /// OpenOptions::new() - /// .create(true) - /// .write(true) - /// .append(true) - /// .open("/tmp/output.log") - /// })) - /// .unwrap(); - /// - /// fern::Dispatch::new().chain(fern::Output::reopen(reopenable, "\n")) - /// # .into_log(); - /// # Ok(()) - /// # } - /// # - /// # fn main() { setup_logger().expect("failed to set up logger"); } - /// ``` - /// [`Dispatch::chain`]: struct.Dispatch.html#method.chain - #[cfg(all(not(windows), feature = "reopen-03"))] - pub fn reopen>>( - reopen: reopen::Reopen, - line_sep: T, - ) -> Self { - Output(OutputInner::Reopen { - stream: reopen, - line_sep: line_sep.into(), - }) - } - - /// Returns an stdout logger using a custom separator. - /// - /// If the default separator of `\n` is acceptable, an `io::Stdout` - /// instance can be passed into `Dispatch::chain()` directly. - /// - /// ``` - /// fern::Dispatch::new().chain(std::io::stdout()) - /// # .into_log(); - /// ``` - /// - /// Example usage: - /// - /// ``` - /// fern::Dispatch::new() - /// // some unix tools use null bytes as message terminators so - /// // newlines in messages can be treated differently. - /// .chain(fern::Output::stdout("\0")) - /// # .into_log(); - /// ``` - pub fn stdout>>(line_sep: T) -> Self { - Output(OutputInner::Stdout { - stream: io::stdout(), - line_sep: line_sep.into(), - }) - } - - /// Returns an stderr logger using a custom separator. - /// - /// If the default separator of `\n` is acceptable, an `io::Stderr` - /// instance can be passed into `Dispatch::chain()` directly. - /// - /// ``` - /// fern::Dispatch::new().chain(std::io::stderr()) - /// # .into_log(); - /// ``` - /// - /// Example usage: - /// - /// ``` - /// fern::Dispatch::new().chain(fern::Output::stderr("\n\n\n")) - /// # .into_log(); - /// ``` - pub fn stderr>>(line_sep: T) -> Self { - Output(OutputInner::Stderr { - stream: io::stderr(), - line_sep: line_sep.into(), - }) - } - - /// Returns a mpsc::Sender logger using a custom separator. - /// - /// If the default separator of `\n` is acceptable, an - /// `mpsc::Sender` instance can be passed into `Dispatch:: - /// chain()` directly. - /// - /// Each log message will be suffixed with the separator, then sent as a - /// single String to the given sender. - /// - /// ``` - /// use std::sync::mpsc::channel; - /// - /// let (tx, rx) = channel(); - /// fern::Dispatch::new().chain(tx) - /// # .into_log(); - /// ``` - pub fn sender>>(sender: Sender, line_sep: T) -> Self { - Output(OutputInner::Sender { - stream: sender, - line_sep: line_sep.into(), - }) - } - - /// Returns a logger which logs into an RFC5424 syslog. - /// - /// This method takes an additional transform method to turn the log data - /// into RFC5424 data. - /// - /// I've honestly got no clue what the expected keys and values are for - /// this kind of logging, so I'm just going to link [the rfc] instead. - /// - /// If you're an expert on syslog logging and would like to contribute - /// an example to put here, it would be gladly accepted! - /// - /// This requires the `"syslog-4"` feature. - /// - /// [the rfc]: https://tools.ietf.org/html/rfc5424 - #[cfg(all(not(windows), feature = "syslog-4"))] - pub fn syslog_5424(logger: Syslog4Rfc5424Logger, transform: F) -> Self - where - F: Fn(&log::Record) -> (i32, HashMap>, String) - + Sync - + Send - + 'static, - { - Output(OutputInner::Syslog4Rfc5424 { - logger, - transform: Box::new(transform), - }) - } - - /// Returns a logger which simply calls the given function with each - /// message. - /// - /// The function will be called inline in the thread the log occurs on. - /// - /// Example usage: - /// - /// ``` - /// fern::Dispatch::new().chain(fern::Output::call(|record| { - /// // this is mundane, but you can do anything here. - /// println!("{}", record.args()); - /// })) - /// # .into_log(); - /// ``` - pub fn call(func: F) -> Self - where - F: Fn(&log::Record) + Sync + Send + 'static, - { - struct CallShim(F); - impl log::Log for CallShim - where - F: Fn(&log::Record) + Sync + Send + 'static, - { - fn enabled(&self, _: &log::Metadata) -> bool { - true - } - fn log(&self, record: &log::Record) { - (self.0)(record) - } - fn flush(&self) {} - } - - Self::from(Box::new(CallShim(func)) as Box) - } -} - -impl Default for Dispatch { - /// Returns a logger configuration that does nothing with log records. - /// - /// Equivalent to [`Dispatch::new`]. - /// - /// [`Dispatch::new`]: #method.new - fn default() -> Self { - Self::new() - } -} - -impl fmt::Debug for Dispatch { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - struct LevelsDebug<'a>(&'a [(Cow<'static, str>, log::LevelFilter)]); - impl<'a> fmt::Debug for LevelsDebug<'a> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_map() - .entries(self.0.iter().map(|t| (t.0.as_ref(), t.1))) - .finish() - } - } - struct FiltersDebug<'a>(&'a [Box]); - impl<'a> fmt::Debug for FiltersDebug<'a> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_list() - .entries(self.0.iter().map(|_| "")) - .finish() - } - } - f.debug_struct("Dispatch") - .field( - "format", - &self.format.as_ref().map(|_| ""), - ) - .field("children", &self.children) - .field("default_level", &self.default_level) - .field("levels", &LevelsDebug(&self.levels)) - .field("filters", &FiltersDebug(&self.filters)) - .finish() - } -} - -impl fmt::Debug for OutputInner { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - OutputInner::Stdout { - ref stream, - ref line_sep, - } => f - .debug_struct("Output::Stdout") - .field("stream", stream) - .field("line_sep", line_sep) - .finish(), - OutputInner::Stderr { - ref stream, - ref line_sep, - } => f - .debug_struct("Output::Stderr") - .field("stream", stream) - .field("line_sep", line_sep) - .finish(), - OutputInner::File { - ref stream, - ref line_sep, - } => f - .debug_struct("Output::File") - .field("stream", stream) - .field("line_sep", line_sep) - .finish(), - OutputInner::Writer { ref line_sep, .. } => f - .debug_struct("Output::Writer") - .field("stream", &"") - .field("line_sep", line_sep) - .finish(), - #[cfg(all(not(windows), feature = "reopen-03"))] - OutputInner::Reopen { ref line_sep, .. } => f - .debug_struct("Output::Reopen") - .field("stream", &"") - .field("line_sep", line_sep) - .finish(), - OutputInner::Sender { - ref stream, - ref line_sep, - } => f - .debug_struct("Output::Sender") - .field("stream", stream) - .field("line_sep", line_sep) - .finish(), - #[cfg(all(not(windows), feature = "syslog-3"))] - OutputInner::Syslog3(_) => f - .debug_tuple("Output::Syslog3") - .field(&"") - .finish(), - #[cfg(all(not(windows), feature = "syslog-4"))] - OutputInner::Syslog4Rfc3164 { .. } => f - .debug_tuple("Output::Syslog4Rfc3164") - .field(&"") - .finish(), - #[cfg(all(not(windows), feature = "syslog-4"))] - OutputInner::Syslog4Rfc5424 { .. } => f - .debug_tuple("Output::Syslog4Rfc5424") - .field(&"") - .finish(), - OutputInner::Dispatch(ref dispatch) => { - f.debug_tuple("Output::Dispatch").field(dispatch).finish() - } - OutputInner::SharedDispatch(_) => f - .debug_tuple("Output::SharedDispatch") - .field(&"") - .finish(), - OutputInner::OtherBoxed { .. } => f - .debug_tuple("Output::OtherBoxed") - .field(&"") - .finish(), - OutputInner::OtherStatic { .. } => f - .debug_tuple("Output::OtherStatic") - .field(&"") - .finish(), - OutputInner::Panic => f.debug_tuple("Output::Panic").finish(), - #[cfg(feature = "date-based")] - OutputInner::DateBased { ref config } => f - .debug_struct("Output::DateBased") - .field("config", config) - .finish(), - } - } -} - -impl fmt::Debug for Output { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -/// This is used to generate log file suffixed based on date, hour, and minute. -/// -/// The log file will be rotated automatically when the date changes. -#[derive(Debug)] -#[cfg(feature = "date-based")] -pub struct DateBased { - file_prefix: PathBuf, - file_suffix: Cow<'static, str>, - line_sep: Cow<'static, str>, - utc_time: bool, -} - -#[cfg(feature = "date-based")] -impl DateBased { - /// Create new date-based file logger with the given file prefix and - /// strftime-based suffix pattern. - /// - /// On initialization, fern will create a file with the suffix formatted - /// with the current time (either utc or local, see below). Each time a - /// record is logged, the format is checked against the current time, and if - /// the time has changed, the old file is closed and a new one opened. - /// - /// `file_suffix` will be interpreted as an `strftime` format. See - /// [`chrono::format::strftime`] for more information. - /// - /// `file_prefix` may be a full file path, and will be prepended to the - /// suffix to create the final file. - /// - /// Note that no separator will be placed in between `file_name` and - /// `file_suffix_pattern`. So if you call `DateBased::new("hello", - /// "%Y")`, the result will be a filepath `hello2019`. - /// - /// By default, this will use local time. For UTC time instead, use the - /// [`.utc_time()`][DateBased::utc_time] method after creating. - /// - /// By default, this will use `\n` as a line separator. For a custom - /// separator, use the [`.line_sep`][DateBased::line_sep] method - /// after creating. - /// - /// # Examples - /// - /// Containing the date (year, month and day): - /// - /// ``` - /// // logs/2019-10-23-my-program.log - /// let log = fern::DateBased::new("logs/", "%Y-%m-%d-my-program.log"); - /// - /// // program.log.23102019 - /// let log = fern::DateBased::new("my-program.log.", "%d%m%Y"); - /// ``` - /// - /// Containing the hour: - /// - /// ``` - /// // logs/2019-10-23 13 my-program.log - /// let log = fern::DateBased::new("logs/", "%Y-%m-%d %H my-program.log"); - /// - /// // program.log.2310201913 - /// let log = fern::DateBased::new("my-program.log.", "%d%m%Y%H"); - /// ``` - /// - /// Containing the minute: - /// - /// ``` - /// // logs/2019-10-23 13 my-program.log - /// let log = fern::DateBased::new("logs/", "%Y-%m-%d %H my-program.log"); - /// - /// // program.log.2310201913 - /// let log = fern::DateBased::new("my-program.log.", "%d%m%Y%H"); - /// ``` - /// - /// UNIX time, or seconds since 00:00 Jan 1st 1970: - /// - /// ``` - /// // logs/1571822854-my-program.log - /// let log = fern::DateBased::new("logs/", "%s-my-program.log"); - /// - /// // program.log.1571822854 - /// let log = fern::DateBased::new("my-program.log.", "%s"); - /// ``` - /// - /// Hourly, using UTC time: - /// - /// ``` - /// // logs/2019-10-23 23 my-program.log - /// let log = fern::DateBased::new("logs/", "%Y-%m-%d %H my-program.log").utc_time(); - /// - /// // program.log.2310201923 - /// let log = fern::DateBased::new("my-program.log.", "%d%m%Y%H").utc_time(); - /// ``` - /// - /// [`chrono::format::strftime`]: https://docs.rs/chrono/0.4.6/chrono/format/strftime/index.html - pub fn new(file_prefix: T, file_suffix: U) -> Self - where - T: AsRef, - U: Into>, - { - DateBased { - utc_time: false, - file_prefix: file_prefix.as_ref().to_owned(), - file_suffix: file_suffix.into(), - line_sep: "\n".into(), - } - } - - /// Changes the line separator this logger will use. - /// - /// The default line separator is `\n`. - /// - /// # Examples - /// - /// Using a windows line separator: - /// - /// ``` - /// let log = fern::DateBased::new("logs", "%s.log").line_sep("\r\n"); - /// ``` - pub fn line_sep(mut self, line_sep: T) -> Self - where - T: Into>, - { - self.line_sep = line_sep.into(); - self - } - - /// Orients this log file suffix formatting to use UTC time. - /// - /// The default is local time. - /// - /// # Examples - /// - /// This will use UTC time to determine the date: - /// - /// ``` - /// // program.log.2310201923 - /// let log = fern::DateBased::new("my-program.log.", "%d%m%Y%H").utc_time(); - /// ``` - pub fn utc_time(mut self) -> Self { - self.utc_time = true; - self - } - - /// Orients this log file suffix formatting to use local time. - /// - /// This is the default option. - /// - /// # Examples - /// - /// This log file will use local time - the latter method call overrides the - /// former. - /// - /// ``` - /// // program.log.2310201923 - /// let log = fern::DateBased::new("my-program.log.", "%d%m%Y%H") - /// .utc_time() - /// .local_time(); - /// ``` - pub fn local_time(mut self) -> Self { - self.utc_time = false; - self - } -} - -#[cfg(feature = "date-based")] -impl From for Output { - /// Create an output logger which defers to the given date-based logger. Use - /// configuration methods on [DateBased] to set line separator and filename. - fn from(config: DateBased) -> Self { - Output(OutputInner::DateBased { config }) - } -} diff --git a/src/colors.rs b/src/colors.rs index efe495d..18678bb 100644 --- a/src/colors.rs +++ b/src/colors.rs @@ -82,8 +82,8 @@ pub struct WithFgColor where T: fmt::Display, { - text: T, - color: Color, + pub(super) text: T, + pub(super) color: Option, } impl fmt::Display for WithFgColor @@ -91,9 +91,13 @@ where T: fmt::Display, { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "\x1B[{}m", self.color.to_fg_str())?; - fmt::Display::fmt(&self.text, f)?; - write!(f, "\x1B[0m")?; + if let Some(color) = self.color { + write!(f, "\x1B[{}m", color.to_fg_str())?; + fmt::Display::fmt(&self.text, f)?; + write!(f, "\x1B[0m")?; + } else { + fmt::Display::fmt(&self.text, f)?; + } Ok(()) } } @@ -123,7 +127,7 @@ where /// # */ /// # .into_log(); /// ``` -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug)] #[must_use = "builder methods take config by value and thus must be reassigned to variable"] pub struct ColoredLevelConfig { /// The color to color logs with the [`Error`] level. @@ -246,27 +250,18 @@ impl ColoredLevelConfig { impl Default for ColoredLevelConfig { /// Retrieves the default configuration. This has: /// - /// - [`Error`] as [`Color::Red`] - /// - [`Warn`] as [`Color::Yellow`] - /// - [`Info`] as [`Color::White`] - /// - [`Debug`] as [`Color::White`] - /// - [`Trace`] as [`Color::White`] - /// - /// [`Error`]: https://docs.rs/log/0.4/log/enum.Level.html#variant.Error - /// [`Warn`]: https://docs.rs/log/0.4/log/enum.Level.html#variant.Warn - /// [`Info`]: https://docs.rs/log/0.4/log/enum.Level.html#variant.Info - /// [`Debug`]: https://docs.rs/log/0.4/log/enum.Level.html#variant.Debug - /// [`Trace`]: https://docs.rs/log/0.4/log/enum.Level.html#variant.Trace - /// [`Color::White`]: https://docs.rs/colored/1/colored/enum.Color.html#variant.White - /// [`Color::Yellow`]: https://docs.rs/colored/1/colored/enum.Color.html#variant.Yellow - /// [`Color::Red`]: https://docs.rs/colored/1/colored/enum.Color.html#variant.Red + /// - [`Error`](log::Level::Error) as [`Color::Red`] + /// - [`Warn`](log::Level::Warn) as [`Color::Yellow`] + /// - [`Info`](log::Level::Info) as [`Color::White`] + /// - [`Debug`](log::Level::Debug) as [`Color::White`] + /// - [`Trace`](log::Level::Trace) as [`Color::White`] fn default() -> Self { ColoredLevelConfig { error: Color::Red, warn: Color::Yellow, - debug: Color::White, - info: Color::White, - trace: Color::White, + info: Color::Cyan, + debug: Color::Magenta, + trace: Color::BrightMagenta, } } } @@ -275,7 +270,7 @@ impl ColoredLogLevel for Level { fn colored(&self, color: Color) -> WithFgColor { WithFgColor { text: *self, - color: color, + color: Some(color), } } } @@ -313,7 +308,7 @@ mod test { "{}", WithFgColor { text: "test", - color: color, + color: Some(color), } ) ); @@ -327,7 +322,7 @@ mod test { "{:^8}", WithFgColor { text: "test", - color: Yellow, + color: Some(Yellow), } ); assert!(s.contains(" test ")); diff --git a/src/dispatch.rs b/src/dispatch.rs new file mode 100644 index 0000000..f076472 --- /dev/null +++ b/src/dispatch.rs @@ -0,0 +1,1047 @@ +use std::{ + borrow::Cow, + cmp, fmt, + sync::Arc, +}; + +use std::collections::HashMap; + +use log::Log; + +use crate::{Filter, Formatter}; +use crate::logger; + +/// The base dispatch logger. +/// +/// This allows for formatting log records, limiting what records can be passed +/// through, and then dispatching records to other dispatch loggers or output +/// loggers. +/// +/// Note that all methods are position-insensitive. +/// `Dispatch::new().format(a).chain(b)` produces the exact same result +/// as `Dispatch::new().chain(b).format(a)`. Given this, it is preferred to put +/// 'format' and other modifiers before 'chain' for the sake of clarity. +/// +/// Example usage demonstrating all features: +/// +/// ```no_run +/// # // no_run because this creates log files. +/// use std::{fs, io}; +/// +/// # fn setup_logger() -> Result<(), fern::InitError> { +/// fern::Dispatch::new() +/// .format(|out, message, record| { +/// out.finish(format_args!( +/// "[{}][{}] {}", +/// record.level(), +/// record.target(), +/// message, +/// )) +/// }) +/// .chain( +/// fern::Dispatch::new() +/// // by default only accept warn messages +/// .level(log::LevelFilter::Warn) +/// // accept info messages from the current crate too +/// .level_for("my_crate", log::LevelFilter::Info) +/// // `io::Stdout`, `io::Stderr` and `io::File` can be directly passed in. +/// .chain(io::stdout()), +/// ) +/// .chain( +/// fern::Dispatch::new() +/// // output all messages +/// .level(log::LevelFilter::Trace) +/// // except for hyper, in that case only show info messages +/// .level_for("hyper", log::LevelFilter::Info) +/// // `log_file(x)` equates to +/// // `OpenOptions::new().write(true).append(true).create(true).open(x)` +/// .chain(fern::log_file("persistent-log.log")?) +/// .chain( +/// fs::OpenOptions::new() +/// .write(true) +/// .create(true) +/// .truncate(true) +/// .create(true) +/// .open("/tmp/temp.log")?, +/// ), +/// ) +/// .chain( +/// fern::Dispatch::new() +/// .level(log::LevelFilter::Error) +/// .filter(|_meta_data| { +/// // as an example, randomly reject half of the messages +/// # /* +/// rand::random() +/// # */ +/// # true +/// }) +/// .chain(io::stderr()), +/// ) +/// // and finally, set as the global logger! +/// .apply()?; +/// # Ok(()) +/// # } +/// # +/// # fn main() { setup_logger().expect("failed to set up logger") } +/// ``` +#[must_use = "this is only a logger configuration and must be consumed with into_log() or apply()"] +pub struct Dispatch { + format: Option>, + children: Vec, + /// The level for all messages without override + default_level: log::LevelFilter, + /// Level overrides for specific modules + levels: Vec<(Cow<'static, str>, log::LevelFilter)>, + filters: Vec>, +} + +/// Logger which is usable as an output for multiple other loggers. +/// +/// This struct contains a built logger stored in an [`Arc`], and can be +/// safely cloned. +/// +/// See [`Dispatch::into_shared`]. +/// +/// [`Arc`]: https://doc.rust-lang.org/std/sync/struct.Arc.html +/// [`Dispatch::into_shared`]: struct.Dispatch.html#method.into_shared +#[derive(Clone)] +pub struct SharedDispatch { + inner: Arc, +} + +impl Log for SharedDispatch { + fn enabled(&self, metadata: &log::Metadata) -> bool { + self.inner.enabled(metadata) + } + + fn log(&self, record: &log::Record) { + self.inner.log(record); + } + + fn flush(&self) { + self.inner.flush(); + } +} + +impl Dispatch { + /// Creates a dispatch, which will initially do nothing. + #[inline] + pub fn new() -> Self { + Dispatch { + format: None, + children: Vec::new(), + default_level: log::LevelFilter::Trace, + levels: Vec::new(), + filters: Vec::new(), + } + } + + /// Sets the formatter of this dispatch. The closure should accept a + /// callback, a message and a log record, and write the resulting + /// format to the writer. + /// + /// The log record is passed for completeness, but the `args()` method of + /// the record should be ignored, and the [`fmt::Arguments`] given + /// should be used instead. `record.args()` may be used to retrieve the + /// _original_ log message, but in order to allow for true log + /// chaining, formatters should use the given message instead whenever + /// including the message in the output. + /// + /// To avoid all allocation of intermediate results, the formatter is + /// "completed" by calling a callback, which then calls the rest of the + /// logging chain with the new formatted message. The callback object keeps + /// track of if it was called or not via a stack boolean as well, so if + /// you don't use `out.finish` the log message will continue down + /// the logger chain unformatted. + /// + /// [`fmt::Arguments`]: https://doc.rust-lang.org/std/fmt/struct.Arguments.html + /// + /// Example usage: + /// + /// ``` + /// fern::Dispatch::new().format(|out, message, record| { + /// out.finish(format_args!( + /// "[{}][{}] {}", + /// record.level(), + /// record.target(), + /// message + /// )) + /// }) + /// # .into_log(); + /// ``` + #[inline] + pub fn format(mut self, formatter: F) -> Self + where + F: Fn(FormatCallback, &fmt::Arguments, &log::Record) + Sync + Send + 'static, + { + self.format = Some(Box::new(formatter)); + self + } + + /// Adds a child to this dispatch. + /// + /// All log records which pass all filters will be formatted and then sent + /// to all child loggers in sequence. + /// + /// Children must implement `Into`. Existing `Log` implementations must + /// be boxed. Alternatively, [`Self::chain_logger`] can be used. + /// + /// Note: If the child logger is also a Dispatch, and cannot accept any log + /// records, it will be dropped. This only happens if the child either + /// has no children itself, or has a minimum log level of + /// [`LevelFilter::Off`]. + /// + /// [`LevelFilter::Off`]: https://docs.rs/log/0.4/log/enum.LevelFilter.html#variant.Off + /// + /// Example usage: + /// + /// ``` + /// fern::Dispatch::new().chain(fern::Dispatch::new().chain(std::io::stdout())) + /// # .into_log(); + /// ``` + #[inline] + pub fn chain>(mut self, logger: T) -> Self { + self.children.push(logger.into().0); + self + } + + /// Like [`Self::chain`], but [`Log`] implementations only + /// + /// `.chain_logger(logger)` is equivalent to `.chain(Box::new(logger))`. + /// Note that this method will become obsolete once Rust has trait specialization. + #[inline] + pub fn chain_logger(mut self, logger: T) -> Self { + self.children.push(OutputInner::Logger(Box::new(logger))); + self + } + + /// Sets the overarching level filter for this logger. All messages not + /// already filtered by something set by [`Dispatch::level_for`] will + /// be affected. + /// + /// All messages filtered will be discarded if less severe than the given + /// level. + /// + /// Default level is [`LevelFilter::Trace`]. + /// + /// [`Dispatch::level_for`]: #method.level_for + /// [`LevelFilter::Trace`]: https://docs.rs/log/0.4/log/enum.LevelFilter.html#variant.Trace + /// + /// Example usage: + /// + /// ``` + /// # fn main() { + /// fern::Dispatch::new().level(log::LevelFilter::Info) + /// # .into_log(); + /// # } + /// ``` + #[inline] + pub fn level(mut self, level: log::LevelFilter) -> Self { + self.default_level = level; + self + } + + /// Sets a per-target log level filter. Default target for log messages is + /// `crate_name::module_name` or + /// `crate_name` for logs in the crate root. Targets can also be set with + /// `info!(target: "target-name", ...)`. + /// + /// For each log record fern will first try to match the most specific + /// level_for, and then progressively more general ones until either a + /// matching level is found, or the default level is used. + /// + /// For example, a log for the target `hyper::http::h1` will first test a + /// level_for for `hyper::http::h1`, then for `hyper::http`, then for + /// `hyper`, then use the default level. + /// + /// Examples: + /// + /// A program wants to include a lot of debugging output, but the library + /// "hyper" is known to work well, so debug output from it should be + /// excluded: + /// + /// ``` + /// # fn main() { + /// fern::Dispatch::new() + /// .level(log::LevelFilter::Trace) + /// .level_for("hyper", log::LevelFilter::Info) + /// # .into_log(); + /// # } + /// ``` + /// + /// A program has a ton of debug output per-module, but there is so much + /// that debugging more than one module at a time is not very useful. + /// The command line accepts a list of modules to debug, while keeping the + /// rest of the program at info level: + /// + /// ``` + /// fn setup_logging(verbose_modules: T) -> Result<(), fern::InitError> + /// where + /// I: AsRef, + /// T: IntoIterator, + /// { + /// let mut config = fern::Dispatch::new().level(log::LevelFilter::Info); + /// + /// for module_name in verbose_modules { + /// config = config.level_for( + /// format!("my_crate_name::{}", module_name.as_ref()), + /// log::LevelFilter::Debug, + /// ); + /// } + /// + /// config.chain(std::io::stdout()).apply()?; + /// + /// Ok(()) + /// } + /// # + /// # // we're ok with apply() failing. + /// # fn main() { let _ = setup_logging(&["hi"]); } + /// ``` + #[inline] + pub fn level_for>>( + mut self, + module: T, + level: log::LevelFilter, + ) -> Self { + let module = module.into(); + + if let Some((index, _)) = self + .levels + .iter() + .enumerate() + .find(|&(_, &(ref name, _))| name == &module) + { + self.levels.remove(index); + } + + self.levels.push((module, level)); + self + } + + /// Adds a custom filter which can reject messages passing through this + /// logger. + /// + /// The logger will continue to process log records only if all filters + /// return `true`. + /// + /// [`Dispatch::level`] and [`Dispatch::level_for`] are preferred if + /// applicable. + /// + /// [`Dispatch::level`]: #method.level + /// [`Dispatch::level_for`]: #method.level_for + /// + /// Example usage: + /// + /// This sends error level messages to stderr and others to stdout. + /// + /// ``` + /// # fn main() { + /// fern::Dispatch::new() + /// .level(log::LevelFilter::Info) + /// .chain( + /// fern::Dispatch::new() + /// .filter(|metadata| { + /// // Reject messages with the `Error` log level. + /// metadata.level() != log::LevelFilter::Error + /// }) + /// .chain(std::io::stderr()), + /// ) + /// .chain( + /// fern::Dispatch::new() + /// .level(log::LevelFilter::Error) + /// .chain(std::io::stdout()), + /// ) + /// # .into_log(); + /// # } + #[inline] + pub fn filter(mut self, filter: F) -> Self + where + F: Fn(&log::Metadata) -> bool + Send + Sync + 'static, + { + self.filters.push(Box::new(filter)); + self + } + + /// Builds this dispatch and stores it in a clonable structure containing + /// an [`Arc`]. + /// + /// Once "shared", the dispatch can be used as an output for multiple other + /// dispatch loggers. + /// + /// Example usage: + /// + /// This separates info and warn messages, sending info to stdout + a log + /// file, and warn to stderr + the same log file. Shared is used so the + /// program only opens "file.log" once. + /// + /// ```no_run + /// # fn setup_logger() -> Result<(), fern::InitError> { + /// + /// let file_out = fern::Dispatch::new() + /// .chain(fern::log_file("file.log")?) + /// .into_shared(); + /// + /// let info_out = fern::Dispatch::new() + /// .level(log::LevelFilter::Debug) + /// .filter(|metadata| + /// // keep only info and debug (reject warn and error) + /// metadata.level() <= log::Level::Info) + /// .chain(std::io::stdout()) + /// .chain(file_out.clone()); + /// + /// let warn_out = fern::Dispatch::new() + /// .level(log::LevelFilter::Warn) + /// .chain(std::io::stderr()) + /// .chain(file_out); + /// + /// fern::Dispatch::new() + /// .chain(info_out) + /// .chain(warn_out) + /// .apply(); + /// + /// # Ok(()) + /// # } + /// # + /// # fn main() { setup_logger().expect("failed to set up logger"); } + /// ``` + /// + /// [`Arc`]: https://doc.rust-lang.org/std/sync/struct.Arc.html + pub fn into_shared(self) -> SharedDispatch { + SharedDispatch { + inner: Arc::new(self.into_dispatch()), + } + } + + /// Builds this into the actual logger implementation. + /// + /// This could probably be refactored, but having everything in one place + /// is also nice. + fn into_dispatch(self) -> DispatchImpl { + let Dispatch { + format, + children, + default_level, + levels, + mut filters, + } = self; + + let mut max_child_level = log::LevelFilter::Off; + + let output = children + .into_iter() + .filter_map(|child| match child { + OutputInner::Dispatch(child) => { + let child_level = child.max_level; + max_child_level = max_child_level.max(child_level); + (child_level > log::LevelFilter::Off) + .then(|| child) + .map(|child| Box::new(child) as Box) + } + OutputInner::DispatchShared(child) => { + let child_level = child.inner.max_level; + max_child_level = max_child_level.max(child_level); + (child_level > log::LevelFilter::Off) + .then(|| child) + .map(|child| Box::new(child) as Box) + } + OutputInner::Logger(child) => { + max_child_level = log::LevelFilter::Trace; + Some(child) + } + }) + .collect(); + + let min_level = levels + .iter() + .map(|t| t.1) + .max() + .map_or(default_level, |lvl| cmp::max(lvl, default_level)); + let real_min = cmp::min(min_level, max_child_level); + + filters.shrink_to_fit(); + + DispatchImpl { + output, + default_level, + max_level: real_min, + levels: levels.into(), + format, + filters, + } + } + + /// Builds this logger into a `Box` and calculates the minimum + /// log level needed to have any effect. + /// + /// While this method is exposed publicly, [`Dispatch::apply`] is typically + /// used instead. + /// + /// The returned LevelFilter is a calculation for all level filters of this + /// logger and child loggers, and is the minimum log level needed to + /// for a record to have any chance of passing through this logger. + /// + /// [`Dispatch::apply`]: #method.apply + /// + /// Example usage: + /// + /// ``` + /// # fn main() { + /// let (min_level, log) = fern::Dispatch::new() + /// .level(log::LevelFilter::Info) + /// .chain(std::io::stdout()) + /// .into_log(); + /// + /// assert_eq!(min_level, log::LevelFilter::Info); + /// # } + /// ``` + pub fn into_log(self) -> (log::LevelFilter, Box) { + let logger = self.into_dispatch(); + let level = logger.max_level; + if level == log::LevelFilter::Off { + (level, Box::new(logger::Null)) + } else { + (level, Box::new(logger)) + } + } + + /// Builds this logger and instantiates it as the global [`log`] logger. + /// + /// # Errors: + /// + /// This function will return an error if a global logger has already been + /// set to a previous logger. + /// + /// [`log`]: https://github.com/rust-lang-nursery/log + pub fn apply(self) -> Result<(), log::SetLoggerError> { + let (max_level, log) = self.into_log(); + + log::set_boxed_logger(log)?; + log::set_max_level(max_level); + + Ok(()) + } +} + +pub (crate) enum LevelConfiguration { + JustDefault, + Minimal(Vec<(Cow<'static, str>, log::LevelFilter)>), + Many(HashMap, log::LevelFilter>), +} + +pub (crate) struct DispatchImpl { + pub output: Vec>, + /// The level when no level override for a module matches + pub default_level: log::LevelFilter, + /// The actual maximum level computed based on the bounds of all children. + /// The global level must be set to at least this value. + pub max_level: log::LevelFilter, + pub levels: LevelConfiguration, + pub format: Option>, + pub filters: Vec>, +} + +/// Callback struct for use within a formatter closure +/// +/// Callbacks are used for formatting in order to allow usage of +/// [`std::fmt`]-based formatting without the allocation of the formatted +/// result which would be required to return it. +/// +/// Example usage: +/// +/// ``` +/// fern::Dispatch::new().format(|callback: fern::FormatCallback, message, record| { +/// callback.finish(format_args!("[{}] {}", record.level(), message)) +/// }) +/// # ; +/// ``` +/// +/// [`std::fmt`]: https://doc.rust-lang.org/std/fmt/index.html +#[must_use = "format callback must be used for log to process correctly"] +pub struct FormatCallback<'a>(InnerFormatCallback<'a>); + +struct InnerFormatCallback<'a>(&'a mut bool, &'a DispatchImpl, &'a log::Record<'a>); + +impl From, log::LevelFilter)>> for LevelConfiguration { + fn from(mut levels: Vec<(Cow<'static, str>, log::LevelFilter)>) -> Self { + // Benchmarked separately: https://gist.github.com/daboross/976978d8200caf86e02acb6805961195 + // Use Vec if there are fewer than 15 items, HashMap if there are more than 15. + match levels.len() { + 0 => LevelConfiguration::JustDefault, + x if x > 15 => LevelConfiguration::Many(levels.into_iter().collect()), + _ => { + levels.shrink_to_fit(); + LevelConfiguration::Minimal(levels) + } + } + } +} + +impl LevelConfiguration { + // inline since we use it literally once. + #[inline] + fn find_module(&self, module: &str) -> Option { + match *self { + LevelConfiguration::JustDefault => None, + _ => { + if let Some(level) = self.find_exact(module) { + return Some(level); + } + + // The manual for loop here lets us just iterate over the module string once + // while still finding each sub-module. For the module string + // "hyper::http::h1", this loop will test first "hyper::http" + // then "hyper". + let mut last_char_colon = false; + + for (index, ch) in module.char_indices().rev() { + if last_char_colon { + last_char_colon = false; + if ch == ':' { + let sub_module = &module[0..index]; + + if let Some(level) = self.find_exact(sub_module) { + return Some(level); + } + } + } else if ch == ':' { + last_char_colon = true; + } + } + + None + } + } + } + + fn find_exact(&self, module: &str) -> Option { + match *self { + LevelConfiguration::JustDefault => None, + LevelConfiguration::Minimal(ref levels) => levels + .iter() + .find(|&&(ref test_module, _)| test_module == module) + .map(|&(_, level)| level), + LevelConfiguration::Many(ref levels) => levels.get(module).cloned(), + } + } +} + +impl Log for DispatchImpl { + fn enabled(&self, metadata: &log::Metadata) -> bool { + self.deep_enabled(metadata) + } + + fn log(&self, record: &log::Record) { + if self.shallow_enabled(record.metadata()) { + match self.format { + Some(ref format) => { + // flag to ensure the log message is completed even if the formatter doesn't + // complete the callback. + let mut callback_called_flag = false; + + (format)( + FormatCallback(InnerFormatCallback( + &mut callback_called_flag, + self, + record, + )), + record.args(), + record, + ); + + if !callback_called_flag { + self.finish_logging(record); + } + } + None => { + self.finish_logging(record); + } + } + } + } + + fn flush(&self) { + for log in &self.output { + log.flush(); + } + } +} + +impl DispatchImpl { + fn finish_logging(&self, record: &log::Record) { + for log in &self.output { + log.log(record); + } + } + + /// Check whether this log's filters prevent the given log from happening. + fn shallow_enabled(&self, metadata: &log::Metadata) -> bool { + metadata.level() + <= self + .levels + .find_module(metadata.target()) + .unwrap_or(self.default_level) + && self.filters.iter().all(|f| f(metadata)) + } + + /// Check whether a log with the given metadata would eventually end up + /// outputting something. + /// + /// This is recursive, and checks children. + fn deep_enabled(&self, metadata: &log::Metadata) -> bool { + self.shallow_enabled(metadata) && self.output.iter().any(|l| l.enabled(metadata)) + } +} + +impl<'a> FormatCallback<'a> { + /// Complete the formatting call that this FormatCallback was created for. + /// + /// This will call the rest of the logging chain using the given formatted + /// message as the new payload message. + /// + /// Example usage: + /// + /// ``` + /// # fern::Dispatch::new() + /// # .format(|callback: fern::FormatCallback, message, record| { + /// callback.finish(format_args!("[{}] {}", record.level(), message)) + /// # }) + /// # .into_log(); + /// ``` + /// + /// See [`format_args!`]. + /// + /// [`format_args!`]: https://doc.rust-lang.org/std/macro.format_args.html + pub fn finish(self, formatted_message: fmt::Arguments) { + let FormatCallback(InnerFormatCallback(callback_called_flag, dispatch, record)) = self; + + // let the dispatch know that we did in fact get called. + *callback_called_flag = true; + + // NOTE: This needs to be updated whenever new things are added to + // `log::Record`. + let new_record = log::RecordBuilder::new() + .args(formatted_message) + .metadata(record.metadata().clone()) + .level(record.level()) + .target(record.target()) + .module_path(record.module_path()) + .file(record.file()) + .line(record.line()) + .build(); + + dispatch.finish_logging(&new_record); + } +} + +/// This enum contains various outputs that you can send messages to. +pub(crate) enum OutputInner { + Dispatch(DispatchImpl), + DispatchShared(SharedDispatch), + Logger(Box), +} + +/// Configuration for a logger output. +pub struct Output(pub(crate) OutputInner); + +impl fmt::Debug for Output { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("Output") + } +} + +impl From for Output { + /// Creates an output logger forwarding all messages to the dispatch. + fn from(log: Dispatch) -> Self { + Output(OutputInner::Dispatch(log.into_dispatch())) + } +} + +impl From for Output { + /// Creates an output logger forwarding all messages to the dispatch. + fn from(log: DispatchImpl) -> Self { + Output(OutputInner::Dispatch(log)) + } +} + +impl From for Output { + /// Creates an output logger forwarding all messages to the dispatch. + fn from(log: SharedDispatch) -> Self { + Output(OutputInner::DispatchShared(log)) + } +} + +impl From> for Output { + fn from(log: Box) -> Self { + Output(OutputInner::Logger(log)) + } +} + +impl From<&'static dyn Log> for Output { + /// Creates an output logger forwarding all messages to the custom logger. + fn from(log: &'static dyn Log) -> Self { + struct LogRef(&'static dyn Log); + + impl Log for LogRef { + fn enabled(&self, metadata: &log::Metadata) -> bool { + self.0.enabled(metadata) + } + + fn log(&self, record: &log::Record) { + self.0.log(record) + } + + fn flush(&self) { + self.0.flush() + } + } + + Output(OutputInner::Logger(Box::new(LogRef(log)))) + } +} + +// impl From for Output { +// /// Creates an output logger which writes all messages to the file with +// /// `\n` as the separator. +// /// +// /// File writes are buffered and flushed once per log record. +// fn from(file: fs::File) -> Self { +// Output(OutputInner::Logger(Box::new(logger::Writer::new(io::BufWriter::new(file))))) +// } +// } + +// impl From> for Output { +// /// Creates an output logger which writes all messages to the writer with +// /// `\n` as the separator. +// /// +// /// This does no buffering and it is up to the writer to do buffering as +// /// needed (eg. wrap it in `BufWriter`). However, flush is called after +// /// each log record. +// fn from(writer: Box) -> Self { +// Output(OutputInner::Logger(Box::new(logger::Writer::new(writer)))) +// } +// } + +// impl From for Output { +// /// Creates an output logger which writes all messages to stdout with the +// /// given handle and `\n` as the separator. +// fn from(stream: io::Stdout) -> Self { +// Output(OutputInner::Logger(Box::new(logger::Writer::from(stream)))) +// } +// } + +// impl From for Output { +// /// Creates an output logger which writes all messages to stderr with the +// /// given handle and `\n` as the separator. +// fn from(stream: io::Stderr) -> Self { +// Output(OutputInner::Logger(Box::new(logger::Writer::from(stream)))) +// } +// } + +// impl From> for Output { +// /// Creates an output logger which writes all messages to the given +// /// mpsc::Sender with '\n' as the separator. +// /// +// /// All messages sent to the mpsc channel are suffixed with '\n'. +// fn from(stream: Sender) -> Self { +// Output(OutputInner::Logger(Box::new(logger::Sender { +// stream: Mutex::new(stream), +// }))) +// } +// } + +impl Default for Dispatch { + /// Returns a logger configuration that does nothing with log records. + /// + /// Equivalent to [`Dispatch::new`]. + /// + /// [`Dispatch::new`]: #method.new + fn default() -> Self { + Self::new() + } +} + +impl fmt::Debug for Dispatch { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + struct LevelsDebug<'a>(&'a [(Cow<'static, str>, log::LevelFilter)]); + impl<'a> fmt::Debug for LevelsDebug<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_map() + .entries(self.0.iter().map(|t| (t.0.as_ref(), t.1))) + .finish() + } + } + struct FiltersDebug<'a>(&'a [Box]); + impl<'a> fmt::Debug for FiltersDebug<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_list() + .entries(self.0.iter().map(|_| "")) + .finish() + } + } + f.debug_struct("Dispatch") + .field( + "format", + &self.format.as_ref().map(|_| ""), + ) + .field("children_count", &self.children.len()) + .field("default_level", &self.default_level) + .field("levels", &LevelsDebug(&self.levels)) + .field("filters", &FiltersDebug(&self.filters)) + .finish() + } +} + +#[cfg(test)] +mod test { + use super::LevelConfiguration; + use log::LevelFilter::*; + + #[test] + fn test_level_config_find_exact_minimal() { + let config = LevelConfiguration::Minimal( + vec![("mod1", Info), ("mod2", Debug), ("mod3", Off)] + .into_iter() + .map(|(k, v)| (k.into(), v)) + .collect(), + ); + + assert_eq!(config.find_exact("mod1"), Some(Info)); + assert_eq!(config.find_exact("mod2"), Some(Debug)); + assert_eq!(config.find_exact("mod3"), Some(Off)); + } + + #[test] + fn test_level_config_find_exact_many() { + let config = LevelConfiguration::Many( + vec![("mod1", Info), ("mod2", Debug), ("mod3", Off)] + .into_iter() + .map(|(k, v)| (k.into(), v)) + .collect(), + ); + + assert_eq!(config.find_exact("mod1"), Some(Info)); + assert_eq!(config.find_exact("mod2"), Some(Debug)); + assert_eq!(config.find_exact("mod3"), Some(Off)); + } + + #[test] + fn test_level_config_simple_hierarchy() { + let config = LevelConfiguration::Minimal( + vec![("mod1", Info), ("mod2::sub_mod", Debug), ("mod3", Off)] + .into_iter() + .map(|(k, v)| (k.into(), v)) + .collect(), + ); + + assert_eq!(config.find_module("mod1::sub_mod"), Some(Info)); + assert_eq!(config.find_module("mod2::sub_mod::sub_mod_2"), Some(Debug)); + assert_eq!(config.find_module("mod3::sub_mod::sub_mod_2"), Some(Off)); + } + + #[test] + fn test_level_config_hierarchy_correct() { + let config = LevelConfiguration::Minimal( + vec![ + ("root", Trace), + ("root::sub1", Debug), + ("root::sub2", Info), + // should work with all insertion orders + ("root::sub2::sub2.3::sub2.4", Error), + ("root::sub2::sub2.3", Warn), + ("root::sub3", Off), + ] + .into_iter() + .map(|(k, v)| (k.into(), v)) + .collect(), + ); + + assert_eq!(config.find_module("root"), Some(Trace)); + assert_eq!(config.find_module("root::other_module"), Some(Trace)); + + // We want to ensure that it does pick up most specific level before trying + // anything more general. + assert_eq!(config.find_module("root::sub1"), Some(Debug)); + assert_eq!(config.find_module("root::sub1::other_module"), Some(Debug)); + + assert_eq!(config.find_module("root::sub2"), Some(Info)); + assert_eq!(config.find_module("root::sub2::other"), Some(Info)); + + assert_eq!(config.find_module("root::sub2::sub2.3"), Some(Warn)); + assert_eq!( + config.find_module("root::sub2::sub2.3::sub2.4"), + Some(Error) + ); + + assert_eq!(config.find_module("root::sub3"), Some(Off)); + assert_eq!( + config.find_module("root::sub3::any::children::of::sub3"), + Some(Off) + ); + } + + #[test] + fn test_level_config_similar_names_are_not_same() { + let config = LevelConfiguration::Minimal( + vec![("root", Trace), ("rootay", Info)] + .into_iter() + .map(|(k, v)| (k.into(), v)) + .collect(), + ); + + assert_eq!(config.find_module("root"), Some(Trace)); + assert_eq!(config.find_module("root::sub"), Some(Trace)); + assert_eq!(config.find_module("rooty"), None); + assert_eq!(config.find_module("rooty::sub"), None); + assert_eq!(config.find_module("rootay"), Some(Info)); + assert_eq!(config.find_module("rootay::sub"), Some(Info)); + } + + #[test] + fn test_level_config_single_colon_is_not_double_colon() { + let config = LevelConfiguration::Minimal( + vec![ + ("root", Trace), + ("root::su", Debug), + ("root::su:b2", Info), + ("root::sub2", Warn), + ] + .into_iter() + .map(|(k, v)| (k.into(), v)) + .collect(), + ); + + assert_eq!(config.find_module("root"), Some(Trace)); + + assert_eq!(config.find_module("root::su"), Some(Debug)); + assert_eq!(config.find_module("root::su::b2"), Some(Debug)); + + assert_eq!(config.find_module("root::su:b2"), Some(Info)); + assert_eq!(config.find_module("root::su:b2::b3"), Some(Info)); + + assert_eq!(config.find_module("root::sub2"), Some(Warn)); + assert_eq!(config.find_module("root::sub2::b3"), Some(Warn)); + } + + #[test] + fn test_level_config_all_chars() { + let config = LevelConfiguration::Minimal( + vec![("♲", Trace), ("☸", Debug), ("♲::☸", Info), ("♲::\t", Debug)] + .into_iter() + .map(|(k, v)| (k.into(), v)) + .collect(), + ); + + assert_eq!(config.find_module("♲"), Some(Trace)); + assert_eq!(config.find_module("♲::other"), Some(Trace)); + + assert_eq!(config.find_module("☸"), Some(Debug)); + assert_eq!(config.find_module("☸::any"), Some(Debug)); + + assert_eq!(config.find_module("♲::☸"), Some(Info)); + assert_eq!(config.find_module("♲☸"), None); + + assert_eq!(config.find_module("♲::\t"), Some(Debug)); + assert_eq!(config.find_module("♲::\t::\n\n::\t"), Some(Debug)); + assert_eq!(config.find_module("♲::\t\t"), Some(Trace)); + } +} diff --git a/src/errors.rs b/src/errors.rs index d33752c..ffaebe0 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,7 +1,5 @@ use std::{error, fmt, io}; -use log; - /// Convenience error combining possible errors which could occur while /// initializing logging. /// diff --git a/src/formatter.rs b/src/formatter.rs new file mode 100644 index 0000000..15da129 --- /dev/null +++ b/src/formatter.rs @@ -0,0 +1,142 @@ +#[cfg(feature = "colored")] +use crate::colors::ColoredLevelConfig; +use crate::{FormatCallback}; + +/// A generic formatter that easily provides good defaults while being configurable +#[derive(Clone, Debug)] +pub struct FormatterBuilder { + #[cfg(feature = "colored")] + color_config: Option, + #[cfg(feature = "chrono")] + chrono: bool, + level: bool, + target: bool, +} + +impl Default for FormatterBuilder { + fn default() -> Self { + FormatterBuilder { + #[cfg(feature = "colored")] + color_config: colored::control::SHOULD_COLORIZE.should_colorize().then(Default::default), + #[cfg(feature = "chrono")] + chrono: true, + level: true, + target: true, + } + } +} + +impl FormatterBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn level(mut self, level: bool) -> Self { + self.level = level; + self + } + + pub fn target(mut self, target: bool) -> Self { + self.target = target; + self + } + + #[cfg(feature = "colored")] + pub fn color(mut self, color: bool) -> Self { + self.color_config = if color { + self.color_config.or_else(Default::default) + } else { + None + }; + self + } + + #[cfg(feature = "colored")] + pub fn color_config( + mut self, + modify_config: impl FnOnce(ColoredLevelConfig) -> ColoredLevelConfig, + ) -> Self { + self.color_config = self.color_config.map(modify_config); + self + } + + #[cfg(feature = "chrono")] + pub fn chrono(mut self, chrono: bool) -> Self { + self.chrono = chrono; + self + } + + #[rustfmt::skip] + pub fn build( + self, + ) -> impl Fn(FormatCallback<'_>, &std::fmt::Arguments<'_>, &log::Record<'_>) + Sync + Send + 'static + { + move |out, message, record| { + /* Type checking is hard */ + #[cfg(not(feature = "chrono"))] + type TimeType = String; + #[cfg(feature = "chrono")] + type TimeType<'a> = chrono::format::DelayedFormat>; + + let time: Option = { + #[cfg(feature = "chrono")] + { + self.chrono + .then(|| chrono::Local::now().format("%Y-%m-%d %H:%M:%S,%3f")) + } + #[cfg(not(feature = "chrono"))] + { + None + } + }; + + /* Type checking is hard */ + #[cfg(not(feature = "colored"))] + type LevelType = log::Level; + #[cfg(feature = "colored")] + type LevelType = crate::colors::WithFgColor; + + let level: Option = if self.level { + #[cfg(feature = "colored")] + { + Some( + self.color_config + .map(|config| config.color(record.level())) + .unwrap_or_else(|| crate::colors::WithFgColor { + text: record.level(), + color: None, + }), + ) + } + #[cfg(not(feature = "colored"))] + { + Some(record.level()) + } + } else { + None + }; + + let target = self.target.then(|| { + if !record.target().is_empty() { + record.target() + } else { + record.module_path().unwrap_or_default() + } + }); + + /* Sadly we cannot store and compose std::fmt::Arguments due to lifetime issues. + * In order to avoid unnecessary write calls nevertheless, we must enumerate all options + */ + match (time, level, target) { + (Some(time), Some(level), Some(target)) => out.finish(format_args!("{} {:<5} [{}] {}", time, level, target, message)), + (Some(time), Some(level), None) => out.finish(format_args!("{} {:<5}: {}", time, level, message)), + (Some(time), None, Some(target)) => out.finish(format_args!("{} [{}] {}", time, target, message)), + (Some(time), None, None) => out.finish(format_args!("{} {}", time, message)), + (None, Some(level), Some(target)) => out.finish(format_args!("{:<5} [{}] {}", level, target, message)), + (None, None, Some(target)) => out.finish(format_args!("[{}] {}", target, message)), + (None, Some(level), None) => out.finish(format_args!("{}: {}", level, message)), + (None, None, None) => out.finish(format_args!("{}", message)), + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 434e2ad..167612e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#![deny(missing_docs)] +// #![deny(missing_docs)] // TODO add back in #![doc(html_root_url = "https://docs.rs/fern/0.6.0")] //! Efficient, configurable logging in Rust. //! @@ -12,6 +12,24 @@ //! fern = "0.5" //! ``` //! +//! # Basic design +//! +//! [`Dispatch`] is a fancy multiplexer to build a tree of sub-loggers: +//! +//! ```text +//! Dispatch +//! ├── Dispatch +//! │ └── Logger +//! ├── Logger +//! └── Logger +//! ``` +//! +//! Each dispatch may control its sub-tree of child loggers by: +//! +//! - Formatting the messages +//! - Setting per module log levels +//! - Arbitrary message filtering +//! //! # Example setup //! //! With fern, all logger configuration is done via builder-like methods on @@ -202,23 +220,32 @@ use std::{ convert::AsRef, fmt, - fs::{File, OpenOptions}, - io, - path::Path, + io::{self, Write}, + sync::mpsc, }; -#[cfg(all(not(windows), feature = "syslog-4"))] -use std::collections::HashMap; - pub use crate::{ - builders::{Dispatch, Output, Panic}, + dispatch::{Dispatch, Output}, errors::InitError, - log_impl::FormatCallback, + dispatch::FormatCallback, }; -mod builders; +pub mod formatter; + +mod dispatch; mod errors; -mod log_impl; + +/// Logger implementations for different output targets +// pub mod logger { + // pub use crate::log_impl::*; + // pub use crate::*; +// } +pub mod logger; + +// For backwards compatibilty +// Also see https://github.com/rust-lang/rust/issues/30827 +//#[deprecated(note = "Moved to fern::logger::Panic")] +//pub use crate::logger::Panic; #[cfg(feature = "colored")] pub mod colors; @@ -237,73 +264,70 @@ pub type Formatter = dyn Fn(FormatCallback, &fmt::Arguments, &log::Record) + Syn /// succeed - false means it should fail. pub type Filter = dyn Fn(&log::Metadata) -> bool + Send + Sync + 'static; -#[cfg(feature = "date-based")] -pub use crate::builders::DateBased; - -#[cfg(all(not(windows), feature = "syslog-4"))] -type Syslog4Rfc3164Logger = syslog4::Logger; +#[inline(always)] +fn fallback_on_error(record: &log::Record, log_func: F) +where + F: FnOnce(&log::Record) -> Result<(), LogError>, +{ + if let Err(error) = log_func(record) { + backup_logging(record, &error) + } +} -#[cfg(all(not(windows), feature = "syslog-4"))] -type Syslog4Rfc5424Logger = syslog4::Logger< - syslog4::LoggerBackend, - (i32, HashMap>, String), - syslog4::Formatter5424, ->; +fn backup_logging(record: &log::Record, error: &LogError) { + let second = write!( + io::stderr(), + "Error performing logging.\ + \n\tattempted to log: {}\ + \n\trecord: {:?}\ + \n\tlogging error: {}", + record.args(), + record, + error + ); -/// Convenience method for opening a log file with common options. -/// -/// Equivalent to: -/// -/// ```no_run -/// std::fs::OpenOptions::new() -/// .write(true) -/// .create(true) -/// .append(true) -/// .open("filename") -/// # ; -/// ``` -/// -/// See [`OpenOptions`] for more information. -/// -/// [`OpenOptions`]: https://doc.rust-lang.org/std/fs/struct.OpenOptions.html -#[inline] -pub fn log_file>(path: P) -> io::Result { - OpenOptions::new() - .write(true) - .create(true) - .append(true) - .open(path) + if let Err(second_error) = second { + panic!( + "Error performing stderr logging after error occurred during regular logging.\ + \n\tattempted to log: {}\ + \n\trecord: {:?}\ + \n\tfirst logging error: {}\ + \n\tstderr error: {}", + record.args(), + record, + error, + second_error, + ); + } } -/// Convenience method for opening a re-openable log file with common options. -/// -/// The file opening is equivalent to: -/// -/// ```no_run -/// std::fs::OpenOptions::new() -/// .write(true) -/// .create(true) -/// .append(true) -/// .open("filename") -/// # ; -/// ``` -/// -/// See [`OpenOptions`] for more information. -/// -/// [`OpenOptions`]: https://doc.rust-lang.org/std/fs/struct.OpenOptions.html -/// -/// This function is not available on Windows, and it requires the `reopen-03` -/// feature to be enabled. -#[cfg(all(not(windows), feature = "reopen-03"))] -#[inline] -pub fn log_reopen(path: &Path, signal: Option) -> io::Result> { - let p = path.to_owned(); - let r = reopen::Reopen::new(Box::new(move || log_file(&p)))?; +#[derive(Debug)] +enum LogError { + Io(io::Error), + Send(mpsc::SendError), + #[cfg(all(not(windows), feature = "syslog-4"))] + Syslog4(syslog4::Error), +} - if let Some(s) = signal { - if let Err(e) = r.handle().register_signal(s) { - return Err(e); +impl fmt::Display for LogError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + LogError::Io(ref e) => write!(f, "{}", e), + LogError::Send(ref e) => write!(f, "{}", e), + #[cfg(all(not(windows), feature = "syslog-4"))] + LogError::Syslog4(ref e) => write!(f, "{}", e), } } - Ok(r) +} + +impl From for LogError { + fn from(error: io::Error) -> Self { + LogError::Io(error) + } +} + +impl From> for LogError { + fn from(error: mpsc::SendError) -> Self { + LogError::Send(error) + } } diff --git a/src/log_impl.rs b/src/log_impl.rs deleted file mode 100644 index f9819a8..0000000 --- a/src/log_impl.rs +++ /dev/null @@ -1,956 +0,0 @@ -use std::{ - borrow::Cow, - collections::HashMap, - fmt, fs, - io::{self, BufWriter, Write}, - sync::{mpsc, Arc, Mutex}, -}; - -#[cfg(feature = "date-based")] -use std::{ - ffi::OsString, - fs::OpenOptions, - path::{Path, PathBuf}, -}; - -use log::{self, Log}; - -use crate::{Filter, Formatter}; - -#[cfg(all(not(windows), feature = "syslog-4"))] -use crate::{Syslog4Rfc3164Logger, Syslog4Rfc5424Logger}; -#[cfg(all(not(windows), feature = "reopen-03"))] -use reopen; - -pub enum LevelConfiguration { - JustDefault, - Minimal(Vec<(Cow<'static, str>, log::LevelFilter)>), - Many(HashMap, log::LevelFilter>), -} - -pub struct Dispatch { - pub output: Vec, - pub default_level: log::LevelFilter, - pub levels: LevelConfiguration, - pub format: Option>, - pub filters: Vec>, -} - -/// Callback struct for use within a formatter closure -/// -/// Callbacks are used for formatting in order to allow usage of -/// [`std::fmt`]-based formatting without the allocation of the formatted -/// result which would be required to return it. -/// -/// Example usage: -/// -/// ``` -/// fern::Dispatch::new().format(|callback: fern::FormatCallback, message, record| { -/// callback.finish(format_args!("[{}] {}", record.level(), message)) -/// }) -/// # ; -/// ``` -/// -/// [`std::fmt`]: https://doc.rust-lang.org/std/fmt/index.html -#[must_use = "format callback must be used for log to process correctly"] -pub struct FormatCallback<'a>(InnerFormatCallback<'a>); - -struct InnerFormatCallback<'a>(&'a mut bool, &'a Dispatch, &'a log::Record<'a>); - -pub enum Output { - Stdout(Stdout), - Stderr(Stderr), - File(File), - Sender(Sender), - #[cfg(all(not(windows), feature = "syslog-3"))] - Syslog3(Syslog3), - #[cfg(all(not(windows), feature = "syslog-4"))] - Syslog4Rfc3164(Syslog4Rfc3164), - #[cfg(all(not(windows), feature = "syslog-4"))] - Syslog4Rfc5424(Syslog4Rfc5424), - Dispatch(Dispatch), - SharedDispatch(Arc), - OtherBoxed(Box), - OtherStatic(&'static dyn Log), - Panic(Panic), - Writer(Writer), - #[cfg(feature = "date-based")] - DateBased(DateBased), - #[cfg(all(not(windows), feature = "reopen-03"))] - Reopen(Reopen), -} - -pub struct Stdout { - pub stream: io::Stdout, - pub line_sep: Cow<'static, str>, -} - -pub struct Stderr { - pub stream: io::Stderr, - pub line_sep: Cow<'static, str>, -} - -pub struct File { - pub stream: Mutex>, - pub line_sep: Cow<'static, str>, -} - -pub struct Sender { - pub stream: Mutex>, - pub line_sep: Cow<'static, str>, -} - -pub struct Writer { - pub stream: Mutex>, - pub line_sep: Cow<'static, str>, -} - -#[cfg(all(not(windows), feature = "reopen-03"))] -pub struct Reopen { - pub stream: Mutex>, - pub line_sep: Cow<'static, str>, -} - -#[cfg(all(not(windows), feature = "syslog-3"))] -pub struct Syslog3 { - pub inner: syslog3::Logger, -} - -#[cfg(all(not(windows), feature = "syslog-4"))] -pub struct Syslog4Rfc3164 { - pub inner: Mutex, -} - -#[cfg(all(not(windows), feature = "syslog-4"))] -pub struct Syslog4Rfc5424 { - pub inner: Mutex, - pub transform: Box< - dyn Fn(&log::Record) -> (i32, HashMap>, String) - + Sync - + Send, - >, -} - -pub struct Panic; - -pub struct Null; - -/// File logger with a dynamic time-based name. -#[derive(Debug)] -#[cfg(feature = "date-based")] -pub struct DateBased { - pub config: DateBasedConfig, - pub state: Mutex, -} - -#[derive(Debug)] -#[cfg(feature = "date-based")] -pub enum ConfiguredTimezone { - Local, - Utc, -} - -#[derive(Debug)] -#[cfg(feature = "date-based")] -pub struct DateBasedConfig { - pub line_sep: Cow<'static, str>, - /// This is a Path not an str so it can hold invalid UTF8 paths correctly. - pub file_prefix: PathBuf, - pub file_suffix: Cow<'static, str>, - pub timezone: ConfiguredTimezone, -} - -#[derive(Debug)] -#[cfg(feature = "date-based")] -pub struct DateBasedState { - pub current_suffix: String, - pub file_stream: Option>, -} - -#[cfg(feature = "date-based")] -impl DateBasedState { - pub fn new(current_suffix: String, file_stream: Option) -> Self { - DateBasedState { - current_suffix, - file_stream: file_stream.map(BufWriter::new), - } - } - - pub fn replace_file(&mut self, new_suffix: String, new_file: Option) { - if let Some(mut old) = self.file_stream.take() { - let _ = old.flush(); - } - self.current_suffix = new_suffix; - self.file_stream = new_file.map(BufWriter::new) - } -} - -#[cfg(feature = "date-based")] -impl DateBasedConfig { - pub fn new( - line_sep: Cow<'static, str>, - file_prefix: PathBuf, - file_suffix: Cow<'static, str>, - timezone: ConfiguredTimezone, - ) -> Self { - DateBasedConfig { - line_sep, - file_prefix, - file_suffix, - timezone, - } - } - - pub fn compute_current_suffix(&self) -> String { - match self.timezone { - ConfiguredTimezone::Utc => chrono::Utc::now().format(&self.file_suffix).to_string(), - ConfiguredTimezone::Local => chrono::Local::now().format(&self.file_suffix).to_string(), - } - } - - pub fn compute_file_path(&self, suffix: &str) -> PathBuf { - let mut path = OsString::from(&*self.file_prefix); - // use the OsString::push method, not PathBuf::push which would add a path - // separator - path.push(suffix); - path.into() - } - - pub fn open_log_file(path: &Path) -> io::Result { - OpenOptions::new() - .write(true) - .create(true) - .append(true) - .open(path) - } - - pub fn open_current_log_file(&self, suffix: &str) -> io::Result { - Self::open_log_file(&self.compute_file_path(&suffix)) - } -} - -impl From, log::LevelFilter)>> for LevelConfiguration { - fn from(mut levels: Vec<(Cow<'static, str>, log::LevelFilter)>) -> Self { - // Benchmarked separately: https://gist.github.com/daboross/976978d8200caf86e02acb6805961195 - // Use Vec if there are fewer than 15 items, HashMap if there are more than 15. - match levels.len() { - 0 => LevelConfiguration::JustDefault, - x if x > 15 => LevelConfiguration::Many(levels.into_iter().collect()), - _ => { - levels.shrink_to_fit(); - LevelConfiguration::Minimal(levels) - } - } - } -} - -impl LevelConfiguration { - // inline since we use it literally once. - #[inline] - fn find_module(&self, module: &str) -> Option { - match *self { - LevelConfiguration::JustDefault => None, - _ => { - if let Some(level) = self.find_exact(module) { - return Some(level); - } - - // The manual for loop here lets us just iterate over the module string once - // while still finding each sub-module. For the module string - // "hyper::http::h1", this loop will test first "hyper::http" - // then "hyper". - let mut last_char_colon = false; - - for (index, ch) in module.char_indices().rev() { - if last_char_colon { - last_char_colon = false; - if ch == ':' { - let sub_module = &module[0..index]; - - if let Some(level) = self.find_exact(sub_module) { - return Some(level); - } - } - } else if ch == ':' { - last_char_colon = true; - } - } - - None - } - } - } - - fn find_exact(&self, module: &str) -> Option { - match *self { - LevelConfiguration::JustDefault => None, - LevelConfiguration::Minimal(ref levels) => levels - .iter() - .find(|&&(ref test_module, _)| test_module == module) - .map(|&(_, level)| level), - LevelConfiguration::Many(ref levels) => levels.get(module).cloned(), - } - } -} - -impl Log for Output { - fn enabled(&self, metadata: &log::Metadata) -> bool { - match *self { - Output::Stdout(ref s) => s.enabled(metadata), - Output::Stderr(ref s) => s.enabled(metadata), - Output::File(ref s) => s.enabled(metadata), - Output::Sender(ref s) => s.enabled(metadata), - Output::Dispatch(ref s) => s.enabled(metadata), - Output::SharedDispatch(ref s) => s.enabled(metadata), - Output::OtherBoxed(ref s) => s.enabled(metadata), - Output::OtherStatic(ref s) => s.enabled(metadata), - #[cfg(all(not(windows), feature = "syslog-3"))] - Output::Syslog3(ref s) => s.enabled(metadata), - #[cfg(all(not(windows), feature = "syslog-4"))] - Output::Syslog4Rfc3164(ref s) => s.enabled(metadata), - #[cfg(all(not(windows), feature = "syslog-4"))] - Output::Syslog4Rfc5424(ref s) => s.enabled(metadata), - Output::Panic(ref s) => s.enabled(metadata), - Output::Writer(ref s) => s.enabled(metadata), - #[cfg(feature = "date-based")] - Output::DateBased(ref s) => s.enabled(metadata), - #[cfg(all(not(windows), feature = "reopen-03"))] - Output::Reopen(ref s) => s.enabled(metadata), - } - } - - fn log(&self, record: &log::Record) { - match *self { - Output::Stdout(ref s) => s.log(record), - Output::Stderr(ref s) => s.log(record), - Output::File(ref s) => s.log(record), - Output::Sender(ref s) => s.log(record), - Output::Dispatch(ref s) => s.log(record), - Output::SharedDispatch(ref s) => s.log(record), - Output::OtherBoxed(ref s) => s.log(record), - Output::OtherStatic(ref s) => s.log(record), - #[cfg(all(not(windows), feature = "syslog-3"))] - Output::Syslog3(ref s) => s.log(record), - #[cfg(all(not(windows), feature = "syslog-4"))] - Output::Syslog4Rfc3164(ref s) => s.log(record), - #[cfg(all(not(windows), feature = "syslog-4"))] - Output::Syslog4Rfc5424(ref s) => s.log(record), - Output::Panic(ref s) => s.log(record), - Output::Writer(ref s) => s.log(record), - #[cfg(feature = "date-based")] - Output::DateBased(ref s) => s.log(record), - #[cfg(all(not(windows), feature = "reopen-03"))] - Output::Reopen(ref s) => s.log(record), - } - } - - fn flush(&self) { - match *self { - Output::Stdout(ref s) => s.flush(), - Output::Stderr(ref s) => s.flush(), - Output::File(ref s) => s.flush(), - Output::Sender(ref s) => s.flush(), - Output::Dispatch(ref s) => s.flush(), - Output::SharedDispatch(ref s) => s.flush(), - Output::OtherBoxed(ref s) => s.flush(), - Output::OtherStatic(ref s) => s.flush(), - #[cfg(all(not(windows), feature = "syslog-3"))] - Output::Syslog3(ref s) => s.flush(), - #[cfg(all(not(windows), feature = "syslog-4"))] - Output::Syslog4Rfc3164(ref s) => s.flush(), - #[cfg(all(not(windows), feature = "syslog-4"))] - Output::Syslog4Rfc5424(ref s) => s.flush(), - Output::Panic(ref s) => s.flush(), - Output::Writer(ref s) => s.flush(), - #[cfg(feature = "date-based")] - Output::DateBased(ref s) => s.flush(), - #[cfg(all(not(windows), feature = "reopen-03"))] - Output::Reopen(ref s) => s.flush(), - } - } -} - -impl Log for Null { - fn enabled(&self, _: &log::Metadata) -> bool { - false - } - - fn log(&self, _: &log::Record) {} - - fn flush(&self) {} -} - -impl Log for Dispatch { - fn enabled(&self, metadata: &log::Metadata) -> bool { - self.deep_enabled(metadata) - } - - fn log(&self, record: &log::Record) { - if self.shallow_enabled(record.metadata()) { - match self.format { - Some(ref format) => { - // flag to ensure the log message is completed even if the formatter doesn't - // complete the callback. - let mut callback_called_flag = false; - - (format)( - FormatCallback(InnerFormatCallback( - &mut callback_called_flag, - self, - record, - )), - record.args(), - record, - ); - - if !callback_called_flag { - self.finish_logging(record); - } - } - None => { - self.finish_logging(record); - } - } - } - } - - fn flush(&self) { - for log in &self.output { - log.flush(); - } - } -} - -impl Dispatch { - fn finish_logging(&self, record: &log::Record) { - for log in &self.output { - log.log(record); - } - } - - /// Check whether this log's filters prevent the given log from happening. - fn shallow_enabled(&self, metadata: &log::Metadata) -> bool { - metadata.level() - <= self - .levels - .find_module(metadata.target()) - .unwrap_or(self.default_level) - && self.filters.iter().all(|f| f(metadata)) - } - - /// Check whether a log with the given metadata would eventually end up - /// outputting something. - /// - /// This is recursive, and checks children. - fn deep_enabled(&self, metadata: &log::Metadata) -> bool { - self.shallow_enabled(metadata) && self.output.iter().any(|l| l.enabled(metadata)) - } -} - -impl<'a> FormatCallback<'a> { - /// Complete the formatting call that this FormatCallback was created for. - /// - /// This will call the rest of the logging chain using the given formatted - /// message as the new payload message. - /// - /// Example usage: - /// - /// ``` - /// # fern::Dispatch::new() - /// # .format(|callback: fern::FormatCallback, message, record| { - /// callback.finish(format_args!("[{}] {}", record.level(), message)) - /// # }) - /// # .into_log(); - /// ``` - /// - /// See [`format_args!`]. - /// - /// [`format_args!`]: https://doc.rust-lang.org/std/macro.format_args.html - pub fn finish(self, formatted_message: fmt::Arguments) { - let FormatCallback(InnerFormatCallback(callback_called_flag, dispatch, record)) = self; - - // let the dispatch know that we did in fact get called. - *callback_called_flag = true; - - // NOTE: This needs to be updated whenever new things are added to - // `log::Record`. - let new_record = log::RecordBuilder::new() - .args(formatted_message) - .metadata(record.metadata().clone()) - .level(record.level()) - .target(record.target()) - .module_path(record.module_path()) - .file(record.file()) - .line(record.line()) - .build(); - - dispatch.finish_logging(&new_record); - } -} - -// No need to write this twice (used for Stdout and Stderr structs) -macro_rules! std_log_impl { - ($ident:ident) => { - impl Log for $ident { - fn enabled(&self, _: &log::Metadata) -> bool { - true - } - - fn log(&self, record: &log::Record) { - fallback_on_error(record, |record| { - if cfg!(feature = "meta-logging-in-format") { - // Formatting first prevents deadlocks when the process of formatting - // itself is logged. note: this is only ever needed if some - // Debug, Display, or other formatting trait itself is - // logging things too. - let msg = format!("{}{}", record.args(), self.line_sep); - - write!(self.stream.lock(), "{}", msg)?; - } else { - write!(self.stream.lock(), "{}{}", record.args(), self.line_sep)?; - } - - Ok(()) - }); - } - - fn flush(&self) { - let _ = self.stream.lock().flush(); - } - } - }; -} - -std_log_impl!(Stdout); -std_log_impl!(Stderr); - -macro_rules! writer_log_impl { - ($ident:ident) => { - impl Log for $ident { - fn enabled(&self, _: &log::Metadata) -> bool { - true - } - - fn log(&self, record: &log::Record) { - fallback_on_error(record, |record| { - if cfg!(feature = "meta-logging-in-format") { - // Formatting first prevents deadlocks on file-logging, - // when the process of formatting itself is logged. - // note: this is only ever needed if some Debug, Display, or other - // formatting trait itself is logging. - let msg = format!("{}{}", record.args(), self.line_sep); - - let mut writer = self.stream.lock().unwrap_or_else(|e| e.into_inner()); - - write!(writer, "{}", msg)?; - - writer.flush()?; - } else { - let mut writer = self.stream.lock().unwrap_or_else(|e| e.into_inner()); - - write!(writer, "{}{}", record.args(), self.line_sep)?; - - writer.flush()?; - } - Ok(()) - }); - } - - fn flush(&self) { - let _ = self - .stream - .lock() - .unwrap_or_else(|e| e.into_inner()) - .flush(); - } - } - }; -} - -writer_log_impl!(File); -writer_log_impl!(Writer); - -#[cfg(all(not(windows), feature = "reopen-03"))] -writer_log_impl!(Reopen); - -impl Log for Sender { - fn enabled(&self, _: &log::Metadata) -> bool { - true - } - - fn log(&self, record: &log::Record) { - fallback_on_error(record, |record| { - let msg = format!("{}{}", record.args(), self.line_sep); - self.stream - .lock() - .unwrap_or_else(|e| e.into_inner()) - .send(msg)?; - Ok(()) - }); - } - - fn flush(&self) {} -} - -#[cfg(any(feature = "syslog-3", feature = "syslog-4"))] -macro_rules! send_syslog { - ($logger:expr, $level:expr, $message:expr) => { - use log::Level; - match $level { - Level::Error => $logger.err($message)?, - Level::Warn => $logger.warning($message)?, - Level::Info => $logger.info($message)?, - Level::Debug | Level::Trace => $logger.debug($message)?, - } - }; -} - -#[cfg(all(not(windows), feature = "syslog-3"))] -impl Log for Syslog3 { - fn enabled(&self, _: &log::Metadata) -> bool { - true - } - - fn log(&self, record: &log::Record) { - fallback_on_error(record, |record| { - let message = record.args(); - send_syslog!(self.inner, record.level(), message); - - Ok(()) - }); - } - fn flush(&self) {} -} - -#[cfg(all(not(windows), feature = "syslog-4"))] -impl Log for Syslog4Rfc3164 { - fn enabled(&self, _: &log::Metadata) -> bool { - true - } - - fn log(&self, record: &log::Record) { - fallback_on_error(record, |record| { - let message = record.args().to_string(); - let mut log = self.inner.lock().unwrap_or_else(|e| e.into_inner()); - send_syslog!(log, record.level(), message); - - Ok(()) - }); - } - fn flush(&self) {} -} - -#[cfg(all(not(windows), feature = "syslog-4"))] -impl Log for Syslog4Rfc5424 { - fn enabled(&self, _: &log::Metadata) -> bool { - true - } - - fn log(&self, record: &log::Record) { - fallback_on_error(record, |record| { - let transformed = (self.transform)(record); - let mut log = self.inner.lock().unwrap_or_else(|e| e.into_inner()); - send_syslog!(log, record.level(), transformed); - - Ok(()) - }); - } - fn flush(&self) {} -} - -impl Log for Panic { - fn enabled(&self, _: &log::Metadata) -> bool { - true - } - - fn log(&self, record: &log::Record) { - panic!("{}", record.args()); - } - - fn flush(&self) {} -} - -#[cfg(feature = "date-based")] -impl Log for DateBased { - fn enabled(&self, _: &log::Metadata) -> bool { - true - } - - fn log(&self, record: &log::Record) { - fallback_on_error(record, |record| { - // Formatting first prevents deadlocks on file-logging, - // when the process of formatting itself is logged. - // note: this is only ever needed if some Debug, Display, or other - // formatting trait itself is logging. - #[cfg(feature = "meta-logging-in-format")] - let msg = format!("{}{}", record.args(), self.config.line_sep); - - let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner()); - - // check if log needs to be rotated - let new_suffix = self.config.compute_current_suffix(); - if state.file_stream.is_none() || state.current_suffix != new_suffix { - let file_open_result = self.config.open_current_log_file(&new_suffix); - match file_open_result { - Ok(file) => { - state.replace_file(new_suffix, Some(file)); - } - Err(e) => { - state.replace_file(new_suffix, None); - return Err(e.into()); - } - } - } - - // either just initialized writer above, or already errored out. - let writer = state.file_stream.as_mut().unwrap(); - - #[cfg(feature = "meta-logging-in-format")] - write!(writer, "{}", msg)?; - #[cfg(not(feature = "meta-logging-in-format"))] - write!(writer, "{}{}", record.args(), self.config.line_sep)?; - - writer.flush()?; - - Ok(()) - }); - } - - fn flush(&self) { - let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner()); - - if let Some(stream) = &mut state.file_stream { - let _ = stream.flush(); - } - } -} - -#[inline(always)] -fn fallback_on_error(record: &log::Record, log_func: F) -where - F: FnOnce(&log::Record) -> Result<(), LogError>, -{ - if let Err(error) = log_func(record) { - backup_logging(record, &error) - } -} - -fn backup_logging(record: &log::Record, error: &LogError) { - let second = write!( - io::stderr(), - "Error performing logging.\ - \n\tattempted to log: {}\ - \n\trecord: {:?}\ - \n\tlogging error: {}", - record.args(), - record, - error - ); - - if let Err(second_error) = second { - panic!( - "Error performing stderr logging after error occurred during regular logging.\ - \n\tattempted to log: {}\ - \n\trecord: {:?}\ - \n\tfirst logging error: {}\ - \n\tstderr error: {}", - record.args(), - record, - error, - second_error, - ); - } -} - -#[derive(Debug)] -enum LogError { - Io(io::Error), - Send(mpsc::SendError), - #[cfg(all(not(windows), feature = "syslog-4"))] - Syslog4(syslog4::Error), -} - -impl fmt::Display for LogError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - LogError::Io(ref e) => write!(f, "{}", e), - LogError::Send(ref e) => write!(f, "{}", e), - #[cfg(all(not(windows), feature = "syslog-4"))] - LogError::Syslog4(ref e) => write!(f, "{}", e), - } - } -} - -impl From for LogError { - fn from(error: io::Error) -> Self { - LogError::Io(error) - } -} - -impl From> for LogError { - fn from(error: mpsc::SendError) -> Self { - LogError::Send(error) - } -} - -#[cfg(all(not(windows), feature = "syslog-4"))] -impl From for LogError { - fn from(error: syslog4::Error) -> Self { - LogError::Syslog4(error) - } -} - -#[cfg(test)] -mod test { - use super::LevelConfiguration; - use log::LevelFilter::*; - - #[test] - fn test_level_config_find_exact_minimal() { - let config = LevelConfiguration::Minimal( - vec![("mod1", Info), ("mod2", Debug), ("mod3", Off)] - .into_iter() - .map(|(k, v)| (k.into(), v)) - .collect(), - ); - - assert_eq!(config.find_exact("mod1"), Some(Info)); - assert_eq!(config.find_exact("mod2"), Some(Debug)); - assert_eq!(config.find_exact("mod3"), Some(Off)); - } - - #[test] - fn test_level_config_find_exact_many() { - let config = LevelConfiguration::Many( - vec![("mod1", Info), ("mod2", Debug), ("mod3", Off)] - .into_iter() - .map(|(k, v)| (k.into(), v)) - .collect(), - ); - - assert_eq!(config.find_exact("mod1"), Some(Info)); - assert_eq!(config.find_exact("mod2"), Some(Debug)); - assert_eq!(config.find_exact("mod3"), Some(Off)); - } - - #[test] - fn test_level_config_simple_hierarchy() { - let config = LevelConfiguration::Minimal( - vec![("mod1", Info), ("mod2::sub_mod", Debug), ("mod3", Off)] - .into_iter() - .map(|(k, v)| (k.into(), v)) - .collect(), - ); - - assert_eq!(config.find_module("mod1::sub_mod"), Some(Info)); - assert_eq!(config.find_module("mod2::sub_mod::sub_mod_2"), Some(Debug)); - assert_eq!(config.find_module("mod3::sub_mod::sub_mod_2"), Some(Off)); - } - - #[test] - fn test_level_config_hierarchy_correct() { - let config = LevelConfiguration::Minimal( - vec![ - ("root", Trace), - ("root::sub1", Debug), - ("root::sub2", Info), - // should work with all insertion orders - ("root::sub2::sub2.3::sub2.4", Error), - ("root::sub2::sub2.3", Warn), - ("root::sub3", Off), - ] - .into_iter() - .map(|(k, v)| (k.into(), v)) - .collect(), - ); - - assert_eq!(config.find_module("root"), Some(Trace)); - assert_eq!(config.find_module("root::other_module"), Some(Trace)); - - // We want to ensure that it does pick up most specific level before trying - // anything more general. - assert_eq!(config.find_module("root::sub1"), Some(Debug)); - assert_eq!(config.find_module("root::sub1::other_module"), Some(Debug)); - - assert_eq!(config.find_module("root::sub2"), Some(Info)); - assert_eq!(config.find_module("root::sub2::other"), Some(Info)); - - assert_eq!(config.find_module("root::sub2::sub2.3"), Some(Warn)); - assert_eq!( - config.find_module("root::sub2::sub2.3::sub2.4"), - Some(Error) - ); - - assert_eq!(config.find_module("root::sub3"), Some(Off)); - assert_eq!( - config.find_module("root::sub3::any::children::of::sub3"), - Some(Off) - ); - } - - #[test] - fn test_level_config_similar_names_are_not_same() { - let config = LevelConfiguration::Minimal( - vec![("root", Trace), ("rootay", Info)] - .into_iter() - .map(|(k, v)| (k.into(), v)) - .collect(), - ); - - assert_eq!(config.find_module("root"), Some(Trace)); - assert_eq!(config.find_module("root::sub"), Some(Trace)); - assert_eq!(config.find_module("rooty"), None); - assert_eq!(config.find_module("rooty::sub"), None); - assert_eq!(config.find_module("rootay"), Some(Info)); - assert_eq!(config.find_module("rootay::sub"), Some(Info)); - } - - #[test] - fn test_level_config_single_colon_is_not_double_colon() { - let config = LevelConfiguration::Minimal( - vec![ - ("root", Trace), - ("root::su", Debug), - ("root::su:b2", Info), - ("root::sub2", Warn), - ] - .into_iter() - .map(|(k, v)| (k.into(), v)) - .collect(), - ); - - assert_eq!(config.find_module("root"), Some(Trace)); - - assert_eq!(config.find_module("root::su"), Some(Debug)); - assert_eq!(config.find_module("root::su::b2"), Some(Debug)); - - assert_eq!(config.find_module("root::su:b2"), Some(Info)); - assert_eq!(config.find_module("root::su:b2::b3"), Some(Info)); - - assert_eq!(config.find_module("root::sub2"), Some(Warn)); - assert_eq!(config.find_module("root::sub2::b3"), Some(Warn)); - } - - #[test] - fn test_level_config_all_chars() { - let config = LevelConfiguration::Minimal( - vec![("♲", Trace), ("☸", Debug), ("♲::☸", Info), ("♲::\t", Debug)] - .into_iter() - .map(|(k, v)| (k.into(), v)) - .collect(), - ); - - assert_eq!(config.find_module("♲"), Some(Trace)); - assert_eq!(config.find_module("♲::other"), Some(Trace)); - - assert_eq!(config.find_module("☸"), Some(Debug)); - assert_eq!(config.find_module("☸::any"), Some(Debug)); - - assert_eq!(config.find_module("♲::☸"), Some(Info)); - assert_eq!(config.find_module("♲☸"), None); - - assert_eq!(config.find_module("♲::\t"), Some(Debug)); - assert_eq!(config.find_module("♲::\t::\n\n::\t"), Some(Debug)); - assert_eq!(config.find_module("♲::\t\t"), Some(Trace)); - } -} diff --git a/src/logger.rs b/src/logger.rs new file mode 100644 index 0000000..90b8650 --- /dev/null +++ b/src/logger.rs @@ -0,0 +1,839 @@ +use std::{ + borrow::Cow, + fs, + io::{self, BufWriter, Write}, + sync::{mpsc, Mutex}, +}; + +#[cfg(feature = "date-based")] +use std::{ + ffi::OsString, + fs::OpenOptions, + path::{Path, PathBuf}, +}; + +use log::{self, Log}; + +use crate::{*, dispatch::{Output, OutputInner}}; + +pub trait CustomLineSep { + fn line_sep(self, line_sep: Cow<'static, str>) -> Self; +} + +/// Returns an stdout logger using a custom separator. +/// +/// If the default separator of `\n` is acceptable, an `io::Stdout` +/// instance can be passed into `Dispatch::chain()` directly. +/// +/// ``` +/// fern::Dispatch::new().chain(std::io::stdout()) +/// # .into_log(); +/// ``` +/// +/// Example usage: +/// +/// ``` +/// fern::Dispatch::new() +/// // some unix tools use null bytes as message terminators so +/// // newlines in messages can be treated differently. +/// .chain(fern::Output::stdout("\0")) +/// # .into_log(); +/// ``` +pub fn stdout() -> impl Log + CustomLineSep + Into { + Writer::new(io::stdout()) +} + +/// Returns an stderr logger using a custom separator. +/// +/// If the default separator of `\n` is acceptable, an `io::Stderr` +/// instance can be passed into `Dispatch::chain()` directly. +/// +/// ``` +/// fern::Dispatch::new().chain(std::io::stderr()) +/// # .into_log(); +/// ``` +/// +/// Example usage: +/// +/// ``` +/// fern::Dispatch::new().chain(fern::Output::stderr("\n\n\n")) +/// # .into_log(); +/// ``` +pub fn stderr() -> impl Log + CustomLineSep + Into { + Writer::new(io::stderr()) +} + +/// Returns a file logger using a custom separator. +/// +/// If the default separator of `\n` is acceptable, an [`fs::File`] +/// instance can be passed into [`Dispatch::chain`] directly. +/// +/// ```no_run +/// # fn setup_logger() -> Result<(), fern::InitError> { +/// fern::Dispatch::new().chain(std::fs::File::create("log")?) +/// # .into_log(); +/// # Ok(()) +/// # } +/// # +/// # fn main() { setup_logger().expect("failed to set up logger"); } +/// ``` +/// +/// ```no_run +/// # fn setup_logger() -> Result<(), fern::InitError> { +/// fern::Dispatch::new().chain(fern::log_file("log")?) +/// # .into_log(); +/// # Ok(()) +/// # } +/// # +/// # fn main() { setup_logger().expect("failed to set up logger"); } +/// ``` +/// +/// Example usage (using [`fern::log_file`]): +/// +/// ```no_run +/// # fn setup_logger() -> Result<(), fern::InitError> { +/// fern::Dispatch::new().chain(fern::Output::file(fern::log_file("log")?, "\r\n")) +/// # .into_log(); +/// # Ok(()) +/// # } +/// # +/// # fn main() { setup_logger().expect("failed to set up logger"); } +/// ``` +/// +/// [`fs::File`]: https://doc.rust-lang.org/std/fs/struct.File.html +/// [`Dispatch::chain`]: struct.Dispatch.html#method.chain +/// [`fern::log_file`]: fn.log_file.html + + +/// Convenience method for opening a log file with common options. +/// +/// Equivalent to: +/// +/// ```no_run +/// std::fs::OpenOptions::new() +/// .write(true) +/// .create(true) +/// .append(true) +/// .open("filename") +/// # ; +/// ``` +/// +/// See [`OpenOptions`] for more information. +/// +/// [`OpenOptions`]: https://doc.rust-lang.org/std/fs/struct.OpenOptions.html +pub fn file(path: impl AsRef) -> std::io::Result> { + OpenOptions::new() + .write(true) + .create(true) + .append(true) + .open(path) + .map(std::io::BufWriter::new) + .map(Writer::new) +} + +/// Returns a logger using arbitrary write object and custom separator. +/// +/// If the default separator of `\n` is acceptable, an `Box` +/// instance can be passed into [`Dispatch::chain`] directly. +/// +/// ```no_run +/// # fn setup_logger() -> Result<(), fern::InitError> { +/// // Anything implementing 'Write' works. +/// let mut writer = std::io::Cursor::new(Vec::::new()); +/// +/// fern::Dispatch::new() +/// // as long as we explicitly cast into a type-erased Box +/// .chain(Box::new(writer) as Box) +/// # .into_log(); +/// # Ok(()) +/// # } +/// # +/// # fn main() { setup_logger().expect("failed to set up logger"); } +/// ``` +/// +/// Example usage: +/// +/// ```no_run +/// # fn setup_logger() -> Result<(), fern::InitError> { +/// let writer = Box::new(std::io::Cursor::new(Vec::::new())); +/// +/// fern::Dispatch::new().chain(fern::Output::writer(writer, "\r\n")) +/// # .into_log(); +/// # Ok(()) +/// # } +/// # +/// # fn main() { setup_logger().expect("failed to set up logger"); } +/// ``` +/// +/// [`Dispatch::chain`]: struct.Dispatch.html#method.chain +pub fn writer(writer: W) -> impl Log + CustomLineSep + Into { + Writer::new(writer) +} + +/// Returns a reopenable logger, i.e., handling SIGHUP. +/// +/// If the default separator of `\n` is acceptable, a `Reopen` +/// instance can be passed into [`Dispatch::chain`] directly. +/// +/// This function is not available on Windows, and it requires the `reopen-03` +/// feature to be enabled. +/// +/// ```no_run +/// use std::fs::OpenOptions; +/// # fn setup_logger() -> Result<(), fern::InitError> { +/// let reopenable = reopen::Reopen::new(Box::new(|| { +/// OpenOptions::new() +/// .create(true) +/// .write(true) +/// .append(true) +/// .open("/tmp/output.log") +/// })) +/// .unwrap(); +/// +/// fern::Dispatch::new().chain(fern::Output::reopen(reopenable, "\n")) +/// # .into_log(); +/// # Ok(()) +/// # } +/// # +/// # fn main() { setup_logger().expect("failed to set up logger"); } +/// ``` +/// [`Dispatch::chain`]: struct.Dispatch.html#method.chain + +/// Convenience method for opening a re-openable log file with common options. +/// +/// The file opening is equivalent to: +/// +/// ```no_run +/// std::fs::OpenOptions::new() +/// .write(true) +/// .create(true) +/// .append(true) +/// .open("filename") +/// # ; +/// ``` +/// +/// See [`OpenOptions`] for more information. +/// +/// [`OpenOptions`]: https://doc.rust-lang.org/std/fs/struct.OpenOptions.html +/// +/// This function is not available on Windows, and it requires the `reopen-03` +/// feature to be enabled. +#[cfg(all(not(windows), feature = "reopen-03"))] +#[allow(deprecated)] +pub fn reopen(path: impl Into, signal: Option) -> io::Result> { + let path = path.into(); + let reopen = reopen::Reopen::new(Box::new(move || { + OpenOptions::new() + .write(true) + .create(true) + .append(true) + .open(&path) + }))?; + + if let Some(s) = signal { + reopen.handle().register_signal(s)?; + } + Ok(Writer::new(reopen)) +} + +/// Returns a mpsc::Sender logger using a custom separator. +/// +/// If the default separator of `\n` is acceptable, an +/// `mpsc::Sender` instance can be passed into `Dispatch:: +/// chain()` directly. +/// +/// Each log message will be suffixed with the separator, then sent as a +/// single String to the given sender. +/// +/// ``` +/// use std::sync::mpsc::channel; +/// +/// let (tx, rx) = channel(); +/// fern::Dispatch::new().chain(tx) +/// # .into_log(); +/// ``` +pub fn sender(sender: mpsc::Sender) -> impl Log + Into { + struct Sender { + stream: Mutex>, + } + + impl Log for Sender { + fn enabled(&self, _: &log::Metadata) -> bool { + true + } + + fn log(&self, record: &log::Record) { + fallback_on_error(record, |record| { + let msg = format!("{}", record.args()); + self.stream + .lock() + .unwrap_or_else(|e| e.into_inner()) + .send(msg)?; + Ok(()) + }); + } + + fn flush(&self) {} + } + + impl From for Output { + fn from(log: Sender) -> Self { + Output(OutputInner::Logger(Box::new(log))) + } + } + + Sender { stream: Mutex::new(sender), } +} + +/// Returns a logger which simply calls the given function with each +/// message. +/// +/// The function will be called inline in the thread the log occurs on. +/// +/// Example usage: +/// +/// ``` +/// fern::Dispatch::new().chain(fern::Output::call(|record| { +/// // this is mundane, but you can do anything here. +/// println!("{}", record.args()); +/// })) +/// # .into_log(); +/// ``` +pub fn call(func: F) -> impl Log + Into +where + F: Fn(&log::Record) + Sync + Send + 'static, +{ + struct CallShim(F); + + impl log::Log for CallShim + where + F: Fn(&log::Record) + Sync + Send + 'static, + { + fn enabled(&self, _: &log::Metadata) -> bool { + true + } + fn log(&self, record: &log::Record) { + (self.0)(record) + } + fn flush(&self) {} + } + + impl From> for Output { + fn from(log: CallShim) -> Output { + Output(OutputInner::Logger(Box::new(log))) + } + } + + CallShim(func) +} + +struct Writer { + stream: Mutex, + line_sep: Cow<'static, str>, +} + +impl Writer { + pub fn new(writer: W) -> Self { + Self { + stream: Mutex::new(writer), + line_sep: "\n".into(), + } + } +} + +impl CustomLineSep for Writer { + fn line_sep(mut self, line_sep: Cow<'static, str>) -> Self { + self.line_sep = line_sep; + self + } +} + +// impl From for Writer { +// fn from(stream: io::Stdout) -> Self { +// Self::new(stream) +// } +// } + +// impl From for Writer { +// fn from(stream: io::Stderr) -> Self { +// Self::new(stream) +// } +// } + +// #[cfg(all(not(windows), feature = "reopen-03"))] +// impl From> for Writer> { +// fn from(stream: reopen::Reopen) -> Self { +// Self::new(stream) +// } +// } + +impl Log for Writer { + fn enabled(&self, _: &log::Metadata) -> bool { + true + } + + fn log(&self, record: &log::Record) { + fallback_on_error(record, |record| { + if cfg!(feature = "meta-logging-in-format") { + // Formatting first prevents deadlocks on file-logging, + // when the process of formatting itself is logged. + // note: this is only ever needed if some Debug, Display, or other + // formatting trait itself is logging. + let msg = format!("{}{}", record.args(), self.line_sep); + + let mut writer = self.stream.lock().unwrap_or_else(|e| e.into_inner()); + + write!(writer, "{}", msg)?; + + writer.flush()?; + } else { + let mut writer = self.stream.lock().unwrap_or_else(|e| e.into_inner()); + + write!(writer, "{}{}", record.args(), self.line_sep)?; + + writer.flush()?; + } + Ok(()) + }); + } + + fn flush(&self) { + let _ = self.stream.lock().unwrap_or_else(|e| e.into_inner()) + .flush(); + } +} + +impl From> for Output { + fn from(log: Writer) -> Self { + Output(OutputInner::Logger(Box::new(log))) + } +} + +/// Writes all messages to the reopen::Reopen file with `line_sep` +/// separator. + +/// Logger which will panic whenever anything is logged. The panic +/// will be exactly the message of the log. +/// +/// `Panic` is useful primarily as a secondary logger, filtered by warning or +/// error. +/// +/// # Examples +/// +/// This configuration will output all messages to stdout and panic if an Error +/// message is sent. +/// +/// ``` +/// fern::Dispatch::new() +/// // format, etc. +/// .chain(std::io::stdout()) +/// .chain( +/// fern::Dispatch::new() +/// .level(log::LevelFilter::Error) +/// .chain(fern::Panic), +/// ) +/// # /* +/// .apply()?; +/// # */ .into_log(); +/// ``` +/// +/// This sets up a "panic on warn+" logger, and ignores errors so it can be +/// called multiple times. +/// +/// This might be useful in test setup, for example, to disallow warn-level +/// messages. +/// +/// ```no_run +/// fn setup_panic_logging() { +/// fern::Dispatch::new() +/// .level(log::LevelFilter::Warn) +/// .chain(fern::Panic) +/// .apply() +/// // ignore errors from setting up logging twice +/// .ok(); +/// } +/// ``` +pub struct Panic; + +impl Log for Panic { + fn enabled(&self, _: &log::Metadata) -> bool { + true + } + + fn log(&self, record: &log::Record) { + panic!("{}", record.args()); + } + + fn flush(&self) {} +} + +impl From for Output { + /// Creates an output logger which will panic with message text for all + /// messages. + fn from(log: Panic) -> Self { + Output(OutputInner::Logger(Box::new(log))) + } +} + +pub struct Null; + +impl Log for Null { + fn enabled(&self, _: &log::Metadata) -> bool { + false + } + + fn log(&self, _: &log::Record) {} + + fn flush(&self) {} +} + +impl From for Output { + fn from(log: Null) -> Self { + Output(OutputInner::Logger(Box::new(log))) + } +} + +/// This is used to generate log file suffixed based on date, hour, and minute. +/// +/// The log file will be rotated automatically when the date changes. +#[derive(Debug)] +#[cfg(feature = "date-based")] +pub struct DateBased { + pub(crate) file_prefix: PathBuf, + pub(crate) file_suffix: Cow<'static, str>, + pub(crate) line_sep: Cow<'static, str>, + pub(crate) utc_time: bool, +} + +#[cfg(feature = "date-based")] +impl DateBased { + /// Create new date-based file logger with the given file prefix and + /// strftime-based suffix pattern. + /// + /// On initialization, fern will create a file with the suffix formatted + /// with the current time (either utc or local, see below). Each time a + /// record is logged, the format is checked against the current time, and if + /// the time has changed, the old file is closed and a new one opened. + /// + /// `file_suffix` will be interpreted as an `strftime` format. See + /// [`chrono::format::strftime`] for more information. + /// + /// `file_prefix` may be a full file path, and will be prepended to the + /// suffix to create the final file. + /// + /// Note that no separator will be placed in between `file_name` and + /// `file_suffix_pattern`. So if you call `DateBased::new("hello", + /// "%Y")`, the result will be a filepath `hello2019`. + /// + /// By default, this will use local time. For UTC time instead, use the + /// [`.utc_time()`][DateBased::utc_time] method after creating. + /// + /// By default, this will use `\n` as a line separator. For a custom + /// separator, use the [`.line_sep`][DateBased::line_sep] method + /// after creating. + /// + /// # Examples + /// + /// Containing the date (year, month and day): + /// + /// ``` + /// // logs/2019-10-23-my-program.log + /// let log = fern::DateBased::new("logs/", "%Y-%m-%d-my-program.log"); + /// + /// // program.log.23102019 + /// let log = fern::DateBased::new("my-program.log.", "%d%m%Y"); + /// ``` + /// + /// Containing the hour: + /// + /// ``` + /// // logs/2019-10-23 13 my-program.log + /// let log = fern::DateBased::new("logs/", "%Y-%m-%d %H my-program.log"); + /// + /// // program.log.2310201913 + /// let log = fern::DateBased::new("my-program.log.", "%d%m%Y%H"); + /// ``` + /// + /// Containing the minute: + /// + /// ``` + /// // logs/2019-10-23 13 my-program.log + /// let log = fern::DateBased::new("logs/", "%Y-%m-%d %H my-program.log"); + /// + /// // program.log.2310201913 + /// let log = fern::DateBased::new("my-program.log.", "%d%m%Y%H"); + /// ``` + /// + /// UNIX time, or seconds since 00:00 Jan 1st 1970: + /// + /// ``` + /// // logs/1571822854-my-program.log + /// let log = fern::DateBased::new("logs/", "%s-my-program.log"); + /// + /// // program.log.1571822854 + /// let log = fern::DateBased::new("my-program.log.", "%s"); + /// ``` + /// + /// Hourly, using UTC time: + /// + /// ``` + /// // logs/2019-10-23 23 my-program.log + /// let log = fern::DateBased::new("logs/", "%Y-%m-%d %H my-program.log").utc_time(); + /// + /// // program.log.2310201923 + /// let log = fern::DateBased::new("my-program.log.", "%d%m%Y%H").utc_time(); + /// ``` + /// + /// [`chrono::format::strftime`]: https://docs.rs/chrono/0.4.6/chrono/format/strftime/index.html + pub fn new(file_prefix: T, file_suffix: U) -> Self + where + T: AsRef, + U: Into>, + { + DateBased { + utc_time: false, + file_prefix: file_prefix.as_ref().to_owned(), + file_suffix: file_suffix.into(), + line_sep: "\n".into(), + } + } + + /// Changes the line separator this logger will use. + /// + /// The default line separator is `\n`. + /// + /// # Examples + /// + /// Using a windows line separator: + /// + /// ``` + /// let log = fern::DateBased::new("logs", "%s.log").line_sep("\r\n"); + /// ``` + pub fn line_sep(mut self, line_sep: T) -> Self + where + T: Into>, + { + self.line_sep = line_sep.into(); + self + } + + /// Orients this log file suffix formatting to use UTC time. + /// + /// The default is local time. + /// + /// # Examples + /// + /// This will use UTC time to determine the date: + /// + /// ``` + /// // program.log.2310201923 + /// let log = fern::DateBased::new("my-program.log.", "%d%m%Y%H").utc_time(); + /// ``` + pub fn utc_time(mut self) -> Self { + self.utc_time = true; + self + } + + /// Orients this log file suffix formatting to use local time. + /// + /// This is the default option. + /// + /// # Examples + /// + /// This log file will use local time - the latter method call overrides the + /// former. + /// + /// ``` + /// // program.log.2310201923 + /// let log = fern::DateBased::new("my-program.log.", "%d%m%Y%H") + /// .utc_time() + /// .local_time(); + /// ``` + pub fn local_time(mut self) -> Self { + self.utc_time = false; + self + } +} + + +/// File logger with a dynamic time-based name. +#[derive(Debug)] +#[cfg(feature = "date-based")] +pub(crate) struct DateBasedImpl { + pub(crate) config: DateBasedConfig, + pub(crate) state: Mutex, +} + +#[derive(Debug)] +#[cfg(feature = "date-based")] +pub(crate) enum ConfiguredTimezone { + Local, + Utc, +} + +#[derive(Debug)] +#[cfg(feature = "date-based")] +pub(crate) struct DateBasedConfig { + pub line_sep: Cow<'static, str>, + /// This is a Path not an str so it can hold invalid UTF8 paths correctly. + pub file_prefix: PathBuf, + pub file_suffix: Cow<'static, str>, + pub timezone: ConfiguredTimezone, +} + +#[derive(Debug)] +#[cfg(feature = "date-based")] +pub(crate) struct DateBasedState { + pub current_suffix: String, + pub file_stream: Option>, +} + +#[cfg(feature = "date-based")] +impl DateBasedState { + pub fn new(current_suffix: String, file_stream: Option) -> Self { + DateBasedState { + current_suffix, + file_stream: file_stream.map(BufWriter::new), + } + } + + pub fn replace_file(&mut self, new_suffix: String, new_file: Option) { + if let Some(mut old) = self.file_stream.take() { + let _ = old.flush(); + } + self.current_suffix = new_suffix; + self.file_stream = new_file.map(BufWriter::new) + } +} + +#[cfg(feature = "date-based")] +impl DateBasedConfig { + pub fn new( + line_sep: Cow<'static, str>, + file_prefix: PathBuf, + file_suffix: Cow<'static, str>, + timezone: ConfiguredTimezone, + ) -> Self { + DateBasedConfig { + line_sep, + file_prefix, + file_suffix, + timezone, + } + } + + pub fn compute_current_suffix(&self) -> String { + match self.timezone { + ConfiguredTimezone::Utc => chrono::Utc::now().format(&self.file_suffix).to_string(), + ConfiguredTimezone::Local => chrono::Local::now().format(&self.file_suffix).to_string(), + } + } + + pub fn compute_file_path(&self, suffix: &str) -> PathBuf { + let mut path = OsString::from(&*self.file_prefix); + // use the OsString::push method, not PathBuf::push which would add a path + // separator + path.push(suffix); + path.into() + } + + pub fn open_log_file(path: &Path) -> io::Result { + OpenOptions::new() + .write(true) + .create(true) + .append(true) + .open(path) + } + + pub fn open_current_log_file(&self, suffix: &str) -> io::Result { + Self::open_log_file(&self.compute_file_path(suffix)) + } +} + +#[cfg(feature = "date-based")] +impl Log for DateBasedImpl { + fn enabled(&self, _: &log::Metadata) -> bool { + true + } + + fn log(&self, record: &log::Record) { + fallback_on_error(record, |record| { + // Formatting first prevents deadlocks on file-logging, + // when the process of formatting itself is logged. + // note: this is only ever needed if some Debug, Display, or other + // formatting trait itself is logging. + #[cfg(feature = "meta-logging-in-format")] + let msg = format!("{}{}", record.args(), self.config.line_sep); + + let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner()); + + // check if log needs to be rotated + let new_suffix = self.config.compute_current_suffix(); + if state.file_stream.is_none() || state.current_suffix != new_suffix { + let file_open_result = self.config.open_current_log_file(&new_suffix); + match file_open_result { + Ok(file) => { + state.replace_file(new_suffix, Some(file)); + } + Err(e) => { + state.replace_file(new_suffix, None); + return Err(e.into()); + } + } + } + + // either just initialized writer above, or already errored out. + let writer = state.file_stream.as_mut().unwrap(); + + #[cfg(feature = "meta-logging-in-format")] + write!(writer, "{}", msg)?; + #[cfg(not(feature = "meta-logging-in-format"))] + write!(writer, "{}{}", record.args(), self.config.line_sep)?; + + writer.flush()?; + + Ok(()) + }); + } + + fn flush(&self) { + let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner()); + + if let Some(stream) = &mut state.file_stream { + let _ = stream.flush(); + } + } +} + +#[cfg(feature = "date-based")] +impl From for Output { + /// Create an output logger which defers to the given date-based logger. Use + /// configuration methods on [DateBased] to set line separator and filename. + fn from(config: DateBased) -> Self { + let config = DateBasedConfig::new( + config.line_sep, + config.file_prefix, + config.file_suffix, + if config.utc_time { + ConfiguredTimezone::Utc + } else { + ConfiguredTimezone::Local + }, + ); + let computed_suffix = config.compute_current_suffix(); + // ignore errors - we'll just retry later. + let initial_file = config.open_current_log_file(&computed_suffix).ok(); + + Output(OutputInner::Logger(Box::new(DateBasedImpl { + config, + state: Mutex::new(DateBasedState::new(computed_suffix, initial_file)), + }))) + } +} + +#[cfg(all(not(windows), feature = "syslog-4"))] +impl From for LogError { + fn from(error: syslog4::Error) -> Self { + LogError::Syslog4(error) + } +} diff --git a/src/syslog.rs b/src/syslog.rs index 399c532..18e7aa9 100644 --- a/src/syslog.rs +++ b/src/syslog.rs @@ -151,3 +151,230 @@ info!(target: "explicit-syslog", "this will also show up!"); # } ``` */ + +use crate::{*, dispatch::{Output, OutputInner}}; + +use std::{ + collections::HashMap, + sync::Mutex, +}; + +use log::Log; + +#[cfg(any(feature = "syslog-3", feature = "syslog-4"))] +macro_rules! send_syslog { + ($logger:expr, $level:expr, $message:expr) => { + use log::Level; + match $level { + Level::Error => $logger.err($message)?, + Level::Warn => $logger.warning($message)?, + Level::Info => $logger.info($message)?, + Level::Debug | Level::Trace => $logger.debug($message)?, + } + }; +} + +/// Passes all messages to the syslog. +/// Creates an output logger which writes all messages to the given syslog +/// output. +/// +/// Log levels are translated trace => debug, debug => debug, info => +/// informational, warn => warning, and error => error. +/// +/// This requires the `"syslog-3"` feature. +#[cfg(all(not(windows), feature = "syslog-3"))] +pub fn syslog3(log: syslog3::Logger) -> impl Log + Into { + struct Syslog3 { + inner: syslog3::Logger, + } + + impl Log for Syslog3 { + fn enabled(&self, _: &log::Metadata) -> bool { + true + } + + fn log(&self, record: &log::Record) { + fallback_on_error(record, |record| { + let message = record.args(); + send_syslog!(self.inner, record.level(), message); + + Ok(()) + }); + } + + fn flush(&self) {} + } + + impl From for Output { + fn from(log: Syslog3) -> Self { + Output(OutputInner::Logger(Box::new(log))) + } + } + + Syslog3 { inner: log } +} + + +#[cfg(all(not(windows), feature = "syslog-4"))] +pub(crate) type Syslog4Rfc3164Logger = syslog4::Logger; + +/// Passes all messages to the syslog. +#[cfg(all(not(windows), feature = "syslog-4"))] +pub fn syslog4_3164(log: Syslog4Rfc3164Logger) -> impl Log + Into { + pub struct Syslog4Rfc3164 { + pub(crate) inner: Mutex, + } + + impl Log for Syslog4Rfc3164 { + fn enabled(&self, _: &log::Metadata) -> bool { + true + } + + fn log(&self, record: &log::Record) { + fallback_on_error(record, |record| { + let message = record.args().to_string(); + let mut log = self.inner.lock().unwrap_or_else(|e| e.into_inner()); + send_syslog!(log, record.level(), message); + + Ok(()) + }); + } + fn flush(&self) {} + } + + impl From for Output { + /// Creates an output logger which writes all messages to the given syslog. + /// + /// Log levels are translated trace => debug, debug => debug, info => + /// informational, warn => warning, and error => error. + /// + /// Note that due to https://github.com/Geal/rust-syslog/issues/41, + /// logging to this backend requires one allocation per log call. + /// + /// This is for RFC 3164 loggers. To use an RFC 5424 logger, use the + /// [`Output::syslog_5424`] helper method. + /// + /// This requires the `"syslog-4"` feature. + fn from(log: Syslog4Rfc3164) -> Self { + Output(OutputInner::Logger(Box::new(log))) + } + } + + Syslog4Rfc3164 { + inner: Mutex::new(log), + } +} + +#[cfg(all(not(windows), feature = "syslog-4"))] +pub(crate) type Syslog4Rfc5424Logger = syslog4::Logger< + syslog4::LoggerBackend, + (i32, HashMap>, String), + syslog4::Formatter5424, +>; + +/// Passes all messages to the syslog. +/// Returns a logger which logs into an RFC5424 syslog. +/// +/// This method takes an additional transform method to turn the log data +/// into RFC5424 data. +/// +/// I've honestly got no clue what the expected keys and values are for +/// this kind of logging, so I'm just going to link [the rfc] instead. +/// +/// If you're an expert on syslog logging and would like to contribute +/// an example to put here, it would be gladly accepted! +/// +/// This requires the `"syslog-4"` feature. +/// +/// [the rfc]: https://tools.ietf.org/html/rfc5424 +#[cfg(all(not(windows), feature = "syslog-4"))] +pub fn syslog4_5424(logger: Syslog4Rfc5424Logger, transform: F) -> impl Log + Into +where + F: Fn(&log::Record) -> (i32, HashMap>, String) + + Sync + + Send + + 'static, +{ + pub struct Syslog4Rfc5424 { + pub(crate) inner: Mutex, + pub(crate) transform: F, + } + + impl Log for Syslog4Rfc5424 + where + F: Fn(&log::Record) -> (i32, HashMap>, String) + + Sync + + Send + + 'static + { + fn enabled(&self, _: &log::Metadata) -> bool { + true + } + + fn log(&self, record: &log::Record) { + fallback_on_error(record, |record| { + let transformed = (self.transform)(record); + let mut log = self.inner.lock().unwrap_or_else(|e| e.into_inner()); + send_syslog!(log, record.level(), transformed); + + Ok(()) + }); + } + fn flush(&self) {} + } + + impl From > for Output where + F: Fn(&log::Record) -> (i32, HashMap>, String) + + Sync + + Send + + 'static + { + fn from(log: Syslog4Rfc5424) -> Self { + Output(OutputInner::Logger(Box::new(log))) + } + } + + Syslog4Rfc5424 { + inner: Mutex::new(logger), + transform, + } +} + + +// #[cfg(all(not(windows), feature = "syslog-3"))] +// impl From for Output { +// /// Creates an output logger which writes all messages to the given syslog +// /// output. +// /// +// /// Log levels are translated trace => debug, debug => debug, info => +// /// informational, warn => warning, and error => error. +// /// +// /// Note that while this takes a Box for convenience (syslog +// /// methods return Boxes), it will be immediately unboxed upon storage +// /// in the configuration structure. This will create a configuration +// /// identical to that created by passing a raw `syslog::Logger`. +// /// +// /// This requires the `"syslog-3"` feature. +// fn from(log: syslog3::Logger) -> Self { +// Output(OutputInner::Logger(Box::new(log))) +// } +// } + +// #[cfg(all(not(windows), feature = "syslog-3"))] +// impl From> for Output { +// /// Creates an output logger which writes all messages to the given syslog +// /// output. +// /// +// /// Log levels are translated trace => debug, debug => debug, info => +// /// informational, warn => warning, and error => error. +// /// +// /// Note that while this takes a Box for convenience (syslog +// /// methods return Boxes), it will be immediately unboxed upon storage +// /// in the configuration structure. This will create a configuration +// /// identical to that created by passing a raw `syslog::Logger`. +// /// +// /// This requires the `"syslog-3"` feature. +// fn from(log: Box) -> Self { +// Output(OutputInner::Logger(1log as _)) +// } +// } diff --git a/tests/channel_logging.rs b/tests/channel_logging.rs index dc71728..2e6cafa 100644 --- a/tests/channel_logging.rs +++ b/tests/channel_logging.rs @@ -11,7 +11,9 @@ fn test_channel_logging() { // Create the channel let (send, recv) = mpsc::channel(); - let (_max_level, logger) = fern::Dispatch::new().chain(send).into_log(); + let (_max_level, logger) = fern::Dispatch::new() + .chain(fern::logger::sender(send)) + .into_log(); let l = &*logger; manual_log(l, Info, "message1"); @@ -19,6 +21,6 @@ fn test_channel_logging() { logger.flush(); - assert_eq!(recv.recv().unwrap(), "message1\n"); - assert_eq!(recv.recv().unwrap(), "message2\n"); + assert_eq!(recv.recv().unwrap(), "message1"); + assert_eq!(recv.recv().unwrap(), "message2"); } diff --git a/tests/enabled_is_deep_check.rs b/tests/enabled_is_deep_check.rs index 9210512..60a7cd4 100644 --- a/tests/enabled_is_deep_check.rs +++ b/tests/enabled_is_deep_check.rs @@ -5,12 +5,12 @@ use log::log_enabled; fn ensure_enabled_is_a_deep_check() { let dummy = fern::Dispatch::new() .level(log::LevelFilter::Warn) - .chain(std::io::stdout()); + .chain(fern::logger::stdout()); let stdout = fern::Dispatch::new() .level(log::LevelFilter::Info) .level_for("abc", log::LevelFilter::Debug) - .chain(std::io::stdout()); + .chain(fern::logger::stdout()); fern::Dispatch::new() .chain(stdout) diff --git a/tests/file_logging.rs b/tests/file_logging.rs index 49e6cf9..c871acc 100644 --- a/tests/file_logging.rs +++ b/tests/file_logging.rs @@ -1,11 +1,12 @@ //! Tests! -use std::{fs, io, io::prelude::*}; +use std::{fs, io::prelude::*}; use log::Level::*; mod support; use support::manual_log; +use fern::logger::CustomLineSep; #[test] fn test_basic_logging_file_logging() { @@ -18,8 +19,8 @@ fn test_basic_logging_file_logging() { let (_max_level, logger) = fern::Dispatch::new() .format(|out, msg, record| out.finish(format_args!("[{}] {}", record.level(), msg))) .level(log::LevelFilter::Info) - .chain(io::stdout()) - .chain(fern::log_file(log_file).expect("Failed to open log file")) + .chain(fern::logger::stdout()) + .chain(fern::logger::file(log_file).expect("Failed to open log file")) .into_log(); let l = &*logger; @@ -79,10 +80,10 @@ fn test_custom_line_separators() { // default format is just the message if not specified // default log level is 'trace' if not specified (logs all messages) // output to the log file with the "\r\n" line separator. - .chain(fern::Output::file( - fern::log_file(&log_file).expect("Failed to open log file"), - "\r\n", - )) + .chain(fern::logger::file(&log_file) + .expect("Failed to open log file") + .line_sep("\r\n".into()), + ) .into_log(); let l = &*logger; diff --git a/tests/global_logging.rs b/tests/global_logging.rs index cc5c894..29e5d2f 100644 --- a/tests/global_logging.rs +++ b/tests/global_logging.rs @@ -22,15 +22,15 @@ impl LogVerify { let formatted_message = format!("{}", record.args()); match &*formatted_message { "[INFO] Test information message" => { - assert_eq!(self.info, false, "expected only one info message"); + assert!(self.info, "expected only one info message"); self.info = true; } "[WARN] Test warning message" => { - assert_eq!(self.warn, false, "expected only one warn message"); + assert!(self.warn, "expected only one warn message"); self.warn = true; } "[ERROR] Test error message" => { - assert_eq!(self.error, false, "expected only one error message"); + assert!(!self.error, "expected only one error message"); self.error = true; } other => panic!("unexpected message: '{}'", other), @@ -85,16 +85,16 @@ fn test_global_logger() { log::logger().flush(); let verify_acquired = verify.0.lock().unwrap(); - assert_eq!( - verify_acquired.info, true, + assert!( + verify_acquired.info, "expected info message to be received" ); - assert_eq!( - verify_acquired.warn, true, + assert!( + verify_acquired.warn, "expected warn message to be received" ); - assert_eq!( - verify_acquired.error, true, + assert!( + verify_acquired.error, "expected error message to be received" ); } diff --git a/tests/meta_logging.rs b/tests/meta_logging.rs index e4807cd..28b37ad 100644 --- a/tests/meta_logging.rs +++ b/tests/meta_logging.rs @@ -4,7 +4,7 @@ //! These tests *will* deadlock if the feature is not enabled, so they're //! disabled by default. #![cfg(feature = "meta-logging-in-format")] -use std::{fmt, fs, io, io::prelude::*}; +use std::{fmt, fs, io::prelude::*}; use log::{Level::*, Log}; @@ -42,8 +42,8 @@ fn file_deadlock() { { let (_max_level, logger) = fern::Dispatch::new() .format(|out, msg, record| out.finish(format_args!("[{}] {}", record.level(), msg))) - .chain(io::stdout()) - .chain(fern::log_file(log_file).expect("Failed to open log file")) + .chain(fern::logger::stdout()) + .chain(fern::logger::file(log_file).expect("Failed to open log file")) .into_log(); let l = &*logger; diff --git a/tests/panic_logging.rs b/tests/panic_logging.rs index 99e6625..73dd0f2 100644 --- a/tests/panic_logging.rs +++ b/tests/panic_logging.rs @@ -8,7 +8,7 @@ use support::manual_log; #[test] #[should_panic(expected = "special panic message here")] fn test_panic_panics() { - let (_max_level, logger) = fern::Dispatch::new().chain(fern::Panic).into_log(); + let (_max_level, logger) = fern::Dispatch::new().chain(fern::logger::Panic).into_log(); let l = &*logger; @@ -20,9 +20,9 @@ fn warn_and_higher_panics_config() -> Box { .chain( fern::Dispatch::new() .level(log::LevelFilter::Warn) - .chain(fern::Panic), + .chain(fern::logger::Panic), ) - .chain(std::io::stdout()) + .chain(fern::logger::stdout()) .into_log(); logger } diff --git a/tests/reopen_logging.rs b/tests/reopen_logging.rs index 52db061..d28dc53 100644 --- a/tests/reopen_logging.rs +++ b/tests/reopen_logging.rs @@ -1,12 +1,14 @@ //! Tests! #![cfg(all(not(windows), feature = "reopen-03"))] -use std::{fs, io, io::prelude::*}; +#![allow(deprecated)] +use std::{fs, io::prelude::*}; use log::Level::*; mod support; use support::manual_log; +use fern::logger::CustomLineSep; #[test] fn test_basic_logging_reopen_logging() { @@ -19,8 +21,8 @@ fn test_basic_logging_reopen_logging() { let (_max_level, logger) = fern::Dispatch::new() .format(|out, msg, record| out.finish(format_args!("[{}] {}", record.level(), msg))) .level(log::LevelFilter::Info) - .chain(io::stdout()) - .chain(fern::log_reopen(&log_file, None).expect("Failed to open log file")) + .chain(fern::logger::stdout()) + .chain(fern::logger::reopen(log_file, None).expect("Failed to open log file")) .into_log(); let l = &*logger; @@ -80,10 +82,10 @@ fn test_custom_line_separators() { // default format is just the message if not specified // default log level is 'trace' if not specified (logs all messages) // output to the log file with the "\r\n" line separator. - .chain(fern::Output::reopen( - fern::log_reopen(&log_file, None).expect("Failed to open log file"), - "\r\n", - )) + .chain(fern::logger::reopen(&log_file, None) + .expect("Failed to open log file") + .line_sep("\r\n".into()), + ) .into_log(); let l = &*logger; diff --git a/tests/write_logging.rs b/tests/write_logging.rs index cd523af..ac74d17 100644 --- a/tests/write_logging.rs +++ b/tests/write_logging.rs @@ -46,11 +46,11 @@ fn test_raw_write_logging() { let (_max_level, logger) = fern::Dispatch::new() .format(|out, msg, record| out.finish(format_args!("[{}] {}", record.level(), msg))) .level(log::LevelFilter::Info) - .chain(io::stdout()) - .chain(Box::new(TestWriter { + .chain(fern::logger::stdout()) + .chain(fern::logger::writer(TestWriter { buf: Vec::new(), flag: flag.clone(), - }) as Box) + })) .into_log(); let l = &*logger;