1#[cfg(feature = "colored")]
2use colored::Colorize;
3use log::{LevelFilter, Log, Metadata, Record, SetLoggerError};
4#[cfg(all(unix, feature = "nonblock-io"))]
5use std::os::fd::AsRawFd;
6use std::sync::Arc;
7use std::sync::atomic::AtomicBool;
8#[cfg(feature = "timestamps")]
9use time::{OffsetDateTime, UtcOffset, format_description::FormatItem};
10
11#[cfg(feature = "macros")]
12pub mod io;
13#[cfg(not(feature = "macros"))]
14mod io;
15
16mod worker;
17
18#[cfg(feature = "macros")]
19mod macros;
20
21#[cfg(feature = "timestamps")]
22#[derive(Clone, Debug, PartialEq)]
23enum Timestamps {
24 None,
25 Utc,
26 UtcOffset(UtcOffset),
27}
28
29#[cfg(feature = "timestamps")]
30const TIMESTAMP_FORMAT_OFFSET: &[FormatItem] = time::macros::format_description!(
31 "[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:3][offset_hour sign:mandatory]:[offset_minute]"
32);
33
34#[cfg(feature = "timestamps")]
35const TIMESTAMP_FORMAT_UTC: &[FormatItem] = time::macros::format_description!(
36 "[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:3]Z"
37);
38
39#[derive(Clone, Debug)]
40pub struct NonBlockingOptions {
41 default_level: LevelFilter,
43
44 module_levels: Vec<(String, LevelFilter)>,
51
52 #[cfg(feature = "colors")]
53 colors: bool,
54
55 #[cfg(feature = "timestamps")]
56 timestamps: Timestamps,
57
58 #[cfg(feature = "timestamps")]
59 timestamps_format: Option<&'static [FormatItem<'static>]>,
60
61 channel_size: usize,
62}
63
64pub struct NonBlockingLoggerBuilder {
65 options: NonBlockingOptions,
66}
67
68impl Default for NonBlockingLoggerBuilder {
69 fn default() -> Self {
70 Self::new()
71 }
72}
73
74pub const DEFAULT_CHANNEL_SIZE: usize = 16384;
75
76impl NonBlockingLoggerBuilder {
77 pub fn new() -> Self {
78 Self {
79 options: NonBlockingOptions {
80 default_level: LevelFilter::Trace,
81 module_levels: Vec::new(),
82
83 #[cfg(feature = "threads")]
84 threads: false,
85
86 #[cfg(feature = "timestamps")]
87 timestamps: Timestamps::Utc,
88
89 #[cfg(feature = "timestamps")]
90 timestamps_format: None,
91
92 #[cfg(feature = "colors")]
93 colors: true,
94
95 channel_size: DEFAULT_CHANNEL_SIZE,
96 },
97 }
98 }
99
100 #[must_use = "You must call init() to begin logging"]
109 pub fn with_level(mut self, level: LevelFilter) -> Self {
110 self.options.default_level = level;
111 self
112 }
113
114 #[must_use = "You must call init() to begin logging"]
115 pub fn with_module_level(mut self, target: &str, level: LevelFilter) -> Self {
116 self.options.module_levels.push((target.to_string(), level));
117 self.options
118 .module_levels
119 .sort_by_key(|(name, _level)| name.len().wrapping_neg());
120 self
121 }
122
123 #[must_use = "You must call init() to begin logging"]
127 #[cfg(feature = "colors")]
128 pub fn with_colors(mut self, colors: bool) -> Self {
129 self.options.colors = colors;
130 self
131 }
132
133 #[must_use = "You must call init() to begin logging"]
137 #[cfg(feature = "timestamps")]
138 pub fn without_timestamps(mut self) -> Self {
139 self.options.timestamps = Timestamps::None;
140 self
141 }
142
143 #[must_use = "You must call init() to begin logging"]
147 #[cfg(feature = "timestamps")]
148 pub fn with_utc_timestamps(mut self) -> Self {
149 self.options.timestamps = Timestamps::Utc;
150 self
151 }
152
153 #[must_use = "You must call init() to begin logging"]
157 #[cfg(feature = "timestamps")]
158 pub fn with_utc_offset(mut self, offset: UtcOffset) -> Self {
159 self.options.timestamps = Timestamps::UtcOffset(offset);
160 self
161 }
162
163 #[must_use = "You must call init() to begin logging"]
170 #[cfg(feature = "timestamps")]
171 pub fn with_timestamp_format(mut self, format: &'static [FormatItem<'static>]) -> Self {
172 self.options.timestamps_format = Some(format);
173 self
174 }
175
176 #[must_use = "You must call init() to begin logging"]
190 pub fn with_channel_size(mut self, size: usize) -> Self {
191 assert!(size > 0, "Channel size must be greater than 0");
192 self.options.channel_size = size;
193 self
194 }
195
196 pub fn init(self) -> Result<NonBlockingLogger, SetLoggerError> {
205 let logger = self.build()?;
206
207 log::set_max_level(logger.max_level());
208 log::set_boxed_logger(Box::new(logger.clone()))?;
209
210 Ok(logger)
211 }
212
213 pub fn build(self) -> Result<NonBlockingLogger, SetLoggerError> {
218 #[cfg(all(feature = "colored", feature = "stderr"))]
219 use_stderr_for_colors();
220
221 #[cfg(not(feature = "stderr"))]
222 {
223 #[cfg(feature = "nonblock-io")]
224 if let Err(err) = io::set_nonblocking(std::io::stdout().as_raw_fd()) {
225 io::write_stdout_with_retry_internal(&format!(
226 "Failed to set STDOUT to non-blocking mode: {}",
227 err
228 ));
229 }
230 }
231
232 #[cfg(feature = "stderr")]
233 {
234 #[cfg(feature = "nonblock-io")]
235 if let Err(err) = io::set_nonblocking(std::io::stderr().as_raw_fd()) {
236 io::write_stderr_with_retry_internal(&format!(
237 "Failed to set STDERR to non-blocking mode: {}",
238 err
239 ));
240 }
241 }
242
243 let (sender, receiver) = crossbeam_channel::bounded(self.options.channel_size);
244
245 let (worker, running) = worker::LogWorker::new(receiver);
246 if let Err(err) = worker.spawn() {
247 println!("Failed to spawn logger worker: {}", err);
248 };
249
250 let logger = NonBlockingLogger {
251 options: self.options,
252 sender,
253 running,
254 };
255
256 Ok(logger)
257 }
258}
259
260#[derive(Debug)]
261pub enum NonBlockingLoggerError {
262 Error { reason: String },
263}
264
265impl std::fmt::Display for NonBlockingLoggerError {
266 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267 match self {
268 NonBlockingLoggerError::Error { reason } => {
269 write!(f, "NonBlockingLoggerError: {}", reason)
270 }
271 }
272 }
273}
274
275impl std::error::Error for NonBlockingLoggerError {}
276
277#[derive(Clone, Debug)]
278pub struct NonBlockingLogger {
279 options: NonBlockingOptions,
280 sender: crossbeam_channel::Sender<worker::WorkerMessage>,
281 running: Arc<AtomicBool>,
282}
283
284impl NonBlockingLogger {
285 pub fn max_level(&self) -> LevelFilter {
286 let max_level = self
287 .options
288 .module_levels
289 .iter()
290 .map(|(_name, level)| level)
291 .copied()
292 .max();
293 max_level
294 .map(|lvl| lvl.max(self.options.default_level))
295 .unwrap_or(self.options.default_level)
296 }
297
298 pub fn shutdown(self) -> Result<(), NonBlockingLoggerError> {
299 let compare = self.running.compare_exchange(
300 true,
301 false,
302 std::sync::atomic::Ordering::SeqCst,
303 std::sync::atomic::Ordering::SeqCst,
304 );
305
306 if compare.is_err() {
307 Err(NonBlockingLoggerError::Error {
308 reason: "Failed to shutdown logger: It was already shutted down".to_string(),
309 })
310 } else {
311 Ok(())
312 }
313 }
314}
315
316impl Log for NonBlockingLogger {
317 fn enabled(&self, metadata: &Metadata) -> bool {
318 &metadata.level().to_level_filter()
319 <= self
320 .options
321 .module_levels
322 .iter()
323 .find(|(name, _level)| metadata.target().starts_with(name))
327 .map(|(_name, level)| level)
328 .unwrap_or(&self.options.default_level)
329 }
330
331 fn log(&self, record: &Record) {
332 if self.enabled(record.metadata()) {
333 let level_string = {
334 #[cfg(feature = "colors")]
335 {
336 if self.options.colors {
337 match record.level() {
338 log::Level::Error => format!("{:<5}", record.level().to_string())
339 .red()
340 .to_string(),
341 log::Level::Warn => format!("{:<5}", record.level().to_string())
342 .yellow()
343 .to_string(),
344 log::Level::Info => format!("{:<5}", record.level().to_string())
345 .cyan()
346 .to_string(),
347 log::Level::Debug => format!("{:<5}", record.level().to_string())
348 .purple()
349 .to_string(),
350 log::Level::Trace => format!("{:<5}", record.level().to_string())
351 .normal()
352 .to_string(),
353 }
354 } else {
355 format!("{:<5}", record.level().to_string())
356 }
357 }
358 #[cfg(not(feature = "colors"))]
359 {
360 format!("{:<5}", record.level().to_string())
361 }
362 };
363
364 let target = if !record.target().is_empty() {
365 record.target()
366 } else {
367 record.module_path().unwrap_or_default()
368 };
369
370 let thread = {
371 #[cfg(feature = "threads")]
372 if self.options.threads {
373 let thread = std::thread::current();
374
375 format!("@{}", {
376 #[cfg(feature = "nightly")]
377 {
378 thread.name().unwrap_or(&thread.id().as_u64().to_string())
379 }
380
381 #[cfg(not(feature = "nightly"))]
382 {
383 thread.name().unwrap_or("?")
384 }
385 })
386 } else {
387 "".to_string()
388 }
389
390 #[cfg(not(feature = "threads"))]
391 ""
392 };
393
394 let timestamp = {
395 #[cfg(feature = "timestamps")]
396 match self.options.timestamps {
397 Timestamps::None => "".to_string(),
398 Timestamps::Utc => format!(
399 "{} ",
400 OffsetDateTime::now_utc()
401 .format(
402 &self
403 .options
404 .timestamps_format
405 .unwrap_or(TIMESTAMP_FORMAT_UTC)
406 )
407 .unwrap()
408 ),
409 Timestamps::UtcOffset(offset) => format!(
410 "{} ",
411 OffsetDateTime::now_utc()
412 .to_offset(offset)
413 .format(
414 &self
415 .options
416 .timestamps_format
417 .unwrap_or(TIMESTAMP_FORMAT_OFFSET)
418 )
419 .unwrap()
420 ),
421 }
422
423 #[cfg(not(feature = "timestamps"))]
424 ""
425 };
426
427 let message = format!(
428 "{}{} [{}{}] {}\r\n",
429 timestamp,
430 level_string,
431 target,
432 thread,
433 record.args()
434 );
435
436 if let Err(err) = self.sender.send(worker::WorkerMessage::Log(message)) {
437 io::write_stderr_with_retry_internal(&format!("Failed to schedule log: {}", err));
438 }
439 }
440 }
441
442 fn flush(&self) {
443 let (done_tx, done_rx) = crossbeam_channel::bounded(1);
444
445 match self.sender.send(worker::WorkerMessage::Flush(done_tx)) {
446 Ok(_) => {
447 let _ = done_rx.recv();
449 }
450 Err(err) => {
451 io::write_stderr_with_retry_internal(&format!(
452 "Failed to send flush request to logger worker: {}",
453 err
454 ));
455 }
456 }
457 }
458}
459
460#[cfg(all(feature = "colored", feature = "stderr"))]
463fn use_stderr_for_colors() {
464 use std::io::{IsTerminal, stderr};
465
466 colored::control::set_override(stderr().is_terminal());
467}