diff --git a/Cargo.toml b/Cargo.toml index f4ab584..d2d2f27 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ governor = "0.6" hyper = { version = "0.14", default-features = false } indexmap = "2.0.0" ipnetwork = "0.20" +log = "0.4.20" once_cell = "1.5" parking_lot = "0.12" proc-macro2 = { version = "1", default-features = false } @@ -61,6 +62,8 @@ slab = "0.4.9" slog = "2.4" slog-async = "2.3" slog-json = "2.3" +slog-scope = "4.4.0" +slog-stdlog = "4.1.1" slog-term = "2.4" tempfile = "3.7" tokio = "1.31.0" diff --git a/foundations/Cargo.toml b/foundations/Cargo.toml index cdb743a..f11d775 100644 --- a/foundations/Cargo.toml +++ b/foundations/Cargo.toml @@ -98,10 +98,13 @@ telemetry-server = [ # Enables logging functionality. logging = [ "dep:governor", + "dep:log", "dep:once_cell", "dep:parking_lot", "dep:slog-async", "dep:slog-json", + "dep:slog-scope", + "dep:slog-stdlog", "dep:slog-term", "dep:slog", "dep:thread_local", @@ -162,6 +165,7 @@ hyper = { workspace = true, optional = true, features = [ "server", ] } indexmap = { workspace = true, optional = true, features = ["serde"] } +log = { workspace = true, optional = true } once_cell = { workspace = true, optional = true } parking_lot = { workspace = true, optional = true } prometheus = { workspace = true, optional = true, features = ["process"] } @@ -179,6 +183,8 @@ slab = { workspace = true, optional = true } slog = { workspace = true, optional = true, features = ["max_level_trace"] } slog-async = { workspace = true, optional = true } slog-json = { workspace = true, optional = true } +slog-scope = { workspace = true, optional = true } +slog-stdlog = { workspace = true, optional = true } slog-term = { workspace = true, optional = true } socket2 = { workspace = true, optional = true } thread_local = { workspace = true, optional = true } diff --git a/foundations/src/telemetry/log/mod.rs b/foundations/src/telemetry/log/mod.rs index 731fbbd..76b6904 100644 --- a/foundations/src/telemetry/log/mod.rs +++ b/foundations/src/telemetry/log/mod.rs @@ -21,9 +21,14 @@ use self::internal::current_log; use crate::telemetry::log::init::build_log_with_drain; use crate::telemetry::settings::LogVerbosity; use crate::Result; +use log::Level as std_level; use slog::{Level, Logger, OwnedKV}; +use slog_scope::{set_global_logger, GlobalLoggerGuard}; use std::sync::Arc; +static mut GLOBAL_LOGGER_GUARD: Option = None; +static GLOBAL_LOGGER_CAPTURE: parking_lot::Once = parking_lot::Once::new(); + #[cfg(any(test, feature = "testing"))] pub use self::testing::TestLogRecord; @@ -59,6 +64,58 @@ pub fn slog_logger() -> Arc> { current_log() } +/// Capture logs from the [`log`](https://docs.rs/log/latest/log/) crate by forwarding them into +/// the current [`slog::Drain`]. Due to the intricacies and subtleties of this method, **you should +/// be very careful when using it**. Note also that this method can only be called once. +/// +/// # Note +/// +/// After calling this method, all `log` logs will be forwarded to the [`slog::Drain`] in use for +/// the rest of the program's lifetime. +/// +/// # Examples +/// ``` +/// use foundations::telemetry::TelemetryContext; +/// use foundations::telemetry::log::capture_global_log_logs; +/// use log::warn as log_warn; +/// +/// let cx = TelemetryContext::test(); +/// capture_global_log_logs(); +/// for i in 0..16 { +/// log_warn!("{}", i); +/// } +/// +/// assert_eq!(cx.log_records().len(), 16); +/// ``` +pub fn capture_global_log_logs() { + unsafe { + // SAFETY: mutating a `static mut` is generally unsafe, but since we're guarding it behind + // a call_once, we should be fine. + GLOBAL_LOGGER_CAPTURE.call_once(|| { + let curr_logger = Arc::clone(&slog_logger()).read().clone(); + let scope_guard = set_global_logger(curr_logger); + + // Convert slog::Level from Foundations settings to log::Level + let normalized_level = match verbosity().0 { + Level::Critical | Level::Error => std_level::Error, + Level::Warning => std_level::Warn, + Level::Info => std_level::Info, + Level::Debug => std_level::Debug, + Level::Trace => std_level::Trace, + }; + + slog_stdlog::init_with_level(normalized_level).unwrap(); + + // Storing the scope guard in a static global guard means that logs will be forwarded + // to the slog::Drain for the entirety of the program's lifetime. This prevents users + // from accidentally calling the method twice and triggering log::SetLoggerErrors, or + // attempting to log messages after dropping the guard and triggering + // slog_scope::NoLoggerSet. + GLOBAL_LOGGER_GUARD = Some(scope_guard); + }); + } +} + // NOTE: `#[doc(hidden)]` + `#[doc(inline)]` for `pub use` trick is used to prevent these macros // to show up in the crate's top level docs.