use log::{Level, LevelFilter, Log, Metadata, Record, SetLoggerError};
use std::io::Write;
use crate::{host, memory};
static LOGGER: HostLogger = HostLogger;
const TRUNC_MARKER: &[u8] = b"... [truncated]";
pub struct HostLogger;
impl Log for HostLogger {
#[inline]
fn enabled(&self, metadata: &Metadata) -> bool {
metadata.level() <= log::max_level()
}
fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
memory::with_buffer(|buf| {
let written = format_log_message(buf, record.args());
host::log::write(host_level(record.metadata()), buf.as_subslice(written));
});
}
}
fn flush(&self) {}
}
fn format_log_message(buf: &mut memory::Buffer, args: &std::fmt::Arguments) -> usize {
let capacity = buf.capacity();
let mut slice = buf.as_mut_slice();
let result = write!(slice, "{}", args);
let mut written = capacity - slice.len();
if result.is_err() {
let start = capacity - TRUNC_MARKER.len();
let slice = buf.as_mut_slice();
slice[start..].copy_from_slice(TRUNC_MARKER);
written = buf.capacity();
}
written
}
impl HostLogger {
pub fn init_with_level(level: Level) -> Result<(), SetLoggerError> {
set_global_logger(level)
}
pub fn init() -> Result<(), SetLoggerError> {
set_global_logger(Level::Info)
}
}
fn set_global_logger(level: Level) -> Result<(), SetLoggerError> {
log::set_max_level(max_level(level.to_level_filter()));
log::set_logger(&LOGGER)
}
fn max_level(level_filter: LevelFilter) -> LevelFilter {
max_level_with(level_filter, |level| host::log::enabled(map_to_host(level)))
}
fn max_level_with(mut level_filter: LevelFilter, is_enabled: impl Fn(Level) -> bool) -> LevelFilter {
while let Some(level) = level_filter.to_level() {
if is_enabled(level) {
return level_filter;
}
level_filter = level_filter.decrement_severity();
}
LevelFilter::Off
}
fn map_to_host(level: Level) -> i32 {
match level {
Level::Error => 2,
Level::Warn => 1,
Level::Info => 0,
Level::Debug => -1,
Level::Trace => -2,
}
}
fn host_level(md: &Metadata) -> i32 {
map_to_host(md.level())
}
#[cfg(test)]
mod tests {
use log::MetadataBuilder;
use super::*;
#[test]
fn test_init_with_level() {
let _result = HostLogger::init_with_level(Level::Info);
}
#[test]
fn map_level_to_host() {
assert_eq!(map_to_host(Level::Error), 2);
assert_eq!(map_to_host(Level::Warn), 1);
assert_eq!(map_to_host(Level::Info), 0);
assert_eq!(map_to_host(Level::Debug), -1);
assert_eq!(map_to_host(Level::Trace), -2);
}
#[test]
fn map_host_level() {
assert_eq!(host_level(&MetadataBuilder::new().target("fatal").build()), 0);
assert_eq!(host_level(&MetadataBuilder::new().level(Level::Error).target("").build()), 2);
assert_eq!(host_level(&MetadataBuilder::new().level(Level::Error).target("fatal").build()), 2);
assert_eq!(host_level(&MetadataBuilder::new().level(Level::Error).target("panic").build()), 2);
}
#[test]
fn test_log_truncation_marker() {
let long_msg = "A".repeat(3000);
memory::with_buffer(|buf| {
let written = super::format_log_message(buf, &format_args!("{}", long_msg));
let slice = buf.as_subslice(written);
assert_eq!(slice.len(), buf.capacity(), "Truncated log should fill the buffer");
assert!(slice.ends_with(TRUNC_MARKER), "Log message should end with truncation marker");
});
}
#[test]
fn test_format_log_message() {
let msg = "Test";
memory::with_buffer(|buf| {
let written = super::format_log_message(buf, &format_args!("{}", msg));
assert_eq!(written, msg.len(), "message should not be truncated");
assert_eq!(buf.as_subslice(written), msg.as_bytes());
});
}
#[test]
fn test_format_log_message_limit() {
let msg = "A".repeat(2048);
memory::with_buffer(|buf| {
let written = super::format_log_message(buf, &format_args!("{}", msg));
assert_eq!(written, msg.len(), "message should not be truncated");
assert_eq!(buf.as_subslice(written), msg.as_bytes());
});
}
#[test]
fn host_logger_enabled_within_max_level() {
log::set_max_level(LevelFilter::Info);
let metadata = log::Metadata::builder().level(Level::Info).target("test").build();
assert!(LOGGER.enabled(&metadata));
}
#[test]
fn host_logger_enabled_below_max_level() {
log::set_max_level(LevelFilter::Info);
let metadata = log::Metadata::builder().level(Level::Error).target("test").build();
assert!(LOGGER.enabled(&metadata));
}
#[test]
fn host_logger_disabled_above_max_level() {
log::set_max_level(LevelFilter::Warn);
let metadata = log::Metadata::builder().level(Level::Debug).target("test").build();
assert!(!LOGGER.enabled(&metadata));
}
#[test]
fn host_logger_log_enabled_message() {
log::set_max_level(LevelFilter::Info);
log::info!("test message");
}
#[test]
fn host_logger_log_disabled_message() {
log::set_max_level(LevelFilter::Error);
log::debug!("this should be filtered");
}
#[test]
fn host_logger_flush() {
LOGGER.flush();
}
#[test]
fn test_init_default_level() {
let _result = HostLogger::init();
}
#[test]
fn test_max_level_enabled() {
let level = max_level(LevelFilter::Info);
assert_eq!(level, LevelFilter::Info);
}
#[test]
fn test_max_level_disabled_decrements() {
let level = max_level(LevelFilter::Trace);
assert!(level < LevelFilter::Trace);
}
#[test]
fn test_max_level_off_stays_off() {
assert_eq!(max_level(LevelFilter::Off), LevelFilter::Off);
}
#[test]
fn test_max_level_returns_off_when_host_disables_everything() {
let level = max_level_with(LevelFilter::Trace, |_| false);
assert_eq!(level, LevelFilter::Off);
}
#[test]
fn test_max_level_stops_at_first_enabled_level() {
let level = max_level_with(LevelFilter::Trace, |level| matches!(level, Level::Warn | Level::Error));
assert_eq!(level, LevelFilter::Warn);
}
#[test]
fn host_logger_log_direct_call() {
log::set_max_level(LevelFilter::Info);
let record = log::Record::builder().level(Level::Info).target("test").args(format_args!("direct log test")).build();
LOGGER.log(&record);
}
#[test]
fn host_logger_log_skips_disabled_level() {
log::set_max_level(LevelFilter::Error);
let record =
log::Record::builder().level(Level::Debug).target("test").args(format_args!("this should be skipped")).build();
LOGGER.log(&record);
}
#[test]
fn test_max_level_decrement_until_enabled() {
let level = LevelFilter::Trace;
let result = max_level(level);
assert_eq!(result, LevelFilter::Info, "max_level should decrement to Warn when only Warn is enabled on host");
}
}