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
34#[derive(Debug)]
38pub struct Logger {
39 path: PathBuf,
40 level: LogLevel,
41}
42
43impl Logger {
44 #[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 #[must_use]
71 pub fn with_level(mut self, level: LogLevel) -> Self {
72 self.level = level;
73 self
74 }
75
76 pub fn path(&self) -> &Path {
78 &self.path
79 }
80
81 pub fn level(&self) -> LogLevel {
83 self.level
84 }
85
86 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 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 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 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
165static GLOBAL_LOGGER: OnceLock<Logger> = OnceLock::new();
168
169pub 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
185pub 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
200pub(crate) fn global() -> Option<&'static Logger> {
202 GLOBAL_LOGGER.get()
203}