1#[cfg(feature = "colored")]
2use colored::Colorize;
3use log::{LevelFilter, Log, Metadata, Record, SetLoggerError};
4use std::os::fd::AsRawFd;
5use std::sync::Arc;
6use std::sync::atomic::AtomicBool;
7#[cfg(feature = "timestamps")]
8use time::{OffsetDateTime, UtcOffset, format_description::FormatItem};
9
10mod io;
11mod worker;
12
13#[cfg(feature = "timestamps")]
14#[derive(Clone, Debug, PartialEq)]
15enum Timestamps {
16 None,
17 Utc,
18 UtcOffset(UtcOffset),
19}
20
21#[cfg(feature = "timestamps")]
22const TIMESTAMP_FORMAT_OFFSET: &[FormatItem] = time::macros::format_description!(
23 "[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:3][offset_hour sign:mandatory]:[offset_minute]"
24);
25
26#[cfg(feature = "timestamps")]
27const TIMESTAMP_FORMAT_UTC: &[FormatItem] = time::macros::format_description!(
28 "[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:3]Z"
29);
30
31#[derive(Clone, Debug)]
32pub struct NonBlockingOptions {
33 default_level: LevelFilter,
35
36 module_levels: Vec<(String, LevelFilter)>,
43
44 #[cfg(feature = "colors")]
45 colors: bool,
46
47 #[cfg(feature = "timestamps")]
48 timestamps: Timestamps,
49
50 #[cfg(feature = "timestamps")]
51 timestamps_format: Option<&'static [FormatItem<'static>]>,
52}
53
54pub struct NonBlockingLoggerBuilder {
55 options: NonBlockingOptions,
56}
57
58impl Default for NonBlockingLoggerBuilder {
59 fn default() -> Self {
60 Self::new()
61 }
62}
63
64impl NonBlockingLoggerBuilder {
65 pub fn new() -> Self {
66 Self {
67 options: NonBlockingOptions {
68 default_level: LevelFilter::Trace,
69 module_levels: Vec::new(),
70
71 #[cfg(feature = "threads")]
72 threads: false,
73
74 #[cfg(feature = "timestamps")]
75 timestamps: Timestamps::Utc,
76
77 #[cfg(feature = "timestamps")]
78 timestamps_format: None,
79
80 #[cfg(feature = "colors")]
81 colors: true,
82 },
83 }
84 }
85
86 #[must_use = "You must call init() to begin logging"]
87 pub fn with_module_level(mut self, target: &str, level: LevelFilter) -> Self {
88 self.options.module_levels.push((target.to_string(), level));
89 self.options
90 .module_levels
91 .sort_by_key(|(name, _level)| name.len().wrapping_neg());
92 self
93 }
94
95 #[must_use = "You must call init() to begin logging"]
99 #[cfg(feature = "colors")]
100 pub fn with_colors(mut self, colors: bool) -> Self {
101 self.options.colors = colors;
102 self
103 }
104
105 #[must_use = "You must call init() to begin logging"]
109 #[cfg(feature = "timestamps")]
110 pub fn without_timestamps(mut self) -> Self {
111 self.options.timestamps = Timestamps::None;
112 self
113 }
114
115 #[must_use = "You must call init() to begin logging"]
119 #[cfg(feature = "timestamps")]
120 pub fn with_utc_timestamps(mut self) -> Self {
121 self.options.timestamps = Timestamps::Utc;
122 self
123 }
124
125 #[must_use = "You must call init() to begin logging"]
129 #[cfg(feature = "timestamps")]
130 pub fn with_utc_offset(mut self, offset: UtcOffset) -> Self {
131 self.options.timestamps = Timestamps::UtcOffset(offset);
132 self
133 }
134
135 #[must_use = "You must call init() to begin logging"]
142 #[cfg(feature = "timestamps")]
143 pub fn with_timestamp_format(mut self, format: &'static [FormatItem<'static>]) -> Self {
144 self.options.timestamps_format = Some(format);
145 self
146 }
147
148 pub fn init(self) -> Result<NonBlockingLogger, SetLoggerError> {
149 #[cfg(all(feature = "colored", feature = "stderr"))]
150 use_stderr_for_colors();
151
152 #[cfg(not(feature = "stderr"))]
153 {
154 if let Err(err) = io::set_nonblocking(std::io::stdout().as_raw_fd()) {
155 io::write_stderr_with_retry(&format!(
156 "Failed to set STDOUT to non-blocking mode: {}",
157 err
158 ));
159 }
160 }
161
162 #[cfg(feature = "stderr")]
163 {
164 if let Err(err) = io::set_nonblocking(std::io::stderr().as_raw_fd()) {
165 io::write_stderr_with_retry(&format!(
166 "Failed to set STDERR to non-blocking mode: {}",
167 err
168 ));
169 }
170 }
171
172 let (sender, receiver) = crossbeam_channel::bounded(1024);
173
174 let (worker, running) = worker::LogWorker::new(receiver);
175 if let Err(err) = worker.spawn() {
176 println!("Failed to spawn logger worker: {}", err);
177 };
178
179 let logger = NonBlockingLogger {
180 options: self.options,
181 sender,
182 running,
183 };
184
185 log::set_max_level(logger.max_level());
186 log::set_boxed_logger(Box::new(logger.clone()))?;
187
188 Ok(logger)
189 }
190}
191
192#[derive(Debug)]
193pub enum NonBlockingLoggerError {
194 Error { reason: String },
195}
196
197impl std::fmt::Display for NonBlockingLoggerError {
198 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199 match self {
200 NonBlockingLoggerError::Error { reason } => {
201 write!(f, "NonBlockingLoggerError: {}", reason)
202 }
203 }
204 }
205}
206
207impl std::error::Error for NonBlockingLoggerError {}
208
209#[derive(Clone, Debug)]
210pub struct NonBlockingLogger {
211 options: NonBlockingOptions,
212 sender: crossbeam_channel::Sender<worker::WorkerMessage>,
213 running: Arc<AtomicBool>,
214}
215
216impl NonBlockingLogger {
217 pub fn max_level(&self) -> LevelFilter {
218 let max_level = self
219 .options
220 .module_levels
221 .iter()
222 .map(|(_name, level)| level)
223 .copied()
224 .max();
225 max_level
226 .map(|lvl| lvl.max(self.options.default_level))
227 .unwrap_or(self.options.default_level)
228 }
229
230 pub fn shutdown(self) -> Result<(), NonBlockingLoggerError> {
231 let compare = self.running.compare_exchange(
232 true,
233 false,
234 std::sync::atomic::Ordering::SeqCst,
235 std::sync::atomic::Ordering::SeqCst,
236 );
237
238 if compare.is_err() {
239 Err(NonBlockingLoggerError::Error {
240 reason: "Failed to shutdown logger: It was already shutted down".to_string(),
241 })
242 } else {
243 Ok(())
244 }
245 }
246}
247
248impl Log for NonBlockingLogger {
249 fn enabled(&self, metadata: &Metadata) -> bool {
250 &metadata.level().to_level_filter()
251 <= self
252 .options
253 .module_levels
254 .iter()
255 .find(|(name, _level)| metadata.target().starts_with(name))
259 .map(|(_name, level)| level)
260 .unwrap_or(&self.options.default_level)
261 }
262
263 fn log(&self, record: &Record) {
264 if self.enabled(record.metadata()) {
265 let level_string = {
266 #[cfg(feature = "colors")]
267 {
268 if self.options.colors {
269 match record.level() {
270 log::Level::Error => format!("{:<5}", record.level().to_string())
271 .red()
272 .to_string(),
273 log::Level::Warn => format!("{:<5}", record.level().to_string())
274 .yellow()
275 .to_string(),
276 log::Level::Info => format!("{:<5}", record.level().to_string())
277 .cyan()
278 .to_string(),
279 log::Level::Debug => format!("{:<5}", record.level().to_string())
280 .purple()
281 .to_string(),
282 log::Level::Trace => format!("{:<5}", record.level().to_string())
283 .normal()
284 .to_string(),
285 }
286 } else {
287 format!("{:<5}", record.level().to_string())
288 }
289 }
290 #[cfg(not(feature = "colors"))]
291 {
292 format!("{:<5}", record.level().to_string())
293 }
294 };
295
296 let target = if !record.target().is_empty() {
297 record.target()
298 } else {
299 record.module_path().unwrap_or_default()
300 };
301
302 let thread = {
303 #[cfg(feature = "threads")]
304 if self.options.threads {
305 let thread = std::thread::current();
306
307 format!("@{}", {
308 #[cfg(feature = "nightly")]
309 {
310 thread.name().unwrap_or(&thread.id().as_u64().to_string())
311 }
312
313 #[cfg(not(feature = "nightly"))]
314 {
315 thread.name().unwrap_or("?")
316 }
317 })
318 } else {
319 "".to_string()
320 }
321
322 #[cfg(not(feature = "threads"))]
323 ""
324 };
325
326 let timestamp = {
327 #[cfg(feature = "timestamps")]
328 match self.options.timestamps {
329 Timestamps::None => "".to_string(),
330 Timestamps::Utc => format!(
331 "{} ",
332 OffsetDateTime::now_utc()
333 .format(
334 &self
335 .options
336 .timestamps_format
337 .unwrap_or(TIMESTAMP_FORMAT_UTC)
338 )
339 .unwrap()
340 ),
341 Timestamps::UtcOffset(offset) => format!(
342 "{} ",
343 OffsetDateTime::now_utc()
344 .to_offset(offset)
345 .format(
346 &self
347 .options
348 .timestamps_format
349 .unwrap_or(TIMESTAMP_FORMAT_OFFSET)
350 )
351 .unwrap()
352 ),
353 }
354
355 #[cfg(not(feature = "timestamps"))]
356 ""
357 };
358
359 let message = format!(
360 "{}{} [{}{}] {}\r\n",
361 timestamp,
362 level_string,
363 target,
364 thread,
365 record.args()
366 );
367
368 if let Err(err) = self.sender.send(worker::WorkerMessage::Log(message)) {
369 io::write_stderr_with_retry(&format!("Failed to schedule log: {}", err));
370 }
371 }
372 }
373
374 fn flush(&self) {
375 let (done_tx, done_rx) = crossbeam_channel::bounded(1);
376
377 match self.sender.send(worker::WorkerMessage::Flush(done_tx)) {
378 Ok(_) => {
379 let _ = done_rx.recv();
381 }
382 Err(err) => {
383 io::write_stderr_with_retry(&format!(
384 "Failed to send flush request to logger worker: {}",
385 err
386 ));
387 }
388 }
389 }
390}
391
392#[cfg(all(feature = "colored", feature = "stderr"))]
395fn use_stderr_for_colors() {
396 use std::io::{IsTerminal, stderr};
397
398 colored::control::set_override(stderr().is_terminal());
399}