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 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 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, }
43 }
44}
45
46#[derive(Debug)]
50pub struct Logger {
51 path: PathBuf,
52 level: LogLevel,
53}
54
55impl Logger {
56 #[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 #[must_use]
83 pub fn with_level(mut self, level: LogLevel) -> Self {
84 self.level = level;
85 self
86 }
87
88 pub fn path(&self) -> &Path {
90 &self.path
91 }
92
93 pub fn level(&self) -> LogLevel {
95 self.level
96 }
97
98 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 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 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 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
177static GLOBAL_LOGGER: OnceLock<Logger> = OnceLock::new();
180
181pub 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
197pub 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
212pub(crate) fn global() -> Option<&'static Logger> {
214 GLOBAL_LOGGER.get()
215}