pub mod unit;
use crate::unit::{
find_log_files, format_u8_as_padded_2_digits, format_u16_as_padded_3_digits,
timestamp_ms_to_datetime,
};
use proc_tools::concat_vars;
use proc_tools_core::{concat_str, replace_multiple_patterns};
use proc_tools_helper::lang_tr;
use std::cmp::{Ordering, PartialOrd};
use std::collections::HashSet;
use std::sync::mpsc::SyncSender;
use std::sync::{OnceLock, RwLock};
use std::time::{SystemTime, UNIX_EPOCH};
use std::{
fs::{self, OpenOptions},
io::Write,
path::Path,
sync::mpsc::{self},
thread::JoinHandle,
};
#[derive(Clone)]
pub enum LogLevel {
Error,
Warn,
Info,
Debug,
Trace,
}
impl LogLevel {
#[inline]
pub(crate) fn to_level_filter(&self) -> log::LevelFilter {
match self {
LogLevel::Error => log::LevelFilter::Error,
LogLevel::Warn => log::LevelFilter::Warn,
LogLevel::Info => log::LevelFilter::Info,
LogLevel::Debug => log::LevelFilter::Debug,
LogLevel::Trace => log::LevelFilter::Trace,
}
}
}
pub(crate) struct LogMessage {
formatted: String,
now: (u32, u8, u8, u8, u8, u8, u16),
}
impl LogMessage {
#[inline]
fn from<T: Into<String>>(level: LogLevel, module_path: &str, message: T) -> Self {
let mut timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis();
match get_timezone_offset() {
TimezoneOffset::PositiveNumber(v) => timestamp += v,
TimezoneOffset::NegativeNumber(v) => timestamp -= v,
};
let (year, month, day, hour, minute, second, millis) = timestamp_ms_to_datetime(timestamp);
const HYPHEN: char = '-';
const COLON: char = ':';
let mut bytes = [0u8; 3];
let month_buf = format_u8_as_padded_2_digits(month, &mut bytes);
let mut bytes = [0u8; 3];
let day_buf = format_u8_as_padded_2_digits(day, &mut bytes);
let mut bytes = [0u8; 3];
let hour_buf = format_u8_as_padded_2_digits(hour, &mut bytes);
let mut bytes = [0u8; 3];
let minute_buf = format_u8_as_padded_2_digits(minute, &mut bytes);
let mut bytes = [0u8; 3];
let second_buf = format_u8_as_padded_2_digits(second, &mut bytes);
let mut bytes = [b'0'; 5];
let millis_buf = format_u16_as_padded_3_digits(millis, &mut bytes);
let level_str = match level {
LogLevel::Error => "ERROR",
LogLevel::Warn => " WARN",
LogLevel::Info => " INFO",
LogLevel::Debug => "DEBUG",
LogLevel::Trace => "TRACE",
};
let message = message.into();
let formatted = concat_vars!(
"[":String,
year :u32,
HYPHEN : char,
month_buf : String,
HYPHEN : char,
day_buf : String,
" " : String,
hour_buf : String,
COLON : char,
minute_buf : String,
COLON : char,
second_buf : String,
"." : String,
millis_buf : String,
"]_[" : String,
level_str : String,
"]_[" : String,
module_path : String,
"] - ": String,
message: String,
"\n" : String
);
LogMessage {
formatted,
now: (year, month, day, hour, minute, second, millis),
}
}
}
static LOG_BACKEND: RwLock<Option<(SyncSender<LogMessage>, JoinHandle<()>)>> = RwLock::new(None);
static TIMEZONE_OFFSET: OnceLock<TimezoneOffset> = OnceLock::new();
static IGNORE_MODULE: OnceLock<HashSet<&'static str>> = OnceLock::new();
#[inline]
pub(crate) fn set_timezone_offset(value: TimezoneOffset) {
TIMEZONE_OFFSET.get_or_init(|| value);
}
#[inline]
pub(crate) fn get_timezone_offset() -> &'static TimezoneOffset {
&*TIMEZONE_OFFSET.get_or_init(|| TimezoneOffset::PositiveNumber(0))
}
#[inline(always)]
fn init_log_backend(mut logger_format: LoggerFormat) -> Result<(), std::io::Error> {
let log_dir = Path::new(&*logger_format.dir_path);
let msg_str = lang_tr!(
cn = "创建日志目录失败,错误信息:",
en = "Failed to create log directory, error message:",
);
if !log_dir.exists() {
fs::create_dir_all(log_dir).map_err(|err| {
std::io::Error::new(err.kind(), concat_str!(msg_str, &err.to_string()))
})?;
}
let file_path: String = concat_str!(&*logger_format.dir_path, "/", &*logger_format.file_name);
let result = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(file_path);
let msg_str = lang_tr!(
cn = "打开日志文件失败,错误信息:",
en = "Failed to open log file, Exception message:"
);
let mut log_file =
result.map_err(|e| std::io::Error::new(e.kind(), concat_str!(msg_str, &e.to_string())))?;
logger_format.file_size = if let Ok(v) = log_file.metadata() {
v.len()
} else {
0
};
let mut is_create_file = false;
let (sender, receiver) = mpsc::sync_channel::<LogMessage>(logger_format.bound as usize);
let handle = std::thread::spawn(move || {
while let Ok(msg) = receiver.recv() {
if logger_format.should_split_by_time(&msg.now) {
logger_format = logger_format.update_filename_for_time(msg.now);
let file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&*logger_format.file_path);
let msg_str = lang_tr!(
cn = "打开日志文件失败,错误信息:",
en = "Failed to open log file, error message:"
);
match file {
Ok(v) => log_file = v,
Err(e) => eprintln!("{}{}", msg_str, e),
}
logger_format.datetime = msg.now;
logger_format.file_size = if let Ok(v) = log_file.metadata() {
v.len()
} else {
0
};
logger_format.index = 0;
is_create_file = true;
}
if logger_format.max_file_triggering_policy != 0
&& logger_format.file_size > logger_format.max_file_triggering_policy
{
logger_format = logger_format.update_filename_for_filesize(msg.now);
let file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&*logger_format.file_path);
let msg_str = lang_tr!(
cn = "打开日志文件失败,错误信息:",
en = "Failed to open log file, error message:"
);
match file {
Ok(v) => log_file = v,
Err(e) => eprintln!("{}{}", msg_str, e),
}
logger_format.file_size = if let Ok(v) = log_file.metadata() {
v.len()
} else {
0
};
is_create_file = true;
};
if is_create_file && logger_format.max_retained_files != 0 {
let result = prune_old_logs(&mut logger_format);
if let Err((msg_str, Some(e))) = result {
eprintln!("{}{}", msg_str, e);
} else if let Err((msg_str, None)) = result {
eprintln!("{}", msg_str);
}
is_create_file = false;
}
#[cfg(feature = "stdout")]
print!("{}", msg.formatted);
#[cfg(all(not(feature = "stdout"), debug_assertions))]
print!("{}", msg.formatted);
match write!(log_file, "{}", msg.formatted) {
Ok(_) => logger_format.file_size += msg.formatted.len() as u64,
Err(e) => {
let msg_str = lang_tr!(
cn = "写入日志文件失败:",
en = "Writing to log file failed:"
);
eprintln!("{}{}", msg_str, e)
}
};
match log_file.flush() {
Ok(_) => {}
Err(e) => {
let msg_str = lang_tr!(
cn = "刷新输出流失败:",
en = "Refresh output stream failed:"
);
eprintln!("{}{}", msg_str, e)
}
};
}
});
let new_backend = (sender, handle);
LOG_BACKEND.write().unwrap().replace(new_backend);
Ok(())
}
#[inline(always)]
fn prune_old_logs(logger_format: &mut LoggerFormat) -> Result<(), (&str, Option<std::io::Error>)> {
let path_vec_result = find_log_files(
logger_format.dir_path.as_ref(),
logger_format.file_pattern.as_ref(),
);
let mut path_vec = match path_vec_result {
Ok(v) => v,
Err(e) => {
let msg = lang_tr!(cn = "删除日志文件失败:", en = "Failed to delete log file:");
return Err((msg, Some(e)));
}
};
if path_vec.len() as u64 <= logger_format.max_retained_files {
return Ok(());
}
let n = path_vec.len() as u64 - logger_format.max_retained_files;
let mut i = 0;
while i < n {
match path_vec.pop() {
None => {
let msg = lang_tr!(
cn = "日志异常,path_vec删除最后一项元素失败",
en = "Log exception, path_cec failed to delete the last element"
);
return Err((msg, None));
}
Some(v) => match fs::remove_file(v) {
Ok(_) => {}
Err(e) => {
let msg =
lang_tr!(cn = "删除日志文件失败:", en = "Failed to delete log file:");
return Err((msg, Some(e)));
}
},
}
i += 1;
}
Ok(())
}
pub struct Logger {
module_path: &'static str,
log_level: LogLevel,
}
impl PartialEq for LogLevel {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(LogLevel::Error, LogLevel::Error)
| (LogLevel::Warn, LogLevel::Warn)
| (LogLevel::Info, LogLevel::Info)
| (LogLevel::Debug, LogLevel::Debug)
| (LogLevel::Trace, LogLevel::Trace) => true,
_ => false,
}
}
}
impl PartialOrd for LogLevel {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
let priority = |level: &LogLevel| match level {
LogLevel::Error => 4,
LogLevel::Warn => 3,
LogLevel::Info => 2,
LogLevel::Debug => 1,
LogLevel::Trace => 0,
};
priority(self).partial_cmp(&priority(other))
}
}
impl Logger {
#[inline]
pub const fn new(module_path: &'static str, log_level: LogLevel) -> Self {
Logger {
module_path,
log_level,
}
}
#[inline(always)]
pub fn init(dir_path: &str, level: LogLevel) -> LoggerFormat {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis();
let (year, month, day, hour, minute, second, millis) = timestamp_ms_to_datetime(timestamp);
LoggerFormat {
dir_path: Box::from(dir_path),
file_name: Box::from("application.log"),
file_path: Box::from(concat_str!(dir_path, "/", "application.log")),
file_pattern: Box::from(""),
level,
max_file_triggering_policy: 0,
bound: 100,
file_size: 0,
index: 0,
time_division_rule: LoggerFormatTimeDivisionRule::None,
datetime: (year, month, day, hour, minute, second, millis),
max_retained_files: 0,
}
}
#[inline(always)]
pub fn shutdown() {
let log_backend = LOG_BACKEND.write().unwrap().take();
match log_backend {
None => return,
Some((sender, handle)) => {
drop(sender); handle.join().unwrap(); }
};
}
pub fn info<T: Into<String>>(&self, message: T) {
self.log(LogLevel::Info, message);
}
pub fn debug<T: Into<String>>(&self, message: T) {
self.log(LogLevel::Debug, message);
}
pub fn warn<T: Into<String>>(&self, message: T) {
self.log(LogLevel::Warn, message);
}
pub fn error<T: Into<String>>(&self, message: T) {
self.log(LogLevel::Error, message);
}
pub fn trace<T: Into<String>>(&self, message: T) {
self.log(LogLevel::Trace, message);
}
#[inline]
fn log<T: Into<String>>(&self, level: LogLevel, message: T) {
if level < self.log_level {
return;
}
let log_backend = LOG_BACKEND.read().unwrap();
let option = log_backend.as_ref();
if let Some((sender, _)) = option {
if let Err(e) = sender.send(LogMessage::from(level, self.module_path, message)) {
let msg_str = lang_tr!(
cn = "日志记录失败,错误信息:",
en = "Logging failed with error message:"
);
eprintln!("{}{}", msg_str, e);
};
} else {
let msg_str = lang_tr!(
cn = "日志记录失败,日志未初始化或已关闭",
en = "Log output failed, Log not initialized or closed"
);
eprintln!("{}", msg_str);
};
}
}
impl log::Log for Logger {
#[inline]
fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
metadata.level() <= self.log_level.to_level_filter()
}
#[inline]
fn log(&self, record: &log::Record<'_>) {
let module_path: &str = record.module_path().unwrap_or_else(|| "None");
if IGNORE_MODULE
.get()
.map_or(false, |module| module.contains(module_path))
{
return;
};
let level = match record.level() {
log::Level::Error => LogLevel::Error,
log::Level::Warn => LogLevel::Warn,
log::Level::Info => LogLevel::Info,
log::Level::Debug => LogLevel::Debug,
log::Level::Trace => LogLevel::Trace,
};
let log_backend = LOG_BACKEND.read().unwrap();
let option = log_backend.as_ref();
if let Some((sender, _)) = option {
if let Err(e) = sender.send(LogMessage::from(
level,
module_path,
record.args().to_string(),
)) {
let msg = lang_tr!(
cn = "日志记录失败,错误信息:",
en = "Logging failed, error message:"
);
eprintln!("{}{}", msg, e);
};
}
}
#[inline]
fn flush(&self) {}
}
pub struct LoggerFormat {
dir_path: Box<str>,
file_name: Box<str>,
file_path: Box<str>,
file_pattern: Box<str>,
level: LogLevel,
datetime: (u32, u8, u8, u8, u8, u8, u16),
time_division_rule: LoggerFormatTimeDivisionRule,
max_file_triggering_policy: u64,
bound: u32,
file_size: u64,
index: u64,
max_retained_files: u64,
}
pub(crate) enum TimezoneOffset {
PositiveNumber(u128),
NegativeNumber(u128),
}
pub enum LoggerFormatTimeDivisionRule {
None,
Year,
Month,
Day,
Hours,
}
impl LoggerFormat {
#[inline]
pub(crate) fn update_filename_for_time(
mut self,
today: (u32, u8, u8, u8, u8, u8, u16),
) -> Self {
let new_file_name = self.get_new_file_name(today);
self.file_name = Box::from(new_file_name);
self.file_path = Box::from(concat_str!(&*self.file_pattern, "/", &*self.file_name));
self
}
#[inline]
pub(crate) fn update_filename_for_filesize(
mut self,
today: (u32, u8, u8, u8, u8, u8, u16),
) -> Self {
self.index = self.index.checked_add(1).unwrap_or(0);
let new_file_name = self.get_new_file_name(today);
self.file_name = Box::from(new_file_name);
self.file_path = Box::from(concat_str!(&*self.dir_path, "/", &*self.file_name));
self
}
#[inline(always)]
fn get_new_file_name(&self, today: (u32, u8, u8, u8, u8, u8, u16)) -> String {
let mut buf = [0u8; 3];
let month = format_u8_as_padded_2_digits(today.1, &mut buf);
let mut buf = [0u8; 3];
let day = format_u8_as_padded_2_digits(today.2, &mut buf);
let mut buf = [0u8; 3];
let hour = format_u8_as_padded_2_digits(today.3, &mut buf);
let mut buf = [0u8; 3];
let minutes = format_u8_as_padded_2_digits(today.4, &mut buf);
let mut buf = [0u8; 3];
let seconds = format_u8_as_padded_2_digits(today.5, &mut buf);
replace_multiple_patterns(
&*self.file_pattern,
&[
("%Y", &concat_vars!(today.0 : u32)),
("%m", &concat_vars!(month : String)),
("%d", &concat_vars!(day : String)),
("%H", &concat_vars!(hour : String)),
("%M", &concat_vars!(minutes : String)),
("%S", &concat_vars!(seconds : String)),
("%i", &concat_vars!(self.index : u64)),
],
)
}
#[inline]
pub(crate) const fn should_split_by_time(&self, now: &(u32, u8, u8, u8, u8, u8, u16)) -> bool {
match self.time_division_rule {
LoggerFormatTimeDivisionRule::None => false,
LoggerFormatTimeDivisionRule::Year => now.0 != self.datetime.0,
LoggerFormatTimeDivisionRule::Month => {
now.0 != self.datetime.0 || now.1 != self.datetime.1
}
LoggerFormatTimeDivisionRule::Day => {
now.0 != self.datetime.0 || now.1 != self.datetime.1 || now.2 != self.datetime.2
}
LoggerFormatTimeDivisionRule::Hours => {
now.0 != self.datetime.0
|| now.1 != self.datetime.1
|| now.2 != self.datetime.2
|| now.3 != self.datetime.3
}
}
}
#[inline]
pub fn set_file_name(mut self, file_name: &str) -> Self {
self.time_division_rule = LoggerFormatTimeDivisionRule::None;
self.file_name = Box::from(file_name);
self.file_path = Box::from(concat_str!(&*self.dir_path, "/", &*self.file_name));
self
}
#[inline]
pub const fn set_bound(mut self, bound: u32) -> Self {
self.bound = bound;
self
}
#[inline]
pub fn set_max_file_triggering_policy(mut self, max_file_triggering_policy: u64) -> Self {
self.max_file_triggering_policy = max_file_triggering_policy;
let datetime = self.datetime.clone();
if self.file_pattern.contains("%i") {
self.index = 0;
return self.update_filename_for_filesize(datetime);
}
self
}
#[inline]
pub fn set_file_pattern(
mut self,
file_pattern: &str,
time_division_rule: LoggerFormatTimeDivisionRule,
) -> Self {
self.time_division_rule = time_division_rule;
self.file_pattern = Box::from(file_pattern);
self
}
#[inline]
pub fn set_timezone_offset(self, offset: i128) -> Self {
if offset < 0 {
set_timezone_offset(TimezoneOffset::NegativeNumber(offset.abs() as u128));
} else {
set_timezone_offset(TimezoneOffset::PositiveNumber(offset as u128));
};
self
}
#[inline]
pub const fn set_max_retained_files(mut self, max_retained_files: u64) -> Self {
self.max_retained_files = max_retained_files;
self
}
#[inline]
pub fn set_ignore_module(self, module: HashSet<&'static str>) -> Self {
IGNORE_MODULE.get_or_init(|| module);
self
}
#[inline]
pub fn start(self) -> Result<(), std::io::Error> {
let box_logger = Box::from(Logger::new("None", self.level.clone()));
log::set_logger(Box::leak(box_logger)).unwrap();
log::set_max_level(self.level.to_level_filter());
if self.file_pattern.is_empty() {
return init_log_backend(self);
}
let mut timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis();
match get_timezone_offset() {
TimezoneOffset::PositiveNumber(v) => timestamp += v,
TimezoneOffset::NegativeNumber(v) => timestamp -= v,
};
let now = timestamp_ms_to_datetime(timestamp);
let mut s = self.update_filename_for_time(now);
s.datetime = now;
init_log_backend(s)
}
}