#![no_std]
extern crate alloc;
#[cfg(feature = "std")]
extern crate std;
use core::sync::atomic::{AtomicU32, AtomicUsize, Ordering, AtomicBool};
use core::cell::UnsafeCell;
use alloc::vec::Vec;
use alloc::string::String;
use alloc::format;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u32)]
pub enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
}
impl LogLevel {
#[inline]
pub const fn as_str(&self) -> &'static str {
match self {
LogLevel::DEBUG => "DEBUG",
LogLevel::INFO => "INFO",
LogLevel::WARN => "WARN",
LogLevel::ERROR => "ERROR",
}
}
}
impl core::fmt::Display for LogLevel {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.as_str())
}
}
impl core::str::FromStr for LogLevel {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_uppercase().as_str() {
"DEBUG" => Ok(LogLevel::DEBUG),
"INFO" => Ok(LogLevel::INFO),
"WARN" => Ok(LogLevel::WARN),
"ERROR" => Ok(LogLevel::ERROR),
_ => Err(format!("Invalid log level: '{}'", s)),
}
}
}
static MINIMAL_LOG_LEVEL: AtomicU32 = AtomicU32::new(if cfg!(debug_assertions) {
LogLevel::DEBUG as u32
} else {
LogLevel::INFO as u32
});
static ONCE: AtomicBool = AtomicBool::new(false);
fn ensure_init() {
if ONCE.load(Ordering::Relaxed) { return; }
if ONCE.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed).is_ok() {
#[cfg(feature = "std")]
{
if let Ok(s) = std::env::var("SCLOG_LEVEL") {
if let Ok(level) = s.parse::<LogLevel>() {
MINIMAL_LOG_LEVEL.store(level as u32, Ordering::Relaxed);
}
}
}
}
}
#[inline]
pub fn set_minimal_log_level(level: LogLevel) {
ONCE.store(true, Ordering::Relaxed);
MINIMAL_LOG_LEVEL.store(level as u32, Ordering::Relaxed);
}
#[inline]
pub fn get_minimal_log_level() -> LogLevel {
ensure_init();
match MINIMAL_LOG_LEVEL.load(Ordering::Relaxed) {
0 => LogLevel::DEBUG,
1 => LogLevel::INFO,
2 => LogLevel::WARN,
3 => LogLevel::ERROR,
_ => unreachable!(),
}
}
#[inline]
pub fn is_enabled(level: LogLevel) -> bool {
ensure_init();
(level as u32) >= MINIMAL_LOG_LEVEL.load(Ordering::Relaxed)
}
pub type LogHandler = fn(level: LogLevel, file: &'static str, line: u32, args: &core::fmt::Arguments);
struct SpinLock<T> {
locked: AtomicBool,
data: UnsafeCell<T>,
}
impl<T> SpinLock<T> {
const fn new(data: T) -> Self {
Self {
locked: AtomicBool::new(false),
data: UnsafeCell::new(data),
}
}
fn lock(&self) -> SpinLockGuard<'_, T> {
while self.locked.compare_exchange_weak(false, true, Ordering::Acquire, Ordering::Relaxed).is_err() {
core::hint::spin_loop();
}
SpinLockGuard {
lock: &self.locked,
data: unsafe { &mut *self.data.get() },
}
}
}
struct SpinLockGuard<'a, T> {
lock: &'a AtomicBool,
data: &'a mut T,
}
impl<T> core::ops::Deref for SpinLockGuard<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target { self.data }
}
impl<T> core::ops::DerefMut for SpinLockGuard<'_, T> {
fn deref_mut(&mut self) -> &mut Self::Target { self.data }
}
impl<T> Drop for SpinLockGuard<'_, T> {
fn drop(&mut self) {
self.lock.store(false, Ordering::Release);
}
}
unsafe impl<T: Send> Sync for SpinLock<T> {}
unsafe impl<T: Send> Send for SpinLock<T> {}
static HANDLER_ID_COUNTER: AtomicUsize = AtomicUsize::new(1);
static LOG_HANDLERS: SpinLock<Vec<(u32, LogHandler)>> = SpinLock::new(Vec::new());
pub fn hook_log_handler(handler: LogHandler) -> u32 {
let id = HANDLER_ID_COUNTER.fetch_add(1, Ordering::Relaxed) as u32;
LOG_HANDLERS.lock().push((id, handler));
id
}
pub fn unhook_log_handler(id: u32) -> bool {
let mut handlers = LOG_HANDLERS.lock();
if let Some(pos) = handlers.iter().position(|(hid, _)| *hid == id) {
handlers.remove(pos);
true
} else {
false
}
}
pub fn clear_log_handlers() {
LOG_HANDLERS.lock().clear();
}
pub fn handler_count() -> usize {
LOG_HANDLERS.lock().len()
}
#[doc(hidden)]
#[inline]
pub fn __log_impl(level: LogLevel, args: core::fmt::Arguments, file: &'static str, line: u32) {
if (level as u32) < MINIMAL_LOG_LEVEL.load(Ordering::Relaxed) {
return;
}
let handlers = LOG_HANDLERS.lock();
if handlers.is_empty() {
return;
}
for (_, handler) in handlers.iter() {
handler(level, file, line, &args);
}
}
#[macro_export]
macro_rules! log {
($level:expr, $($arg:tt)*) => {{
$crate::__log_impl($level, format_args!($($arg)*), file!(), line!());
}};
}
#[macro_export]
macro_rules! log_panic {
($($arg:tt)*) => {{
let args = format_args!($($arg)*);
$crate::__log_impl($crate::LogLevel::ERROR, args, file!(), line!());
panic!("{}", args);
}};
}
#[macro_export]
macro_rules! log_abort {
($($arg:tt)*) => {{
let args = format_args!($($arg)*);
$crate::__log_impl($crate::LogLevel::ERROR, args, file!(), line!());
#[cfg(feature = "std")]
std::process::abort();
#[cfg(not(feature = "std"))]
panic!("Abort: {}", args);
}};
}
macro_rules! gen_log_macros {
($( $name:ident => $level:ident ),* $(,)? ) => {
gen_log_macros!(@internal $($name => $level),* ; $);
};
(@internal $( $name:ident => $level:ident ),* ; $d:tt) => {
$(
#[macro_export]
macro_rules! $name {
($d($d arg:tt)*) => {
{
if $crate::is_enabled($crate::LogLevel::$level) {
$crate::log!($crate::LogLevel::$level, $d($d arg)*);
}
}
};
}
)*
};
}
gen_log_macros!(
log_debug => DEBUG,
log_info => INFO,
log_warn => WARN,
log_error => ERROR,
);
pub trait LogUnwrap<T> {
#[track_caller]
fn log_unwrap(self) -> T;
#[track_caller]
fn log_expect(self, msg: &str) -> T;
#[track_caller]
fn log_unwrap_or(self, default: T) -> T;
#[track_caller]
fn log_unwrap_or_else<F: FnOnce() -> T>(self, f: F) -> T;
#[track_caller]
fn log_unwrap_or_default(self) -> T where T: Default;
#[track_caller]
#[must_use]
fn log_err(self) -> Self where Self: Sized;
#[track_caller]
#[must_use]
fn log_warn_err(self) -> Self where Self: Sized;
#[track_caller]
#[must_use]
fn log_err_at(self, level: LogLevel) -> Self where Self: Sized;
}
impl<T, E: core::fmt::Debug> LogUnwrap<T> for Result<T, E> {
#[track_caller]
fn log_unwrap(self) -> T {
self.log_expect("called log_unwrap on an Err value")
}
#[track_caller]
fn log_expect(self, msg: &str) -> T {
match self {
Ok(val) => val,
Err(e) => {
let loc = core::panic::Location::caller();
__log_impl(LogLevel::ERROR, format_args!("{}: {:?}", msg, e), loc.file(), loc.line());
panic!("{}: {:?}", msg, e);
}
}
}
#[track_caller]
fn log_unwrap_or(self, default: T) -> T {
match self {
Ok(val) => val,
Err(e) => {
let loc = core::panic::Location::caller();
__log_impl(LogLevel::ERROR, format_args!("called log_unwrap_or on an Err value: {:?}", e), loc.file(), loc.line());
default
}
}
}
#[track_caller]
fn log_unwrap_or_else<F: FnOnce() -> T>(self, f: F) -> T {
match self {
Ok(val) => val,
Err(e) => {
let loc = core::panic::Location::caller();
__log_impl(LogLevel::ERROR, format_args!("called log_unwrap_or_else on an Err value: {:?}", e), loc.file(), loc.line());
f()
}
}
}
#[track_caller]
fn log_unwrap_or_default(self) -> T where T: Default {
match self {
Ok(val) => val,
Err(e) => {
let loc = core::panic::Location::caller();
__log_impl(LogLevel::ERROR, format_args!("called log_unwrap_or_default on an Err value: {:?}", e), loc.file(), loc.line());
T::default()
}
}
}
#[track_caller]
fn log_err(self) -> Self {
if let Err(ref e) = self {
let loc = core::panic::Location::caller();
__log_impl(LogLevel::ERROR, format_args!("Error: {:?}", e), loc.file(), loc.line());
}
self
}
#[track_caller]
fn log_warn_err(self) -> Self {
if let Err(ref e) = self {
let loc = core::panic::Location::caller();
__log_impl(LogLevel::WARN, format_args!("Error: {:?}", e), loc.file(), loc.line());
}
self
}
#[track_caller]
fn log_err_at(self, level: LogLevel) -> Self {
if let Err(ref e) = self {
let loc = core::panic::Location::caller();
__log_impl(level, format_args!("Error: {:?}", e), loc.file(), loc.line());
}
self
}
}
impl<T> LogUnwrap<T> for Option<T> {
#[track_caller]
fn log_unwrap(self) -> T {
self.log_expect("called log_unwrap on a None value")
}
#[track_caller]
fn log_expect(self, msg: &str) -> T {
match self {
Some(val) => val,
None => {
let loc = core::panic::Location::caller();
__log_impl(LogLevel::ERROR, format_args!("{}", msg), loc.file(), loc.line());
panic!("{}", msg);
}
}
}
#[track_caller]
fn log_unwrap_or(self, default: T) -> T {
if self.is_none() {
let loc = core::panic::Location::caller();
__log_impl(LogLevel::ERROR, format_args!("called log_unwrap_or on a None value"), loc.file(), loc.line());
}
self.unwrap_or(default)
}
#[track_caller]
fn log_unwrap_or_else<F: FnOnce() -> T>(self, f: F) -> T {
if self.is_none() {
let loc = core::panic::Location::caller();
__log_impl(LogLevel::ERROR, format_args!("called log_unwrap_or_else on a None value"), loc.file(), loc.line());
}
self.unwrap_or_else(f)
}
#[track_caller]
fn log_unwrap_or_default(self) -> T where T: Default {
if self.is_none() {
let loc = core::panic::Location::caller();
__log_impl(LogLevel::ERROR, format_args!("called log_unwrap_or_default on a None value"), loc.file(), loc.line());
}
self.unwrap_or_default()
}
#[track_caller]
fn log_err(self) -> Self {
if self.is_none() {
let loc = core::panic::Location::caller();
__log_impl(LogLevel::ERROR, format_args!("Value is None"), loc.file(), loc.line());
}
self
}
#[track_caller]
fn log_warn_err(self) -> Self {
if self.is_none() {
let loc = core::panic::Location::caller();
__log_impl(LogLevel::WARN, format_args!("Value is None"), loc.file(), loc.line());
}
self
}
#[track_caller]
fn log_err_at(self, level: LogLevel) -> Self {
if self.is_none() {
let loc = core::panic::Location::caller();
__log_impl(level, format_args!("Value is None"), loc.file(), loc.line());
}
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
#[test]
fn test_log_level_filtering() {
set_minimal_log_level(LogLevel::WARN);
assert!(!is_enabled(LogLevel::DEBUG));
assert!(!is_enabled(LogLevel::INFO));
assert!(is_enabled(LogLevel::WARN));
assert!(is_enabled(LogLevel::ERROR));
}
#[test]
fn test_handler_registration() {
fn dummy_handler(_level: LogLevel, _file: &'static str, _line: u32, _args: &core::fmt::Arguments) {}
let id = hook_log_handler(dummy_handler);
assert!(unhook_log_handler(id));
assert!(!unhook_log_handler(id));
}
#[test]
fn test_concurrent_logging() {
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = counter.clone();
fn test_handler(_level: LogLevel, _file: &'static str, _line: u32, _args: &core::fmt::Arguments) {}
let _id = hook_log_handler(test_handler);
let handles: Vec<_> = (0..10)
.map(|_| {
let c = counter_clone.clone();
std::thread::spawn(move || {
for _ in 0..100 {
log!(LogLevel::INFO, "test");
c.fetch_add(1, Ordering::Relaxed);
}
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
assert_eq!(counter.load(Ordering::Relaxed), 1000);
}
#[test]
fn test_log_level_parsing() {
use core::str::FromStr;
assert_eq!(LogLevel::from_str("DEBUG").unwrap(), LogLevel::DEBUG);
assert_eq!(LogLevel::from_str("info").unwrap(), LogLevel::INFO);
assert_eq!(LogLevel::from_str("warn").unwrap(), LogLevel::WARN);
assert_eq!(LogLevel::from_str("Error").unwrap(), LogLevel::ERROR);
assert!(LogLevel::from_str("invalid").is_err());
}
#[test]
fn test_log_location() {
set_minimal_log_level(LogLevel::DEBUG);
clear_log_handlers();
static LAST_MSG: SpinLock<String> = SpinLock::new(String::new());
fn test_handler(_level: LogLevel, file: &'static str, line: u32, args: &core::fmt::Arguments) {
let mut m = LAST_MSG.lock();
*m = format!("[{}:{}] {}", file, line, args);
}
hook_log_handler(test_handler);
log_info!("test location");
let msg = LAST_MSG.lock().clone();
assert!(msg.contains("lib.rs:"));
assert!(msg.contains("test location"));
}
#[test]
#[should_panic(expected = "panic message")]
fn test_log_panic_macro() {
clear_log_handlers();
log_panic!("panic message");
}
#[test]
fn test_log_unwrap_extensions() {
set_minimal_log_level(LogLevel::DEBUG);
static COUNTER: AtomicUsize = AtomicUsize::new(0);
fn test_handler(_level: LogLevel, _file: &'static str, _line: u32, _args: &core::fmt::Arguments) {
COUNTER.fetch_add(1, Ordering::Relaxed);
}
hook_log_handler(test_handler);
let res_ok: Result<i32, &str> = Ok(10);
assert_eq!(res_ok.log_unwrap_or(0), 10);
assert_eq!(COUNTER.load(Ordering::Relaxed), 0);
let res_err: Result<i32, &str> = Err("error");
assert_eq!(res_err.log_unwrap_or(5), 5);
assert_eq!(COUNTER.load(Ordering::Relaxed), 1);
assert_eq!(Ok::<i32, &str>(10).log_unwrap_or_else(|| 0), 10);
assert_eq!(Err::<i32, &str>("error").log_unwrap_or_else(|| 5), 5);
assert_eq!(COUNTER.load(Ordering::Relaxed), 2);
assert_eq!(Ok::<i32, &str>(10).log_unwrap_or_default(), 10);
assert_eq!(Err::<i32, &str>("error").log_unwrap_or_default(), 0);
assert_eq!(COUNTER.load(Ordering::Relaxed), 3);
let opt_some = Some(10);
assert_eq!(opt_some.log_unwrap_or(0), 10);
assert_eq!(COUNTER.load(Ordering::Relaxed), 3);
let opt_none: Option<i32> = None;
assert_eq!(opt_none.log_unwrap_or(5), 5);
assert_eq!(COUNTER.load(Ordering::Relaxed), 4);
let res_err: Result<i32, &str> = Err("error");
let res_err = res_err.log_err();
assert!(res_err.is_err());
assert_eq!(COUNTER.load(Ordering::Relaxed), 5);
let opt_none: Option<i32> = None;
let opt_none = opt_none.log_warn_err();
assert!(opt_none.is_none());
assert_eq!(COUNTER.load(Ordering::Relaxed), 6);
let opt_none: Option<i32> = None;
set_minimal_log_level(LogLevel::DEBUG);
let opt_none = opt_none.log_err_at(LogLevel::DEBUG);
assert!(opt_none.is_none());
assert_eq!(COUNTER.load(Ordering::Relaxed), 7);
}
}