pub mod appender;
pub mod config;
pub mod error;
#[cfg(test)]
mod tests;
use std::sync::Arc;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use crate::{
appender::LogAppender,
config::{LogFormat, LogLevel, RollingPolicy},
error::Result,
};
use self::appender::rolling::{RollingFileAppender, RollingStrategy};
pub use config::{
ConsoleConfig, JsonFormatConfig, LoggerConfig, SizeRollingConfig, TextFormatConfig, TimePeriod,
TimeRollingConfig,
};
pub use error::LoggerError;
#[derive(Clone, Debug)]
pub struct Logger {
config: config::LoggerConfig,
appender: Arc<dyn LogAppender>,
}
impl Logger {
pub fn new(config: config::LoggerConfig) -> Result<Self> {
std::fs::create_dir_all(&config.dir)?;
let appender: Arc<dyn LogAppender> = if config.split_by_level {
let mut appenders = Vec::new();
for level in [
LogLevel::Trace,
LogLevel::Debug,
LogLevel::Info,
LogLevel::Warn,
LogLevel::Error,
] {
let filename = format!("{}.{:?}", config.filename, level).to_lowercase();
let path = config.dir.join(&filename);
let appender: Arc<dyn LogAppender> = match config.rolling_policy {
RollingPolicy::Time(ref time_config) => Arc::new(RollingFileAppender::new(
path,
RollingStrategy::Time(time_config.clone()),
)?),
RollingPolicy::Size(ref size_config) => Arc::new(RollingFileAppender::new(
path,
RollingStrategy::Size(size_config.clone()),
)?),
RollingPolicy::Compound { ref time, ref size } => {
let time_appender = RollingFileAppender::new(
path.clone(),
RollingStrategy::Time(time.clone()),
)?;
let size_appender =
RollingFileAppender::new(path, RollingStrategy::Size(size.clone()))?;
Arc::new(CompoundAppender::new(time_appender, size_appender)?)
}
};
appenders.push((level, appender));
}
Arc::new(LevelSplitAppender::new(appenders))
} else {
let path = config.dir.join(&config.filename);
match config.rolling_policy {
RollingPolicy::Time(ref time_config) => Arc::new(RollingFileAppender::new(
path,
RollingStrategy::Time(time_config.clone()),
)?),
RollingPolicy::Size(ref size_config) => Arc::new(RollingFileAppender::new(
path,
RollingStrategy::Size(size_config.clone()),
)?),
RollingPolicy::Compound { ref time, ref size } => {
let time_appender = RollingFileAppender::new(
path.clone(),
RollingStrategy::Time(time.clone()),
)?;
let size_appender =
RollingFileAppender::new(path, RollingStrategy::Size(size.clone()))?;
Arc::new(CompoundAppender::new(time_appender, size_appender)?)
}
}
};
Ok(Self { config, appender })
}
pub fn write(&self, level: LogLevel, message: &str) -> Result<()> {
if level as u8 >= self.config.level as u8 {
let formatted_message = self.format_message(level, message)?;
self.appender.write(level, &formatted_message)?;
}
Ok(())
}
pub fn flush(&self) -> Result<()> {
self.appender.flush()
}
fn format_message(&self, level: LogLevel, message: &str) -> Result<String> {
match &self.config.format {
LogFormat::Text(config) => {
let mut parts = Vec::new();
let now = chrono::Local::now();
parts.push(now.format(&config.time_format).to_string());
if config.show_level {
parts.push(format!("[{}]", level));
}
if config.show_thread_id {
let thread = std::thread::current();
parts.push(format!("[Thread-{:?}]", thread.id()));
}
if config.show_target {
parts.push("[ruoyi-logger]".to_string());
}
if config.show_file || config.show_line {
let location = std::panic::Location::caller();
let mut file_info = String::new();
if config.show_file {
file_info.push_str(&format!("[{}]", location.file()));
}
if config.show_line {
file_info.push_str(&format!(":{}", location.line()));
}
parts.push(file_info);
}
parts.push(message.to_string());
Ok(parts.join(" "))
}
LogFormat::Json(config) => {
use serde_json::json;
let message_json = if message.trim().starts_with('{') {
match serde_json::from_str(message) {
Ok(json) => json,
Err(_) => json!(message),
}
} else {
json!(message)
};
let mut log_entry = json!({
"timestamp": chrono::Local::now().format(&config.time_format).to_string(),
"level": format!("{}", level),
});
if let Some(obj) = message_json.as_object() {
for (key, value) in obj {
log_entry[key] = value.clone();
}
} else {
log_entry["message"] = message_json;
}
if config.include_thread {
let thread = std::thread::current();
log_entry["thread_id"] = json!(format!("{:?}", thread.id()));
}
if config.include_caller {
let location = std::panic::Location::caller();
log_entry["caller"] = json!({
"file": location.file(),
"line": location.line(),
});
}
if config.pretty {
Ok(serde_json::to_string_pretty(&log_entry)?)
} else {
Ok(serde_json::to_string(&log_entry)?)
}
}
}
}
}
#[derive(Debug)]
struct CompoundAppender {
time_appender: RollingFileAppender,
size_appender: RollingFileAppender,
}
impl CompoundAppender {
fn new(time_appender: RollingFileAppender, size_appender: RollingFileAppender) -> Result<Self> {
Ok(Self {
time_appender,
size_appender,
})
}
}
impl LogAppender for CompoundAppender {
fn write(&self, level: LogLevel, message: &str) -> Result<()> {
self.time_appender.write(level, message)?;
self.size_appender.write(level, message)
}
fn flush(&self) -> Result<()> {
self.time_appender.flush()?;
self.size_appender.flush()
}
}
impl Clone for CompoundAppender {
fn clone(&self) -> Self {
Self {
time_appender: self.time_appender.clone(),
size_appender: self.size_appender.clone(),
}
}
}
#[derive(Debug)]
struct LevelSplitAppender {
appenders: Vec<(LogLevel, Arc<dyn LogAppender>)>,
}
impl LevelSplitAppender {
fn new(appenders: Vec<(LogLevel, Arc<dyn LogAppender>)>) -> Self {
Self { appenders }
}
}
impl LogAppender for LevelSplitAppender {
fn write(&self, level: LogLevel, message: &str) -> Result<()> {
if let Some((_, appender)) = self.appenders.iter().find(|(l, _)| *l == level) {
appender.write(level, message)?;
}
Ok(())
}
fn flush(&self) -> Result<()> {
for (_, appender) in &self.appenders {
appender.flush()?;
}
Ok(())
}
}
impl Clone for LevelSplitAppender {
fn clone(&self) -> Self {
Self {
appenders: self.appenders.clone(),
}
}
}
pub fn init(config: LoggerConfig) -> Result<()> {
let logger = Logger::new(config.clone())?;
let layer = LogLayer::new(logger);
tracing_subscriber::registry()
.with(layer)
.try_init()
.map_err(|e| error::LoggerError::Other(e.to_string()))?;
Ok(())
}
#[derive(Debug)]
struct LogLayer {
logger: Logger,
}
impl LogLayer {
fn new(logger: Logger) -> Self {
Self { logger }
}
}
impl<S> tracing_subscriber::Layer<S> for LogLayer
where
S: tracing::Subscriber,
{
fn on_event(
&self,
event: &tracing::Event<'_>,
_ctx: tracing_subscriber::layer::Context<'_, S>,
) {
let level = event.metadata().level();
let level = match *level {
tracing::Level::ERROR => LogLevel::Error,
tracing::Level::WARN => LogLevel::Warn,
tracing::Level::INFO => LogLevel::Info,
tracing::Level::DEBUG => LogLevel::Debug,
tracing::Level::TRACE => LogLevel::Trace,
};
let mut message = String::new();
let mut visitor = MessageVisitor(&mut message);
event.record(&mut visitor);
if let Err(e) = self.logger.write(level, &message) {
eprintln!("Failed to write log: {}", e);
}
}
}
struct MessageVisitor<'a>(&'a mut String);
impl<'a> tracing::field::Visit for MessageVisitor<'a> {
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
if field.name() == "message" {
self.0.push_str(&format!("{:?}", value));
}
}
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
if field.name() == "message" {
self.0.push_str(value);
}
}
}