//! # A specialized Logger for [`pt`](../libpt/index.html) //! //! This crate is part of [`pt`](../libpt/index.html), but can also be used as a standalone //! module. //! //! For the library version, only the basic [`tracing`] is used, so that it is possible for //! the end user to use the [`tracing`] frontend they desire. //! //! I did decide to create a [`Logger`] struct. This struct is mainly intended to be used with the //! python module of [`pt`](../libpt/index.html), but is still just as usable in other contexts. //! You can use this struct when use of the macros is not possible, but the macros should generally //! be preferred. //! //! ## Technologies used for logging: //! - [`tracing`]: base logging crate //! - [`tracing_appender`]: Used to log to files //! - [`tracing_subscriber`]: Used to do actual logging, formatting, to stdout #![warn(clippy::pedantic, clippy::style, clippy::nursery)] use std::{ fmt, path::PathBuf, sync::atomic::{AtomicBool, Ordering}, }; pub mod error; use error::Error; /// This is the magic dependency where the cool stuff happens /// /// I'm just repackaging it a little to make it more ergonomic pub use tracing; pub use tracing::{debug, error, info, trace, warn, Level}; use tracing_appender::{self, non_blocking::NonBlocking}; use tracing_subscriber::fmt::{format::FmtSpan, time}; use anyhow::{bail, Result}; /// The log level used when none is specified pub const DEFAULT_LOG_LEVEL: Level = Level::INFO; /// The path where logs are stored when no path is given. /// /// Currently, this is `/dev/null`, meaning they will be written to the void = discarded. pub const DEFAULT_LOG_DIR: &str = "/dev/null"; static INITIALIZED: AtomicBool = AtomicBool::new(false); /// Builder for a well configured [Logger] /// /// This struct helps configure a global logger that can be used with either macros or methods, see /// [Logger]. /// /// ## Examples /// /// ``` /// # use libpt_log::{Logger, info}; /// # fn main() { /// Logger::builder() /// .uptime(true) /// .build(); /// info!("hello world"); /// # } /// /// ``` #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] #[allow(clippy::struct_excessive_bools)] // it's just true/false values, not states, and I don't // need to reinvent the wheel pub struct LoggerBuilder { /// create and log to logfiles log_to_file: bool, /// logfiles would be created here log_dir: PathBuf, /// use ANSI control sequences ansi: bool, /// show which source file produces a log display_filename: bool, /// show the log level of the message display_level: bool, /// show target context display_target: bool, /// sets the maximum verbosity level. /// /// For example, if set to [Error](Level::ERROR), logs at [Info](Level::INFO) will not be /// printed. If set to [Debug](Level::DEBUG), logs at [Info](Level::INFO) will be printed. max_level: Level, /// show the id of the thread that created this message display_thread_ids: bool, /// show the name of the thread that created this message display_thread_names: bool, /// show which line in the source file produces a log display_line_number: bool, /// splits a log over multiple lines, looks like a python traceback pretty: bool, /// show when the log was created show_time: bool, /// show timestamps as uptime (duration since the logger was initialized) uptime: bool, } impl LoggerBuilder { /// use the configured settings to build and initialize a new global [Logger] /// /// This will build a functional [Logger]. You don't need to use the [Logger] struct, it's /// better to use the macros: /// /// * `error!` /// * `warn!` /// * `info!` /// * `debug!` /// * `trace!` /// /// instead of the methods of the [Logger] struct. You can however use the [Logger] struct in /// cases where usage of a macro is bad or you are somehow working with multiple loggers. /// /// ## Examples /// /// ``` /// # use libpt_log::{Logger, info}; /// # fn main() { /// Logger::builder() /// .uptime(true) /// .build(); /// info!("hello world"); /// # } /// /// ``` /// # Errors /// /// This function will return an error if a global Logger was aready initialized. This module /// uses the [tracing] crate for logging, so if a [tracing] logger is initialized elsewhere, /// this method will error. pub fn build(self) -> Result { // only init if no init has been performed yet if INITIALIZED.load(Ordering::Relaxed) { warn!("trying to reinitialize the logger, ignoring"); bail!(Error::Usage("logging is already initialized".to_string())); } let subscriber = tracing_subscriber::fmt::Subscriber::builder() .with_level(self.display_level) .with_max_level(self.max_level) .with_ansi(self.ansi) .with_target(self.display_target) .with_file(self.display_filename) .with_thread_ids(self.display_thread_ids) .with_line_number(self.display_line_number) .with_thread_names(self.display_thread_names) .with_span_events(FmtSpan::FULL); // HACK: somehow find a better solution for this // I know this is hacky, but I couldn't get it any other way. I couldn't even find a // project that could do it any other way. You can't apply one after another, because the // type is changed every time. When using `Box`, some methods complain about // not being in trait bounds. match (self.log_to_file, self.show_time, self.pretty, self.uptime) { (true, true, true, true) => { let subscriber = subscriber .with_writer(new_file_appender(self.log_dir)) .with_timer(time::uptime()) .pretty() .finish(); tracing::subscriber::set_global_default(subscriber)?; } (true, true, true, false) => { let subscriber = subscriber .with_writer(new_file_appender(self.log_dir)) .pretty() .finish(); tracing::subscriber::set_global_default(subscriber)?; } (true, false, true, _) => { let subscriber = subscriber .with_writer(new_file_appender(self.log_dir)) .without_time() .pretty() .finish(); tracing::subscriber::set_global_default(subscriber)?; } (true, true, false, true) => { let subscriber = subscriber .with_writer(new_file_appender(self.log_dir)) .with_timer(time::uptime()) .finish(); tracing::subscriber::set_global_default(subscriber)?; } (true, true, false, false) => { let subscriber = subscriber .with_writer(new_file_appender(self.log_dir)) .finish(); tracing::subscriber::set_global_default(subscriber)?; } (true, false, false, _) => { let subscriber = subscriber .with_writer(new_file_appender(self.log_dir)) .without_time() .finish(); tracing::subscriber::set_global_default(subscriber)?; } (false, true, true, true) => { let subscriber = subscriber.pretty().with_timer(time::uptime()).finish(); tracing::subscriber::set_global_default(subscriber)?; } (false, true, true, false) => { let subscriber = subscriber.pretty().with_timer(time::uptime()).finish(); tracing::subscriber::set_global_default(subscriber)?; } (false, false, true, _) => { let subscriber = subscriber.without_time().pretty().finish(); tracing::subscriber::set_global_default(subscriber)?; } (false, true, false, true) => { let subscriber = subscriber.with_timer(time::uptime()).finish(); tracing::subscriber::set_global_default(subscriber)?; } (false, true, false, false) => { let subscriber = subscriber.finish(); tracing::subscriber::set_global_default(subscriber)?; } (false, false, false, _) => { let subscriber = subscriber.without_time().finish(); tracing::subscriber::set_global_default(subscriber)?; } } INITIALIZED.store(true, Ordering::Relaxed); Ok(Logger {}) } /// enable or disable logging to and creating of logfiles /// /// If you want to log to a file, don't forget to set [`Self::log_dir`]! /// /// Default: false #[must_use] pub const fn log_to_file(mut self, log_to_file: bool) -> Self { self.log_to_file = log_to_file; self } /// set a directory where logfiles would be created in /// /// Enable or disable creation and logging to logfiles with [`log_to_file`](Self::log_to_file). /// /// Default: [`DEFAULT_LOG_DIR`] (/dev/null) #[must_use] pub fn log_dir(mut self, log_dir: PathBuf) -> Self { self.log_dir = log_dir; self } /// enable or disable ANSI control sequences /// /// Disabling ANSI control sequences might improve compatibility and readability when the logs /// are displayed by a program that does not interpret them. /// /// Keeping ANSI control sequences enabled has the disadvantage of added colors for the logs. /// /// Default: true #[must_use] pub const fn ansi(mut self, ansi: bool) -> Self { self.ansi = ansi; self } /// when making a log, display the source file in which a log was crated in /// /// Default: false #[must_use] pub const fn display_filename(mut self, display_filename: bool) -> Self { self.display_filename = display_filename; self } /// when making a log, display the log level of the message /// /// Default: true #[must_use] pub const fn display_level(mut self, display_level: bool) -> Self { self.display_level = display_level; self } /// show target context /// /// Default: false #[must_use] pub const fn display_target(mut self, display_target: bool) -> Self { self.display_target = display_target; self } /// show the id of the thread that created this message /// /// Default: false #[must_use] pub const fn display_thread_ids(mut self, display_thread_ids: bool) -> Self { self.display_thread_ids = display_thread_ids; self } /// show the name of the thread that created this message /// /// Default: false #[must_use] pub const fn display_thread_names(mut self, display_thread_names: bool) -> Self { self.display_thread_names = display_thread_names; self } /// show which line in the source file produces a log /// /// Default: false #[must_use] pub const fn display_line_number(mut self, display_line_number: bool) -> Self { self.display_line_number = display_line_number; self } /// splits a log over multiple lines, looks like a python traceback /// /// Default: false #[must_use] pub const fn pretty(mut self, pretty: bool) -> Self { self.pretty = pretty; self } /// show timestamps as uptime (duration since the logger was initialized) /// /// Default: false #[must_use] pub const fn uptime(mut self, uptime: bool) -> Self { self.uptime = uptime; self } /// set the lowest loglevel to be displayed /// /// Default: [`Level::INFO`] #[must_use] pub const fn set_level(mut self, max_level: Level) -> Self { self.max_level = max_level; self } } impl Default for LoggerBuilder { fn default() -> Self { Self { log_to_file: false, log_dir: PathBuf::from(DEFAULT_LOG_DIR), ansi: true, display_filename: false, display_level: true, display_target: false, max_level: DEFAULT_LOG_LEVEL, display_thread_ids: false, display_thread_names: false, display_line_number: false, pretty: false, show_time: true, uptime: false, } } } /// ## Logger for [`pt`](libpt) /// /// A logger is generally a functionality that let's you write information from your library or /// application in a more structured manner than if you just wrote all information to `stdout` or /// `stderr` with the likes of `println!` or `eprintln!`. /// /// It offers writing to multiple targets, such as both the terminal and a log file, and allows /// users to choose the verbosity of the information that gets printed by selecting a /// [Loglevel](Level). /// /// ## Levels /// /// * [ERROR](Level::ERROR) – Something broke /// * [WARN](Level::WARN) – Something is bad /// * [INFO](Level::INFO) – Useful information for users /// * [DEBUG](Level::DEBUG) – Useful information for developers /// * [TRACE](Level::TRACE) – Very verbose information for developers (often for libraries) /// /// ## Usage /// /// You don't need to use the [Logger] struct, it's better to use the macros instead: /// /// * [`error!`] /// * [`warn!`] /// * [`info!`] /// * [`debug!`] /// * [`trace!`] /// /// You can however use the [Logger] struct in cases where usage of a macro is impossible or /// you are somehow working with multiple loggers. The macros offer additional functionalities, /// suck as full `format!` support and context, see [`tracing`], which we use as backend. /// /// ## Examples /// /// ``` /// # use libpt_log::{Logger, info}; /// # fn main() { /// Logger::builder() /// .uptime(true) /// .build(); /// info!("hello world"); /// # } /// /// ``` pub struct Logger; /// ## Main implementation impl Logger { /// Get a new [`LoggerBuilder`] #[must_use] pub fn builder() -> LoggerBuilder { LoggerBuilder::default() } /// ## logging at [`Level::ERROR`] pub fn error(&self, printable: T) where T: fmt::Display, { error!("{}", printable); } /// ## logging at [`Level::WARN`] pub fn warn(&self, printable: T) where T: fmt::Display, { warn!("{}", printable); } /// ## logging at [`Level::INFO`] pub fn info(&self, printable: T) where T: fmt::Display, { info!("{}", printable); } /// ## logging at [`Level::DEBUG`] pub fn debug(&self, printable: T) where T: fmt::Display, { debug!("{}", printable); } /// ## logging at [`Level::TRACE`] pub fn trace(&self, printable: T) where T: fmt::Display, { trace!("{}", printable); } } impl fmt::Debug for Logger { /// ## DEBUG representation for [`Logger`] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "Logger: {{initialized: {}}} ", INITIALIZED.load(Ordering::Relaxed) ) } } impl Default for Logger { fn default() -> Self { LoggerBuilder::default() .build() .expect("building a Logger failed") } } fn new_file_appender(log_dir: PathBuf) -> tracing_appender::rolling::RollingFileAppender { tracing_appender::rolling::daily(log_dir, format!("{}.log", env!("CARGO_CRATE_NAME"))) }