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}