Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions foundations/src/telemetry/log/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ pub(crate) fn init(
(LogOutput::TracingRsCompat, _) => AsyncDrain::new(tracing_slog::TracingSlogDrain {})
.chan_size(CHANNEL_SIZE)
.build_with_guard(),
(LogOutput::Custom(drain), _) => AsyncDrain::new(Arc::clone(drain))
.chan_size(CHANNEL_SIZE)
.build_with_guard(),
};

let root_drain = wrap_root_drain(settings, async_drain.fuse());
Expand Down
38 changes: 12 additions & 26 deletions foundations/src/telemetry/log/testing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ use crate::telemetry::log::init::{LogHarness, build_log_with_drain, wrap_root_dr
use crate::telemetry::log::internal::LoggerWithKvNestingTracking;
use crate::telemetry::settings::LoggingSettings;
use parking_lot::RwLock as ParkingRwLock;
use slog::{Discard, Drain, KV, Key, Level, Never, OwnedKVList, Record, Serializer};
use slog::{
Drain, KV, Key, Level, Never, OwnedKVList, Record, SendSyncRefUnwindSafeDrain, Serializer,
};
use std::fmt::Arguments;
use std::sync::{Arc, RwLock};

Expand Down Expand Up @@ -69,14 +71,18 @@ where
}
}

pub(crate) fn create_test_log(
pub(crate) fn create_test_log<D>(
settings: &LoggingSettings,
) -> (LoggerWithKvNestingTracking, TestLogRecords) {
forward: Option<D>,
) -> (LoggerWithKvNestingTracking, TestLogRecords)
where
D: SendSyncRefUnwindSafeDrain<Ok = (), Err = Never> + 'static,
{
let log_records = Arc::new(RwLock::new(vec![]));

let drain: TestLogDrain<Discard> = TestLogDrain {
let drain: TestLogDrain<D> = TestLogDrain {
records: Arc::clone(&log_records),
forward: None,
forward,
};
let drain = wrap_root_drain(settings, drain);

Expand Down Expand Up @@ -105,25 +111,5 @@ pub(crate) fn create_test_log_for_tracing_compat(
crate::telemetry::settings::LogOutput::TracingRsCompat
));

let base_drain = TracingSlogDrain {};

let log_records = Arc::new(RwLock::new(vec![]));

let drain = TestLogDrain {
records: Arc::clone(&log_records),
forward: Some(base_drain.fuse()),
};
let drain = wrap_root_drain(settings, drain);

let logger = build_log_with_drain(settings.verbosity, slog::o!(), Arc::clone(&drain));
let log = LoggerWithKvNestingTracking::new(logger);

let _ = LogHarness::override_for_testing(LogHarness {
root_log: Arc::new(ParkingRwLock::new(log.clone())),
root_drain: drain,
settings: settings.clone(),
log_scope_stack: Default::default(),
});

(log, log_records)
create_test_log(settings, Some(TracingSlogDrain {}.fuse()))
}
67 changes: 60 additions & 7 deletions foundations/src/telemetry/settings/logging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,23 @@ use crate::utils::feature_use;

use std::path::PathBuf;

feature_use!(cfg(feature = "logging"), {
use slog::{Never, SendSyncRefUnwindSafeDrain};
use std::sync::Arc;

// NOTE: we technically don't need a feature gate here, but if we don't add
// it then docs don't mark this re-export as available on when `logging` is
// enabled.
pub use slog::Level;

/// A custom slog drain for use with [`LogOutput::Custom`].
pub type CustomDrain = Arc<dyn SendSyncRefUnwindSafeDrain<Ok = (), Err = Never>>;
});

feature_use!(cfg(feature = "settings"), {
use crate::settings::settings;
});

// NOTE: we technically don't need a feature gate here, but if we don't add it then docs don't
// mark this re-export as available on when `logging` is enabled.
#[cfg(feature = "logging")]
pub use slog::Level;

/// Logging settings.
#[cfg_attr(feature = "settings", settings(crate_path = "crate"))]
#[cfg_attr(not(feature = "settings"), derive(Clone, Default, Debug))]
Expand Down Expand Up @@ -39,8 +47,11 @@ pub struct LoggingSettings {
}

/// Log output destination.
#[cfg_attr(feature = "settings", settings(crate_path = "crate"))]
#[cfg_attr(not(feature = "settings"), derive(Clone, Debug, Default))]
#[cfg_attr(
feature = "settings",
settings(crate_path = "crate", impl_debug = false)
)]
#[cfg_attr(not(feature = "settings"), derive(Clone, Default))]
pub enum LogOutput {
/// Write log to terminal.
#[default]
Expand All @@ -58,6 +69,48 @@ pub enum LogOutput {
///verbosity will not be respected
#[cfg(feature = "tracing-rs-compat")]
TracingRsCompat,

/// User-provided drain. Not serializable — set programmatically only.
///
/// [`LogFormat`] is ignored for this variant; the custom drain is responsible for its
/// own formatting. All other [`LoggingSettings`] (verbosity, field redaction, rate
/// limiting) still apply.
///
/// # Examples
///
/// Combine a terminal drain with a JSON file drain:
///
/// ```ignore
/// use slog::{Drain, Duplicate};
/// use slog_term::{FullFormat, TermDecorator};
/// use slog_json::Json;
/// use std::fs::File;
/// use std::sync::Arc;
///
/// let term = FullFormat::new(TermDecorator::new().build()).build().fuse();
/// let file = File::create("/var/log/app.json").unwrap();
/// let json = Json::new(file).build().fuse();
/// let combined = Duplicate::new(term, json).fuse();
///
/// settings.logging.output = LogOutput::Custom(Arc::new(combined));
/// ```
#[cfg(feature = "logging")]
#[cfg_attr(feature = "settings", serde(skip))]
Custom(CustomDrain),
}

impl std::fmt::Debug for LogOutput {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Terminal => write!(f, "Terminal"),
Self::Stderr => write!(f, "Stderr"),
Self::File(path) => f.debug_tuple("File").field(path).finish(),
#[cfg(feature = "tracing-rs-compat")]
Self::TracingRsCompat => write!(f, "TracingRsCompat"),
#[cfg(feature = "logging")]
Self::Custom(_) => write!(f, "Custom(...)"),
}
}
}

/// Format of the log output.
Expand Down
28 changes: 23 additions & 5 deletions foundations/src/telemetry/testing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,13 @@ impl TestTelemetryContext {
pub(crate) fn new() -> Self {
#[cfg(feature = "logging")]
let (log, log_records) = {
create_test_log(&LoggingSettings {
verbosity: LogVerbosity::Trace,
..Default::default()
})
create_test_log(
&LoggingSettings {
verbosity: LogVerbosity::Trace,
..Default::default()
},
None::<slog::Discard>,
)
};

#[cfg(feature = "tracing")]
Expand Down Expand Up @@ -83,7 +86,22 @@ impl TestTelemetryContext {
/// with the settings
#[cfg(feature = "logging")]
pub fn set_logging_settings(&mut self, logging_settings: LoggingSettings) {
let (log, log_records) = { create_test_log(&logging_settings) };
let (log, log_records) = { create_test_log(&logging_settings, None::<slog::Discard>) };
*self.inner.log.write() = log;
self.log_records = log_records;
}

/// Overrides the logging settings on the test telemetry context with a custom drain.
///
/// Records are forwarded to the custom drain **and** captured for assertions via
/// [`log_records`][Self::log_records].
#[cfg(feature = "logging")]
pub fn set_custom_log_drain(
&mut self,
logging_settings: LoggingSettings,
custom_drain: super::settings::CustomDrain,
) {
let (log, log_records) = create_test_log(&logging_settings, Some(custom_drain));
*self.inner.log.write() = log;
self.log_records = log_records;
}
Expand Down
41 changes: 41 additions & 0 deletions foundations/tests/custom_drain.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use std::sync::{Arc, Mutex};

use foundations::telemetry::TestTelemetryContext;
use foundations::telemetry::log::error;
use foundations::telemetry::settings::LoggingSettings;
use slog::{Drain, Never, OwnedKVList, Record};

struct CapturingDrain {
messages: Arc<Mutex<Vec<String>>>,
}

impl Drain for CapturingDrain {
type Ok = ();
type Err = Never;

fn log(&self, record: &Record, _: &OwnedKVList) -> Result<(), Never> {
self.messages
.lock()
.unwrap()
.push(format!("{}", record.msg()));
Ok(())
}
}

#[foundations::telemetry::with_test_telemetry(test)]
fn custom_drain_receives_log_records(mut ctx: TestTelemetryContext) {
let messages = Arc::new(Mutex::new(Vec::new()));
let drain = CapturingDrain {
messages: Arc::clone(&messages),
};

ctx.set_custom_log_drain(LoggingSettings::default(), Arc::new(drain));

error!("hello from custom drain");

let msgs = messages.lock().unwrap();
assert!(
msgs.iter().any(|m| m.contains("hello from custom drain")),
"custom drain did not receive the expected log record; got: {msgs:?}"
);
}
Loading