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