1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386
use colored::*;
use std::{
fmt::Display,
fs,
io::{self, Error, Write},
path::PathBuf,
};
mod macros;
/// LoggingLevel enum.
///
/// It goes from LoggingLevel::Trace to LoggingLevel::Critical.
///
/// Trace prints everything, Info prints info and anything With a higher precedence.
///
/// Critical has the highest precedence and only prints critical logs.
///
/// The precedence is: Critical > Error > Warning > Info > Trace.
///
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum LoggingLevel {
Trace, // everything
Info, // no debug, info and everything with a higher precedence (info, warning, error, critical)
Warning, // no info, warning and everything with a higher precedence (warning, error, and critical)
Error, // no warning, error and everything with a higher precedence (error, critical)
Critical, // will only log critical
}
impl Display for LoggingLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Trace => write!(f, "DEBUG"),
Self::Info => write!(f, "INFO"),
Self::Warning => write!(f, "WARNING"),
Self::Error => write!(f, "ERROR"),
Self::Critical => write!(f, "CRITICAL"),
}
}
}
/// LoggerBuilder struct.
///
/// Call the Logger::new() function to get and instance of LoggerBuilder
#[derive(Debug)]
pub struct LoggerBuilder {
level: LoggingLevel,
log_to_file: bool,
json: Option<bool>,
filename: Option<PathBuf>,
}
impl LoggerBuilder {
/// Set the logging level
pub fn level(&mut self, logging_level: LoggingLevel) -> &mut Self {
self.level = logging_level;
self
}
/// Set the filename. Filename should be a PathBuf
///
/// If this method is not called, then logging to file will be turned off.
pub fn filename(&mut self, name: PathBuf) -> &mut Self {
self.log_to_file = true;
self.filename = Some(name);
self
}
/// Returns an instance of Logger
///
/// This will not return an error if logging to file is off.
pub fn build(&mut self) -> Result<Logger, String> {
if self.log_to_file {
match self.filename.as_ref().unwrap().extension() {
Some(i) => self.json = Some(i == "json"),
None => return Err("Pls provide filename with extension".to_string()),
};
return Ok(Logger {
file: Some({
match open(self.filename.as_ref().unwrap()) {
Ok(i) => i,
Err(e) => return Err(e.to_string()),
}
}),
level: self.level,
log_to_file: self.log_to_file,
json: self.json,
filename: self.filename.clone(),
json_object: {
Some(json::object! {
logs: json_array![
],
})
},
});
}
Ok(Logger {
file: None,
level: self.level,
log_to_file: false,
json: None,
filename: None,
json_object: None,
})
}
}
/// The Logger struct.
///
/// Logger::new() returns a LoggerBuilder.
/// Call the build method on the LoggerBuilder instance to get a Logger instance.
///
/// # Example
///
/// ```
/// let mut logger = Logger::new()
/// .level(LoggingLevel::Error)
/// .filename(PathBuf::from("Logs.json"))
/// .build()
/// .unwrap();
///
/// logger.info("informative msg".to_string());
/// logger.critical("CRITICAL ERROR".to_string());
/// ```
/// "informative msg" will not be printed as it is info, but level is set to error.
/// "CRITICAL ERROR" will be printed as critical has a higher precendece than error.
///
/// If logging to a file is not needed
///
/// # Example
///
/// ```
/// let mut logger = Logger::new()
/// .level(LoggingLevel::Trace)
/// .build()
/// .unwrap();
///
/// logger.info("Informative message");
/// ```
///
/// The build method will not return an error here as no file is being opened.
/// The fields for file, json, filename, and json_object will be set None.
///
/// Json is parsed differently compared to normal parsing.
///
/// An example of normal parsing. Date is in YYYY-MM-DD format. All timestamps are in local timezone based on system time.
///
/// # Example
///
/// ```
/// 2024-03-06 12:04:01 CRITICAL: msg
/// 2024-03-06 12:04:01 ERROR: msg
/// ```
///
/// An example of json parsing
///
/// ```
/// {
/// "logs": [
/// {
/// "date": "2024-03-06",
/// "time": "12:01:53",
/// "message": "msg",
/// "type": "CRITICAL"
/// },
/// {
/// "date": "2024-03-06",
/// "time": "12:01:53",
/// "message": "msg",
/// "type": "ERROR"
/// }
/// ]
/// }
/// ```
///
#[derive(Debug)]
pub struct Logger {
file: Option<fs::File>, // the file to be written to
level: LoggingLevel, // the logging level
log_to_file: bool, // whether to log to file
json: Option<bool>, // whether it is json
filename: Option<PathBuf>, // the filename
json_object: Option<json::JsonValue>, // json object to be written
}
impl Logger {
/// Constructor function.
/// Returns a LoggerBuilder instance.
pub fn new() -> LoggerBuilder {
LoggerBuilder {
level: LoggingLevel::Trace,
log_to_file: false,
json: None,
filename: None,
}
}
/// Returns a reference to the filename.
///
/// It will return None if log_to_file is false
pub fn get_filename(&self) -> Option<&PathBuf> {
self.filename.as_ref()
}
/// Set the logging level
pub fn set_level(&mut self, level: LoggingLevel) {
self.level = level;
}
/// Critical
///
/// Ignores logging level
pub fn critical(&mut self, msg: String) {
self.log(msg, LoggingLevel::Critical)
}
/// Error
///
/// Only works until and for LoggingLevel::Error.
///
/// It is ignored by LoggingLevel::Critical
pub fn error(&mut self, msg: String) {
self.log(msg, LoggingLevel::Error)
}
/// Info
///
/// Only works until and for LoggingLevel::Info
pub fn info(&mut self, msg: String) {
self.log(msg, LoggingLevel::Info)
}
/// Warning
///
/// Only works until and for LoggingLevel::Warning
pub fn warning(&mut self, msg: String) {
self.log(msg, LoggingLevel::Warning)
}
/// Debug
///
/// Only for LoggingLevel::Trace
pub fn debug(&mut self, msg: String) {
self.log(msg, LoggingLevel::Trace)
}
fn log(&mut self, msg: String, level: LoggingLevel) {
if !self.log_to_file && level < self.level {
return;
}
let (date_string, time) = self.get_date_time_checked(&msg);
let msg_parsed = msg.replace("{%D}", &date_string).replace("{%T}", &time);
let formatted = format!("{}: {}", level, msg_parsed);
if self.level <= level {
use LoggingLevel::*;
match level {
Trace => println!("{}", &formatted.blue()),
Info => println!("{}", &formatted.bright_green()),
Warning => println!("{}", &formatted.yellow()),
Error => println!("{}", &formatted.bright_red()),
Critical => println!("{}", &formatted.on_red().white())
}
}
if self.log_to_file {
let file_formatted = format!("{} {} {}: {}\n", date_string, time, level, msg_parsed);
self.write_to_file(&date_string, &time, &msg_parsed, &file_formatted, level)
}
}
// writes to file
fn write_to_file(
&mut self,
date_string: &str,
time: &str,
msg_parsed: &str,
file_formatted: &str,
level: LoggingLevel,
) {
match self.json.unwrap() {
true => {
let data_to_be_written = json::object! {
date: string!(date_string),
time: string!(time),
message: string!(msg_parsed),
type: string!(level)
};
self.json_object.as_mut().unwrap()["logs"]
.push(data_to_be_written)
.unwrap_or_else(|_| {
println!(
"Error while parsing json. The msg: {} will not be written to the file",
msg_parsed
)
})
}
false => {
self.file
.as_mut()
.unwrap()
.write_all(file_formatted.as_bytes())
.unwrap_or_else(|_| {
println!("Error in writing to file. msg: {}", msg_parsed);
});
}
}
}
// checks if getting date and time is needed and returns it if it is
fn get_date_time_checked(&self, msg: &str) -> (String, String) {
let mut date_string = "".to_string();
let mut time = "".to_string();
if self.log_to_file || msg.contains("{%D}") || msg.contains("{%T}") {
let temp = get_date_time();
date_string = temp.0;
time = temp.1;
}
(date_string, time)
}
}
// json is written when the Logger goes out of scope and is dropped
impl Drop for Logger {
fn drop(&mut self) {
if self.log_to_file {
match self.json.unwrap() {
true => {
self.file
.as_mut()
.unwrap()
.write_all(
self.json_object
.as_mut()
.unwrap()
.to_string()
.trim()
.as_bytes(),
)
.unwrap_or_else(|error| {
println!("Error writing json: {}", error);
});
}
false => {}
}
}
}
}
// get local date and time based on system timezone
fn get_date_time() -> (String, String) {
let date = chrono::Local::now().naive_local().date();
let date_string = date.to_string();
let time = chrono::Local::now().time().format("%H:%M:%S").to_string();
(date_string, time)
}
// open file, and create it if it is not opened
fn open(filename: &PathBuf) -> Result<fs::File, Error> {
let file = match fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(filename)
{
Ok(i) => i,
Err(error) => match error.kind() {
io::ErrorKind::NotFound => fs::File::create(filename)?,
_ => panic!("Something went wrong in opening the file"),
},
};
Ok(file)
}