use std::fmt;
use std::io;
use std::sync::{Arc, RwLock};
use slog::{Drain, KV, OwnedKVList, Record};
#[derive(Default, Clone)]
pub struct MemoryDrainForTest {
records: Arc<RwLock<Vec<String>>>,
}
impl MemoryDrainForTest {
pub fn new() -> (Self, MemoryDrainForTestInspector) {
let drain = Self::default();
let inspector = MemoryDrainForTestInspector::new(&drain);
(drain, inspector)
}
}
pub struct MemoryDrainForTestInspector {
records: Arc<RwLock<Vec<String>>>,
}
impl MemoryDrainForTestInspector {
fn new(memory_drain: &MemoryDrainForTest) -> Self {
Self {
records: memory_drain.records.clone(),
}
}
pub fn search_logs(&self, text: &str) -> Vec<String> {
self.records
.read()
.unwrap()
.iter()
.filter(|record| record.contains(text))
.cloned()
.collect()
}
pub fn contains_log(&self, text: &str) -> bool {
self.records
.read()
.unwrap()
.iter()
.any(|record| record.contains(text))
}
}
impl fmt::Display for MemoryDrainForTestInspector {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.records.read().unwrap().join("\n"))
}
}
impl Drain for MemoryDrainForTest {
type Ok = ();
type Err = io::Error;
fn log(&self, record: &Record, values: &OwnedKVList) -> Result<Self::Ok, Self::Err> {
let mut kv_serializer = KVSerializer::default();
record.kv().serialize(record, &mut kv_serializer)?;
values.serialize(record, &mut kv_serializer)?;
let msg = format!(
"{} {}; {}",
record.level().as_str(),
record.msg(),
kv_serializer.content
);
println!("{msg}");
self.records.write().unwrap().push(msg);
Ok(())
}
}
#[derive(Default)]
struct KVSerializer {
content: String,
}
impl slog::Serializer for KVSerializer {
fn emit_arguments(&mut self, key: slog::Key, val: &std::fmt::Arguments) -> slog::Result {
use std::fmt::Write;
let prefix = if self.content.is_empty() { "" } else { ", " };
write!(self.content, "{prefix}{key}={val:?}")
.map_err(|_| io::Error::other("Failed to serialize log"))?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use slog::info;
use super::*;
#[test]
fn test_log_format() {
let (drain, log_inspector) = MemoryDrainForTest::new();
let logger = slog::Logger::root(drain.clone().fuse(), slog::o!("shared" => "shared"));
info!(logger, "test format"; "key_3" => "value three", "key_2" => "value two", "key_1" => "value one");
let results = log_inspector.search_logs("test format");
assert_eq!(
"INFO test format; key_1=value one, key_2=value two, key_3=value three, shared=shared",
results[0]
);
}
#[test]
fn displaying_inspector_returns_all_log_messages() {
let (drain, log_inspector) = MemoryDrainForTest::new();
let logger = slog::Logger::root(drain.fuse(), slog::o!());
info!(logger, "message one"; "key" => "value1");
info!(logger, "message two"; "key" => "value2");
let display = format!("{log_inspector}");
assert_eq!(
display,
"INFO message one; key=value1\nINFO message two; key=value2"
);
}
#[test]
fn can_search_for_log_messages() {
let (drain, log_inspector) = MemoryDrainForTest::new();
let logger = slog::Logger::root(drain.clone().fuse(), slog::o!());
info!(logger, "test message"; "key" => "value");
info!(logger, "another message"; "key2" => "value2");
let results = log_inspector.search_logs("test");
assert_eq!(results.len(), 1);
assert!(results[0].contains("test message"));
assert!(log_inspector.contains_log("test message"));
}
#[tokio::test]
async fn test_concurrent_logging_from_two_tasks() {
let (drain, log_inspector) = MemoryDrainForTest::new();
let drain_clone1 = drain.clone();
let drain_clone2 = drain.clone();
let handle1 = tokio::spawn(async move {
let logger = slog::Logger::root(drain_clone1.fuse(), slog::o!());
info!(logger, "async test 1"; "key" => "value");
});
let handle2 = tokio::spawn(async move {
let logger = slog::Logger::root(drain_clone2.fuse(), slog::o!());
info!(logger, "async test 2"; "key" => "value");
});
handle1.await.unwrap();
handle2.await.unwrap();
let results = log_inspector.search_logs("async test");
assert_eq!(results.len(), 2);
assert!(results.iter().any(|r| r.contains("async test 1")));
assert!(results.iter().any(|r| r.contains("async test 2")));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn test_concurrent_logging_from_multiple_threads() {
let (drain, log_inspector) = MemoryDrainForTest::new();
let mut join_set = tokio::task::JoinSet::new();
for i in 0..10 {
let drain_clone = drain.clone();
join_set.spawn(async move {
let logger = slog::Logger::root(drain_clone.fuse(), slog::o!());
info!(logger, "multi thread test {i}"; "thread_id" => i);
});
}
join_set.join_all().await;
let results = log_inspector.search_logs("multi thread test");
assert_eq!(results.len(), 10);
for i in 0..10 {
assert!(results.iter().any(|r| r.contains(&format!("multi thread test {i}"))));
}
}
}