1 //! This module implements logging facilities. 2 //! 3 //! # FFI 4 //! 5 //! [`stracciatella_c_api::c::logger`] contains a C interface for this module. 6 //! 7 //! [`stracciatella_c_api::c::logger`]: ../../stracciatella_c_api/c/logger/index.html 8 9 use std::path::Path; 10 use std::sync::atomic::{AtomicUsize, Ordering}; 11 12 use log::{ 13 logger, set_boxed_logger, set_max_level, Level, LevelFilter, Log, Metadata, MetadataBuilder, 14 Record, 15 }; 16 17 static GLOBAL_LOG_LEVEL: AtomicUsize = AtomicUsize::new(LogLevel::Info as usize); 18 19 #[derive(Debug, PartialEq, Copy, Clone)] 20 #[repr(C)] 21 /// Enum to represent log levels in the application 22 pub enum LogLevel { 23 Error = 0, 24 Warn = 1, 25 Info = 2, 26 Debug = 3, 27 Trace = 4, 28 } 29 30 impl From<LogLevel> for Level { from(other: LogLevel) -> Level31 fn from(other: LogLevel) -> Level { 32 match other { 33 LogLevel::Debug => Level::Debug, 34 LogLevel::Error => Level::Error, 35 LogLevel::Info => Level::Info, 36 LogLevel::Trace => Level::Trace, 37 LogLevel::Warn => Level::Warn, 38 } 39 } 40 } 41 42 impl From<LogLevel> for usize { from(other: LogLevel) -> usize43 fn from(other: LogLevel) -> usize { 44 other as usize 45 } 46 } 47 48 impl From<usize> for LogLevel { from(other: usize) -> LogLevel49 fn from(other: usize) -> LogLevel { 50 match other { 51 0 => LogLevel::Error, 52 1 => LogLevel::Warn, 53 2 => LogLevel::Info, 54 3 => LogLevel::Debug, 55 4 => LogLevel::Trace, 56 _ => panic!("Unexpected log level: {}", other), 57 } 58 } 59 } 60 61 /// Runtime level filter to filter messages based on a global variable 62 /// 63 /// Other log levels should be set to max level in order for the filter 64 /// to work properly 65 struct RuntimeLevelFilter { 66 logger: Box<dyn Log>, 67 } 68 69 impl RuntimeLevelFilter { init(logger: Box<dyn Log>)70 fn init(logger: Box<dyn Log>) { 71 let filter = RuntimeLevelFilter { logger }; 72 73 set_max_level(LevelFilter::max()); 74 if set_boxed_logger(Box::new(filter)).is_err() { 75 log::warn!("Error initializing logger: Logger already set"); 76 } 77 } 78 get_global_log_level() -> Level79 fn get_global_log_level() -> Level { 80 let current_level = GLOBAL_LOG_LEVEL.load(Ordering::Relaxed); 81 LogLevel::from(current_level).into() 82 } 83 } 84 85 impl Log for RuntimeLevelFilter { enabled(&self, metadata: &Metadata) -> bool86 fn enabled(&self, metadata: &Metadata) -> bool { 87 let current_level = Self::get_global_log_level(); 88 metadata.level() <= current_level 89 } 90 log(&self, record: &Record)91 fn log(&self, record: &Record) { 92 if self.enabled(record.metadata()) { 93 self.logger.log(record); 94 } 95 } 96 flush(&self)97 fn flush(&self) { 98 self.logger.flush() 99 } 100 } 101 102 /// Convenience struct to group logging functionality 103 pub struct Logger; 104 105 impl Logger { 106 /// Initializes the logging system 107 /// 108 /// Needs to be called once at start of the game engine. Any log messages send 109 /// before will be discarded. init(log_file: &Path)110 pub fn init(log_file: &Path) { 111 #[cfg(not(target_os = "android"))] 112 { 113 use log::warn; 114 use simplelog::{ 115 CombinedLogger, Config, SharedLogger, SimpleLogger, TermLogger, TerminalMode, 116 WriteLogger, 117 }; 118 use std::fs::File; 119 120 let mut config = Config::default(); 121 config.target = Some(Level::Error); 122 config.thread = None; 123 config.time_format = Some("%FT%T"); 124 let logger: Box<dyn SharedLogger>; 125 126 if let Some(termlogger) = 127 TermLogger::new(LevelFilter::max(), config, TerminalMode::Mixed) 128 { 129 logger = termlogger; 130 } else { 131 logger = SimpleLogger::new(LevelFilter::max(), config); // no colors 132 } 133 134 match File::create(&log_file) { 135 Ok(f) => RuntimeLevelFilter::init(CombinedLogger::new(vec![ 136 logger, 137 WriteLogger::new(LevelFilter::max(), config, f), 138 ])), 139 Err(err) => { 140 RuntimeLevelFilter::init(CombinedLogger::new(vec![logger])); 141 warn!("Failed to log to {:?}: {}", &log_file, err); 142 } 143 } 144 } 145 #[cfg(target_os = "android")] 146 { 147 let config = android_logger::Config::default() 148 .with_min_level(Level::Trace) 149 .with_tag("JA2"); 150 RuntimeLevelFilter::init(Box::new(android_logger::AndroidLogger::new(config))); 151 } 152 } 153 154 /// Sets the global log level to a specific value set_level(level: LogLevel)155 pub fn set_level(level: LogLevel) { 156 GLOBAL_LOG_LEVEL.store(level.into(), Ordering::Relaxed); 157 } 158 159 /// Logs message with specific metadata 160 /// 161 /// Can be used e.g. in C++ or scripting log_with_custom_metadata(level: LogLevel, message: &str, target: &str)162 pub fn log_with_custom_metadata(level: LogLevel, message: &str, target: &str) { 163 let level = level.into(); 164 let logger = logger(); 165 let metadata = MetadataBuilder::new().level(level).target(target).build(); 166 167 if logger.enabled(&metadata) { 168 logger.log( 169 &Record::builder() 170 .metadata(metadata) 171 .args(format_args!("{}", message)) 172 .build(), 173 ); 174 } 175 } 176 } 177