1mod format;
2mod levels;
3mod macros;
4mod timing;
5
6use chrono::Local;
7use std::{
8 fmt,
9 io::{self, Write},
10 sync::{
11 atomic::{AtomicBool, AtomicU8, Ordering},
12 Mutex, OnceLock,
13 },
14};
15use std::{process, thread};
16use termion::color;
17
18#[cfg(feature = "structured")]
19mod structured;
20
21pub use format::{FormatPlaceholder, FormatTemplate};
22pub use levels::LogLevel;
23pub use timing::TimedOperation;
24
25#[cfg(feature = "structured")]
26pub use structured::structured::LogEvent;
27
28pub struct Logger {
30 verbose: AtomicBool,
31 min_level: AtomicU8,
32 writer: Mutex<Box<dyn Write + Send>>,
33 context: Mutex<Vec<(String, String)>>,
34 format: Mutex<FormatTemplate>,
35}
36
37impl Default for Logger {
38 fn default() -> Self {
39 Self {
40 verbose: AtomicBool::new(false),
41 min_level: AtomicU8::new(LogLevel::INFO as u8),
42 writer: Mutex::new(Box::new(io::stdout())),
43 context: Mutex::new(Vec::new()),
44 format: Mutex::new(FormatTemplate::parse("{symbol} {context}{message}")),
45 }
46 }
47}
48
49static LOGGER: OnceLock<Logger> = OnceLock::new();
51
52pub fn logger() -> &'static Logger {
53 LOGGER.get_or_init(Logger::default)
54}
55
56impl Logger {
57 pub fn set_format(&self, template: &str) -> &Self {
58 *self.format.lock().unwrap() = FormatTemplate::parse(template);
59 self
60 }
61
62 pub fn use_simple_format(&self) -> &Self {
64 self.set_format("{symbol} {message}")
65 }
66
67 pub fn use_detailed_format(&self) -> &Self {
68 self.set_format("[{level}] {datetime} {message}")
69 }
70
71 pub fn use_debug_format(&self) -> &Self {
72 self.set_format("{datetime} [{level}] <{file}:{line}> {message}")
73 }
74
75 pub fn verbose(&self, enabled: bool) -> &Self {
76 self.verbose.store(enabled, Ordering::Relaxed);
77 self
78 }
79
80 pub fn min_level(&self, level: LogLevel) -> &Self {
81 self.min_level.store(level as u8, Ordering::Relaxed);
82 self
83 }
84
85 pub fn set_writer(&self, writer: Box<dyn Write + Send>) -> io::Result<()> {
86 *self.writer.lock().unwrap() = writer;
87 Ok(())
88 }
89
90 pub fn add_context<K, V>(&self, key: K, value: V) -> ContextGuard
91 where
92 K: Into<String>,
93 V: Into<String>,
94 {
95 let mut context = self.context.lock().unwrap();
96 context.push((key.into(), value.into()));
97 ContextGuard(context.len() - 1)
98 }
99
100 pub fn should_log(&self, level: LogLevel) -> bool {
101 let min_level = self.min_level.load(Ordering::Relaxed);
102 level as u8 >= min_level
103 }
104
105 pub fn write_log(
106 &self,
107 level: LogLevel,
108 message: &str,
109 file: &str,
110 line: u32,
111 ) -> io::Result<()> {
112 let format = self.format.lock().unwrap();
113 let mut output = String::new();
114
115 for part in &format.parts {
116 match part {
117 FormatPlaceholder::Level => {
118 output.push_str(&format!("{:?}", level));
119 }
120 FormatPlaceholder::Symbol => {
121 output.push_str(level.symbol());
122 }
123 FormatPlaceholder::Message => {
124 output.push_str(message);
125 }
126 FormatPlaceholder::Time => {
127 output.push_str(&Local::now().format("%H:%M:%S").to_string());
128 }
129 FormatPlaceholder::Date => {
130 output.push_str(&Local::now().format("%Y-%m-%d").to_string());
131 }
132 FormatPlaceholder::DateTime => {
133 output.push_str(&Local::now().format("%Y-%m-%d %H:%M:%S").to_string());
134 }
135 FormatPlaceholder::ThreadName => {
136 let name = thread::current().name().map_or_else(
137 || format!("Thread-{:?}", thread::current().id()),
138 ToString::to_string,
139 );
140 output.push_str(&name);
141 }
142 FormatPlaceholder::ThreadId => {
143 output.push_str(&format!("{:?}", thread::current().id()));
144 }
145 FormatPlaceholder::ProcessId => {
146 output.push_str(&process::id().to_string());
147 }
148 FormatPlaceholder::File => {
149 output.push_str(file);
150 }
151 FormatPlaceholder::Line => {
152 output.push_str(&line.to_string());
153 }
154 FormatPlaceholder::Context => {
155 let context = self.context.lock().unwrap();
156 if !context.is_empty() {
157 output.push('[');
158 for (i, (key, value)) in context.iter().enumerate() {
159 if i > 0 {
160 output.push_str(", ");
161 }
162 output.push_str(&format!("{}={}", key, value));
163 }
164 output.push_str("] ");
165 }
166 }
167 FormatPlaceholder::Text(text) => {
168 output.push_str(text);
169 }
170 }
171 }
172
173 output.push('\n');
174
175 let colored_output = format!(
177 "{}{}{}",
178 color::Fg(level.color()),
179 output,
180 color::Fg(color::Reset)
181 );
182
183 let mut writer = self.writer.lock().unwrap();
185 writer.write_all(colored_output.as_bytes())?;
186 writer.flush()?;
187
188 Ok(())
189 }
190
191 #[cfg(feature = "structured")]
192 pub fn structured_format(&self) -> &Self {
193 self.set_format("{datetime} {level} {message}")
194 }
195
196 #[cfg(feature = "structured")]
197 pub fn write_structured_event(&self, event: &LogEvent) -> io::Result<()> {
198 let mut fields_str = String::new();
199 if !event.fields.is_empty() {
200 fields_str.push('[');
201 for (i, (key, value)) in event.fields.iter().enumerate() {
202 if i > 0 {
203 fields_str.push_str(", ");
204 }
205 fields_str.push_str(&format!("{}={}", key, value));
206 }
207 fields_str.push(']');
208 }
209
210 self.write_log(
211 event.level,
212 &if fields_str.is_empty() {
213 event.message.clone()
214 } else {
215 format!("{} {}", event.message, fields_str)
216 },
217 &event.file,
218 event.line,
219 )
220 }
221}
222
223pub struct ContextGuard(usize);
225
226impl Drop for ContextGuard {
227 fn drop(&mut self) {
228 if let Some(logger) = LOGGER.get() {
229 let mut context = logger.context.lock().unwrap();
230 if self.0 < context.len() {
231 context.remove(self.0);
232 }
233 }
234 }
235}
236
237#[derive(Debug)]
239pub struct LogError {
240 message: String,
241}
242
243impl LogError {
244 pub fn new<S: Into<String>>(message: S) -> Self {
245 Self {
246 message: message.into(),
247 }
248 }
249}
250
251impl fmt::Display for LogError {
252 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253 write!(f, "{}", self.message)
254 }
255}
256
257impl std::error::Error for LogError {}
258
259impl From<io::Error> for LogError {
260 fn from(err: io::Error) -> Self {
261 LogError::new(err.to_string())
262 }
263}
264
265pub struct Progress {
267 message: String,
268 total: Option<u64>,
269 current: u64,
270}
271
272impl Progress {
273 pub fn new<S: Into<String>>(message: S) -> Self {
274 Self {
275 message: message.into(),
276 total: None,
277 current: 0,
278 }
279 }
280
281 pub fn with_total<S: Into<String>>(message: S, total: u64) -> Self {
282 Self {
283 message: message.into(),
284 total: Some(total),
285 current: 0,
286 }
287 }
288
289 pub fn inc(&mut self, amount: u64) {
290 self.current += amount;
291 let message = self.message.clone();
292 self.update(&message);
293 }
294
295 pub fn update<S: Into<String>>(&mut self, message: S) {
296 self.message = message.into();
297 if let Some(total) = self.total {
298 logger()
299 .write_log(
300 LogLevel::INFO,
301 &format!("{} [{}/{}]", self.message, self.current, total),
302 file!(),
303 line!(),
304 )
305 .ok();
306 } else {
307 logger()
308 .write_log(
309 LogLevel::INFO,
310 &format!("{} [{}]", self.message, self.current),
311 file!(),
312 line!(),
313 )
314 .ok();
315 }
316 }
317
318 pub fn finish(self) {
319 logger()
320 .write_log(
321 LogLevel::SUCCESS,
322 &format!("{} [Complete]", self.message),
323 file!(),
324 line!(),
325 )
326 .ok();
327 }
328
329 pub fn finish_with_message<S: Into<String>>(self, message: S) {
330 logger()
331 .write_log(LogLevel::SUCCESS, &message.into(), file!(), line!())
332 .ok();
333 }
334}