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