use std::fmt;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Mutex, OnceLock};
use std::time::SystemTime;
static PDO_TRACE_ENABLED: AtomicBool = AtomicBool::new(false);
static MAILBOX_TRACE_ENABLED: AtomicBool = AtomicBool::new(false);
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LogCategory {
Error = 0,
Warning = 1,
Message = 2,
Mailbox = 3,
PDO = 4,
Debug = 5,
Local = 6,
}
impl LogCategory {
pub fn all() -> &'static [LogCategory] {
&[
LogCategory::Error,
LogCategory::Warning,
LogCategory::Message,
LogCategory::Mailbox,
LogCategory::PDO,
LogCategory::Debug,
LogCategory::Local,
]
}
pub fn default_view() -> &'static [LogCategory] {
&[
LogCategory::Error,
LogCategory::Warning,
LogCategory::Message,
]
}
pub fn from_value(v: i32) -> Option<Self> {
match v {
0 => Some(Self::Error),
1 => Some(Self::Warning),
2 => Some(Self::Message),
3 => Some(Self::Mailbox),
4 => Some(Self::PDO),
5 => Some(Self::Debug),
6 => Some(Self::Local),
_ => None,
}
}
}
impl fmt::Display for LogCategory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = match self {
Self::Error => "ERROR",
Self::Warning => "WARNING",
Self::Message => "MESSAGE",
Self::Mailbox => "MAILBOX",
Self::PDO => "PDO",
Self::Debug => "DEBUG",
Self::Local => "LOCAL",
};
write!(f, "{}", name)
}
}
#[derive(Debug, Clone)]
pub struct LogEntry {
pub timestamp: SystemTime,
pub category: LogCategory,
pub message: String,
}
impl LogEntry {
pub fn new(category: LogCategory, message: &str) -> Self {
Self {
timestamp: SystemTime::now(),
category,
message: message.to_string(),
}
}
}
impl fmt::Display for LogEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let duration = self.timestamp
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default();
let secs = duration.as_secs();
let millis = duration.subsec_millis();
write!(f, "[{}.{:03}] [{}] {}", secs, millis, self.category, self.message)
}
}
const MAX_ENTRIES: usize = 10000;
const TRIM_COUNT: usize = 2000;
pub type LogUpdatedCallback = Box<dyn Fn(&LogEntry) + Send + Sync>;
pub struct LogView {
entries: Mutex<Vec<LogEntry>>,
filter: Mutex<Vec<LogCategory>>,
retain: Mutex<usize>,
on_updated: Mutex<Option<LogUpdatedCallback>>,
}
impl LogView {
fn new() -> Self {
Self {
entries: Mutex::new(Vec::new()),
filter: Mutex::new(LogCategory::default_view().to_vec()),
retain: Mutex::new(0),
on_updated: Mutex::new(None),
}
}
pub fn set_on_updated<F>(&self, callback: F)
where
F: Fn(&LogEntry) + Send + Sync + 'static,
{
let mut cb = self.on_updated.lock().unwrap();
*cb = Some(Box::new(callback));
}
pub fn clear_on_updated(&self) {
let mut cb = self.on_updated.lock().unwrap();
*cb = None;
}
pub fn retain_count(&self) -> usize {
*self.retain.lock().unwrap()
}
pub fn set_retain_count(&self, count: usize) {
let mut r = self.retain.lock().unwrap();
*r = count;
}
pub fn count(&self) -> usize {
let entries = self.entries.lock().unwrap();
let filter = self.filter.lock().unwrap();
entries.iter().filter(|e| filter.contains(&e.category)).count()
}
pub fn get_all(&self) -> Vec<LogEntry> {
self.entries.lock().unwrap().clone()
}
pub fn get_filtered(&self) -> Vec<LogEntry> {
let entries = self.entries.lock().unwrap();
let filter = self.filter.lock().unwrap();
entries.iter()
.filter(|e| filter.contains(&e.category))
.cloned()
.collect()
}
pub fn set_filter(&self, categories: &[LogCategory]) {
let mut filter = self.filter.lock().unwrap();
*filter = categories.to_vec();
}
pub fn reset_filter(&self) {
let mut filter = self.filter.lock().unwrap();
*filter = LogCategory::all().to_vec();
}
fn add(&self, entry: LogEntry) {
if let Ok(cb) = self.on_updated.lock() {
if let Some(ref callback) = *cb {
callback(&entry);
}
}
let mut entries = self.entries.lock().unwrap();
entries.push(entry);
let max = {
let r = self.retain.lock().unwrap();
if *r > 0 { *r } else { MAX_ENTRIES }
};
let trim = if max == MAX_ENTRIES { TRIM_COUNT } else { max / 5 };
if entries.len() > max {
entries.drain(..trim.max(1));
}
}
fn clear_inner(&self) {
self.entries.lock().unwrap().clear();
}
}
pub struct LogManager {
default_view: LogView,
}
static INSTANCE: OnceLock<LogManager> = OnceLock::new();
impl LogManager {
pub fn instance() -> &'static LogManager {
INSTANCE.get_or_init(|| LogManager {
default_view: LogView::new(),
})
}
pub fn default_view(&self) -> &LogView {
&self.default_view
}
pub fn pdo_trace_enabled(&self) -> bool {
PDO_TRACE_ENABLED.load(Ordering::Relaxed)
}
pub fn set_pdo_trace_enabled(&self, enabled: bool) {
PDO_TRACE_ENABLED.store(enabled, Ordering::Relaxed);
}
pub fn mailbox_trace_enabled(&self) -> bool {
MAILBOX_TRACE_ENABLED.load(Ordering::Relaxed)
}
pub fn set_mailbox_trace_enabled(&self, enabled: bool) {
MAILBOX_TRACE_ENABLED.store(enabled, Ordering::Relaxed);
}
pub fn add_log(&self, category: LogCategory, message: &str) {
if category == LogCategory::Debug && !cfg!(debug_assertions) {
return;
}
if category == LogCategory::PDO && !PDO_TRACE_ENABLED.load(Ordering::Relaxed) {
return;
}
if category == LogCategory::Mailbox && !MAILBOX_TRACE_ENABLED.load(Ordering::Relaxed) {
return;
}
let entry = LogEntry::new(category, message);
self.default_view.add(entry);
}
pub fn clear(&self) {
self.default_view.clear_inner();
}
}