libpt_log/
lib.rs

1//! # A specialized Logger for [`pt`](../libpt/index.html)
2//!
3//! This crate is part of [`pt`](../libpt/index.html), but can also be used as a standalone
4//! module.
5//!
6//! For the library version, only the basic [`tracing`] is used, so that it is possible for
7//! the end user to use the [`tracing`] frontend they desire.
8//!
9//! I did decide to create a [`Logger`] struct. This struct is mainly intended to be used with the
10//! python module of [`pt`](../libpt/index.html), but is still just as usable in other contexts.
11//! You can use this struct when use of the macros is not possible, but the macros should generally
12//! be preferred.
13//!
14//! ## Technologies used for logging:
15//! - [`tracing`]: base logging crate
16//! - [`tracing_appender`]: Used to log to files
17//! - [`tracing_subscriber`]: Used to do actual logging, formatting, to stdout
18#![warn(clippy::pedantic, clippy::style, clippy::nursery)]
19
20use std::{
21    fmt,
22    path::PathBuf,
23    sync::atomic::{AtomicBool, Ordering},
24};
25
26pub mod error;
27use error::Error;
28
29/// This is the magic dependency where the cool stuff happens
30///
31/// I'm just repackaging it a little to make it more ergonomic
32pub use tracing;
33pub use tracing::{debug, error, info, trace, warn, Level};
34use tracing_appender::{self};
35use tracing_subscriber::fmt::{format::FmtSpan, time};
36
37use anyhow::{bail, Result};
38/// The log level used when none is specified
39pub const DEFAULT_LOG_LEVEL: Level = Level::INFO;
40/// The path where logs are stored when no path is given.
41///
42/// Currently, this is `/dev/null`, meaning they will be written to the void = discarded.
43pub const DEFAULT_LOG_DIR: &str = "/dev/null";
44
45static INITIALIZED: AtomicBool = AtomicBool::new(false);
46
47/// Builder for a well configured [Logger]
48///
49/// This struct helps configure a global logger that can be used with either macros or methods, see
50/// [Logger].
51///
52/// ## Examples
53///
54/// ```
55/// # use libpt_log::{Logger, info};
56/// # fn main() {
57/// Logger::builder()
58///     .uptime(true)
59///     .build();
60/// info!("hello world");
61/// # }
62///
63/// ```
64#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
65#[allow(clippy::struct_excessive_bools)] // it's just true/false values, not states, and I don't
66                                         // need to reinvent the wheel
67pub struct LoggerBuilder {
68    /// create and log to logfiles
69    log_to_file: bool,
70    /// logfiles would be created here
71    log_dir: PathBuf,
72    /// use ANSI control sequences
73    ansi: bool,
74    /// show which source file produces a log
75    display_filename: bool,
76    /// show the log level of the message
77    display_level: bool,
78    /// show target context
79    display_target: bool,
80    /// sets the maximum verbosity level.
81    ///
82    /// For example, if set to [Error](Level::ERROR), logs at [Info](Level::INFO) will not be
83    /// printed. If set to [Debug](Level::DEBUG), logs at [Info](Level::INFO) will be printed.
84    max_level: Level,
85    /// show the id of the thread that created this message
86    display_thread_ids: bool,
87    /// show the name of the thread that created this message
88    display_thread_names: bool,
89    /// show which line in the source file produces a log
90    display_line_number: bool,
91    /// splits a log over multiple lines, looks like a python traceback
92    pretty: bool,
93    /// show when the log was created
94    show_time: bool,
95    /// show timestamps as uptime (duration since the logger was initialized)
96    uptime: bool,
97    /// log when span things happen
98    span_events: FmtSpan,
99}
100
101impl LoggerBuilder {
102    /// use the configured settings to build and initialize a new global [Logger]
103    ///
104    /// This will build a functional [Logger]. You don't need to use the [Logger] struct, it's
105    /// better to use the macros:
106    ///
107    /// * `error!`
108    /// * `warn!`
109    /// * `info!`
110    /// * `debug!`
111    /// * `trace!`
112    ///
113    /// instead of the methods of the [Logger] struct. You can however use the [Logger] struct in
114    /// cases where usage of a macro is bad or you are somehow working with multiple loggers.
115    ///
116    /// ## Examples
117    ///
118    /// ```
119    /// # use libpt_log::{Logger, info};
120    /// # fn main() {
121    /// Logger::builder()
122    ///     .uptime(true)
123    ///     .build();
124    /// info!("hello world");
125    /// # }
126    ///
127    /// ```
128    /// # Errors
129    ///
130    /// This function will return an error if a global Logger was aready initialized. This module
131    /// uses the [tracing] crate for logging, so if a [tracing] logger is initialized elsewhere,
132    /// this method will error.
133    pub fn build(self) -> Result<Logger> {
134        // only init if no init has been performed yet
135        if INITIALIZED.load(Ordering::Relaxed) {
136            warn!("trying to reinitialize the logger, ignoring");
137            bail!(Error::Usage("logging is already initialized".to_string()));
138        }
139        let subscriber = tracing_subscriber::fmt::Subscriber::builder()
140            .with_level(self.display_level)
141            .with_max_level(self.max_level)
142            .with_ansi(self.ansi)
143            .with_target(self.display_target)
144            .with_file(self.display_filename)
145            .with_thread_ids(self.display_thread_ids)
146            .with_line_number(self.display_line_number)
147            .with_thread_names(self.display_thread_names)
148            .with_span_events(self.span_events);
149        // HACK: somehow find a better solution for this
150        // I know this is hacky, but I couldn't get it any other way. I couldn't even find a
151        // project that could do it any other way. You can't apply one after another, because the
152        // type is changed every time. When using `Box<dyn Whatever>`, some methods complain about
153        // not being in trait bounds.
154        match (self.log_to_file, self.show_time, self.pretty, self.uptime) {
155            (true, true, true, true) => {
156                let subscriber = subscriber
157                    .with_writer(new_file_appender(self.log_dir))
158                    .with_timer(time::uptime())
159                    .pretty()
160                    .finish();
161                tracing::subscriber::set_global_default(subscriber)?;
162            }
163            (true, true, true, false) => {
164                let subscriber = subscriber
165                    .with_writer(new_file_appender(self.log_dir))
166                    .pretty()
167                    .finish();
168                tracing::subscriber::set_global_default(subscriber)?;
169            }
170            (true, false, true, _) => {
171                let subscriber = subscriber
172                    .with_writer(new_file_appender(self.log_dir))
173                    .without_time()
174                    .pretty()
175                    .finish();
176                tracing::subscriber::set_global_default(subscriber)?;
177            }
178            (true, true, false, true) => {
179                let subscriber = subscriber
180                    .with_writer(new_file_appender(self.log_dir))
181                    .with_timer(time::uptime())
182                    .finish();
183                tracing::subscriber::set_global_default(subscriber)?;
184            }
185            (true, true, false, false) => {
186                let subscriber = subscriber
187                    .with_writer(new_file_appender(self.log_dir))
188                    .finish();
189                tracing::subscriber::set_global_default(subscriber)?;
190            }
191            (true, false, false, _) => {
192                let subscriber = subscriber
193                    .with_writer(new_file_appender(self.log_dir))
194                    .without_time()
195                    .finish();
196                tracing::subscriber::set_global_default(subscriber)?;
197            }
198            (false, true, true, true) => {
199                let subscriber = subscriber.pretty().with_timer(time::uptime()).finish();
200                tracing::subscriber::set_global_default(subscriber)?;
201            }
202            (false, true, true, false) => {
203                let subscriber = subscriber.pretty().with_timer(time::uptime()).finish();
204                tracing::subscriber::set_global_default(subscriber)?;
205            }
206            (false, false, true, _) => {
207                let subscriber = subscriber.without_time().pretty().finish();
208                tracing::subscriber::set_global_default(subscriber)?;
209            }
210            (false, true, false, true) => {
211                let subscriber = subscriber.with_timer(time::uptime()).finish();
212                tracing::subscriber::set_global_default(subscriber)?;
213            }
214            (false, true, false, false) => {
215                let subscriber = subscriber.finish();
216                tracing::subscriber::set_global_default(subscriber)?;
217            }
218            (false, false, false, _) => {
219                let subscriber = subscriber.without_time().finish();
220                tracing::subscriber::set_global_default(subscriber)?;
221            }
222        }
223        INITIALIZED.store(true, Ordering::Relaxed);
224        Ok(Logger {})
225    }
226
227    /// enable or disable logging to and creating of logfiles
228    ///
229    /// If you want to log to a file, don't forget to set [`Self::log_dir`]!
230    ///
231    /// Default: false
232    #[must_use]
233    pub const fn log_to_file(mut self, log_to_file: bool) -> Self {
234        self.log_to_file = log_to_file;
235        self
236    }
237
238    /// set a directory where logfiles would be created in
239    ///
240    /// Enable or disable creation and logging to logfiles with [`log_to_file`](Self::log_to_file).
241    ///
242    /// Default: [`DEFAULT_LOG_DIR`] (/dev/null)
243    #[must_use]
244    pub fn log_dir(mut self, log_dir: PathBuf) -> Self {
245        self.log_dir = log_dir;
246        self
247    }
248
249    /// enable or disable ANSI control sequences
250    ///
251    /// Disabling ANSI control sequences might improve compatibility and readability when the logs
252    /// are displayed by a program that does not interpret them.
253    ///
254    /// Keeping ANSI control sequences enabled has the disadvantage of added colors for the logs.
255    ///
256    /// Default: true
257    #[must_use]
258    pub const fn ansi(mut self, ansi: bool) -> Self {
259        self.ansi = ansi;
260        self
261    }
262
263    /// when making a log, display the source file in which a log was crated in
264    ///
265    /// Default: false
266    #[must_use]
267    pub const fn display_filename(mut self, display_filename: bool) -> Self {
268        self.display_filename = display_filename;
269        self
270    }
271
272    /// when making a log, display the time of the message
273    ///
274    /// Default: true
275    #[must_use]
276    pub const fn display_time(mut self, show_time: bool) -> Self {
277        self.show_time = show_time;
278        self
279    }
280
281    /// when making a log, display the log level of the message
282    ///
283    /// Default: true
284    #[must_use]
285    pub const fn display_level(mut self, display_level: bool) -> Self {
286        self.display_level = display_level;
287        self
288    }
289
290    /// show target context
291    ///
292    /// Default: false
293    #[must_use]
294    pub const fn display_target(mut self, display_target: bool) -> Self {
295        self.display_target = display_target;
296        self
297    }
298
299    /// show the id of the thread that created this message
300    ///
301    /// Default: false
302    #[must_use]
303    pub const fn display_thread_ids(mut self, display_thread_ids: bool) -> Self {
304        self.display_thread_ids = display_thread_ids;
305        self
306    }
307
308    /// show the name of the thread that created this message
309    ///
310    /// Default: false
311    #[must_use]
312    pub const fn display_thread_names(mut self, display_thread_names: bool) -> Self {
313        self.display_thread_names = display_thread_names;
314        self
315    }
316
317    /// show which line in the source file produces a log
318    ///
319    /// Default: false
320    #[must_use]
321    pub const fn display_line_number(mut self, display_line_number: bool) -> Self {
322        self.display_line_number = display_line_number;
323        self
324    }
325
326    /// splits a log over multiple lines, looks like a python traceback
327    ///
328    /// Default: false
329    #[must_use]
330    pub const fn pretty(mut self, pretty: bool) -> Self {
331        self.pretty = pretty;
332        self
333    }
334
335    /// show timestamps as uptime (duration since the logger was initialized)
336    ///
337    /// Default: false
338    #[must_use]
339    pub const fn uptime(mut self, uptime: bool) -> Self {
340        self.uptime = uptime;
341        self
342    }
343
344    /// set the lowest loglevel to be displayed
345    ///
346    /// Default: [`Level::INFO`]
347    #[must_use]
348    pub const fn set_level(mut self, max_level: Level) -> Self {
349        self.max_level = max_level;
350        self
351    }
352
353    /// set how span events are handled
354    ///
355    /// Default: [`FmtSpan::NONE`]
356    #[must_use]
357    pub const fn span_events(mut self, span_events: FmtSpan) -> Self {
358        self.span_events = span_events;
359        self
360    }
361}
362
363impl Default for LoggerBuilder {
364    fn default() -> Self {
365        Self {
366            log_to_file: false,
367            log_dir: PathBuf::from(DEFAULT_LOG_DIR),
368            ansi: true,
369            display_filename: false,
370            display_level: true,
371            display_target: false,
372            max_level: DEFAULT_LOG_LEVEL,
373            display_thread_ids: false,
374            display_thread_names: false,
375            display_line_number: false,
376            pretty: false,
377            show_time: true,
378            uptime: false,
379            span_events: FmtSpan::NONE,
380        }
381    }
382}
383
384/// ## Logger for `libpt`
385///
386/// A logger is generally a functionality that let's you write information from your library or
387/// application in a more structured manner than if you just wrote all information to `stdout` or
388/// `stderr` with the likes of `println!` or `eprintln!`.
389///
390/// It offers writing to multiple targets, such as both the terminal and a log file, and allows
391/// users to choose the verbosity of the information that gets printed by selecting a
392/// [Loglevel](Level).
393///
394/// ## Levels
395///
396/// * [ERROR](Level::ERROR) – Something broke
397/// * [WARN](Level::WARN) – Something is bad
398/// * [INFO](Level::INFO) – Useful information for users
399/// * [DEBUG](Level::DEBUG) – Useful information for developers
400/// * [TRACE](Level::TRACE) – Very verbose information for developers (often for libraries)
401///
402/// ## Usage
403///
404/// You don't need to use the [Logger] struct, it's better to use the macros instead:
405///
406/// * [`error!`]
407/// * [`warn!`]
408/// * [`info!`]
409/// * [`debug!`]
410/// * [`trace!`]
411///
412/// You can however use the [Logger] struct in cases where usage of a macro is impossible or
413/// you are somehow working with multiple loggers. The macros offer additional functionalities,
414/// suck as full `format!` support and context, see [`tracing`], which we use as backend.
415///
416/// ## Examples
417///
418/// ```
419/// # use libpt_log::{Logger, info};
420/// # fn main() {
421/// Logger::builder()
422///     .uptime(true)
423///     .build();
424/// info!("hello world");
425/// # }
426///
427/// ```
428pub struct Logger;
429
430/// ## Main implementation
431impl Logger {
432    /// Get a new [`LoggerBuilder`]
433    #[must_use]
434    pub fn builder() -> LoggerBuilder {
435        LoggerBuilder::default()
436    }
437
438    /// ## logging at [`Level::ERROR`]
439    pub fn error<T>(&self, printable: T)
440    where
441        T: fmt::Display,
442    {
443        error!("{}", printable);
444    }
445    /// ## logging at [`Level::WARN`]
446    pub fn warn<T>(&self, printable: T)
447    where
448        T: fmt::Display,
449    {
450        warn!("{}", printable);
451    }
452    /// ## logging at [`Level::INFO`]
453    pub fn info<T>(&self, printable: T)
454    where
455        T: fmt::Display,
456    {
457        info!("{}", printable);
458    }
459    /// ## logging at [`Level::DEBUG`]
460    pub fn debug<T>(&self, printable: T)
461    where
462        T: fmt::Display,
463    {
464        debug!("{}", printable);
465    }
466    /// ## logging at [`Level::TRACE`]
467    pub fn trace<T>(&self, printable: T)
468    where
469        T: fmt::Display,
470    {
471        trace!("{}", printable);
472    }
473}
474
475impl fmt::Debug for Logger {
476    /// ## DEBUG representation for [`Logger`]
477    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
478        write!(
479            f,
480            "Logger: {{initialized: {}}} ",
481            INITIALIZED.load(Ordering::Relaxed)
482        )
483    }
484}
485
486impl Default for Logger {
487    fn default() -> Self {
488        LoggerBuilder::default()
489            .build()
490            .expect("building a Logger failed")
491    }
492}
493
494fn new_file_appender(log_dir: PathBuf) -> tracing_appender::rolling::RollingFileAppender {
495    tracing_appender::rolling::daily(
496        log_dir,
497        format!(
498            "{}.log",
499            libpt_core::get_crate_name().unwrap_or_else(|| "logfile".to_string())
500        ),
501    )
502}