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}