powerpack_logger/
lib.rs

1//! A simple file logger for use in Alfred workflows.
2//!
3//! The logger comes preconfigured with sensible defaults for use in Alfred
4//! workflows.
5//!
6//! # Usage
7//!
8//! This crate is re-exported from the `powerpack` crate, so you can access it
9//! using `powerpack::logger`.
10//!
11//! Using all the defaults. The following will write to a file at
12//! ```text
13//! {alfred_workflow_cache}/powerpack.log
14//! ```
15//!
16//! And the log level will be set to `Debug` if the workflow is running in debug
17//! mode, otherwise it will be set to `Info`.
18//!
19//! ```no_run
20//! # mod powerpack { pub extern crate powerpack_logger as logger; } // mock re-export
21//! use powerpack::logger;
22//!
23//! logger::Builder::new().init();
24//! ```
25//!
26//! `Logger::builder()` returns a builder where you can further configure the
27//! logger. For example to set the filename and the log level.
28//!
29//! ```no_run
30//! # mod powerpack { pub extern crate powerpack_logger as logger; } // mock re-export
31//! use powerpack::logger;
32//!
33//! const FILENAME: &str = concat!(env!("CARGO_PKG_NAME"), "-", env!("CARGO_PKG_VERSION"), ".log");
34//! logger::Builder::new()
35//!     .filename(FILENAME)
36//!     .max_level(logger::LevelFilter::Warn)
37//!     .init();
38//! ```
39
40use std::borrow::Cow;
41use std::fs;
42use std::io::prelude::*;
43use std::path::PathBuf;
44use std::sync::{Arc, Mutex};
45
46use log::Log;
47use thiserror::Error;
48
49use powerpack_env as env;
50
51pub use log::LevelFilter;
52
53/// An error that can occur when using the logger.
54#[derive(Debug, Error)]
55pub enum Error {
56    /// Raised when the home directory cannot be determined.
57    #[error("home directory not found")]
58    NoHomeDir,
59
60    /// An I/O error occurred.
61    #[error("io error")]
62    Io(#[from] std::io::Error),
63
64    /// Failed to set the logger.
65    #[error("failed to set logger")]
66    SetLogger(#[from] log::SetLoggerError),
67}
68
69/// A simple logger that writes log messages to a file.
70#[derive(Debug, Clone)]
71pub struct Logger {
72    file: Arc<Mutex<fs::File>>,
73    max_level: LevelFilter,
74}
75
76/// A builder for configuring the file logger.
77#[derive(Debug, Clone)]
78pub struct Builder {
79    directory: Option<PathBuf>,
80    filename: Option<Cow<'static, str>>,
81    max_level: Option<LevelFilter>,
82}
83
84impl Default for Builder {
85    #[inline]
86    fn default() -> Self {
87        Self::new()
88    }
89}
90
91impl Builder {
92    /// Returns a new builder for configuring the logger.
93    #[inline]
94    pub fn new() -> Self {
95        Self {
96            directory: None,
97            filename: None,
98            max_level: None,
99        }
100    }
101
102    /// Set the directory where the log file will be stored.
103    ///
104    /// Defaults to the Alfred workflow cache directory.
105    #[inline]
106    pub fn directory(mut self, dir: impl Into<PathBuf>) -> Self {
107        self.directory = Some(dir.into());
108        self
109    }
110
111    /// Set the name of the log file relative to the directory.
112    ///
113    /// Defaults to `powerpack.log`.
114    #[inline]
115    pub fn filename(mut self, filename: impl Into<Cow<'static, str>>) -> Self {
116        self.filename = Some(filename.into());
117        self
118    }
119
120    /// Set the maximum log level
121    #[inline]
122    pub fn max_level(mut self, max_level: LevelFilter) -> Self {
123        self.max_level = Some(max_level);
124        self
125    }
126
127    fn build(self) -> Result<Logger, Error> {
128        let directory = match self.directory {
129            Some(directory) => directory,
130            None => env::try_workflow_cache_or_default().ok_or(Error::NoHomeDir)?,
131        };
132        let filename = self.filename.as_deref().unwrap_or("powerpack.log");
133        let path = directory.join(filename);
134
135        fs::create_dir_all(directory)?;
136
137        let file = Arc::new(Mutex::new(
138            fs::OpenOptions::new()
139                .create(true)
140                .append(true)
141                .open(path)?,
142        ));
143
144        let max_level = self.max_level.unwrap_or_else(|| {
145            if env::is_debug() {
146                LevelFilter::Debug
147            } else {
148                LevelFilter::Info
149            }
150        });
151
152        Ok(Logger { file, max_level })
153    }
154
155    /// Try to initialize the logger
156    #[inline]
157    pub fn try_init(self) -> Result<(), Error> {
158        let logger = self.build()?;
159        let max_level = logger.max_level;
160        log::set_boxed_logger(Box::new(logger))?;
161        log::set_max_level(max_level);
162        Ok(())
163    }
164
165    /// Initialize the logger
166    ///
167    /// # Panics
168    ///
169    /// Panics if the logger has already been initialized or there are IO errors
170    /// when creating/accessing the log file.
171    ///
172    /// Use [`Builder::try_init`] if you want to handle the error.
173    #[track_caller]
174    #[inline]
175    pub fn init(self) {
176        self.try_init().expect("failed to initialize logger");
177    }
178
179    /// Initialize the logger if it hasn't already been initialized.
180    ///
181    /// # Panics
182    ///
183    /// Panics if there are IO errors when creating/accessing the log file.
184    #[track_caller]
185    #[inline]
186    pub fn init_idempotent(self) {
187        match self.try_init() {
188            Ok(()) => {}
189            Err(Error::SetLogger(_)) => {}
190            r => r.expect("failed to initialize logger"),
191        }
192    }
193}
194
195impl Logger {
196    fn try_log(&self, record: &log::Record) -> Result<(), Box<dyn std::error::Error + '_>> {
197        let time = jiff::Timestamp::now().strftime("%Y-%m-%dT%H:%M:%S");
198        let mut f = self.file.lock()?;
199        writeln!(f, "[{}] [{}] {}", time, record.level(), record.args())?;
200        f.flush()?;
201        Ok(())
202    }
203
204    fn try_flush(&self) -> Result<(), Box<dyn std::error::Error + '_>> {
205        let mut f = self.file.lock()?;
206        f.flush()?;
207        Ok(())
208    }
209}
210
211impl Log for Logger {
212    fn enabled(&self, metadata: &log::Metadata) -> bool {
213        metadata.level() <= self.max_level
214    }
215
216    fn log(&self, record: &log::Record) {
217        if self.enabled(record.metadata()) {
218            self.try_log(record).expect("failed to log message");
219        }
220    }
221
222    fn flush(&self) {
223        self.try_flush().expect("failed to flush log file");
224    }
225}