mithril_common/test/logging/
memory_logger.rs

1use std::fmt;
2use std::io;
3use std::sync::{Arc, RwLock};
4
5use slog::{Drain, KV, OwnedKVList, Record};
6
7/// A testing infrastructure for logging that consists of two main components:
8/// - [MemoryDrainForTest]: A slog Drain that stores records in memory while still logging them to stdout
9/// - [MemoryDrainForTestInspector]: A component that provides methods to analyze stored logs
10///
11/// Records are stored as formatted strings in a thread-safe vector using `Arc<RwLock>`.
12/// Each log record follows the format:
13///
14/// `{LEVEL} {MESSAGE}; {KEY1}={VALUE1}, {KEY2}={VALUE2}, ...`
15///
16/// where:
17/// - LEVEL: The log level (DEBUG, INFO, etc.)
18/// - MESSAGE: The main log message
19/// - KEY=VALUE pairs: Additional context values attached to the log
20///
21/// # Performance Considerations
22///
23/// This drain implementation is designed for testing purposes and is not optimized for performance.
24/// It uses an RwLock for each log operation and string formatting, which may introduce significant overhead.
25#[derive(Default, Clone)]
26pub struct MemoryDrainForTest {
27    records: Arc<RwLock<Vec<String>>>,
28}
29
30impl MemoryDrainForTest {
31    /// Creates a new instance of `MemoryDrainForTest`
32    pub fn new() -> (Self, MemoryDrainForTestInspector) {
33        let drain = Self::default();
34        let inspector = MemoryDrainForTestInspector::new(&drain);
35        (drain, inspector)
36    }
37}
38
39/// A component that provides methods to analyze logs stored by [MemoryDrainForTest].
40pub struct MemoryDrainForTestInspector {
41    records: Arc<RwLock<Vec<String>>>,
42}
43
44impl MemoryDrainForTestInspector {
45    fn new(memory_drain: &MemoryDrainForTest) -> Self {
46        Self {
47            records: memory_drain.records.clone(),
48        }
49    }
50
51    /// Returns all log records that contain the specified text
52    ///
53    /// This method performs a case-sensitive search through all stored log records
54    /// and returns copies of the matching records.
55    pub fn search_logs(&self, text: &str) -> Vec<String> {
56        self.records
57            .read()
58            .unwrap()
59            .iter()
60            .filter(|record| record.contains(text))
61            .cloned()
62            .collect()
63    }
64
65    /// Checks if any log record contains the specified text
66    ///
67    /// This method performs a case-sensitive search and returns true if any log
68    /// record contains the specified text.
69    pub fn contains_log(&self, text: &str) -> bool {
70        self.records
71            .read()
72            .unwrap()
73            .iter()
74            .any(|record| record.contains(text))
75    }
76}
77
78impl fmt::Display for MemoryDrainForTestInspector {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        write!(f, "{}", self.records.read().unwrap().join("\n"))
81    }
82}
83
84impl Drain for MemoryDrainForTest {
85    type Ok = ();
86    type Err = io::Error;
87
88    fn log(&self, record: &Record, values: &OwnedKVList) -> Result<Self::Ok, Self::Err> {
89        let mut kv_serializer = KVSerializer::default();
90        record.kv().serialize(record, &mut kv_serializer)?;
91        values.serialize(record, &mut kv_serializer)?;
92
93        let msg = format!(
94            "{} {}; {}",
95            record.level().as_str(),
96            record.msg(),
97            kv_serializer.content
98        );
99        // Print to stdout as well for easier debugging when `contains_log` fails
100        println!("{msg}");
101
102        self.records.write().unwrap().push(msg);
103        Ok(())
104    }
105}
106
107#[derive(Default)]
108struct KVSerializer {
109    content: String,
110}
111
112impl slog::Serializer for KVSerializer {
113    fn emit_arguments(&mut self, key: slog::Key, val: &std::fmt::Arguments) -> slog::Result {
114        use std::fmt::Write;
115
116        let prefix = if self.content.is_empty() { "" } else { ", " };
117        write!(self.content, "{prefix}{key}={val:?}")
118            .map_err(|_| io::Error::other("Failed to serialize log"))?;
119        Ok(())
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use slog::info;
126
127    use super::*;
128
129    #[test]
130    fn test_log_format() {
131        let (drain, log_inspector) = MemoryDrainForTest::new();
132        let logger = slog::Logger::root(drain.clone().fuse(), slog::o!("shared" => "shared"));
133
134        // Note: keys seem to be logged in invert order
135        info!(logger, "test format"; "key_3" => "value three", "key_2" => "value two", "key_1" => "value one");
136
137        let results = log_inspector.search_logs("test format");
138        assert_eq!(
139            "INFO test format; key_1=value one, key_2=value two, key_3=value three, shared=shared",
140            results[0]
141        );
142    }
143
144    #[test]
145    fn displaying_inspector_returns_all_log_messages() {
146        let (drain, log_inspector) = MemoryDrainForTest::new();
147        let logger = slog::Logger::root(drain.fuse(), slog::o!());
148
149        info!(logger, "message one"; "key" => "value1");
150        info!(logger, "message two"; "key" => "value2");
151
152        let display = format!("{log_inspector}");
153        assert_eq!(
154            display,
155            "INFO message one; key=value1\nINFO message two; key=value2"
156        );
157    }
158
159    #[test]
160    fn can_search_for_log_messages() {
161        let (drain, log_inspector) = MemoryDrainForTest::new();
162        let logger = slog::Logger::root(drain.clone().fuse(), slog::o!());
163
164        info!(logger, "test message"; "key" => "value");
165        info!(logger, "another message"; "key2" => "value2");
166
167        let results = log_inspector.search_logs("test");
168        assert_eq!(results.len(), 1);
169        assert!(results[0].contains("test message"));
170        assert!(log_inspector.contains_log("test message"));
171    }
172
173    #[tokio::test]
174    async fn test_concurrent_logging_from_two_tasks() {
175        let (drain, log_inspector) = MemoryDrainForTest::new();
176        let drain_clone1 = drain.clone();
177        let drain_clone2 = drain.clone();
178
179        let handle1 = tokio::spawn(async move {
180            let logger = slog::Logger::root(drain_clone1.fuse(), slog::o!());
181            info!(logger, "async test 1"; "key" => "value");
182        });
183        let handle2 = tokio::spawn(async move {
184            let logger = slog::Logger::root(drain_clone2.fuse(), slog::o!());
185            info!(logger, "async test 2"; "key" => "value");
186        });
187
188        handle1.await.unwrap();
189        handle2.await.unwrap();
190
191        let results = log_inspector.search_logs("async test");
192        assert_eq!(results.len(), 2);
193        assert!(results.iter().any(|r| r.contains("async test 1")));
194        assert!(results.iter().any(|r| r.contains("async test 2")));
195    }
196
197    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
198    async fn test_concurrent_logging_from_multiple_threads() {
199        let (drain, log_inspector) = MemoryDrainForTest::new();
200        let mut join_set = tokio::task::JoinSet::new();
201
202        for i in 0..10 {
203            let drain_clone = drain.clone();
204            join_set.spawn(async move {
205                let logger = slog::Logger::root(drain_clone.fuse(), slog::o!());
206                info!(logger, "multi thread test {i}"; "thread_id" => i);
207            });
208        }
209
210        join_set.join_all().await;
211
212        let results = log_inspector.search_logs("multi thread test");
213        assert_eq!(results.len(), 10);
214        for i in 0..10 {
215            assert!(results.iter().any(|r| r.contains(&format!("multi thread test {i}"))));
216        }
217    }
218}