use log::{LevelFilter, Log, Metadata, Record, SetLoggerError};
use std::collections::VecDeque;
use std::sync::{Arc, Mutex, OnceLock, Weak};
use std::time::{SystemTime, UNIX_EPOCH};
const DEFAULT_CAPACITY: usize = 256;
pub type HostLogFn = dyn for<'a, 'b> Fn(&'a str, &'b str) + Send + Sync;
#[derive(Debug, Clone)]
pub struct LogRecord {
pub timestamp_secs: u64,
pub level: String,
pub target: String,
pub message: String,
}
impl LogRecord {
pub fn now(
level: impl Into<String>,
target: impl Into<String>,
message: impl Into<String>,
) -> Self {
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
Self {
timestamp_secs: ts,
level: level.into(),
target: target.into(),
message: message.into(),
}
}
}
#[derive(Debug)]
pub struct LogBuffer {
inner: Mutex<VecDeque<LogRecord>>,
capacity: usize,
}
impl Default for LogBuffer {
fn default() -> Self {
Self::new(DEFAULT_CAPACITY)
}
}
impl LogBuffer {
pub fn new(capacity: usize) -> Self {
assert!(capacity > 0, "LogBuffer capacity must be > 0");
Self {
inner: Mutex::new(VecDeque::with_capacity(capacity)),
capacity,
}
}
pub fn push(&self, record: LogRecord) {
let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
if inner.len() >= self.capacity {
inner.pop_front();
}
inner.push_back(record);
}
pub fn try_push(&self, record: LogRecord) -> bool {
let Some(mut inner) = self.inner.try_lock().ok() else {
return false;
};
if inner.len() >= self.capacity {
inner.pop_front();
}
inner.push_back(record);
true
}
pub fn snapshot(&self) -> Vec<LogRecord> {
self.inner
.lock()
.unwrap_or_else(|e| e.into_inner())
.iter()
.cloned()
.collect()
}
pub fn try_snapshot(&self) -> Option<Vec<LogRecord>> {
let inner = self.inner.try_lock().ok()?;
Some(inner.iter().cloned().collect())
}
pub fn render_trace(&self, limit: usize) -> String {
let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
let start = if inner.len() > limit {
inner.len() - limit
} else {
0
};
let mut out = String::with_capacity(limit * 128);
for record in inner.range(start..) {
out.push_str(&format!(
"[{}] {} {}: {}\n",
record.timestamp_secs, record.level, record.target, record.message
));
}
out
}
pub fn try_render_trace(&self, limit: usize) -> Option<String> {
let inner = self.inner.try_lock().ok()?;
let start = if inner.len() > limit {
inner.len() - limit
} else {
0
};
let mut out = String::with_capacity(limit * 128);
for record in inner.range(start..) {
out.push_str(&format!(
"[{}] {} {}: {}\n",
record.timestamp_secs, record.level, record.target, record.message
));
}
Some(out)
}
pub fn try_render_trace_into(&self, writer: &mut impl std::fmt::Write, limit: usize) -> usize {
let inner = match self.inner.try_lock() {
Ok(guard) => guard,
Err(_) => return 0,
};
let start = if inner.len() > limit {
inner.len() - limit
} else {
0
};
let mut count = 0;
for record in inner.range(start..) {
let _ = writeln!(
writer,
"[{}] {} {}: {}",
record.timestamp_secs, record.level, record.target, record.message
);
count += 1;
}
count
}
pub fn len(&self) -> usize {
self.inner.lock().unwrap_or_else(|e| e.into_inner()).len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn capacity(&self) -> usize {
self.capacity
}
}
static NAM_LOGGER: OnceLock<NamLogger> = OnceLock::new();
fn level_str(level: log::Level) -> &'static str {
match level {
log::Level::Error => "ERROR",
log::Level::Warn => "WARN",
log::Level::Info => "INFO",
log::Level::Debug => "DEBUG",
log::Level::Trace => "TRACE",
}
}
fn format_wall_clock_time() -> String {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
let secs = now.as_secs();
let millis = now.subsec_millis();
let hours = (secs / 3600) % 24;
let minutes = (secs / 60) % 60;
let seconds = secs % 60;
format!("{:02}:{:02}:{:02}.{:03}", hours, minutes, seconds, millis)
}
#[derive(Debug)]
pub struct NamLogger {
buffer: LogBuffer,
sinks: Mutex<Vec<Weak<HostLogFn>>>,
standalone_mode: bool,
max_level: Mutex<LevelFilter>,
}
impl NamLogger {
pub fn init_standalone(level_filter: LevelFilter) -> Result<(), SetLoggerError> {
let logger = NAM_LOGGER.get_or_init(|| NamLogger {
buffer: LogBuffer::default(),
sinks: Mutex::new(Vec::new()),
standalone_mode: true,
max_level: Mutex::new(level_filter),
});
log::set_logger(logger)?;
log::set_max_level(level_filter);
Ok(())
}
pub fn init_plugin(level_filter: LevelFilter) -> Result<&'static NamLogger, SetLoggerError> {
let logger = NAM_LOGGER.get_or_init(|| NamLogger {
buffer: LogBuffer::default(),
sinks: Mutex::new(Vec::new()),
standalone_mode: false,
max_level: Mutex::new(level_filter),
});
match log::set_logger(logger) {
Ok(()) => {
log::set_max_level(level_filter);
Ok(logger)
}
Err(_) => {
log::set_max_level(level_filter);
Ok(logger)
}
}
}
pub fn global() -> Option<&'static NamLogger> {
NAM_LOGGER.get()
}
pub fn log_buffer() -> Option<&'static LogBuffer> {
NAM_LOGGER.get().map(|l| &l.buffer)
}
pub fn register_sink(&self, sink: &Arc<HostLogFn>) {
let mut sinks = self.sinks.lock().unwrap_or_else(|e| e.into_inner());
sinks.push(Arc::downgrade(sink));
}
pub fn set_max_level(&self, level: LevelFilter) {
*self.max_level.lock().unwrap_or_else(|e| e.into_inner()) = level;
log::set_max_level(level);
}
}
impl Log for NamLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
let max = *self.max_level.lock().unwrap_or_else(|e| e.into_inner());
metadata.level() <= max
}
fn log(&self, record: &Record) {
if !self.enabled(record.metadata()) {
return;
}
let level = record.level();
let target = record.target();
let message = record.args().to_string();
let log_record = LogRecord::now(level_str(level), target, &message);
self.buffer.push(log_record);
if self.standalone_mode {
eprintln!(
"{time} {level:5} {target}: {message}",
time = format_wall_clock_time(),
level = level_str(level),
target = record.target(),
message = record.args()
);
}
let mut sinks = self.sinks.lock().unwrap_or_else(|e| e.into_inner());
let severity = level_str(level);
sinks.retain(|weak| {
if let Some(sink) = weak.upgrade() {
sink(severity, &message);
true
} else {
false
}
});
}
fn flush(&self) {}
}
#[cfg(test)]
#[path = "logger_test.rs"]
mod logger_test;