use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[repr(u8)]
#[derive(Default)]
pub enum LogLevel {
Unset = 0,
Debug = 10,
#[default]
Info = 20,
Warn = 30,
Error = 40,
Fatal = 50,
}
impl LogLevel {
pub fn as_str(&self) -> &'static str {
match self {
Self::Unset => "UNSET",
Self::Debug => "DEBUG",
Self::Info => "INFO",
Self::Warn => "WARN",
Self::Error => "ERROR",
Self::Fatal => "FATAL",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s.to_uppercase().as_str() {
"UNSET" => Some(Self::Unset),
"DEBUG" => Some(Self::Debug),
"INFO" => Some(Self::Info),
"WARN" | "WARNING" => Some(Self::Warn),
"ERROR" | "ERR" => Some(Self::Error),
"FATAL" | "CRITICAL" => Some(Self::Fatal),
_ => None,
}
}
pub fn syslog_severity(&self) -> u8 {
match self {
Self::Unset => 7, Self::Debug => 7, Self::Info => 6, Self::Warn => 4, Self::Error => 3, Self::Fatal => 2, }
}
}
impl std::fmt::Display for LogLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogFilter {
pub min_level: LogLevel,
pub participant_pattern: Option<String>,
pub topic_pattern: Option<String>,
pub node_pattern: Option<String>,
pub message_pattern: Option<String>,
}
impl Default for LogFilter {
fn default() -> Self {
Self {
min_level: LogLevel::Info,
participant_pattern: None,
topic_pattern: None,
node_pattern: None,
message_pattern: None,
}
}
}
impl LogFilter {
pub fn all() -> Self {
Self {
min_level: LogLevel::Unset,
..Default::default()
}
}
pub fn min_level(level: LogLevel) -> Self {
Self {
min_level: level,
..Default::default()
}
}
pub fn matches(&self, entry: &super::LogEntry) -> bool {
if entry.level < self.min_level {
return false;
}
if let Some(ref pattern) = self.participant_pattern {
if !glob_match(pattern, &entry.participant_id) {
return false;
}
}
if let Some(ref pattern) = self.topic_pattern {
if let Some(ref topic) = entry.topic {
if !glob_match(pattern, topic) {
return false;
}
}
}
if let Some(ref pattern) = self.node_pattern {
if let Some(ref node) = entry.node_name {
if !glob_match(pattern, node) {
return false;
}
}
}
if let Some(ref pattern) = self.message_pattern {
if !entry.message.contains(pattern) {
return false;
}
}
true
}
}
fn glob_match(pattern: &str, text: &str) -> bool {
let mut pattern_chars = pattern.chars().peekable();
let mut text_chars = text.chars().peekable();
while let Some(p) = pattern_chars.next() {
match p {
'*' => {
while pattern_chars.peek() == Some(&'*') {
pattern_chars.next();
}
if pattern_chars.peek().is_none() {
return true;
}
let remaining_pattern: String = pattern_chars.collect();
while text_chars.peek().is_some() {
let remaining_text: String = text_chars.clone().collect();
if glob_match(&remaining_pattern, &remaining_text) {
return true;
}
text_chars.next();
}
return glob_match(&remaining_pattern, "");
}
'?' => {
if text_chars.next().is_none() {
return false;
}
}
c => {
if text_chars.next() != Some(c) {
return false;
}
}
}
}
text_chars.peek().is_none()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_log_level_ordering() {
assert!(LogLevel::Debug < LogLevel::Info);
assert!(LogLevel::Info < LogLevel::Warn);
assert!(LogLevel::Warn < LogLevel::Error);
assert!(LogLevel::Error < LogLevel::Fatal);
}
#[test]
fn test_log_level_from_str() {
assert_eq!(LogLevel::parse("debug"), Some(LogLevel::Debug));
assert_eq!(LogLevel::parse("INFO"), Some(LogLevel::Info));
assert_eq!(LogLevel::parse("Warning"), Some(LogLevel::Warn));
assert_eq!(LogLevel::parse("ERR"), Some(LogLevel::Error));
assert_eq!(LogLevel::parse("invalid"), None);
}
#[test]
fn test_glob_match_exact() {
assert!(glob_match("hello", "hello"));
assert!(!glob_match("hello", "world"));
assert!(!glob_match("hello", "hello!"));
}
#[test]
fn test_glob_match_star() {
assert!(glob_match("*", "anything"));
assert!(glob_match("*", ""));
assert!(glob_match("hello*", "hello"));
assert!(glob_match("hello*", "hello world"));
assert!(glob_match("*world", "hello world"));
assert!(glob_match("hello*world", "hello big world"));
assert!(!glob_match("hello*world", "hello big moon"));
}
#[test]
fn test_glob_match_question() {
assert!(glob_match("h?llo", "hello"));
assert!(glob_match("h?llo", "hallo"));
assert!(!glob_match("h?llo", "hllo"));
assert!(!glob_match("h?llo", "heello"));
}
#[test]
fn test_glob_match_combined() {
assert!(glob_match("rt/*", "rt/rosout"));
assert!(glob_match("rt/*", "rt/topic/nested"));
assert!(glob_match("*/rosout", "rt/rosout"));
assert!(glob_match("rt/ros?ut", "rt/rosout"));
}
#[test]
fn test_filter_level() {
use super::super::LogEntry;
let filter = LogFilter::min_level(LogLevel::Warn);
let debug_entry = LogEntry {
level: LogLevel::Debug,
message: "test".to_string(),
participant_id: "test".to_string(),
..Default::default()
};
let warn_entry = LogEntry {
level: LogLevel::Warn,
message: "test".to_string(),
participant_id: "test".to_string(),
..Default::default()
};
assert!(!filter.matches(&debug_entry));
assert!(filter.matches(&warn_entry));
}
}