Skip to main content

log_easy/
logger.rs

1use std::fs::OpenOptions;
2use std::io::{Error, Result, Write};
3use std::path::{Path, PathBuf};
4use std::sync::OnceLock;
5
6use chrono::Local;
7
8/// Severity level used for filtering log messages.
9///
10/// Levels are ordered from least to most severe:
11/// `Trace < Debug < Info < Warn < Error`.
12#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
13pub enum LogLevel {
14    Trace,
15    Debug,
16    Info,
17    Warn,
18    Error,
19}
20
21impl LogLevel {
22    /// All supported log levels, ordered from most to least severe.
23    pub const ALL: [Self; 5] = [
24        Self::Error,
25        Self::Warn,
26        Self::Info,
27        Self::Debug,
28        Self::Trace,
29    ];
30
31    /// Converts the `LogLevel` to its string representation.
32    pub const fn as_str(self) -> &'static str {
33        match self {
34            LogLevel::Trace => "TRACE",
35            LogLevel::Debug => "DEBUG",
36            LogLevel::Info => "INFO",
37            LogLevel::Warn => "WARN",
38            LogLevel::Error => "ERROR",
39        }
40    }
41}
42
43impl std::fmt::Display for LogLevel {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        f.write_str(self.as_str())
46    }
47}
48
49/// Returned when a string does not represent a supported log level.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct ParseLogLevelError {
52    invalid_value: String,
53}
54
55impl std::fmt::Display for ParseLogLevelError {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        write!(
58            f,
59            "invalid log level '{}'. Expected one of: ERROR, WARN, INFO, DEBUG, TRACE",
60            self.invalid_value
61        )
62    }
63}
64
65impl std::error::Error for ParseLogLevelError {}
66
67impl std::str::FromStr for LogLevel {
68    type Err = ParseLogLevelError;
69
70    fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
71        match value.trim().to_ascii_uppercase().as_str() {
72            "TRACE" | "5" => Ok(Self::Trace),
73            "DEBUG" | "4" => Ok(Self::Debug),
74            "INFO" | "3" => Ok(Self::Info),
75            "WARN" | "WARNING" | "2" => Ok(Self::Warn),
76            "ERROR" | "1" => Ok(Self::Error),
77            _ => Err(ParseLogLevelError {
78                invalid_value: value.to_owned(),
79            }),
80        }
81    }
82}
83
84/// A file logger that writes messages to `path` and filters by `level`.
85///
86/// By default, the logger starts at `LogLevel::Info`.
87#[derive(Debug)]
88pub struct Logger {
89    path: PathBuf,
90    level: LogLevel,
91}
92
93impl Logger {
94    /// Creates a new `Logger` that writes to `path`.
95    ///
96    /// The default level is `LogLevel::Info`.
97    ///
98    /// # Examples
99    /// ```rust
100    /// use log_easy::Logger;
101    /// let logger = Logger::new("app.log");
102    /// ```
103    #[must_use]
104    pub fn new<P: Into<PathBuf>>(path: P) -> Self {
105        Self {
106            path: path.into(),
107            level: LogLevel::Info,
108        }
109    }
110
111    /// Returns a new `Logger` configured with the given minimum log level.
112    ///
113    /// Messages below this level are ignored.
114    ///
115    /// # Examples
116    /// ```rust
117    /// use log_easy::{Logger, LogLevel};
118    /// let logger = Logger::new("app.log").with_level(LogLevel::Debug);
119    /// ```
120    #[must_use]
121    pub fn with_level(mut self, level: LogLevel) -> Self {
122        self.level = level;
123        self
124    }
125
126    /// Gets the log file path.
127    pub fn path(&self) -> &Path {
128        &self.path
129    }
130
131    /// Gets the current log level.
132    pub fn level(&self) -> LogLevel {
133        self.level
134    }
135
136    /// Returns whether a message at `level` would be logged.
137    pub fn enabled(&self, level: LogLevel) -> bool {
138        level >= self.level
139    }
140
141    /// Attempts to log a message with the specified log level.
142    /// Returns a Result indicating success or failure.
143    fn try_log_line(&self, msg_level: LogLevel, message: &str) -> Result<()> {
144        if !self.enabled(msg_level) {
145            return Ok(());
146        }
147
148        let mut file = OpenOptions::new()
149            .create(true)
150            .append(true)
151            .open(&self.path)
152            .map_err(|e| {
153                Error::new(
154                    e.kind(),
155                    format!("Failed to open log file {}: {}", self.path.display(), e),
156                )
157            })?;
158
159        let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S");
160        let log_entry = format!("[{}][{}] {}\n", timestamp, msg_level.as_str(), message);
161
162        file.write_all(log_entry.as_bytes()).map_err(|e| {
163            Error::new(
164                e.kind(),
165                format!("Failed to write to log file {}: {}", self.path.display(), e),
166            )
167        })?;
168
169        Ok(())
170    }
171
172    /// Logs a message with the specified log level.
173    /// Errors are printed to stderr.
174    fn log_line(&self, msg_level: LogLevel, message: &str) {
175        if let Err(e) = self.try_log_line(msg_level, message) {
176            eprintln!("log-easy: {}", e);
177        }
178    }
179
180    //---CONVENIENCE METHODS (Errors are printed to stderr)---
181    /// These methods log messages at their respective levels.
182    /// If an error occurs (e.g., file write failure), it is printed to stderr but not returned.
183    pub fn trace(&self, msg: &str) {
184        self.log_line(LogLevel::Trace, msg);
185    }
186    pub fn debug(&self, msg: &str) {
187        self.log_line(LogLevel::Debug, msg);
188    }
189    pub fn info(&self, msg: &str) {
190        self.log_line(LogLevel::Info, msg);
191    }
192    pub fn warn(&self, msg: &str) {
193        self.log_line(LogLevel::Warn, msg);
194    }
195    pub fn error(&self, msg: &str) {
196        self.log_line(LogLevel::Error, msg);
197    }
198
199    //---TRY CONVENIENCE METHODS (Returns error for user handling)---
200    /// Fallible variants of the logging methods.
201    ///
202    /// Unlike `info()`/`warn()` etc., these return `std::io::Result<()>` so callers can handle failures.
203    pub fn try_trace(&self, msg: &str) -> Result<()> {
204        self.try_log_line(LogLevel::Trace, msg)
205    }
206    pub fn try_debug(&self, msg: &str) -> Result<()> {
207        self.try_log_line(LogLevel::Debug, msg)
208    }
209    pub fn try_info(&self, msg: &str) -> Result<()> {
210        self.try_log_line(LogLevel::Info, msg)
211    }
212    pub fn try_warn(&self, msg: &str) -> Result<()> {
213        self.try_log_line(LogLevel::Warn, msg)
214    }
215    pub fn try_error(&self, msg: &str) -> Result<()> {
216        self.try_log_line(LogLevel::Error, msg)
217    }
218}
219
220//--- GLOBAL LOGGER INSTANCE ---
221
222static GLOBAL_LOGGER: OnceLock<Logger> = OnceLock::new();
223
224/// Initialize the global logger used by the `info!()` / `try_info!()` macros.
225///
226/// Call this once at program startup.
227///
228/// # Errors
229/// Returns `AlreadyExists` if the global logger has already been initialized.
230pub fn init<P: Into<PathBuf>>(path: P) -> Result<()> {
231    GLOBAL_LOGGER.set(Logger::new(path)).map_err(|_| {
232        Error::new(
233            std::io::ErrorKind::AlreadyExists,
234            "logger already initialized",
235        )
236    })?;
237    Ok(())
238}
239
240/// Initialize the global logger with configured settings (e.g., log level).
241/// Useful if you want to set a different log level for the global logger.
242///
243/// # Errors
244/// Returns `AlreadyExists` if the global logger has already been initialized.
245pub fn init_with(logger: Logger) -> Result<()> {
246    GLOBAL_LOGGER.set(logger).map_err(|_| {
247        Error::new(
248            std::io::ErrorKind::AlreadyExists,
249            "logger already initialized",
250        )
251    })?;
252    Ok(())
253}
254
255/// Get a reference to the global logger (for macros).
256pub(crate) fn global() -> Option<&'static Logger> {
257    GLOBAL_LOGGER.get()
258}