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    /// Converts the `LogLevel` to its string representation.
23    pub fn as_str(self) -> &'static str {
24        match self {
25            LogLevel::Trace => "TRACE",
26            LogLevel::Debug => "DEBUG",
27            LogLevel::Info => "INFO",
28            LogLevel::Warn => "WARN",
29            LogLevel::Error => "ERROR",
30        }
31    }
32}
33
34/// A file logger that writes messages to `path` and filters by `level`.
35///
36/// By default, the logger starts at `LogLevel::Info`.
37#[derive(Debug)]
38pub struct Logger {
39    path: PathBuf,
40    level: LogLevel,
41}
42
43impl Logger {
44    /// Creates a new `Logger` that writes to `path`.
45    ///
46    /// The default level is `LogLevel::Info`.
47    ///
48    /// # Examples
49    /// ```rust
50    /// use log_easy::Logger;
51    /// let logger = Logger::new("app.log");
52    /// ```
53    #[must_use]
54    pub fn new<P: Into<PathBuf>>(path: P) -> Self {
55        Self {
56            path: path.into(),
57            level: LogLevel::Info,
58        }
59    }
60
61    /// Returns a new `Logger` configured with the given minimum log level.
62    ///
63    /// Messages below this level are ignored.
64    ///
65    /// # Examples
66    /// ```rust
67    /// use log_easy::{Logger, LogLevel};
68    /// let logger = Logger::new("app.log").with_level(LogLevel::Debug);
69    /// ```
70    #[must_use]
71    pub fn with_level(mut self, level: LogLevel) -> Self {
72        self.level = level;
73        self
74    }
75
76    /// Gets the log file path.
77    pub fn path(&self) -> &Path {
78        &self.path
79    }
80
81    /// Gets the current log level.
82    pub fn level(&self) -> LogLevel {
83        self.level
84    }
85
86    /// Attempts to log a message with the specified log level.
87    /// Returns a Result indicating success or failure.
88    fn try_log_line(&self, msg_level: LogLevel, message: &str) -> Result<()> {
89        if msg_level < self.level {
90            return Ok(());
91        }
92
93        let mut file = OpenOptions::new()
94            .create(true)
95            .append(true)
96            .open(&self.path)
97            .map_err(|e| {
98                Error::new(
99                    e.kind(),
100                    format!("Failed to open log file {}: {}", self.path.display(), e),
101                )
102            })?;
103
104        let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S");
105        let log_entry = format!("[{}][{}] {}\n", timestamp, msg_level.as_str(), message);
106
107        file.write_all(log_entry.as_bytes()).map_err(|e| {
108            Error::new(
109                e.kind(),
110                format!("Failed to write to log file {}: {}", self.path.display(), e),
111            )
112        })?;
113
114        Ok(())
115    }
116
117    /// Logs a message with the specified log level.
118    /// Errors are printed to stderr.
119    fn log_line(&self, msg_level: LogLevel, message: &str) {
120        if let Err(e) = self.try_log_line(msg_level, message) {
121            eprintln!("log-easy: {}", e);
122        }
123    }
124
125    //---CONVENIENCE METHODS (Errors are printed to stderr)---
126    /// These methods log messages at their respective levels.
127    /// If an error occurs (e.g., file write failure), it is printed to stderr but not returned.
128    pub fn trace(&self, msg: &str) {
129        self.log_line(LogLevel::Trace, msg);
130    }
131    pub fn debug(&self, msg: &str) {
132        self.log_line(LogLevel::Debug, msg);
133    }
134    pub fn info(&self, msg: &str) {
135        self.log_line(LogLevel::Info, msg);
136    }
137    pub fn warn(&self, msg: &str) {
138        self.log_line(LogLevel::Warn, msg);
139    }
140    pub fn error(&self, msg: &str) {
141        self.log_line(LogLevel::Error, msg);
142    }
143
144    //---TRY CONVENIENCE METHODS (Returns error for user handling)---
145    /// Fallible variants of the logging methods.
146    ///
147    /// Unlike `info()`/`warn()` etc., these return `std::io::Result<()>` so callers can handle failures.
148    pub fn try_trace(&self, msg: &str) -> Result<()> {
149        self.try_log_line(LogLevel::Trace, msg)
150    }
151    pub fn try_debug(&self, msg: &str) -> Result<()> {
152        self.try_log_line(LogLevel::Debug, msg)
153    }
154    pub fn try_info(&self, msg: &str) -> Result<()> {
155        self.try_log_line(LogLevel::Info, msg)
156    }
157    pub fn try_warn(&self, msg: &str) -> Result<()> {
158        self.try_log_line(LogLevel::Warn, msg)
159    }
160    pub fn try_error(&self, msg: &str) -> Result<()> {
161        self.try_log_line(LogLevel::Error, msg)
162    }
163}
164
165//--- GLOBAL LOGGER INSTANCE ---
166
167static GLOBAL_LOGGER: OnceLock<Logger> = OnceLock::new();
168
169/// Initialize the global logger used by the `info!()` / `try_info!()` macros.
170///
171/// Call this once at program startup.
172///
173/// # Errors
174/// Returns `AlreadyExists` if the global logger has already been initialized.
175pub fn init<P: Into<PathBuf>>(path: P) -> Result<()> {
176    GLOBAL_LOGGER.set(Logger::new(path)).map_err(|_| {
177        Error::new(
178            std::io::ErrorKind::AlreadyExists,
179            "logger already initialized",
180        )
181    })?;
182    Ok(())
183}
184
185/// Initialize the global logger with configured settings (e.g., log level).
186/// Useful if you want to set a different log level for the global logger.
187///
188/// # Errors
189/// Returns `AlreadyExists` if the global logger has already been initialized.
190pub fn init_with(logger: Logger) -> Result<()> {
191    GLOBAL_LOGGER.set(logger).map_err(|_| {
192        Error::new(
193            std::io::ErrorKind::AlreadyExists,
194            "logger already initialized",
195        )
196    })?;
197    Ok(())
198}
199
200/// Get a reference to the global logger (for macros).
201pub(crate) fn global() -> Option<&'static Logger> {
202    GLOBAL_LOGGER.get()
203}