1use crate::config::{LogChannel, LogSection};
27use parking_lot::RwLock;
28use std::collections::HashMap;
29use std::sync::OnceLock;
30
31pub use crate::orm::logger::{LogEntry, LogLevel, Logger, LoggerFactory, StructuredLogger};
33
34static LOG_FACADE: OnceLock<LogFacade> = OnceLock::new();
36
37pub struct LogFacade {
41 default_channel: String,
43 logger: StructuredLogger,
45 channels: RwLock<HashMap<String, StructuredLogger>>,
47}
48
49impl LogFacade {
50 pub fn new(section: &LogSection) -> Self {
52 let default_channel = section.default.clone();
53 let default_log_level = section
54 .channels
55 .get(&default_channel)
56 .map(|c| parse_level(&c.level))
57 .unwrap_or(LogLevel::Info);
58 let logger = StructuredLogger::with_level(default_log_level);
59
60 let mut channels = HashMap::new();
61 for (name, channel_cfg) in §ion.channels {
62 channels.insert(name.clone(), channel_to_logger(channel_cfg));
63 }
64
65 LogFacade {
66 default_channel,
67 logger,
68 channels: RwLock::new(channels),
69 }
70 }
71
72 pub fn init(section: &LogSection) -> &'static LogFacade {
76 LOG_FACADE.get_or_init(|| LogFacade::new(section))
77 }
78
79 pub fn instance() -> Option<&'static LogFacade> {
83 LOG_FACADE.get()
84 }
85
86 pub fn default_channel(&self) -> &str {
88 &self.default_channel
89 }
90
91 pub fn logger(&self) -> &StructuredLogger {
93 &self.logger
94 }
95
96 pub fn channel(&self, name: &str) -> Option<ChannelRef<'_>> {
100 if self.channels.read().contains_key(name) {
101 Some(ChannelRef {
102 facade: self,
103 name: name.to_string(),
104 })
105 } else {
106 None
107 }
108 }
109
110 pub fn channel_names(&self) -> Vec<String> {
112 self.channels.read().keys().cloned().collect()
113 }
114
115 pub fn log(&self, level: LogLevel, msg: &str) {
117 self.logger.log(level, msg);
118 match level {
119 LogLevel::Debug => tracing::debug!("{}", msg),
120 LogLevel::Info => tracing::info!("{}", msg),
121 LogLevel::Warn => tracing::warn!("{}", msg),
122 LogLevel::Error => tracing::error!("{}", msg),
123 }
124 }
125
126 pub fn debug(&self, msg: &str) {
128 self.log(LogLevel::Debug, msg);
129 }
130
131 pub fn info(&self, msg: &str) {
133 self.log(LogLevel::Info, msg);
134 }
135
136 pub fn warn(&self, msg: &str) {
138 self.log(LogLevel::Warn, msg);
139 }
140
141 pub fn error(&self, msg: &str) {
143 self.log(LogLevel::Error, msg);
144 }
145}
146
147impl std::fmt::Debug for LogFacade {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 f.debug_struct("LogFacade")
150 .field("default_channel", &self.default_channel)
151 .field("channels", &self.channels.read().keys().collect::<Vec<_>>())
152 .finish()
153 }
154}
155
156pub struct ChannelRef<'a> {
160 facade: &'a LogFacade,
161 name: String,
162}
163
164impl<'a> ChannelRef<'a> {
165 pub fn name(&self) -> &str {
167 &self.name
168 }
169
170 pub fn log(&self, level: LogLevel, msg: &str) {
172 let guard = self.facade.channels.read();
173 if let Some(logger) = guard.get(&self.name) {
174 logger.log(level, msg);
175 }
176 match level {
177 LogLevel::Debug => tracing::debug!("[{}] {}", self.name, msg),
178 LogLevel::Info => tracing::info!("[{}] {}", self.name, msg),
179 LogLevel::Warn => tracing::warn!("[{}] {}", self.name, msg),
180 LogLevel::Error => tracing::error!("[{}] {}", self.name, msg),
181 }
182 }
183
184 pub fn debug(&self, msg: &str) {
186 self.log(LogLevel::Debug, msg);
187 }
188
189 pub fn info(&self, msg: &str) {
191 self.log(LogLevel::Info, msg);
192 }
193
194 pub fn warn(&self, msg: &str) {
196 self.log(LogLevel::Warn, msg);
197 }
198
199 pub fn error(&self, msg: &str) {
201 self.log(LogLevel::Error, msg);
202 }
203}
204
205pub fn parse_level(s: &str) -> LogLevel {
210 match s.to_lowercase().as_str() {
211 "debug" => LogLevel::Debug,
212 "info" => LogLevel::Info,
213 "warn" | "warning" => LogLevel::Warn,
214 "error" => LogLevel::Error,
215 _ => LogLevel::Info,
216 }
217}
218
219fn channel_to_logger(channel: &LogChannel) -> StructuredLogger {
221 StructuredLogger::with_level(parse_level(&channel.level))
222}
223
224#[cfg(test)]
229mod tests {
230 use super::*;
231 use crate::config::{LogChannel, LogSection};
232
233 fn make_log_section() -> LogSection {
235 let mut channels = HashMap::new();
236 channels.insert(
237 "file".to_string(),
238 LogChannel {
239 r#type: "file".to_string(),
240 path: "runtime/logs".to_string(),
241 level: "info".to_string(),
242 max_files: 30,
243 format: "%{time} [%{level}] %{message}".to_string(),
244 },
245 );
246 channels.insert(
247 "console".to_string(),
248 LogChannel {
249 r#type: "console".to_string(),
250 path: String::new(),
251 level: "debug".to_string(),
252 max_files: 0,
253 format: "%{time} [%{level}] %{message}".to_string(),
254 },
255 );
256 LogSection {
257 default: "file".to_string(),
258 channels,
259 }
260 }
261
262 #[test]
264 fn test_parse_level() {
265 assert_eq!(parse_level("debug"), LogLevel::Debug);
266 assert_eq!(parse_level("DEBUG"), LogLevel::Debug);
267 assert_eq!(parse_level("Debug"), LogLevel::Debug);
268 assert_eq!(parse_level("info"), LogLevel::Info);
269 assert_eq!(parse_level("INFO"), LogLevel::Info);
270 assert_eq!(parse_level("warn"), LogLevel::Warn);
271 assert_eq!(parse_level("warning"), LogLevel::Warn);
272 assert_eq!(parse_level("WARN"), LogLevel::Warn);
273 assert_eq!(parse_level("error"), LogLevel::Error);
274 assert_eq!(parse_level("ERROR"), LogLevel::Error);
275 assert_eq!(parse_level("unknown"), LogLevel::Info);
277 assert_eq!(parse_level(""), LogLevel::Info);
278 }
279
280 #[test]
282 fn test_log_facade_new() {
283 let section = make_log_section();
284 let facade = LogFacade::new(§ion);
285
286 assert_eq!(facade.default_channel(), "file");
287 let names = facade.channel_names();
288 assert_eq!(names.len(), 2);
289 assert!(names.contains(&"file".to_string()));
290 assert!(names.contains(&"console".to_string()));
291 }
292
293 #[test]
295 fn test_default_logger_level() {
296 let section = make_log_section();
297 let facade = LogFacade::new(§ion);
298
299 assert_eq!(facade.logger().level(), LogLevel::Info);
301
302 facade.debug("debug msg - should be filtered");
304 let entries = facade.logger().entries();
305 assert!(entries.iter().all(|e| e.level != LogLevel::Debug));
306 }
307
308 #[test]
310 fn test_log_to_default_logger() {
311 let section = make_log_section();
312 let facade = LogFacade::new(§ion);
313
314 facade.info("test info message");
315 facade.warn("test warn message");
316 facade.error("test error message");
317
318 let entries = facade.logger().entries();
319 assert!(entries.iter().any(|e| e.message == "test info message"));
320 assert!(entries.iter().any(|e| e.message == "test warn message"));
321 assert!(entries.iter().any(|e| e.message == "test error message"));
322 }
323
324 #[test]
326 fn test_channel_access() {
327 let section = make_log_section();
328 let facade = LogFacade::new(§ion);
329
330 let file_channel = facade.channel("file");
332 assert!(file_channel.is_some());
333 let file_channel = file_channel.unwrap();
334 assert_eq!(file_channel.name(), "file");
335
336 let console_channel = facade.channel("console");
338 assert!(console_channel.is_some());
339
340 assert!(facade.channel("nonexistent").is_none());
342 }
343
344 #[test]
346 fn test_console_channel_debug_level() {
347 let section = make_log_section();
348 let facade = LogFacade::new(§ion);
349
350 let console = facade.channel("console").unwrap();
351 console.debug("debug msg");
352 console.info("info msg");
353 console.warn("warn msg");
354 console.error("error msg");
355
356 let guard = facade.channels.read();
358 let console_logger = guard.get("console").unwrap();
359 let entries = console_logger.entries();
360 assert_eq!(entries.len(), 4);
361 }
362
363 #[test]
365 fn test_log_facade_init_singleton() {
366 let section = make_log_section();
367 let facade = LogFacade::init(§ion);
368
369 let facade2 = LogFacade::instance();
371 assert!(facade2.is_some());
372 assert!(std::ptr::eq(facade, facade2.unwrap()));
373
374 let section2 = make_log_section();
376 let facade3 = LogFacade::init(§ion2);
377 assert!(std::ptr::eq(facade, facade3));
378 }
379
380 #[test]
382 fn test_load_from_config_file() {
383 let config_dir = std::env::current_dir().ok().and_then(|d| {
385 let mut current = d.clone();
386 for _ in 0..5 {
387 if current.join("config").exists() {
388 return Some(current.join("config"));
389 }
390 if let Some(parent) = current.parent() {
391 current = parent.to_path_buf();
392 } else {
393 break;
394 }
395 }
396 None
397 });
398
399 let Some(config_dir) = config_dir else {
400 eprintln!("跳过:未找到 config 目录");
401 return;
402 };
403
404 let log_path = config_dir.join("log.yml");
405 if !log_path.exists() {
406 eprintln!("跳过:未找到 log.yml");
407 return;
408 }
409
410 let content = std::fs::read_to_string(&log_path).unwrap();
411 let section: LogSection = serde_yml::from_str(&content).unwrap();
412
413 assert_eq!(section.default, "file");
415
416 assert!(section.channels.contains_key("file"));
418 assert!(section.channels.contains_key("console"));
419
420 let file_channel = section.channels.get("file").unwrap();
422 assert_eq!(file_channel.r#type, "file");
423 assert_eq!(file_channel.level, "info");
424 assert_eq!(file_channel.max_files, 30);
425
426 let console_channel = section.channels.get("console").unwrap();
428 assert_eq!(console_channel.r#type, "console");
429 assert_eq!(console_channel.level, "debug");
430 }
431
432 #[test]
434 fn test_log_facade_with_empty_channels() {
435 let section = LogSection::default();
436 let facade = LogFacade::new(§ion);
437
438 assert_eq!(facade.logger().level(), LogLevel::Info);
440 assert_eq!(facade.default_channel(), "");
441 }
442
443 #[test]
445 fn test_log_facade_debug_format() {
446 let section = make_log_section();
447 let facade = LogFacade::new(§ion);
448
449 let debug_str = format!("{:?}", facade);
450 assert!(debug_str.contains("LogFacade"));
451 assert!(debug_str.contains("file"));
452 }
453}