Skip to main content

lux_lib/
logging.rs

1use std::collections::VecDeque;
2use std::sync::Mutex;
3
4use lazy_static::lazy_static;
5use serde::Serialize;
6
7const MAX_BACKLOG: usize = 200;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
10#[serde(rename_all = "lowercase")]
11pub enum LogLevel {
12    Info,
13    Warn,
14    Error,
15}
16
17#[derive(Debug, Clone, Serialize)]
18pub struct LogEntry {
19    pub level: LogLevel,
20    pub message: String,
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub target: Option<String>,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum LoggingState {
27    Disabled,
28    Enabled,
29}
30
31lazy_static! {
32    static ref LOG_BUFFER: Mutex<VecDeque<LogEntry>> = Mutex::new(VecDeque::new());
33    static ref LOG_STATE: Mutex<LoggingState> = Mutex::new(LoggingState::Disabled);
34}
35
36#[allow(clippy::expect_used)]
37pub fn set_state(state: LoggingState) {
38    *LOG_STATE.lock().expect("log lock poisoned") = state;
39}
40
41#[allow(clippy::expect_used)]
42pub fn state() -> LoggingState {
43    *LOG_STATE.lock().expect("log lock poisoned")
44}
45
46#[allow(clippy::expect_used)]
47pub fn info(message: String, target: Option<String>) {
48    match state() {
49        LoggingState::Enabled => push(LogLevel::Info, message, target),
50        LoggingState::Disabled => println!("{}", message),
51    }
52}
53
54pub fn warn(message: String, target: Option<String>) {
55    match state() {
56        LoggingState::Enabled => push(LogLevel::Warn, message, target),
57        LoggingState::Disabled => eprintln!("{}", message),
58    }
59}
60
61#[allow(clippy::expect_used)]
62pub fn error(message: String, target: Option<String>) {
63    match state() {
64        LoggingState::Enabled => push(LogLevel::Error, message, target),
65        LoggingState::Disabled => eprintln!("{}", message),
66    }
67}
68
69#[allow(clippy::expect_used)]
70pub fn push(level: LogLevel, message: String, target: Option<String>) {
71    if let LoggingState::Disabled = state() {
72        return;
73    }
74
75    let entry = LogEntry {
76        level,
77        message,
78        target,
79    };
80
81    let mut buffer = LOG_BUFFER.lock().expect("log lock poisoned");
82
83    buffer.push_back(entry);
84
85    if buffer.len() > MAX_BACKLOG {
86        buffer.pop_front();
87    }
88}
89
90#[allow(clippy::expect_used)]
91pub fn drain() -> Vec<LogEntry> {
92    if let LoggingState::Disabled = state() {
93        return Vec::new();
94    }
95
96    let mut buffer = LOG_BUFFER.lock().expect("log lock poisoned");
97    buffer.drain(..).collect()
98}
99
100#[allow(clippy::expect_used)]
101pub fn clear() {
102    let mut buffer = LOG_BUFFER.lock().expect("log lock poisoned");
103
104    buffer.clear();
105}
106
107#[cfg(test)]
108mod tests {
109    use serial_test::serial;
110
111    use super::*;
112
113    fn reset() {
114        set_state(LoggingState::Disabled);
115        clear();
116    }
117
118    #[test]
119    #[serial]
120    fn test_push_and_drain() {
121        reset();
122        set_state(LoggingState::Enabled);
123        push(LogLevel::Info, "info".to_string(), None);
124        push(
125            LogLevel::Warn,
126            "warning".to_string(),
127            Some("build".to_string()),
128        );
129        push(
130            LogLevel::Error,
131            "error".to_string(),
132            Some("fetch".to_string()),
133        );
134
135        let entries = drain();
136        assert_eq!(entries.len(), 3);
137        assert!(matches!(entries[0].level, LogLevel::Info));
138        assert_eq!(entries[0].message, "info");
139        assert!(entries[0].target.is_none());
140        assert!(matches!(entries[1].level, LogLevel::Warn));
141        assert_eq!(entries[1].message, "warning");
142        assert_eq!(entries[1].target.as_deref(), Some("build"));
143        assert!(matches!(entries[2].level, LogLevel::Error));
144
145        assert!(drain().is_empty());
146    }
147
148    #[test]
149    #[serial]
150    fn test_push_noop_when_disabled() {
151        reset();
152        set_state(LoggingState::Disabled);
153        push(LogLevel::Info, "should not appear".to_string(), None);
154        assert!(drain().is_empty());
155    }
156}