1#![doc = include_str!("../README.md")]
2
3use std::time::{Duration, Instant};
4
5use thiserror::Error;
6
7mod format;
8
9#[derive(Debug, Error)]
20#[non_exhaustive]
21pub enum LoggerError {
22 #[error("a global logger is already initialized")]
25 AlreadyInitialized,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
35pub enum ColorChoice {
36 Never,
38 #[default]
42 Auto,
43 Always,
46}
47
48impl ColorChoice {
49 fn write_style(self) -> env_logger::WriteStyle {
52 match self {
53 Self::Never => env_logger::WriteStyle::Never,
54 Self::Auto => env_logger::WriteStyle::Auto,
55 Self::Always => env_logger::WriteStyle::Always,
56 }
57 }
58}
59
60#[derive(Debug)]
89#[must_use = "call .init() to enable logging"]
90pub struct LoggerBuilder {
91 level: LevelFilter,
92 format: format::LineFormat,
93}
94
95impl Default for LoggerBuilder {
96 fn default() -> Self {
97 Self::new()
98 }
99}
100
101impl LoggerBuilder {
102 pub fn new() -> Self {
105 Self {
106 level: LevelFilter::Info,
107 format: format::LineFormat::default(),
108 }
109 }
110
111 pub fn level(mut self, level: LevelFilter) -> Self {
113 self.level = level;
114 self
115 }
116
117 pub fn color(mut self, choice: ColorChoice) -> Self {
120 self.format.color = choice;
121 self
122 }
123
124 pub fn source_location(mut self, source_location: bool) -> Self {
127 self.format.source_location = source_location;
128 self
129 }
130
131 pub fn init(&self) -> Result<(), LoggerError> {
142 let start = Instant::now();
143 self.env_builder(move || start.elapsed())
144 .try_init()
145 .map_err(|_| LoggerError::AlreadyInitialized)
146 }
147
148 fn env_builder(
158 &self,
159 elapsed: impl Fn() -> Duration + Send + Sync + 'static,
160 ) -> env_logger::Builder {
161 let formatter = format::LineFormatter::new(self.format, elapsed);
162
163 let mut builder = env_logger::Builder::new();
164 builder.target(env_logger::Target::Stderr);
165 builder.filter_level(self.level);
166 builder.write_style(self.format.color.write_style());
167 builder.format(move |buf, record| formatter.format(buf, record));
168 builder
169 }
170}
171
172pub use log::LevelFilter;
179pub use log::{debug, error, info, trace, warn};
180
181#[cfg(test)]
186mod tests {
187 use super::*;
188 use env_logger::Target;
189 use log::{Level, Log, Record};
190 use std::io::Write;
191 use std::sync::{Arc, Mutex};
192
193 #[derive(Clone, Default)]
195 struct Capture(Arc<Mutex<Vec<u8>>>);
196
197 impl Capture {
198 fn contents(&self) -> String {
199 String::from_utf8(self.0.lock().unwrap().clone()).unwrap()
200 }
201 }
202
203 impl Write for Capture {
204 fn write(&mut self, data: &[u8]) -> std::io::Result<usize> {
205 self.0.lock().unwrap().write(data)
206 }
207
208 fn flush(&mut self) -> std::io::Result<()> {
209 Ok(())
210 }
211 }
212
213 fn piped(builder: &LoggerBuilder, elapsed: Duration) -> (env_logger::Logger, Capture) {
217 let capture = Capture::default();
218 let env_logger = builder
219 .env_builder(move || elapsed)
220 .target(Target::Pipe(Box::new(capture.clone())))
221 .build();
222 (env_logger, capture)
223 }
224
225 fn emit(log: &impl Log, level: Level, message: &str) {
226 log.log(
227 &Record::builder()
228 .level(level)
229 .target("my_crate")
230 .file(Some("src/lib.rs"))
231 .line(Some(42))
232 .args(format_args!("{message}"))
233 .build(),
234 );
235 }
236
237 fn lines(capture: &Capture) -> Vec<String> {
238 capture.contents().lines().map(str::to_owned).collect()
239 }
240
241 #[test]
242 fn test_default_level_filters_below_info() {
243 let (log, capture) = piped(&LoggerBuilder::new(), Duration::ZERO);
244
245 emit(&log, Level::Trace, "dropped");
246 emit(&log, Level::Debug, "dropped");
247 emit(&log, Level::Info, "kept");
248 emit(&log, Level::Error, "kept too");
249
250 assert_eq!(
251 lines(&capture),
252 [
253 "[00:00:00.000 | INFO | my_crate | src/lib.rs:42 ] kept",
254 "[00:00:00.000 | ERROR | my_crate | src/lib.rs:42 ] kept too",
255 ]
256 );
257 }
258
259 #[test]
260 fn test_explicit_level_is_applied() {
261 let builder = LoggerBuilder::new().level(LevelFilter::Warn);
262 let (log, capture) = piped(&builder, Duration::ZERO);
263
264 emit(&log, Level::Info, "dropped");
265 emit(&log, Level::Warn, "kept");
266
267 assert_eq!(
268 lines(&capture),
269 ["[00:00:00.000 | WARN | my_crate | src/lib.rs:42 ] kept"]
270 );
271 }
272
273 #[test]
274 fn test_trace_level_emits_everything() {
275 let builder = LoggerBuilder::new().level(LevelFilter::Trace);
276 let (log, capture) = piped(&builder, Duration::ZERO);
277
278 emit(&log, Level::Trace, "finest");
279
280 assert_eq!(
281 lines(&capture),
282 ["[00:00:00.000 | TRACE | my_crate | src/lib.rs:42 ] finest"]
283 );
284 }
285
286 #[test]
287 fn test_source_location_off_omits_the_segment() {
288 let builder = LoggerBuilder::new().source_location(false);
289 let (log, capture) = piped(&builder, Duration::ZERO);
290
291 emit(&log, Level::Info, "short");
292
293 assert_eq!(
294 lines(&capture),
295 ["[00:00:00.000 | INFO | my_crate ] short"]
296 );
297 }
298
299 #[test]
300 fn test_elapsed_time_comes_from_the_clock() {
301 let (log, capture) = piped(&LoggerBuilder::new(), Duration::from_millis(125_120));
302
303 emit(&log, Level::Info, "later");
304
305 assert_eq!(
306 lines(&capture),
307 ["[00:02:05.120 | INFO | my_crate | src/lib.rs:42 ] later"]
308 );
309 }
310
311 fn is_colored(text: &str) -> bool {
313 text.contains('\x1b')
314 }
315
316 #[test]
317 fn test_color_always_colors_a_line_that_is_not_going_to_a_terminal() {
318 let builder = LoggerBuilder::new().color(ColorChoice::Always);
320 let (log, capture) = piped(&builder, Duration::ZERO);
321
322 emit(&log, Level::Error, "colored");
323
324 let output = capture.contents();
325 assert!(is_colored(&output), "expected color, got {output:?}");
326 assert!(output.contains("[00:00:00.000 | ERROR | my_crate | src/lib.rs:42 ] colored"));
327 }
328
329 #[test]
330 fn test_color_auto_leaves_a_line_that_is_not_going_to_a_terminal_plain() {
331 let (log, capture) = piped(&LoggerBuilder::new(), Duration::ZERO);
332
333 emit(&log, Level::Error, "plain");
334
335 let output = capture.contents();
336 assert!(!is_colored(&output), "unexpected color in {output:?}");
337 }
338
339 const STDERR_CHILD: &str = "CAESIUM_STDERR_CHILD";
342
343 const CHILD_COLORS: [(Option<ColorChoice>, &str); 4] = [
346 (None, "unset"),
347 (Some(ColorChoice::Never), "never"),
348 (Some(ColorChoice::Auto), "auto"),
349 (Some(ColorChoice::Always), "always"),
350 ];
351
352 #[test]
356 #[ignore = "helper run by test_init_writes_lines_to_stderr_not_stdout and the color tests"]
357 fn stderr_child() {
358 let Some(value) = std::env::var_os(STDERR_CHILD) else {
359 return;
360 };
361 let (color, _) = CHILD_COLORS
362 .into_iter()
363 .find(|(_, name)| value == *name)
364 .unwrap_or_else(|| panic!("unknown color choice {value:?}"));
365 let mut builder = LoggerBuilder::new().source_location(false);
366 if let Some(color) = color {
367 builder = builder.color(color);
368 }
369 builder.init().unwrap();
370 info!("to stderr");
371 }
372
373 struct ChildOutput {
376 stdout: String,
377 stderr: String,
378 }
379
380 impl ChildOutput {
381 fn stderr_line(&self) -> &str {
383 self.stderr
384 .lines()
385 .find(|line| line.contains("to stderr"))
386 .unwrap_or_else(|| panic!("line missing from stderr: {:?}", self.stderr))
387 }
388
389 fn is_colored(&self) -> bool {
390 is_colored(self.stderr_line())
391 }
392 }
393
394 fn run_child(color: Option<ColorChoice>, env: &[(&str, &str)]) -> ChildOutput {
403 let (_, value) = CHILD_COLORS
404 .into_iter()
405 .find(|(choice, _)| *choice == color)
406 .expect("every color choice is in CHILD_COLORS");
407 let output = std::process::Command::new(std::env::current_exe().unwrap())
408 .args(["tests::stderr_child", "--exact", "--ignored", "--nocapture"])
409 .env_remove("NO_COLOR")
410 .env_remove("CLICOLOR")
411 .env_remove("CLICOLOR_FORCE")
412 .env(STDERR_CHILD, value)
413 .envs(env.iter().copied())
414 .output()
415 .unwrap();
416 assert!(output.status.success(), "child failed: {output:?}");
417 ChildOutput {
418 stdout: String::from_utf8(output.stdout).unwrap(),
419 stderr: String::from_utf8(output.stderr).unwrap(),
420 }
421 }
422
423 #[test]
424 fn test_init_writes_lines_to_stderr_not_stdout() {
425 let output = run_child(None, &[]);
426
427 assert!(
430 output
431 .stderr_line()
432 .ends_with("| INFO | caesium::tests ] to stderr"),
433 "{:?}",
434 output.stderr
435 );
436 assert!(
437 !output.stdout.contains("to stderr"),
438 "line written to stdout: {:?}",
439 output.stdout
440 );
441 }
442
443 #[test]
444 fn test_color_auto_colors_lines_when_stderr_supports_color() {
445 let output = run_child(Some(ColorChoice::Auto), &[("CLICOLOR_FORCE", "1")]);
446
447 assert!(output.is_colored(), "{:?}", output.stderr);
448 }
449
450 #[test]
451 fn test_color_auto_leaves_lines_plain_when_stderr_does_not_support_color() {
452 let output = run_child(Some(ColorChoice::Auto), &[]);
453
454 assert!(!output.is_colored(), "{:?}", output.stderr);
455 }
456
457 #[test]
458 fn test_color_auto_respects_no_color() {
459 let output = run_child(
460 Some(ColorChoice::Auto),
461 &[("CLICOLOR_FORCE", "1"), ("NO_COLOR", "1")],
462 );
463
464 assert!(!output.is_colored(), "{:?}", output.stderr);
465 }
466
467 #[test]
468 fn test_color_never_leaves_lines_plain_even_when_stderr_supports_color() {
469 let output = run_child(Some(ColorChoice::Never), &[("CLICOLOR_FORCE", "1")]);
470
471 assert!(!output.is_colored(), "{:?}", output.stderr);
472 }
473
474 #[test]
475 fn test_color_defaults_to_auto() {
476 let output = run_child(None, &[("CLICOLOR_FORCE", "1")]);
477
478 assert!(output.is_colored(), "{:?}", output.stderr);
479 }
480
481 #[test]
482 fn test_init_fails_once_a_logger_is_initialized() {
483 let builder = LoggerBuilder::new();
484
485 let _ = builder.init();
488 let second = builder.init();
489
490 assert!(
491 matches!(second, Err(LoggerError::AlreadyInitialized)),
492 "expected AlreadyInitialized, got {second:?}"
493 );
494 }
495
496 #[test]
497 fn test_already_initialized_has_a_message_and_no_source() {
498 let error = LoggerError::AlreadyInitialized;
499
500 assert_eq!(error.to_string(), "a global logger is already initialized");
501 assert!(std::error::Error::source(&error).is_none());
502 }
503
504 #[test]
505 fn test_builder_implements_debug() {
506 let debug = format!("{:?}", LoggerBuilder::new());
507
508 assert!(debug.starts_with("LoggerBuilder"), "got {debug:?}");
509 }
510}