use std::{
collections::HashMap,
ffi::CString,
sync::{Mutex, OnceLock},
};
use crate::{rcl_bindings::*, ENTITY_LIFECYCLE_MUTEX};
mod logging_configuration;
pub(crate) use logging_configuration::*;
mod log_params;
pub use log_params::*;
mod logger;
pub use logger::*;
#[macro_export]
macro_rules! log {
($to_log_params: expr, $($args:tt)*) => {{
use std::sync::{Once, OnceLock, Mutex};
use std::time::{SystemTime, Instant};
(|| {
let params = $crate::ToLogParams::to_log_params($to_log_params);
if !params.get_user_filter() {
return;
}
let mut first_time = false;
static REMEMBER_FIRST_TIME: Once = Once::new();
REMEMBER_FIRST_TIME.call_once(|| first_time = true);
let logger_name = params.get_logger_name();
let severity = params.get_severity();
match params.get_occurence() {
$crate::LogOccurrence::Once => {
if first_time {
$crate::log_unconditional!(severity, logger_name, $($args)*);
}
return;
}
$crate::LogOccurrence::SkipFirst => {
if first_time {
return;
}
}
$crate::LogOccurrence::All => (),
}
let throttle = params.get_throttle();
if throttle > std::time::Duration::ZERO {
match params.get_throttle_clock() {
$crate::ThrottleClock::SteadyTime => {
static LAST_LOG_STEADY_TIME: OnceLock<Mutex<Instant>> = OnceLock::new();
let last_log_time = LAST_LOG_STEADY_TIME.get_or_init(|| {
Mutex::new(Instant::now())
});
if !first_time {
let now = Instant::now();
let mut previous = last_log_time.lock().unwrap();
if now >= *previous + throttle {
*previous = now;
} else {
return;
}
}
}
$crate::ThrottleClock::SystemTime => {
static LAST_LOG_SYSTEM_TIME: OnceLock<Mutex<SystemTime>> = OnceLock::new();
let last_log_time = LAST_LOG_SYSTEM_TIME.get_or_init(|| {
Mutex::new(SystemTime::now())
});
if !first_time {
let now = SystemTime::now();
let mut previous = last_log_time.lock().unwrap();
if now >= *previous + throttle {
*previous = now;
} else {
return;
}
}
}
$crate::ThrottleClock::Clock(clock) => {
static LAST_LOG_CLOCK_TIME: OnceLock<Mutex<$crate::Time>> = OnceLock::new();
let last_log_time = LAST_LOG_CLOCK_TIME.get_or_init(|| {
Mutex::new(clock.now())
});
if !first_time {
let now = clock.now();
let mut previous = last_log_time.lock().unwrap();
let new_interval = !now.compare_with(
&(previous.clone() + throttle),
|now, interval| now < interval,
)
.is_some_and(|eval| eval);
if new_interval {
*previous = now;
} else {
return;
}
}
}
}
}
$crate::log_unconditional!(severity, logger_name, $($args)*);
})();
}};
}
#[macro_export]
macro_rules! log_debug {
($to_log_params: expr, $($args:tt)*) => {{
let log_params = $crate::ToLogParams::to_log_params($to_log_params);
$crate::log!($crate::ToLogParams::debug(log_params), $($args)*);
}}
}
#[macro_export]
macro_rules! log_info {
($to_log_params: expr, $($args:tt)*) => {{
let log_params = $crate::ToLogParams::to_log_params($to_log_params);
$crate::log!($crate::ToLogParams::info(log_params), $($args)*);
}}
}
#[macro_export]
macro_rules! log_warn {
($to_log_params: expr, $($args:tt)*) => {{
let log_params = $crate::ToLogParams::to_log_params($to_log_params);
$crate::log!($crate::ToLogParams::warn(log_params), $($args)*);
}}
}
#[macro_export]
macro_rules! log_error {
($to_log_params: expr, $($args:tt)*) => {{
let log_params = $crate::ToLogParams::to_log_params($to_log_params);
$crate::log!($crate::ToLogParams::error(log_params), $($args)*);
}}
}
#[macro_export]
macro_rules! log_fatal {
($to_log_params: expr, $($args:tt)*) => {{
let log_params = $crate::ToLogParams::to_log_params($to_log_params);
$crate::log!($crate::ToLogParams::fatal(log_params), $($args)*);
}}
}
#[doc(hidden)]
#[macro_export]
macro_rules! log_unconditional {
($severity: expr, $logger_name: expr, $($args:tt)*) => {{
use std::{ffi::CString, sync::OnceLock};
static FUNCTION_NAME: OnceLock<CString> = OnceLock::new();
let function_name = FUNCTION_NAME.get_or_init(|| {
let function_name = $crate::function!()
.strip_suffix("::{{closure}}::{{closure}}")
.unwrap();
CString::new(function_name).unwrap_or(
CString::new("<invalid name>").unwrap()
)
});
static FILE_NAME: OnceLock<CString> = OnceLock::new();
let file_name = FILE_NAME.get_or_init(|| {
CString::new(file!()).unwrap_or(
CString::new("<invalid name>").unwrap()
)
});
match CString::new(std::fmt::format(format_args!($($args)*))) {
Ok(message) => {
unsafe { $crate::impl_log($severity, $logger_name, &message, &function_name, &file_name, line!()) };
}
Err(err) => {
let message = CString::new(format!(
"Unable to format log message into a valid c-string. Error: {}", err
)).unwrap();
unsafe {
$crate::impl_log(
$crate::LogSeverity::Error,
&$crate::LoggerName::Unvalidated("logger"),
&message,
&function_name,
&file_name,
line!(),
);
}
}
}
}}
}
#[doc(hidden)]
pub unsafe fn impl_log(
severity: LogSeverity,
logger_name: &LoggerName,
message: &CString,
function: &CString,
file: &CString,
line: u32,
) {
let send_log = |severity: LogSeverity, logger_name: &CString, message: &CString| {
let location = rcutils_log_location_t {
function_name: function.as_ptr(),
file_name: file.as_ptr(),
line_number: line as usize,
};
static FORMAT_STRING: OnceLock<CString> = OnceLock::new();
let format_string = FORMAT_STRING.get_or_init(|| CString::new("%s").unwrap());
let severity = severity.as_native();
let _lifecycle = ENTITY_LIFECYCLE_MUTEX.lock().unwrap();
#[cfg(test)]
{
if log_handler::is_using_custom_handler() {
unsafe {
rcutils_log(
&location,
severity as i32,
logger_name.as_ptr(),
message.as_ptr(),
);
}
} else {
unsafe {
rcutils_log(
&location,
severity as i32,
logger_name.as_ptr(),
format_string.as_ptr(),
message.as_ptr(),
);
}
}
}
#[cfg(not(test))]
{
unsafe {
rcutils_log(
&location,
severity as i32,
logger_name.as_ptr(),
format_string.as_ptr(),
message.as_ptr(),
);
}
}
};
match logger_name {
LoggerName::Validated(c_name) => {
send_log(severity, c_name, message);
}
LoggerName::Unvalidated(str_name) => {
static NAME_MAP: OnceLock<Mutex<HashMap<String, CString>>> = OnceLock::new();
let name_map = NAME_MAP.get_or_init(Default::default);
{
let name_map_guard = name_map.lock().unwrap();
if let Some(c_name) = name_map_guard.get(*str_name) {
send_log(severity, c_name, message);
return;
}
}
let c_name = match CString::new(str_name.to_string()) {
Ok(c_name) => c_name,
Err(_) => {
static INVALID_MSG: OnceLock<CString> = OnceLock::new();
let invalid_msg = INVALID_MSG.get_or_init(|| {
CString::new(
"Failed to convert logger name into a c-string. \
Check for null terminators inside the string.",
)
.unwrap()
});
static INTERNAL_LOGGER_NAME: OnceLock<CString> = OnceLock::new();
let internal_logger_name =
INTERNAL_LOGGER_NAME.get_or_init(|| CString::new("logger").unwrap());
send_log(severity, internal_logger_name, invalid_msg);
return;
}
};
send_log(severity, &c_name, message);
name_map
.lock()
.unwrap()
.insert(str_name.to_string(), c_name);
}
}
}
#[macro_export]
#[doc(hidden)]
macro_rules! function {
() => {{
fn f() {}
fn type_name_of<T>(_: T) -> &'static str {
std::any::type_name::<T>()
}
let name = type_name_of(f);
name.strip_suffix("::f").unwrap()
}};
}
#[cfg(test)]
mod tests {
use crate::{log_handler::*, test_helpers::*, *};
use std::{
sync::{Arc, Mutex},
time::Duration,
};
#[test]
fn test_logging_macros() -> Result<(), RclrsError> {
log!("please do not crash", "%n");
let graph = construct_test_graph("test_logging_macros")?;
let log_collection: Arc<Mutex<Vec<LogEntry<'static>>>> = Arc::new(Mutex::new(Vec::new()));
let inner_log_collection = log_collection.clone();
log_handler::set_logging_output_handler(move |log_entry: log_handler::LogEntry| {
inner_log_collection
.lock()
.unwrap()
.push(log_entry.into_owned());
})
.unwrap();
let last_logger_name = || {
log_collection
.lock()
.unwrap()
.last()
.unwrap()
.logger_name
.clone()
};
let last_message = || {
log_collection
.lock()
.unwrap()
.last()
.unwrap()
.message
.clone()
};
let last_location = || {
log_collection
.lock()
.unwrap()
.last()
.unwrap()
.location
.clone()
};
let last_severity = || log_collection.lock().unwrap().last().unwrap().severity;
let count_message = |message: &str| {
let mut count = 0;
for log in log_collection.lock().unwrap().iter() {
if log.message == message {
count += 1;
}
}
count
};
let node = graph.node1;
log!(&*node, "Logging with node dereference");
assert_eq!(last_logger_name(), node.logger().name());
assert_eq!(last_message(), "Logging with node dereference");
assert_eq!(last_severity(), LogSeverity::Info);
assert_eq!(
last_location().function_name,
"rclrs::logging::tests::test_logging_macros",
);
for _ in 0..10 {
log!(node.once(), "Logging once");
}
assert_eq!(count_message("Logging once"), 1);
assert_eq!(last_severity(), LogSeverity::Info);
log!(node.logger(), "Logging with node logger");
assert_eq!(last_message(), "Logging with node logger");
assert_eq!(last_severity(), LogSeverity::Info);
log!(node.debug(), "Debug from node");
assert_ne!(last_message(), "Debug from node");
assert_ne!(last_severity(), LogSeverity::Debug);
log!(node.info(), "Info from node");
assert_eq!(last_message(), "Info from node");
assert_eq!(last_severity(), LogSeverity::Info);
log!(node.warn(), "Warn from node");
assert_eq!(last_message(), "Warn from node");
assert_eq!(last_severity(), LogSeverity::Warn);
log!(node.error(), "Error from node");
assert_eq!(last_message(), "Error from node");
assert_eq!(last_severity(), LogSeverity::Error);
log!(node.fatal(), "Fatal from node");
assert_eq!(last_message(), "Fatal from node");
assert_eq!(last_severity(), LogSeverity::Fatal);
log_debug!(node.logger(), "log_debug macro");
log_info!(node.logger(), "log_info macro");
log_warn!(node.logger(), "log_warn macro");
log_error!(node.logger(), "log_error macro");
log_fatal!(node.logger(), "log_fatal macro");
log!(node.only_if(false), "This should not be logged",);
log!(node.only_if(true), "This should be logged",);
for i in 0..3 {
log!(node.warn().skip_first(), "Formatted warning #{}", i);
}
assert_eq!(count_message("Formatted warning #0"), 0);
assert_eq!(count_message("Formatted warning #1"), 1);
assert_eq!(count_message("Formatted warning #2"), 1);
node.logger().set_level(LogSeverity::Debug).unwrap();
log_debug!(node.logger(), "This debug message appears");
assert_eq!(last_message(), "This debug message appears");
assert_eq!(last_severity(), LogSeverity::Debug);
node.logger().set_level(LogSeverity::Info).unwrap();
log_debug!(node.logger(), "This debug message does not appear");
assert_ne!(last_message(), "This debug message does not appear");
log!("custom logger name", "message for custom logger");
assert_eq!(last_logger_name(), "custom logger name");
assert_eq!(last_message(), "message for custom logger");
for _ in 0..3 {
log!(
"custom logger name once".once(),
"one-time message for custom logger",
);
}
assert_eq!(last_logger_name(), "custom logger name once");
assert_eq!(last_severity(), LogSeverity::Info);
assert_eq!(count_message("one-time message for custom logger"), 1);
for _ in 0..3 {
log!(
"custom logger name skip".error().skip_first(),
"error for custom logger",
);
}
assert_eq!(last_logger_name(), "custom logger name skip");
assert_eq!(last_severity(), LogSeverity::Error);
assert_eq!(count_message("error for custom logger"), 2);
let (clock, source) = Clock::with_source();
source.set_ros_time_override(0);
for i in 0..15 {
log!(
"logger"
.throttle(Duration::from_nanos(10))
.throttle_clock(ThrottleClock::Clock(&clock)),
"custom clock throttled message",
);
source.set_ros_time_override(i);
}
assert_eq!(count_message("custom clock throttled message"), 2);
reset_logging_output_handler();
Ok(())
}
#[test]
fn test_function_macro() {
assert_eq!(function!(), "rclrs::logging::tests::test_function_macro");
}
}