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#[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 pub const ALL: [Self; 5] = [
24 Self::Error,
25 Self::Warn,
26 Self::Info,
27 Self::Debug,
28 Self::Trace,
29 ];
30
31 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#[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#[derive(Debug)]
88pub struct Logger {
89 path: PathBuf,
90 level: LogLevel,
91}
92
93impl Logger {
94 #[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 #[must_use]
121 pub fn with_level(mut self, level: LogLevel) -> Self {
122 self.level = level;
123 self
124 }
125
126 pub fn path(&self) -> &Path {
128 &self.path
129 }
130
131 pub fn level(&self) -> LogLevel {
133 self.level
134 }
135
136 pub fn enabled(&self, level: LogLevel) -> bool {
138 level >= self.level
139 }
140
141 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 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 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 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
220static GLOBAL_LOGGER: OnceLock<Logger> = OnceLock::new();
223
224pub 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
240pub 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
255pub(crate) fn global() -> Option<&'static Logger> {
257 GLOBAL_LOGGER.get()
258}