use crate::config::{LogChannel, LogSection};
use parking_lot::RwLock;
use std::collections::HashMap;
use std::sync::OnceLock;
pub use sz_orm_logger::{LogEntry, LogLevel, Logger, LoggerFactory, StructuredLogger};
static LOG_FACADE: OnceLock<LogFacade> = OnceLock::new();
pub struct LogFacade {
default_channel: String,
logger: StructuredLogger,
channels: RwLock<HashMap<String, StructuredLogger>>,
}
impl LogFacade {
pub fn new(section: &LogSection) -> Self {
let default_channel = section.default.clone();
let default_log_level = section
.channels
.get(&default_channel)
.map(|c| parse_level(&c.level))
.unwrap_or(LogLevel::Info);
let logger = StructuredLogger::with_level(default_log_level);
let mut channels = HashMap::new();
for (name, channel_cfg) in §ion.channels {
channels.insert(name.clone(), channel_to_logger(channel_cfg));
}
LogFacade {
default_channel,
logger,
channels: RwLock::new(channels),
}
}
pub fn init(section: &LogSection) -> &'static LogFacade {
LOG_FACADE.get_or_init(|| LogFacade::new(section))
}
pub fn instance() -> Option<&'static LogFacade> {
LOG_FACADE.get()
}
pub fn default_channel(&self) -> &str {
&self.default_channel
}
pub fn logger(&self) -> &StructuredLogger {
&self.logger
}
pub fn channel(&self, name: &str) -> Option<ChannelRef<'_>> {
if self.channels.read().contains_key(name) {
Some(ChannelRef {
facade: self,
name: name.to_string(),
})
} else {
None
}
}
pub fn channel_names(&self) -> Vec<String> {
self.channels.read().keys().cloned().collect()
}
pub fn log(&self, level: LogLevel, msg: &str) {
self.logger.log(level, msg);
match level {
LogLevel::Debug => tracing::debug!("{}", msg),
LogLevel::Info => tracing::info!("{}", msg),
LogLevel::Warn => tracing::warn!("{}", msg),
LogLevel::Error => tracing::error!("{}", msg),
}
}
pub fn debug(&self, msg: &str) {
self.log(LogLevel::Debug, msg);
}
pub fn info(&self, msg: &str) {
self.log(LogLevel::Info, msg);
}
pub fn warn(&self, msg: &str) {
self.log(LogLevel::Warn, msg);
}
pub fn error(&self, msg: &str) {
self.log(LogLevel::Error, msg);
}
}
impl std::fmt::Debug for LogFacade {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LogFacade")
.field("default_channel", &self.default_channel)
.field("channels", &self.channels.read().keys().collect::<Vec<_>>())
.finish()
}
}
pub struct ChannelRef<'a> {
facade: &'a LogFacade,
name: String,
}
impl<'a> ChannelRef<'a> {
pub fn name(&self) -> &str {
&self.name
}
pub fn log(&self, level: LogLevel, msg: &str) {
let guard = self.facade.channels.read();
if let Some(logger) = guard.get(&self.name) {
logger.log(level, msg);
}
match level {
LogLevel::Debug => tracing::debug!("[{}] {}", self.name, msg),
LogLevel::Info => tracing::info!("[{}] {}", self.name, msg),
LogLevel::Warn => tracing::warn!("[{}] {}", self.name, msg),
LogLevel::Error => tracing::error!("[{}] {}", self.name, msg),
}
}
pub fn debug(&self, msg: &str) {
self.log(LogLevel::Debug, msg);
}
pub fn info(&self, msg: &str) {
self.log(LogLevel::Info, msg);
}
pub fn warn(&self, msg: &str) {
self.log(LogLevel::Warn, msg);
}
pub fn error(&self, msg: &str) {
self.log(LogLevel::Error, msg);
}
}
pub fn parse_level(s: &str) -> LogLevel {
match s.to_lowercase().as_str() {
"debug" => LogLevel::Debug,
"info" => LogLevel::Info,
"warn" | "warning" => LogLevel::Warn,
"error" => LogLevel::Error,
_ => LogLevel::Info,
}
}
fn channel_to_logger(channel: &LogChannel) -> StructuredLogger {
StructuredLogger::with_level(parse_level(&channel.level))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{LogChannel, LogSection};
fn make_log_section() -> LogSection {
let mut channels = HashMap::new();
channels.insert(
"file".to_string(),
LogChannel {
r#type: "file".to_string(),
path: "runtime/logs".to_string(),
level: "info".to_string(),
max_files: 30,
format: "%{time} [%{level}] %{message}".to_string(),
},
);
channels.insert(
"console".to_string(),
LogChannel {
r#type: "console".to_string(),
path: String::new(),
level: "debug".to_string(),
max_files: 0,
format: "%{time} [%{level}] %{message}".to_string(),
},
);
LogSection {
default: "file".to_string(),
channels,
}
}
#[test]
fn test_parse_level() {
assert_eq!(parse_level("debug"), LogLevel::Debug);
assert_eq!(parse_level("DEBUG"), LogLevel::Debug);
assert_eq!(parse_level("Debug"), LogLevel::Debug);
assert_eq!(parse_level("info"), LogLevel::Info);
assert_eq!(parse_level("INFO"), LogLevel::Info);
assert_eq!(parse_level("warn"), LogLevel::Warn);
assert_eq!(parse_level("warning"), LogLevel::Warn);
assert_eq!(parse_level("WARN"), LogLevel::Warn);
assert_eq!(parse_level("error"), LogLevel::Error);
assert_eq!(parse_level("ERROR"), LogLevel::Error);
assert_eq!(parse_level("unknown"), LogLevel::Info);
assert_eq!(parse_level(""), LogLevel::Info);
}
#[test]
fn test_log_facade_new() {
let section = make_log_section();
let facade = LogFacade::new(§ion);
assert_eq!(facade.default_channel(), "file");
let names = facade.channel_names();
assert_eq!(names.len(), 2);
assert!(names.contains(&"file".to_string()));
assert!(names.contains(&"console".to_string()));
}
#[test]
fn test_default_logger_level() {
let section = make_log_section();
let facade = LogFacade::new(§ion);
assert_eq!(facade.logger().level(), LogLevel::Info);
facade.debug("debug msg - should be filtered");
let entries = facade.logger().entries();
assert!(entries.iter().all(|e| e.level != LogLevel::Debug));
}
#[test]
fn test_log_to_default_logger() {
let section = make_log_section();
let facade = LogFacade::new(§ion);
facade.info("test info message");
facade.warn("test warn message");
facade.error("test error message");
let entries = facade.logger().entries();
assert!(entries.iter().any(|e| e.message == "test info message"));
assert!(entries.iter().any(|e| e.message == "test warn message"));
assert!(entries.iter().any(|e| e.message == "test error message"));
}
#[test]
fn test_channel_access() {
let section = make_log_section();
let facade = LogFacade::new(§ion);
let file_channel = facade.channel("file");
assert!(file_channel.is_some());
let file_channel = file_channel.unwrap();
assert_eq!(file_channel.name(), "file");
let console_channel = facade.channel("console");
assert!(console_channel.is_some());
assert!(facade.channel("nonexistent").is_none());
}
#[test]
fn test_console_channel_debug_level() {
let section = make_log_section();
let facade = LogFacade::new(§ion);
let console = facade.channel("console").unwrap();
console.debug("debug msg");
console.info("info msg");
console.warn("warn msg");
console.error("error msg");
let guard = facade.channels.read();
let console_logger = guard.get("console").unwrap();
let entries = console_logger.entries();
assert_eq!(entries.len(), 4);
}
#[test]
fn test_log_facade_init_singleton() {
let section = make_log_section();
let facade = LogFacade::init(§ion);
let facade2 = LogFacade::instance();
assert!(facade2.is_some());
assert!(std::ptr::eq(facade, facade2.unwrap()));
let section2 = make_log_section();
let facade3 = LogFacade::init(§ion2);
assert!(std::ptr::eq(facade, facade3));
}
#[test]
fn test_load_from_config_file() {
let config_dir = std::env::current_dir().ok().and_then(|d| {
let mut current = d.clone();
for _ in 0..5 {
if current.join("config").exists() {
return Some(current.join("config"));
}
if let Some(parent) = current.parent() {
current = parent.to_path_buf();
} else {
break;
}
}
None
});
let Some(config_dir) = config_dir else {
eprintln!("跳过:未找到 config 目录");
return;
};
let log_path = config_dir.join("log.yml");
if !log_path.exists() {
eprintln!("跳过:未找到 log.yml");
return;
}
let content = std::fs::read_to_string(&log_path).unwrap();
let section: LogSection = serde_yaml::from_str(&content).unwrap();
assert_eq!(section.default, "file");
assert!(section.channels.contains_key("file"));
assert!(section.channels.contains_key("console"));
let file_channel = section.channels.get("file").unwrap();
assert_eq!(file_channel.r#type, "file");
assert_eq!(file_channel.level, "info");
assert_eq!(file_channel.max_files, 30);
let console_channel = section.channels.get("console").unwrap();
assert_eq!(console_channel.r#type, "console");
assert_eq!(console_channel.level, "debug");
}
#[test]
fn test_log_facade_with_empty_channels() {
let section = LogSection::default();
let facade = LogFacade::new(§ion);
assert_eq!(facade.logger().level(), LogLevel::Info);
assert_eq!(facade.default_channel(), "");
}
#[test]
fn test_log_facade_debug_format() {
let section = make_log_section();
let facade = LogFacade::new(§ion);
let debug_str = format!("{:?}", facade);
assert!(debug_str.contains("LogFacade"));
assert!(debug_str.contains("file"));
}
}