bt_logger/lib.rs
1/// A simple and lightweight logger for Rust with various features such as logging to different output destinations (stdout, stderr), formatting log messages, and level checking.
2///
3/// LogLevel
4/// Represents the different levels of log severity. The values are ordered from lowest to highest severity.
5/// NONE = No logging,
6/// FATAL = Fatal error,
7/// ERROR = Error message,
8/// WARN = Warning message,
9/// INFO = Informational message,
10/// DEBUG = Debugging message,
11/// TRACE = Trace message,
12/// VERBOSE = Very verbose message
13///
14/// LogTarget
15/// Represents the different output destinations for log messages.
16/// STD_ERROR: Logs to stderr
17/// STD_OUT: Logs to stdout
18/// STD_BOTH: Logs to both stdout and stderr
19///
20/// Macros
21/// bt_logger provides several macros for logging different levels of messages. Each macro:
22/// - Checks if the log level is sufficient to log the message.
23/// - If sufficient, formats the message using get_formatted_msg.
24/// - Calls log_msg with the formatted message.
25///
26/// log_fatal!(function name, message, message arguments)
27/// log_error!(function name, message, message arguments)
28/// log_warning!(function name, message, message arguments)
29/// log_info!(function name, message, message arguments)
30/// log_debug!(function name, message, message arguments)
31/// log_trace!(function name, message, message arguments)
32/// log_verbose!(function name, message, message arguments)
33/// get_fatal!(function name, message, message arguments)
34/// get_error!(function name, message, message arguments)
35///
36/// Usage
37/// To use the bt_logger module, you would create a logger instance with the desired configuration and then use the macros to log messages at different levels. For example:
38/// build_logger("BACHUETECH", "My Application", LogLevel::INFO, LogTarget::StdOut);
39/// .......
40/// log_info!("function_name","Hello, {}", "Bachuetech User");
41///
42/// This code builds a logger instance with the tag BACHUETECH, application name My Application, and log level INFO. It then uses the log_info! macro to log a message to stdout.
43/// If the logger is not create the application will stop with the first call of a loggin macro
44/// build the logger as early as possible to avoid issue using the build_logger
45///
46/// The log output is:
47/// TAG APPLICATION CURRENT_UTC_TIME(YYYY-mm-ddTHH:MM:SS.3fz) level(one capital letter) source(module path::function)|>|message
48
49 pub(crate) mod io_utils;
50
51 use std::path::PathBuf;
52 use std::str::FromStr;
53 use std::{env, fmt, fs};
54 use chrono::prelude::*;
55 use once_cell::sync::{Lazy, OnceCell};
56 use tokio::runtime::Runtime;
57
58 use crate::io_utils::log_to_file;
59
60 #[macro_use]
61 pub mod macros;
62
63
64const LOG_ENV_VAR_LOG_LEVEL: &str = "btlogger_log_level";
65const LOG_ENV_VAR_LOG_OUTPUT: &str = "btlogger_log_output";
66
67/// Create a static runtime
68static ASYNC_RUNTIME: Lazy<Runtime> = Lazy::new(|| {
69 Runtime::new().expect("Failed to create Tokio runtime")
70});
71
72static LOGGER: OnceCell<Logger> = OnceCell::new();
73
74/// LogLevel
75/// Represents the different levels of log severity. The values are ordered from lowest to highest severity.
76/// NONE = No logging,
77/// FATAL = Fatal error,
78/// ERROR = Error message,
79/// WARN = Warning message,
80/// INFO = Informational message,
81/// DEBUG = Debugging message,
82/// TRACE = Trace message,
83/// VERBOSE = Very verbose message
84 #[repr(u8)]
85 #[derive(Clone)]
86 pub enum LogLevel{
87 NONE = 100,
88 FATAL = 60,
89 ERROR = 50,
90 WARN = 40,
91 INFO = 30,
92 DEBUG = 20,
93 TRACE = 10,
94 VERBOSE = 0,
95 }
96
97 impl fmt::Display for LogLevel{
98 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
99 match *self{
100 LogLevel::NONE => write!(f, "N"),
101 LogLevel::FATAL => write!(f, "F"),
102 LogLevel::ERROR => write!(f, "E"),
103 LogLevel::WARN => write!(f, "W"),
104 LogLevel::INFO => write!(f, "I"),
105 LogLevel::DEBUG => write!(f, "D"),
106 LogLevel::TRACE => write!(f, "T"),
107 LogLevel::VERBOSE => write!(f, "V"),
108 }
109 }
110 }
111
112 impl LogLevel {
113 fn from_str(log_level: &str) -> LogLevel{
114 match log_level.to_uppercase().as_str(){
115 "N" | "NONE" => LogLevel::NONE,
116 "F" | "FATAL" => LogLevel::FATAL,
117 "E" | "ERR" | "ERROR" => LogLevel::ERROR,
118 "W" | "WARN" | "WARNING"=> LogLevel::WARN,
119 "I" | "INFO" => LogLevel::INFO,
120 "D" | "DEBUG" => LogLevel::DEBUG,
121 "T" | "TRACE" => LogLevel::TRACE,
122 "V" | "VERB" | "VERBOSE"=> LogLevel::VERBOSE,
123 _ => LogLevel::NONE,
124 }
125 }
126
127 }
128/// LogTarget
129/// Represents the different output destinations for log messages.
130/// STD_ERROR: Logs to stderr
131/// STD_OUT: Logs to stdout
132/// STD_BOTH: Logs to both stdout and stderr
133/// NONE: Does not send log to any of the STD.
134 #[allow(non_camel_case_types)]
135 #[derive(Debug, Clone)]
136 pub enum LogTarget{
137 STD_ERROR,
138 STD_OUT,
139 STD_BOTH,
140 NONE,
141 }
142
143 impl LogTarget {
144 pub fn from_str(log_destination: &str) -> LogTarget{
145 match log_destination.to_uppercase().as_str(){
146 "ERR" | "ERROR" | "STDERR" | "E" => LogTarget::STD_ERROR,
147 "STANDARD" | "STD" | "STDOUT" | "S" | "O" => LogTarget::STD_OUT,
148 "NONE" | "N" => LogTarget::NONE,
149 _ => LogTarget::STD_ERROR
150 }
151 }
152 }
153/// Logger
154/// Represents a logger instance with its configuration.
155/// log_tag: The tag used in log messages
156/// log_app: The application name used in log messages
157/// current_log_level_value: The current log level value (0-100)
158/// output_destination: The output destination for log messages (StdError, StdOut, or StdBoth)
159 #[derive(Clone)]
160 pub struct Logger{ //ToDo, check if this can be private
161 log_tag: String,
162 log_app: String,
163 current_log_level_value: u8,
164 output_destination: LogTarget,
165 destination_file: Option<String>,
166 }
167
168 impl Logger{
169 ///Creates a new logger instance with the given configuration.
170 ///If file cannot be open then it will be ignored as None.
171 fn new(tag: &str, application: &str, level: LogLevel, output:LogTarget, destination_file: Option<String>) -> Self{
172 let dest = if destination_file.is_some() {
173 match PathBuf::from_str(&destination_file.unwrap()){
174 Ok(file_path) => {
175 //if let Some(fp) = file_path.to_str(){
176 if let Some(parent) = file_path.parent() {
177 if fs::create_dir_all(parent).is_err(){ None } else { let p: String = file_path.to_string_lossy().into(); Some(p)}
178 }else{ None }
179 },
180 Err(_) => None,
181 }
182 }else {None};
183
184 Logger { log_tag: tag.to_owned(), log_app: application.to_owned(),
185 current_log_level_value: level as u8, output_destination: output,
186 destination_file: dest.into() }
187 }
188
189 ///Returns the current time as a string in the format "YYYY-MM-DDTHH:MM:SS.SSSZ".
190 pub(crate) fn get_current_time() -> String{
191 let current_time: DateTime<Utc> = Utc::now();
192 // Format the current time as a string
193 current_time.format("%Y-%m-%dT%H:%M:%S%.3f%z").to_string()
194 }
195
196 ///Formats a log message with the given level, source, and message.
197 fn get_formatted_msg(&self, level: LogLevel, source: &str, msg: &String) -> String{
198 let current_time = Logger::get_current_time();
199 format!("{} {} {} {} {}|>|{}", self.log_tag, self.log_app, current_time, level, source, msg)
200 }
201
202 ///Logs a message with the given level, message, and module and function name.
203 pub fn log_msg(&self, msg: &String, level: LogLevel, module: &str, function: &str){ //source: &str){
204 let log_msg = self.get_formatted_msg(level, &format!("{}::{}",module, function), msg);
205 let output_dest = self.output_destination.clone();
206 let dest_file = self.destination_file.clone();
207
208 ASYNC_RUNTIME.spawn(async move{
209 match output_dest {
210 LogTarget::STD_ERROR =>{log_stderr(&log_msg);},
211 LogTarget::STD_OUT => {log_stdout(&log_msg);},
212 LogTarget::STD_BOTH => {log_stderr(&log_msg);
213 log_stdout(&log_msg); },
214 LogTarget::NONE => (),
215 }
216 if let Some(df) = dest_file {
217 log_to_file(df.as_str(),log_msg.as_str());
218 }
219 });
220 }
221
222 ///Get formatted message with the given level, message, and module and function name.
223 pub fn get_msg(&self, msg: &String, level: LogLevel, module: &str, function: &str) -> String { //source: &str){
224 let log_msg = self.get_formatted_msg(level, &format!("{}::{}",module, function), msg);
225 log_msg
226 }
227
228 //Check if a particular log_level has to be logged based on the currect configuration
229 pub fn log_this(&self, log_level: LogLevel) -> bool{
230 if log_level as u8 >= self.current_log_level_value {
231 return true
232 }
233 false
234 }
235 }
236
237 /*lazy_static! {
238 static ref LOGGER: Mutex<Option<Logger>> = Mutex::new(None);
239 }*/
240
241 ///Logs a message to stdout.
242 fn log_stdout(message: &String ){
243 println!("{}", message);
244 }
245
246 ///Logs a message to stderr.
247 fn log_stderr(message: &String){
248 eprintln!("{}", message);
249 }
250
251 ///Build the logger. Run this function first
252/// Configures a global logger instance based on the provided environment variables and options.
253///
254/// This function takes in a tag, an application name, a log level, and a log target (output), and optional destination file.
255///
256/// # Arguments
257///
258/// * `tag`: The tag or identifier for the logger.
259/// * `application`: The application name associated with the logger.
260/// * `level`: An enum log level to use for this logger instance.
261/// * `output`: An enum target to output logs to (e.g. standard error, file, etc.).
262/// * `path_file`: Optional Absolute path to the file to log as String. If path is invalid or file cannot be open then is ignored
263 pub fn build_logger(tag: &str, application: &str, level: LogLevel, output:LogTarget, path_file: Option<String>){
264 let _ = LOGGER.set(Logger::new(tag, application, level, output, path_file));
265 }
266
267 ///Build the logger using environment variables and default to parameters. Run this function first
268/// Configures a global logger instance based on the provided environment variables and options.
269///
270/// This function takes in a tag, an application name, a log level, and a log target,
271/// and uses the `LOG_ENV_VAR_LOG_LEVEL` (btlogger_log_level) and `LOG_ENV_VAR_LOG_OUTPUT` (btlogger_log_output) environment variables
272/// to override or set default values for the logger configuration.
273///
274/// If no environment variables are set, it will use the provided options to configure the logger.
275///
276/// # Arguments
277///
278/// * `tag`: The tag or identifier for the logger.
279/// * `application`: The application name associated with the logger.
280/// * `level`: An enum default log level to use for this logger instance.
281/// * `output`: An enum default target to output logs to (e.g. standard error, file, etc.).
282/// * `path_file`: Optional Absolute path to the file to log as String. If path is invalid or file cannot be open then is ignored
283 pub fn build_logger_env(tag: &str, application: &str, level: LogLevel, output:LogTarget, path_file: Option<String>){
284 let int_level: LogLevel;
285 let int_dest: LogTarget;
286 match env::var(LOG_ENV_VAR_LOG_LEVEL){
287 Ok(levll) => int_level = LogLevel::from_str(&levll),
288 Err(_) => int_level = level,
289 }
290
291 match env::var(LOG_ENV_VAR_LOG_OUTPUT){
292 Ok(levlo) => int_dest = LogTarget::from_str(&levlo),
293 Err(_) => int_dest = output,
294 }
295
296
297 build_logger(tag, application, int_level, int_dest, path_file);
298 }
299
300 ///Build the logger. Run this function first
301/// Builds a logger configuration based on the provided arguments.
302///
303/// This function takes in a tag and an application name as required parameters,
304/// and uses the provided vector of strings to override or set default values for
305/// the log level and target.
306///
307/// If no arguments are provided, it will build a logger with the error log level
308/// and standard error target. Otherwise, it will use the specified argument values
309/// to configure the logger.
310///
311/// # Arguments
312///
313/// * `tag`: The tag or identifier for the logger.
314/// * `application`: The application name associated with the logger.
315/// * `args`: A vector of strings containing key-value pairs for configuring the logger. The keys are "LOGLVL" or "LOGDST". They set:
316/// LOGLVL: the log level (level)
317/// LOGDST: output target (out_target)
318/// LOGFILE: Absolute path to the file to log as String
319 pub fn build_logger_args(tag: &str, application: &str, args: &Vec<String>){
320 if args.len() < 1{
321 //build_logger(tag, application, LogLevel::ERROR, LogTarget::STD_ERROR, None );
322 #[cfg(debug_assertions)]
323 build_logger(tag, application, LogLevel::VERBOSE, LogTarget::STD_OUT, None);
324 #[cfg(not(debug_assertions))]
325 build_logger(tag, application, LogLevel::WARN, LogTarget::STD_ERROR, None);
326 }else{
327 #[cfg(debug_assertions)]
328 let mut level = LogLevel::VERBOSE;
329 #[cfg(not(debug_assertions))]
330 let mut level = LogLevel::WARN;
331 #[cfg(debug_assertions)]
332 let mut out_target = LogTarget::STD_OUT;
333 #[cfg(not(debug_assertions))]
334 let mut out_target = LogTarget::STD_ERROR;
335 let mut dest_file = None;
336 for param in args{
337 match param.split_once("="){
338 Some(t) => {
339 match t.0.to_uppercase().as_str() {
340 "LOGLVL" => level = LogLevel::from_str(t.1),
341 "LOGDST" => out_target = LogTarget::from_str(t.1),
342 "LOGFILE" => dest_file = Some(t.1.to_owned()),
343 _ => (),
344 }
345 }
346 None => () ,
347 }
348
349 }
350 build_logger_env(tag, application, level, out_target, dest_file);
351 }
352 }
353
354 pub fn get_logger() -> Option<Logger>{
355 /*let _logger = LOGGER.lock().unwrap();
356 _logger.clone().unwrap()*/
357 if LOGGER.get().is_some() {
358 return Some(LOGGER.get().unwrap().clone())
359 }else{
360 let l_msg = format!("{} {} {} {} {}|>|{}", "BACHUETECH", "bt_logger", Logger::get_current_time(), LogLevel::WARN, "get_logger", "BT Logger is not initialized");
361 println!("{}", l_msg);
362 None
363 }
364 }