use chrono::Local;
use colored::*;
use core::fmt;
use flate2::write::GzEncoder;
use flate2::Compression;
use log::{LevelFilter, Metadata, Record};
use std::fs::File;
use std::io::{self, Read, Write};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::sync::RwLock;
use std::thread::JoinHandle;
static HANDLE: RwLock<Option<Handle>> = RwLock::new(None);
pub use log::{debug, error, info, trace, warn};
#[macro_export]
macro_rules! app {
() => {
env!("CARGO_PKG_NAME")
};
}
#[macro_export]
macro_rules! run {
() => {
$crate::stdout()
.module_filter(|m: &str| {
let pkg = env!("CARGO_PKG_NAME").replace('-', "_");
m == pkg.as_str() || m.starts_with(&format!("{}::", pkg))
})
.start();
};
}
#[allow(non_camel_case_types)]
pub type level = LevelFilter;
fn get_level(level: &str) -> LevelFilter {
let level = level.to_lowercase();
match &*level {
"debug" => level::Debug,
"trace" => level::Trace,
"info" => level::Info,
"warn" => level::Warn,
"error" => level::Error,
"off" => level::Off,
_ => level::Debug,
}
}
pub fn set_level<T: fmt::Display>(level: T) {
log::set_max_level(get_level(&level.to_string()));
}
enum Action {
Write(String),
Tee(String),
Flush,
Exit,
Redirect(String),
}
pub struct Handle {
tx: std::sync::mpsc::Sender<Action>,
thread: Option<JoinHandle<()>>,
persistent: Arc<AtomicBool>, }
pub struct Log2 {
tx: std::sync::mpsc::Sender<Action>,
rx: Option<std::sync::mpsc::Receiver<Action>>,
levels: [ColoredString; 6],
path: String,
persistent: Arc<AtomicBool>, tee: bool,
module: bool,
line: bool,
filesize: u64,
count: usize,
level: String,
compression: bool,
module_filter: Option<Box<dyn Fn(&str) -> bool + Send>>,
formatter: Option<Box<dyn Fn(&Record, bool) -> String + Send>>,
}
struct Context {
rx: std::sync::mpsc::Receiver<Action>,
path: String,
size: u64,
count: usize,
compression: bool,
}
impl Log2 {
pub fn new() -> Self {
let (tx, rx) = std::sync::mpsc::channel();
let levels = [
"OFF".black(),
"ERROR".bright_red(),
"WARN".yellow(),
"INFO".green(),
"DEBUG".bright_blue(),
"TRACE".cyan(),
];
Self {
tx,
rx: Some(rx),
levels,
path: String::new(),
persistent: Arc::new(AtomicBool::new(false)),
tee: false,
module: true,
line: true,
filesize: 100 * 1024 * 1024,
count: 10,
level: String::new(),
compression: false,
module_filter: None,
formatter: None,
}
}
pub fn module(mut self, show: bool) -> Self {
self.module = show;
self.line = false;
self
}
pub fn module_with_line(mut self, show: bool) -> Self {
self.module = show;
self.line = show;
self
}
pub fn tee(mut self, stdout: bool) -> Self {
self.tee = stdout;
self
}
pub fn size(mut self, filesize: u64) -> Self {
if self.count <= 1 {
self.filesize = u64::MAX;
} else {
self.filesize = filesize;
}
self
}
pub fn rotate(mut self, count: usize) -> Self {
self.count = count;
if self.count <= 1 {
self.filesize = u64::MAX;
}
self
}
pub fn module_filter(mut self, filter: impl Fn(&str) -> bool + Send + 'static) -> Self {
self.module_filter = Some(Box::new(filter));
self
}
pub fn format<F: Fn(&Record, bool) -> String + Send + 'static>(mut self, formatter: F) -> Self {
self.formatter = Some(Box::new(formatter));
self
}
pub fn level<T: fmt::Display>(mut self, name: T) -> Self {
self.level = name.to_string();
self
}
pub fn start(self) {
let n = self.level.clone();
let handle = start_log2(self);
if !n.is_empty() {
set_level(n);
}
*HANDLE.write().unwrap() = Some(handle);
}
pub fn compress(mut self, on: bool) -> Self {
self.compression = on;
self
}
}
unsafe impl Sync for Log2 {}
impl log::Log for Log2 {
fn enabled(&self, metadata: &Metadata) -> bool {
let n = get_level(&self.level);
metadata.level() >= n
}
fn log(&self, record: &Record) {
let module = record.module_path().unwrap_or("unknown");
if let Some(filter) = &self.module_filter {
if !filter(module) {
return;
}
}
let mut origin = String::new();
if self.module {
let mut marker = String::new();
marker.push_str(module);
if self.line {
let num = record.line().map(|l| l.to_string()).unwrap_or_default();
marker.push_str(&format!(":{}", num));
}
origin.push_str(&format!("[{}] ", marker));
}
if self.tee {
let content;
if let Some(format) = &self.formatter {
content = format(record, true);
} else {
let level = &self.levels[record.level() as usize];
let open = "[".truecolor(0x87, 0x87, 0x87);
let close = "]".truecolor(0x87, 0x87, 0x87);
content = format!(
"{open}{}{close} {open}{}{close} {origin}{}\n",
Local::now().format("%Y-%m-%d %H:%M:%S%.3f"),
level,
record.args()
);
}
let _ = self.tx.send(Action::Tee(content));
}
if self.persistent.load(Ordering::SeqCst) {
let content;
if let Some(format) = &self.formatter {
content = format(record, false);
} else {
content = format!(
"[{}] [{}] {origin}{}\n",
Local::now().format("%Y-%m-%d %H:%M:%S%.3f"),
record.level(),
record.args()
);
}
let _ = self.tx.send(Action::Write(content));
}
}
fn flush(&self) {
let _ = self.tx.send(Action::Flush);
}
}
impl Handle {
pub fn stop(&mut self) {
if let Some(thread) = self.thread.take() {
let _ = self.tx.send(Action::Exit);
let _ = thread.join();
}
}
pub fn set_level<T: fmt::Display>(&self, level: T) {
crate::set_level(level);
}
pub fn redirect(&mut self, path: &str) {
let dir = std::path::Path::new(path);
if let Some(dir) = dir.parent() {
let _ = std::fs::create_dir_all(dir);
}
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
.expect("error to open file");
self.persistent.store(true, Ordering::SeqCst);
let _ = self.tx.send(Action::Redirect(path.into()));
}
pub fn flush(&self) {
let _ = self.tx.send(Action::Flush);
}
}
impl Drop for Handle {
fn drop(&mut self) {
self.stop();
}
}
fn rotate(ctx: &Context) -> Result<std::fs::File, std::io::Error> {
let size = std::fs::metadata(&ctx.path)?.len();
let dot = ctx.path.rfind(".").unwrap_or(0);
let mut suffix = "";
let mut prefix = &ctx.path[..];
if dot > 0 {
suffix = &ctx.path[dot..];
prefix = &ctx.path[0..dot];
}
if size >= ctx.size {
for i in (0..ctx.count - 1).rev() {
let mut from = format!("{prefix}.{}{suffix}", i);
if i == 0 {
from = ctx.path.clone();
}
let to = format!("{prefix}.{}{suffix}", i + 1);
maintain(ctx, &from, &to, i);
}
}
let file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&ctx.path)?;
Ok(file)
}
fn maintain(ctx: &Context, from: &str, to: &str, index: usize) {
if ctx.compression {
if index == 0 {
if compress_file(from, to).is_ok() {
let _ = std::fs::remove_file(from);
}
} else {
let from = format!("{}.gz", from);
let to = format!("{}.gz", to);
let _ = std::fs::rename(&from, &to);
}
} else {
let _ = std::fs::rename(from, to);
}
}
fn compress_file(from: &str, to: &str) -> Result<(), io::Error> {
let to = if to.ends_with(".gz") {
to.to_string()
} else {
format!("{}.gz", to)
};
let mut input = File::open(from)?;
let output = File::create(&to)?;
let mut encoder = GzEncoder::new(output, Compression::default());
let mut buffer = vec![0; 8192];
loop {
let bytes_read = input.read(&mut buffer)?;
if bytes_read == 0 {
break;
}
encoder.write_all(&buffer[0..bytes_read])?;
}
encoder.finish()?;
Ok(())
}
fn now() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
}
fn worker(mut ctx: Context) -> Result<(), std::io::Error> {
let mut target: Option<std::fs::File> = None;
let mut size: u64 = 0;
let mut last = size;
if !ctx.path.is_empty() {
let file = rotate(&ctx)?;
size = file.metadata()?.len();
target = Some(file);
}
let timeout = std::time::Duration::from_secs(1);
let mut ts = now();
loop {
if let Ok(action) = ctx.rx.recv_timeout(timeout) {
match action {
Action::Write(line) => {
let file = target.as_mut().unwrap();
let buf = line.as_bytes();
file.write_all(buf)?;
size += buf.len() as u64;
if size >= ctx.size {
drop(target);
let f = rotate(&ctx)?;
size = f.metadata()?.len();
target = Some(f);
}
}
Action::Tee(line) => {
print!("{line}");
}
Action::Flush => {
if let Some(file) = &mut target {
file.flush()?;
}
}
Action::Exit => {
if let Some(file) = &mut target {
file.flush()?;
}
break;
}
Action::Redirect(path) => {
ctx.path = path;
drop(target);
let file = rotate(&ctx)?;
size = file.metadata()?.len();
target = Some(file);
}
}
}
if let Some(file) = &mut target {
let n: u64 = now();
if size > last && n - ts >= 1 {
ts = n;
file.flush()?;
last = size;
}
}
}
Ok(())
}
pub fn start() {
let mut logger = Log2::new();
logger.tee = true;
let handle = start_log2(logger);
*HANDLE.write().unwrap() = Some(handle);
}
pub fn handle() -> Option<std::sync::RwLockWriteGuard<'static, Option<Handle>>> {
HANDLE.write().ok()
}
pub fn reset() {
*HANDLE.write().unwrap() = None;
}
pub fn stdout() -> Log2 {
let mut logger = Log2::new();
logger.tee = true;
logger
}
pub fn open(path: &str) -> Log2 {
let dir = std::path::Path::new(path);
if let Some(dir) = dir.parent() {
let _ = std::fs::create_dir_all(dir);
}
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
.expect("error to open file");
let mut logger = Log2::new();
logger.path = path.to_string();
logger.persistent = Arc::new(AtomicBool::new(true));
logger
}
fn start_log2(mut logger: Log2) -> Handle {
let rx = logger.rx.take().unwrap();
let ctx = Context {
rx,
path: logger.path.clone(),
size: logger.filesize,
count: logger.count,
compression: logger.compression,
};
let mut handle = Handle {
tx: logger.tx.clone(),
thread: None,
persistent: logger.persistent.clone(),
};
let thread = std::thread::spawn(move || {
if let Err(message) = worker(ctx) {
println!("error: {message}");
}
});
handle.thread = Some(thread);
if log::set_boxed_logger(Box::new(logger)).is_ok() {
log::set_max_level(LevelFilter::Trace);
}
handle
}