Skip to main content

secret_scraper/
logging.rs

1//! Logging and tracing setup.
2
3use std::{
4    io::{self, Write},
5    sync::atomic::{AtomicBool, Ordering},
6};
7
8use tracing_appender::non_blocking::WorkerGuard;
9use tracing_subscriber::{EnvFilter, filter, fmt, prelude::*};
10
11static LOGGING_DISABLED: AtomicBool = AtomicBool::new(false);
12
13/// Log level selected by the CLI startup path.
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15pub enum LogLevel {
16    /// Emit warnings and errors only.
17    Warn,
18    /// Emit informational logs plus warnings and errors.
19    Info,
20}
21
22impl LogLevel {
23    fn as_filter(self) -> &'static str {
24        match self {
25            Self::Warn => "warn",
26            Self::Info => "info",
27        }
28    }
29}
30
31/// Select the CLI log level: `--verbose` enables info output, otherwise warning.
32pub fn cli_log_level(verbose: bool) -> LogLevel {
33    if verbose {
34        LogLevel::Info
35    } else {
36        LogLevel::Warn
37    }
38}
39
40/// Initialize stdout and file tracing with an explicit level.
41///
42/// Returns a [`WorkerGuard`] that must be kept alive for non-blocking file logs
43/// to flush correctly.
44pub fn init_tracing_with_level(level: LogLevel) -> WorkerGuard {
45    LOGGING_DISABLED.store(false, Ordering::SeqCst);
46
47    let file_appender = tracing_appender::rolling::daily("./logs", "scraper.log");
48    let (file_writer, guard) = tracing_appender::non_blocking(file_appender);
49
50    let filter = EnvFilter::try_new(level.as_filter()).expect("valid filter");
51    let shutdown_filter = filter::filter_fn(|_| !LOGGING_DISABLED.load(Ordering::SeqCst));
52
53    let stdout_layer = fmt::layer()
54        .with_writer(std::io::stdout)
55        .with_ansi(true)
56        .compact()
57        .with_filter(shutdown_filter.clone());
58
59    let file_layer = fmt::layer()
60        .json()
61        .with_writer(file_writer)
62        .with_ansi(false)
63        .with_target(true)
64        .with_file(true)
65        .with_line_number(true)
66        .with_filter(shutdown_filter);
67
68    tracing_subscriber::registry()
69        .with(filter)
70        .with(stdout_layer)
71        .with(file_layer)
72        .init();
73
74    guard
75}
76
77/// Disable tracing output and print the shutdown notification.
78pub fn notify_shutdown(mut writer: impl Write) -> io::Result<()> {
79    disable_logging();
80    writeln!(writer, "Shutdown...")
81}
82
83/// Disable future tracing output.
84pub fn disable_logging() {
85    LOGGING_DISABLED.store(true, Ordering::SeqCst);
86}
87
88/// Return whether shutdown has disabled logging.
89#[cfg(test)]
90pub fn logging_disabled() -> bool {
91    LOGGING_DISABLED.load(Ordering::SeqCst)
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn shutdown_notification_disables_logging_and_prints_message() {
100        let mut out = Vec::new();
101
102        notify_shutdown(&mut out).expect("notify shutdown");
103
104        assert!(logging_disabled());
105        assert_eq!(
106            String::from_utf8(out).expect("utf8 output"),
107            "Shutdown...\n"
108        );
109    }
110
111    #[test]
112    fn cli_log_level_defaults_to_warn_and_verbose_enables_info() {
113        assert_eq!(cli_log_level(false), LogLevel::Warn);
114        assert_eq!(cli_log_level(true), LogLevel::Info);
115    }
116}