use crate::core::Level;
use e_utils::parse::MyParseFormat;
use e_utils::Result;
use fern::FormatCallback;
use log::{logger, RecordBuilder};
use log::{LevelFilter, Record};
use serde::Serialize;
use serde_repr::{Deserialize_repr, Serialize_repr};
use std::borrow::Cow;
use std::collections::HashMap;
use std::{
fmt::Arguments,
fs::{self, File},
iter::FromIterator,
path::{Path, PathBuf},
};
use tauri::{
plugin::{self, TauriPlugin},
Manager, Runtime,
};
pub use fern;
use time::OffsetDateTime;
const DEFAULT_MAX_FILE_SIZE: u128 = 40000;
const DEFAULT_ROTATION_STRATEGY: RotationStrategy = RotationStrategy::KeepOne;
const DEFAULT_TIMEZONE_STRATEGY: TimezoneStrategy = TimezoneStrategy::UseUtc;
const DEFAULT_LOG_TARGETS: [LogTarget; 2] = [LogTarget::Stdout, LogTarget::LogDir];
#[cfg(target_os = "windows")]
const LINEFEED: &'static str = "\r";
#[cfg(not(target_os = "windows"))]
const LINEFEED: &'static str = "";
pub fn init_layer_log<P>(
fname: Option<String>,
folder: P,
level: Level,
_format: String,
output_list: Vec<String>,
filter_target: Vec<String>,
) -> Result<Builder>
where
P: AsRef<Path> + Clone,
{
let olist: Vec<LogTarget> = output_list
.into_iter()
.filter_map(|x| {
Some(match &*x {
"stdout" => LogTarget::Stdout,
"stderr" => LogTarget::Stderr,
"folder" => LogTarget::Folder(folder.as_ref().to_path_buf()),
"logDir" => LogTarget::LogDir,
"webview" => LogTarget::Webview,
_ => return None,
})
})
.collect();
let mut log = Builder::default()
.rotation_strategy(RotationStrategy::KeepAll)
.targets(olist)
.format(move |callback, message, record| {
let target = record.target();
if filter_target.iter().find(|x| &***x == target).is_none() {
let time = "{time}".parse_format().unwrap();
let date = "{date}".parse_format().unwrap();
callback.finish(format_args!(
"[{}][{}][{}][{}] {}{LINEFEED}",
date,
time,
target,
record.level(),
message
))
}
})
.level(level.to_level_filter());
if let Some(x) = fname {
log = log.log_name(x);
}
Ok(log)
}
#[derive(Debug, Clone, Deserialize_repr, Serialize_repr)]
#[repr(u16)]
pub enum LogLevel {
Trace = 1,
Debug,
Info,
Warn,
Error,
}
impl From<LogLevel> for log::Level {
fn from(log_level: LogLevel) -> Self {
match log_level {
LogLevel::Trace => log::Level::Trace,
LogLevel::Debug => log::Level::Debug,
LogLevel::Info => log::Level::Info,
LogLevel::Warn => log::Level::Warn,
LogLevel::Error => log::Level::Error,
}
}
}
impl From<log::Level> for LogLevel {
fn from(log_level: log::Level) -> Self {
match log_level {
log::Level::Trace => LogLevel::Trace,
log::Level::Debug => LogLevel::Debug,
log::Level::Info => LogLevel::Info,
log::Level::Warn => LogLevel::Warn,
log::Level::Error => LogLevel::Error,
}
}
}
#[derive(Debug)]
pub enum RotationStrategy {
KeepAll,
KeepOne,
}
#[derive(Debug, Clone)]
pub enum TimezoneStrategy {
UseUtc,
UseLocal,
}
impl TimezoneStrategy {
pub fn get_now(&self) -> OffsetDateTime {
match self {
TimezoneStrategy::UseUtc => OffsetDateTime::now_utc(),
TimezoneStrategy::UseLocal => {
OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc())
} }
}
}
#[derive(Debug, Serialize, Clone)]
struct RecordPayload {
message: String,
level: LogLevel,
}
#[derive(Debug)]
pub enum LogTarget {
Stdout,
Stderr,
Folder(PathBuf),
LogDir,
Webview,
}
#[tauri::command]
fn log(
level: LogLevel,
message: String,
location: Option<&str>,
file: Option<&str>,
line: Option<u32>,
key_values: Option<HashMap<String, String>>,
) {
let location = location.unwrap_or("webview");
let mut builder = RecordBuilder::new();
builder
.level(level.into())
.target(location)
.file(file)
.line(line);
let key_values = key_values.unwrap_or_default();
let mut kv = HashMap::new();
for (k, v) in key_values.iter() {
kv.insert(k.as_str(), v.as_str());
}
builder.key_values(&kv);
logger().log(&builder.args(format_args!("{message}")).build());
}
pub type TauriBuilder = Builder;
#[derive(Debug)]
pub struct Builder {
dispatch: fern::Dispatch,
rotation_strategy: RotationStrategy,
timezone_strategy: TimezoneStrategy,
max_file_size: u128,
targets: Vec<LogTarget>,
log_name: Option<String>,
}
impl Default for Builder {
fn default() -> Self {
let format =
time::format_description::parse("[[[year]-[month]-[day]][[[hour]:[minute]:[second]]")
.unwrap();
let dispatch = fern::Dispatch::new().format(move |out, message, record| {
out.finish(format_args!(
"{}[{}][{}] {}",
DEFAULT_TIMEZONE_STRATEGY.get_now().format(&format).unwrap(),
record.level(),
record.target(),
message
))
});
Self {
dispatch,
rotation_strategy: DEFAULT_ROTATION_STRATEGY,
timezone_strategy: DEFAULT_TIMEZONE_STRATEGY,
max_file_size: DEFAULT_MAX_FILE_SIZE,
targets: DEFAULT_LOG_TARGETS.into(),
log_name: None,
}
}
}
impl Builder {
pub fn new() -> Self {
Default::default()
}
pub fn rotation_strategy(mut self, rotation_strategy: RotationStrategy) -> Self {
self.rotation_strategy = rotation_strategy;
self
}
pub fn timezone_strategy(mut self, timezone_strategy: TimezoneStrategy) -> Self {
self.timezone_strategy = timezone_strategy.clone();
let format =
time::format_description::parse("[[[year]-[month]-[day]][[[hour]:[minute]:[second]]")
.unwrap();
self.dispatch = fern::Dispatch::new().format(move |out, message, record| {
out.finish(format_args!(
"{}[{}][{}] {}",
timezone_strategy.get_now().format(&format).unwrap(),
record.level(),
record.target(),
message
))
});
self
}
pub fn max_file_size(mut self, max_file_size: u128) -> Self {
self.max_file_size = max_file_size;
self
}
pub fn format<F>(mut self, formatter: F) -> Self
where
F: Fn(FormatCallback<'_>, &Arguments<'_>, &Record<'_>) + Sync + Send + 'static,
{
self.dispatch = self.dispatch.format(formatter);
self
}
pub fn level(mut self, level_filter: impl Into<LevelFilter>) -> Self {
self.dispatch = self.dispatch.level(level_filter.into());
self
}
pub fn level_for(mut self, module: impl Into<Cow<'static, str>>, level: LevelFilter) -> Self {
self.dispatch = self.dispatch.level_for(module, level);
self
}
pub fn filter<F>(mut self, filter: F) -> Self
where
F: Fn(&log::Metadata<'_>) -> bool + Send + Sync + 'static,
{
self.dispatch = self.dispatch.filter(filter);
self
}
pub fn target(mut self, target: LogTarget) -> Self {
self.targets.push(target);
self
}
pub fn targets(mut self, targets: impl IntoIterator<Item = LogTarget>) -> Self {
self.targets = Vec::from_iter(targets);
self
}
pub fn log_name<S: Into<String>>(mut self, log_name: S) -> Self {
self.log_name = Some(log_name.into());
self
}
#[cfg(feature = "tauri-colored")]
pub fn with_colors(self, colors: fern::colors::ColoredLevelConfig) -> Self {
let format =
time::format_description::parse("[[[year]-[month]-[day]][[[hour]:[minute]:[second]]")
.unwrap();
let timezone_strategy = self.timezone_strategy.clone();
self.format(move |out, message, record| {
out.finish(format_args!(
"{}[{}][{}] {}",
timezone_strategy.get_now().format(&format).unwrap(),
colors.color(record.level()),
record.target(),
message
))
})
}
pub fn build<R: Runtime>(mut self) -> TauriPlugin<R> {
plugin::Builder::new("log")
.invoke_handler(tauri::generate_handler![log])
.setup(move |app_handle| {
let log_name = self
.log_name
.as_deref()
.unwrap_or_else(|| &app_handle.package_info().name);
for target in &self.targets {
self.dispatch = self.dispatch.chain(match target {
LogTarget::Stdout => fern::Output::from(std::io::stdout()),
LogTarget::Stderr => fern::Output::from(std::io::stderr()),
LogTarget::Folder(path) => {
if !path.exists() {
fs::create_dir_all(path).unwrap();
}
fern::log_file(get_log_file_path(
&path,
log_name,
&self.rotation_strategy,
&self.timezone_strategy,
self.max_file_size,
)?)?
.into()
}
LogTarget::LogDir => {
let path = app_handle.path_resolver().app_log_dir().unwrap();
if !path.exists() {
fs::create_dir_all(&path).unwrap();
}
fern::log_file(get_log_file_path(
&path,
log_name,
&self.rotation_strategy,
&self.timezone_strategy,
self.max_file_size,
)?)?
.into()
}
LogTarget::Webview => {
let app_handle = app_handle.clone();
fern::Output::call(move |record| {
let payload = RecordPayload {
message: record.args().to_string(),
level: record.level().into(),
};
let app_handle = app_handle.clone();
tauri::async_runtime::spawn(async move {
app_handle.emit_all("log://log", payload).unwrap();
});
})
}
});
}
self.dispatch.apply()?;
Ok(())
})
.build()
}
}
fn get_log_file_path(
dir: &impl AsRef<Path>,
log_name: &str,
rotation_strategy: &RotationStrategy,
timezone_strategy: &TimezoneStrategy,
max_file_size: u128,
) -> plugin::Result<PathBuf> {
let path = dir.as_ref().join(log_name);
if path.exists() {
let log_size = File::open(&path)?.metadata()?.len() as u128;
if log_size > max_file_size {
match rotation_strategy {
RotationStrategy::KeepAll => {
let to = dir.as_ref().join(format!(
"{}_{}",
log_name,
timezone_strategy
.get_now()
.format(
&time::format_description::parse("[year]-[month]-[day]_[hour]-[minute]-[second]")
.unwrap()
)
.unwrap(),
));
if to.is_file() {
let mut to_bak = to.clone();
to_bak.set_file_name(format!(
"{}.bak",
to_bak.file_name().unwrap().to_string_lossy()
));
fs::rename(&to, to_bak)?;
}
fs::rename(&path, to)?;
}
RotationStrategy::KeepOne => {
fs::remove_file(&path)?;
}
}
}
}
Ok(path)
}