use crate::level::{parse_level, LevelInto};
use crate::out_kind::OutKind;
use crate::{InnerLevel, SimpleResult};
use log::LevelFilter;
use log4rs::append::console::ConsoleAppender;
use log4rs::append::rolling_file::policy::compound::roll::fixed_window::FixedWindowRoller;
use log4rs::append::rolling_file::policy::compound::trigger::size::SizeTrigger;
use log4rs::append::rolling_file::policy::compound::CompoundPolicy;
use log4rs::append::rolling_file::RollingFileAppender;
use log4rs::config::runtime::LoggerBuilder;
use log4rs::config::{Appender, Config, Logger, Root};
use log4rs::encode::pattern::PatternEncoder;
use once_cell::sync::OnceCell;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
const SIMPLE_LOG_FILE: &str = "simple_log_file";
const SIMPLE_LOG_CONSOLE: &str = "simple_log_console";
const SIMPLE_LOG_BASE_NAME: &str = "simple_log";
pub const DEFAULT_DATE_TIME_FORMAT: &str = "%Y-%m-%d %H:%M:%S.%f";
pub const DEFAULT_HOUR_TIME_FORMAT: &str = "%H:%M:%S.%f";
struct LogConf {
log_config: LogConfig,
handle: log4rs::Handle,
}
static LOG_CONF: OnceCell<Mutex<LogConf>> = OnceCell::new();
fn init_log_conf(mut log_config: LogConfig) -> SimpleResult<()> {
let config = build_config(&mut log_config)?;
let handle = log4rs::init_config(config).map_err(|e| e.to_string())?;
LOG_CONF.get_or_init(|| Mutex::new(LogConf { log_config, handle }));
Ok(())
}
pub fn update_log_conf(mut log_config: LogConfig) -> SimpleResult<LogConfig> {
let log_conf = LOG_CONF.get().unwrap();
let mut guard = log_conf.lock().unwrap();
let config = build_config(&mut log_config)?;
guard.log_config = log_config;
guard.handle.set_config(config);
Ok(guard.log_config.clone())
}
pub fn update_log_level<S: LevelInto>(level: S) -> SimpleResult<LogConfig> {
let log_conf = LOG_CONF.get().unwrap();
let mut guard = log_conf.lock().unwrap();
guard.log_config.set_level(level)?;
let config = build_config(&mut guard.log_config)?;
guard.handle.set_config(config);
Ok(guard.log_config.clone())
}
pub fn get_log_conf() -> SimpleResult<LogConfig> {
let log_conf = LOG_CONF.get().unwrap();
let config = log_conf.lock().unwrap().log_config.clone();
Ok(config)
}
use crate::level::deserialize_level;
use crate::out_kind::deserialize_out_kind;
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub struct LogConfig {
#[serde(default)]
pub path: Option<String>,
#[serde(default)]
pub directory: Option<String>,
#[serde(deserialize_with = "deserialize_level")]
pub level: InnerLevel,
#[serde(default)]
pub size: u64,
#[serde(deserialize_with = "deserialize_out_kind", default)]
pub out_kind: Vec<OutKind>,
#[serde(default)]
pub roll_count: u32,
#[serde(default)]
pub time_format: Option<String>,
}
impl Default for LogConfig {
fn default() -> Self {
LogConfig {
path: None,
directory: None,
level: (LevelFilter::Debug, vec![]),
size: 0,
out_kind: vec![],
roll_count: 0,
time_format: None,
}
}
}
impl LogConfig {
fn default_basename(&self) -> String {
let arg0 = std::env::args()
.next()
.unwrap_or_else(|| SIMPLE_LOG_BASE_NAME.to_owned());
let path = Path::new(&arg0)
.file_stem()
.map(OsStr::to_string_lossy)
.unwrap_or(Cow::Borrowed(SIMPLE_LOG_BASE_NAME))
.to_string();
format!("{path}.log")
}
pub fn get_path(&self) -> Option<&String> {
self.path.as_ref()
}
pub fn get_directory(&self) -> Option<&String> {
self.directory.as_ref()
}
pub fn get_level(&self) -> &str {
self.level.0.as_str()
}
pub fn get_size(&self) -> u64 {
self.size
}
pub fn get_out_kind(&self) -> &Vec<OutKind> {
&self.out_kind
}
pub fn get_roll_count(&self) -> u32 {
self.roll_count
}
pub fn get_time_format(&self) -> Option<&String> {
self.time_format.as_ref()
}
pub(crate) fn set_level<T: LevelInto>(&mut self, level: T) -> SimpleResult<()> {
let level = level.into_level();
let level = parse_level(level)?;
self.level = level;
Ok(())
}
}
pub struct LogConfigBuilder(LogConfig);
impl LogConfigBuilder {
pub fn builder() -> Self {
LogConfigBuilder(LogConfig::default())
}
pub fn path<S: Into<String>>(mut self, path: S) -> LogConfigBuilder {
self.0.path = Some(path.into());
self
}
pub fn directory<S: Into<String>>(mut self, directory: S) -> LogConfigBuilder {
self.0.directory = Some(directory.into());
self
}
pub fn level<S: LevelInto>(mut self, level: S) -> SimpleResult<LogConfigBuilder> {
self.0.set_level(level)?;
Ok(self)
}
pub fn size(mut self, size: u64) -> LogConfigBuilder {
self.0.size = size;
self
}
pub fn output_file(mut self) -> LogConfigBuilder {
self.0.out_kind.push(OutKind::File);
self
}
pub fn output_console(mut self) -> LogConfigBuilder {
self.0.out_kind.push(OutKind::Console);
self
}
pub fn roll_count(mut self, roll_count: u32) -> LogConfigBuilder {
self.0.roll_count = roll_count;
self
}
pub fn time_format<S: Into<String>>(mut self, time_format: S) -> LogConfigBuilder {
self.0.time_format = Some(time_format.into());
self
}
pub fn build(self) -> LogConfig {
self.0
}
}
pub fn new(log_config: LogConfig) -> SimpleResult<()> {
let mut log_config = log_config;
init_default_log(&mut log_config);
init_log_conf(log_config)?;
Ok(())
}
pub fn quick() -> SimpleResult<()> {
quick_log_level::<_, &str>("debug", None)
}
pub fn quick_log_level<S: LevelInto, P: Into<String>>(
level: S,
path: Option<P>,
) -> SimpleResult<()> {
let level = level.into_level();
let level = parse_level(level)?;
let mut config = LogConfig {
path: path.map(|v| v.into()),
directory: None,
level,
size: 0,
out_kind: vec![],
roll_count: 0,
time_format: None,
};
init_default_log(&mut config);
init_log_conf(config)?;
Ok(())
}
pub fn console<S: LevelInto>(level: S) -> SimpleResult<()> {
let level = level.into_level();
let level = parse_level(level)?;
let config = LogConfig {
path: None,
directory: None,
level,
size: 0,
out_kind: vec![OutKind::Console],
roll_count: 0,
time_format: Some(DEFAULT_DATE_TIME_FORMAT.to_string()),
};
init_log_conf(config)?;
Ok(())
}
pub fn file<P: Into<String>, S: LevelInto>(
path: P,
level: S,
size: u64,
roll_count: u32,
) -> SimpleResult<()> {
let level = level.into_level();
let level = parse_level(level)?;
let config = LogConfig {
path: Some(path.into()),
directory: None,
level,
size,
out_kind: vec![OutKind::File],
roll_count,
time_format: Some(DEFAULT_DATE_TIME_FORMAT.to_string()),
};
init_log_conf(config)?;
Ok(())
}
fn build_config(log: &mut LogConfig) -> SimpleResult<Config> {
let mut config_builder = Config::builder();
let mut root_builder = Root::builder();
for kind in &log.out_kind {
match kind {
OutKind::File => {
if log.directory.is_some() && log.path.is_none() {
log.path = Some(log.default_basename());
}
if log.path.is_some() {
config_builder = config_builder
.appender(Appender::builder().build(SIMPLE_LOG_FILE, file_appender(log)?));
root_builder = root_builder.appender(SIMPLE_LOG_FILE);
}
}
OutKind::Console => {
let console = ConsoleAppender::builder()
.encoder(Box::new(encoder(log.time_format.as_ref(), true)))
.build();
config_builder = config_builder
.appender(Appender::builder().build(SIMPLE_LOG_CONSOLE, Box::new(console)));
root_builder = root_builder.appender(SIMPLE_LOG_CONSOLE);
}
}
}
for target in &log.level.1 {
config_builder = config_builder.logger(LoggerBuilder::build(
Logger::builder(),
&target.name,
target.level,
));
}
let config = config_builder
.build(root_builder.build(log.level.0))
.map_err(|e| e.to_string())?;
Ok(config)
}
fn init_default_log(log: &mut LogConfig) {
if let Some(path) = &log.path {
if path.trim().is_empty() {
let file_name = log.default_basename();
log.path = Some(format!("./tmp/{}", file_name));
}
}
if log.size == 0 {
log.size = 10 }
if log.roll_count == 0 {
log.roll_count = 10
}
if log.out_kind.is_empty() {
log.out_kind
.append(&mut vec![OutKind::Console, OutKind::File])
}
}
fn encoder(time_format: Option<&String>, color: bool) -> PatternEncoder {
let time_format = if let Some(format) = time_format {
format.to_string()
} else {
DEFAULT_DATE_TIME_FORMAT.to_string()
};
let color_level = match color {
true => "{h({l:5})}",
false => "{l:5}",
};
let mut pattern = format!("{{d({})}} [{}] ", time_format, color_level);
#[cfg(feature = "target")]
{
pattern += "[{t:7}] <{M}:{L}>:{m}{n}";
}
#[cfg(not(feature = "target"))]
{
pattern += "<{M}:{L}>:{m}{n}";
}
PatternEncoder::new(pattern.as_str())
}
fn file_appender(log: &LogConfig) -> SimpleResult<Box<RollingFileAppender>> {
let path = log
.path
.as_ref()
.expect("Expected the path to write the log file, but it is empty");
let mut path = PathBuf::from(path);
if let Some(directory) = &log.directory {
let buf = PathBuf::from(directory);
path = buf.join(path);
}
let roll = FixedWindowRoller::builder()
.base(0)
.build(
format!("{}.{{}}.gz", path.display()).as_str(),
log.roll_count,
)
.map_err(|e| e.to_string())?;
let trigger = SizeTrigger::new(log.size * 1024 * 1024);
let policy = CompoundPolicy::new(Box::new(trigger), Box::new(roll));
let logfile = RollingFileAppender::builder()
.encoder(Box::new(encoder(log.time_format.as_ref(), false)))
.build(path.clone(), Box::new(policy))
.map_err(|e| e.to_string())?;
Ok(Box::new(logfile))
}