1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
10#[repr(u8)]
11#[derive(Default)]
12pub enum LogLevel {
13 Unset = 0,
15 Debug = 10,
17 #[default]
19 Info = 20,
20 Warn = 30,
22 Error = 40,
24 Fatal = 50,
26}
27
28impl LogLevel {
29 pub fn as_str(&self) -> &'static str {
31 match self {
32 Self::Unset => "UNSET",
33 Self::Debug => "DEBUG",
34 Self::Info => "INFO",
35 Self::Warn => "WARN",
36 Self::Error => "ERROR",
37 Self::Fatal => "FATAL",
38 }
39 }
40
41 pub fn parse(s: &str) -> Option<Self> {
43 match s.to_uppercase().as_str() {
44 "UNSET" => Some(Self::Unset),
45 "DEBUG" => Some(Self::Debug),
46 "INFO" => Some(Self::Info),
47 "WARN" | "WARNING" => Some(Self::Warn),
48 "ERROR" | "ERR" => Some(Self::Error),
49 "FATAL" | "CRITICAL" => Some(Self::Fatal),
50 _ => None,
51 }
52 }
53
54 pub fn syslog_severity(&self) -> u8 {
56 match self {
57 Self::Unset => 7, Self::Debug => 7, Self::Info => 6, Self::Warn => 4, Self::Error => 3, Self::Fatal => 2, }
64 }
65}
66
67impl std::fmt::Display for LogLevel {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 write!(f, "{}", self.as_str())
70 }
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct LogFilter {
76 pub min_level: LogLevel,
78 pub participant_pattern: Option<String>,
80 pub topic_pattern: Option<String>,
82 pub node_pattern: Option<String>,
84 pub message_pattern: Option<String>,
86}
87
88impl Default for LogFilter {
89 fn default() -> Self {
90 Self {
91 min_level: LogLevel::Info,
92 participant_pattern: None,
93 topic_pattern: None,
94 node_pattern: None,
95 message_pattern: None,
96 }
97 }
98}
99
100impl LogFilter {
101 pub fn all() -> Self {
103 Self {
104 min_level: LogLevel::Unset,
105 ..Default::default()
106 }
107 }
108
109 pub fn min_level(level: LogLevel) -> Self {
111 Self {
112 min_level: level,
113 ..Default::default()
114 }
115 }
116
117 pub fn matches(&self, entry: &super::LogEntry) -> bool {
119 if entry.level < self.min_level {
121 return false;
122 }
123
124 if let Some(ref pattern) = self.participant_pattern {
126 if !glob_match(pattern, &entry.participant_id) {
127 return false;
128 }
129 }
130
131 if let Some(ref pattern) = self.topic_pattern {
133 if let Some(ref topic) = entry.topic {
134 if !glob_match(pattern, topic) {
135 return false;
136 }
137 }
138 }
139
140 if let Some(ref pattern) = self.node_pattern {
142 if let Some(ref node) = entry.node_name {
143 if !glob_match(pattern, node) {
144 return false;
145 }
146 }
147 }
148
149 if let Some(ref pattern) = self.message_pattern {
151 if !entry.message.contains(pattern) {
152 return false;
153 }
154 }
155
156 true
157 }
158}
159
160fn glob_match(pattern: &str, text: &str) -> bool {
163 let mut pattern_chars = pattern.chars().peekable();
164 let mut text_chars = text.chars().peekable();
165
166 while let Some(p) = pattern_chars.next() {
167 match p {
168 '*' => {
169 while pattern_chars.peek() == Some(&'*') {
171 pattern_chars.next();
172 }
173
174 if pattern_chars.peek().is_none() {
176 return true;
177 }
178
179 let remaining_pattern: String = pattern_chars.collect();
181 while text_chars.peek().is_some() {
182 let remaining_text: String = text_chars.clone().collect();
183 if glob_match(&remaining_pattern, &remaining_text) {
184 return true;
185 }
186 text_chars.next();
187 }
188 return glob_match(&remaining_pattern, "");
190 }
191 '?' => {
192 if text_chars.next().is_none() {
194 return false;
195 }
196 }
197 c => {
198 if text_chars.next() != Some(c) {
200 return false;
201 }
202 }
203 }
204 }
205
206 text_chars.peek().is_none()
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[test]
215 fn test_log_level_ordering() {
216 assert!(LogLevel::Debug < LogLevel::Info);
217 assert!(LogLevel::Info < LogLevel::Warn);
218 assert!(LogLevel::Warn < LogLevel::Error);
219 assert!(LogLevel::Error < LogLevel::Fatal);
220 }
221
222 #[test]
223 fn test_log_level_from_str() {
224 assert_eq!(LogLevel::parse("debug"), Some(LogLevel::Debug));
225 assert_eq!(LogLevel::parse("INFO"), Some(LogLevel::Info));
226 assert_eq!(LogLevel::parse("Warning"), Some(LogLevel::Warn));
227 assert_eq!(LogLevel::parse("ERR"), Some(LogLevel::Error));
228 assert_eq!(LogLevel::parse("invalid"), None);
229 }
230
231 #[test]
232 fn test_glob_match_exact() {
233 assert!(glob_match("hello", "hello"));
234 assert!(!glob_match("hello", "world"));
235 assert!(!glob_match("hello", "hello!"));
236 }
237
238 #[test]
239 fn test_glob_match_star() {
240 assert!(glob_match("*", "anything"));
241 assert!(glob_match("*", ""));
242 assert!(glob_match("hello*", "hello"));
243 assert!(glob_match("hello*", "hello world"));
244 assert!(glob_match("*world", "hello world"));
245 assert!(glob_match("hello*world", "hello big world"));
246 assert!(!glob_match("hello*world", "hello big moon"));
247 }
248
249 #[test]
250 fn test_glob_match_question() {
251 assert!(glob_match("h?llo", "hello"));
252 assert!(glob_match("h?llo", "hallo"));
253 assert!(!glob_match("h?llo", "hllo"));
254 assert!(!glob_match("h?llo", "heello"));
255 }
256
257 #[test]
258 fn test_glob_match_combined() {
259 assert!(glob_match("rt/*", "rt/rosout"));
260 assert!(glob_match("rt/*", "rt/topic/nested"));
261 assert!(glob_match("*/rosout", "rt/rosout"));
262 assert!(glob_match("rt/ros?ut", "rt/rosout"));
263 }
264
265 #[test]
266 fn test_filter_level() {
267 use super::super::LogEntry;
268
269 let filter = LogFilter::min_level(LogLevel::Warn);
270
271 let debug_entry = LogEntry {
272 level: LogLevel::Debug,
273 message: "test".to_string(),
274 participant_id: "test".to_string(),
275 ..Default::default()
276 };
277
278 let warn_entry = LogEntry {
279 level: LogLevel::Warn,
280 message: "test".to_string(),
281 participant_id: "test".to_string(),
282 ..Default::default()
283 };
284
285 assert!(!filter.matches(&debug_entry));
286 assert!(filter.matches(&warn_entry));
287 }
288}