Skip to main content

autopush_common/
logging.rs

1use std::io;
2
3use gethostname::gethostname;
4use slog::{self, Drain};
5use slog_mozlog_json::MozLogJson;
6
7use crate::errors::Result;
8
9/// Default number of records slog-async buffers before it begins dropping them.
10///
11/// slog-async's own default of 128 is trivially overrun: the consumer only needs
12/// to stall briefly (scheduling, a slow write) for the buffer to fill, and each
13/// dropped record then produces an ERROR level "channel overflow" report of its
14/// own, converting filtered-out records into unfilterable noise. Sized instead
15/// for seconds of headroom at a high logging rate; ~110 bytes per slot is
16/// allocated up front, so this costs roughly 2MB resident.
17pub const DEFAULT_LOG_CHAN_SIZE: usize = 20_000;
18
19/// Initialize logging.
20///
21/// `chan_size` is the slog-async buffer depth; see [`DEFAULT_LOG_CHAN_SIZE`]. A
22/// value of 0 is treated as the default: crossbeam would otherwise give us a
23/// rendezvous channel, blocking every logging call until the consumer picks the
24/// record up.
25pub fn init_logging(json: bool, chan_size: usize, name: &str, version: &str) -> Result<()> {
26    let chan_size = if chan_size == 0 {
27        DEFAULT_LOG_CHAN_SIZE
28    } else {
29        chan_size
30    };
31    // NOTE: `slog_envlogger` (the RUST_LOG filter) must be the *outermost*
32    // drain, wrapping `slog_async`. Nested inside it instead, every record is
33    // queued to the async channel before anyone checks its level, so RUST_LOG
34    // can't relieve channel pressure and filtered records still cause overflow.
35    let (logger, filter_level) = if json {
36        let hostname = gethostname().to_string_lossy().to_string();
37
38        let drain = MozLogJson::new(io::stdout())
39            .logger_name(format!("{name}-{version}"))
40            .msg_type(format!("{name}:log"))
41            .hostname(hostname)
42            .build()
43            .fuse();
44        let drain = slog_async::Async::new(drain)
45            .chan_size(chan_size)
46            .build()
47            .fuse();
48        let drain = slog_envlogger::new(drain);
49        let filter_level = slog_envlogger::EnvLogger::filter(&drain);
50        (slog::Logger::root(drain, slog_o!()), filter_level)
51    } else {
52        let decorator = slog_term::TermDecorator::new().build();
53        let drain = slog_term::FullFormat::new(decorator).build().fuse();
54        let drain = slog_async::Async::new(drain)
55            .chan_size(chan_size)
56            .build()
57            .fuse();
58        let drain = slog_envlogger::new(drain);
59        let filter_level = slog_envlogger::EnvLogger::filter(&drain);
60        (slog::Logger::root(drain, slog_o!()), filter_level)
61    };
62    // XXX: cancel slog_scope's NoGlobalLoggerSet for now, it's difficult to
63    // prevent it from potentially panicing during tests. reset_logging resets
64    // the global logger during shutdown anyway:
65    // https://github.com/slog-rs/slog/issues/169
66    slog_scope::set_global_logger(logger).cancel_reset();
67    // Register the `log` -> `slog` bridge, then set `log`'s ceiling separately:
68    // `init_with_level` takes a `log::Level`, which can't express "off".
69    slog_stdlog::init_with_level(log::Level::Error).ok();
70    log::set_max_level(log_max_level(filter_level));
71    Ok(())
72}
73
74/// Translate the `RUST_LOG` filter's maximum level into a ceiling for the `log`
75/// crate.
76///
77/// Our own `slog` macros reach the logger directly, so this governs only records
78/// arriving via `log` -- that is, our dependencies. hyper/h2/tonic/tower emit
79/// `tracing` events and, with no `tracing` subscriber installed, those fall
80/// through to `log`; h2 in particular traces per HTTP/2 frame.
81///
82/// `slog_stdlog::init` would set the ceiling to TRACE, admitting all of it.
83/// Deriving the ceiling from the filter's own maximum instead means a record no
84/// directive could ever accept is rejected by `log::max_level()` before it is
85/// built, rather than being constructed and then dropped by the filter. Records
86/// between this ceiling and a narrower per-module directive still reach the
87/// filter, which remains the authority on what is actually logged.
88fn log_max_level(filter: slog::FilterLevel) -> log::LevelFilter {
89    match filter {
90        slog::FilterLevel::Off => log::LevelFilter::Off,
91        // `log` has no Critical; Error is the nearest ceiling that admits it.
92        slog::FilterLevel::Critical | slog::FilterLevel::Error => log::LevelFilter::Error,
93        slog::FilterLevel::Warning => log::LevelFilter::Warn,
94        slog::FilterLevel::Info => log::LevelFilter::Info,
95        slog::FilterLevel::Debug => log::LevelFilter::Debug,
96        slog::FilterLevel::Trace => log::LevelFilter::Trace,
97    }
98}
99
100pub fn reset_logging() {
101    let logger = slog::Logger::root(slog::Discard, o!());
102    slog_scope::set_global_logger(logger).cancel_reset();
103}
104
105/// Initialize logging to `slog_term::TestStdoutWriter` for tests
106///
107/// Note: unfortunately this disables slog's `TermDecorator` (which can't be
108/// captured by cargo test) color output
109pub fn init_test_logging() {
110    let decorator = slog_term::PlainSyncDecorator::new(slog_term::TestStdoutWriter);
111    let drain = std::sync::Mutex::new(slog_term::FullFormat::new(decorator).build()).fuse();
112    let logger = slog::Logger::root(drain, slog::o!());
113    slog_scope::set_global_logger(logger).cancel_reset();
114    slog_stdlog::init().ok();
115}
116
117/// Return parallelism/number of CPU information to log at startup
118pub fn parallelism_banner() -> String {
119    format!(
120        "available_parallelism: {:?} num_cpus: {} num_cpus (phys): {}",
121        std::thread::available_parallelism(),
122        num_cpus::get(),
123        num_cpus::get_physical()
124    )
125}