1use libc::c_int;
23use std::sync::Arc;
24
25mod backend;
26#[cfg(feature = "tracing")]
27mod tracing_layer;
28
29#[cfg(feature = "tracing")]
30pub use tracing_layer::{XlogLayer, XlogLayerConfig, XlogLayerHandle};
31
32#[derive(Debug, Copy, Clone, PartialEq, Eq)]
34pub enum LogLevel {
35 Verbose,
37 Debug,
39 Info,
41 Warn,
43 Error,
45 Fatal,
47 None,
49}
50
51#[derive(Debug, Copy, Clone, PartialEq, Eq)]
53pub enum AppenderMode {
54 Async,
56 Sync,
58}
59
60#[derive(Debug, Copy, Clone, PartialEq, Eq)]
62pub enum CompressMode {
63 Zlib,
65 Zstd,
67}
68
69#[derive(Debug, Copy, Clone, PartialEq, Eq)]
71pub enum FileIoAction {
72 None,
74 Success,
76 Unnecessary,
78 OpenFailed,
80 ReadFailed,
82 WriteFailed,
84 CloseFailed,
86 RemoveFailed,
88}
89
90impl From<c_int> for FileIoAction {
91 fn from(value: c_int) -> Self {
92 match value {
93 1 => FileIoAction::Success,
94 2 => FileIoAction::Unnecessary,
95 3 => FileIoAction::OpenFailed,
96 4 => FileIoAction::ReadFailed,
97 5 => FileIoAction::WriteFailed,
98 6 => FileIoAction::CloseFailed,
99 7 => FileIoAction::RemoveFailed,
100 _ => FileIoAction::None,
101 }
102 }
103}
104
105#[derive(Debug, Copy, Clone, PartialEq, Eq)]
111pub struct RawLogMeta {
112 pub pid: i64,
114 pub tid: i64,
116 pub maintid: i64,
118 pub trace_log: bool,
120}
121
122impl Default for RawLogMeta {
123 fn default() -> Self {
124 Self {
125 pid: -1,
126 tid: -1,
127 maintid: -1,
128 trace_log: false,
129 }
130 }
131}
132
133impl RawLogMeta {
134 pub const fn new(pid: i64, tid: i64, maintid: i64) -> Self {
136 Self {
137 pid,
138 tid,
139 maintid,
140 trace_log: false,
141 }
142 }
143
144 pub const fn with_trace_log(mut self, trace_log: bool) -> Self {
146 self.trace_log = trace_log;
147 self
148 }
149}
150
151#[derive(Debug, thiserror::Error)]
153pub enum XlogError {
154 #[error("log_dir and name_prefix must be non-empty")]
155 InvalidConfig,
157 #[error("logger `{name_prefix}` is already initialized with a different config")]
158 ConfigConflict {
160 name_prefix: String,
162 },
163 #[error("xlog initialization failed")]
164 InitFailed,
166}
167
168#[derive(Debug, Clone, PartialEq, Eq)]
170pub struct XlogConfig {
171 pub log_dir: String,
173 pub name_prefix: String,
175 pub pub_key: Option<String>,
177 pub cache_dir: Option<String>,
179 pub cache_days: i32,
181 pub mode: AppenderMode,
183 pub compress_mode: CompressMode,
185 pub compress_level: i32,
187}
188
189impl XlogConfig {
190 pub fn new(log_dir: impl Into<String>, name_prefix: impl Into<String>) -> Self {
192 Self {
193 log_dir: log_dir.into(),
194 name_prefix: name_prefix.into(),
195 pub_key: None,
196 cache_dir: None,
197 cache_days: 0,
198 mode: AppenderMode::Async,
199 compress_mode: CompressMode::Zlib,
200 compress_level: 6,
201 }
202 }
203
204 pub fn pub_key(mut self, key: impl Into<String>) -> Self {
206 self.pub_key = Some(key.into());
207 self
208 }
209
210 pub fn cache_dir(mut self, dir: impl Into<String>) -> Self {
212 self.cache_dir = Some(dir.into());
213 self
214 }
215
216 pub fn cache_days(mut self, days: i32) -> Self {
218 self.cache_days = days;
219 self
220 }
221
222 pub fn mode(mut self, mode: AppenderMode) -> Self {
224 self.mode = mode;
225 self
226 }
227
228 pub fn compress_mode(mut self, mode: CompressMode) -> Self {
230 self.compress_mode = mode;
231 self
232 }
233
234 pub fn compress_level(mut self, level: i32) -> Self {
236 self.compress_level = level;
237 self
238 }
239}
240
241#[derive(Clone)]
246pub struct Xlog {
247 inner: Arc<Inner>,
248}
249
250struct Inner {
251 backend: Arc<dyn backend::XlogBackend>,
252 name_prefix: String,
253}
254
255impl Xlog {
256 pub fn init(config: XlogConfig, level: LogLevel) -> Result<Self, XlogError> {
264 Self::new(config, level)
265 }
266
267 #[doc(hidden)]
268 pub fn new(config: XlogConfig, level: LogLevel) -> Result<Self, XlogError> {
269 let backend = backend::provider().new_instance(&config, level)?;
270 Ok(Self {
271 inner: Arc::new(Inner {
272 backend,
273 name_prefix: config.name_prefix,
274 }),
275 })
276 }
277
278 pub fn get(name_prefix: &str) -> Option<Self> {
280 let backend = backend::provider().get_instance(name_prefix)?;
281 Some(Self {
282 inner: Arc::new(Inner {
283 backend,
284 name_prefix: name_prefix.to_string(),
285 }),
286 })
287 }
288
289 #[doc(hidden)]
290 pub fn appender_open(config: XlogConfig, level: LogLevel) -> Result<(), XlogError> {
295 backend::provider().appender_open(&config, level)
296 }
297
298 #[doc(hidden)]
299 pub fn appender_close() {
300 backend::provider().appender_close();
301 }
302
303 #[doc(hidden)]
304 pub fn flush_all(sync: bool) {
305 backend::provider().flush_all(sync);
306 }
307
308 #[cfg(any(
309 target_os = "ios",
310 target_os = "macos",
311 target_os = "tvos",
312 target_os = "watchos"
313 ))]
314 #[doc(hidden)]
315 pub fn set_console_fun(fun: ConsoleFun) {
316 backend::provider().set_console_fun(fun);
317 }
318
319 pub fn instance(&self) -> usize {
321 self.inner.backend.instance()
322 }
323
324 pub fn is_enabled(&self, level: LogLevel) -> bool {
326 self.inner.backend.is_enabled(level)
327 }
328
329 pub fn level(&self) -> LogLevel {
331 self.inner.backend.level()
332 }
333
334 pub fn set_level(&self, level: LogLevel) {
336 self.inner.backend.set_level(level);
337 }
338
339 pub fn set_appender_mode(&self, mode: AppenderMode) {
341 self.inner.backend.set_appender_mode(mode);
342 }
343
344 pub fn flush(&self, sync: bool) {
346 self.inner.backend.flush(sync);
347 }
348
349 pub fn set_console_log_open(&self, open: bool) {
351 self.inner.backend.set_console_log_open(open);
352 }
353
354 pub fn set_max_file_size(&self, max_bytes: i64) {
356 self.inner.backend.set_max_file_size(max_bytes);
357 }
358
359 pub fn set_max_alive_time(&self, alive_seconds: i64) {
361 self.inner.backend.set_max_alive_time(alive_seconds);
362 }
363
364 #[track_caller]
369 pub fn log(&self, level: LogLevel, tag: Option<&str>, msg: impl AsRef<str>) {
370 if !self.is_enabled(level) {
371 return;
372 }
373 let loc = std::panic::Location::caller();
374 self.write_with_meta(level, tag, loc.file(), "", loc.line(), msg.as_ref());
375 }
376
377 #[track_caller]
379 pub fn write(&self, level: LogLevel, tag: Option<&str>, msg: &str) {
380 if !self.is_enabled(level) {
381 return;
382 }
383 self.write_with_meta(level, tag, "", "", 0, msg);
384 }
385
386 pub fn write_with_meta(
390 &self,
391 level: LogLevel,
392 tag: Option<&str>,
393 file: &str,
394 func: &str,
395 line: u32,
396 msg: &str,
397 ) {
398 self.write_with_meta_raw(level, tag, file, func, line, msg, RawLogMeta::default());
399 }
400
401 #[allow(clippy::too_many_arguments)]
406 pub fn write_with_meta_raw(
407 &self,
408 level: LogLevel,
409 tag: Option<&str>,
410 file: &str,
411 func: &str,
412 line: u32,
413 msg: &str,
414 raw_meta: RawLogMeta,
415 ) {
416 if !self.is_enabled(level) {
417 return;
418 }
419 self.inner.backend.write_with_meta(
420 level,
421 tag.unwrap_or(&self.inner.name_prefix),
422 file,
423 func,
424 line,
425 msg,
426 raw_meta,
427 );
428 }
429
430 #[doc(hidden)]
434 pub fn appender_write_with_meta_raw(
435 level: LogLevel,
436 tag: Option<&str>,
437 file: &str,
438 func: &str,
439 line: u32,
440 msg: &str,
441 raw_meta: RawLogMeta,
442 ) {
443 if !backend::provider().global_is_enabled(level) {
444 return;
445 }
446 backend::provider().write_global_with_meta(
447 level,
448 tag.unwrap_or(""),
449 file,
450 func,
451 line,
452 msg,
453 raw_meta,
454 );
455 }
456
457 #[doc(hidden)]
458 pub fn current_log_path() -> Option<String> {
459 backend::provider().current_log_path()
460 }
461
462 #[doc(hidden)]
463 pub fn current_log_cache_path() -> Option<String> {
464 backend::provider().current_log_cache_path()
465 }
466
467 #[doc(hidden)]
468 pub fn filepaths_from_timespan(timespan: i32, prefix: &str) -> Vec<String> {
469 backend::provider().filepaths_from_timespan(timespan, prefix)
470 }
471
472 #[doc(hidden)]
473 pub fn make_logfile_name(timespan: i32, prefix: &str) -> Vec<String> {
474 backend::provider().make_logfile_name(timespan, prefix)
475 }
476
477 #[doc(hidden)]
478 pub fn oneshot_flush(config: XlogConfig) -> Result<FileIoAction, XlogError> {
479 backend::provider().oneshot_flush(&config)
480 }
481
482 #[doc(hidden)]
483 pub fn dump(buffer: &[u8]) -> String {
484 backend::provider().dump(buffer)
485 }
486
487 #[doc(hidden)]
488 pub fn memory_dump(buffer: &[u8]) -> String {
489 backend::provider().memory_dump(buffer)
490 }
491}
492
493#[cfg(any(
494 target_os = "ios",
495 target_os = "macos",
496 target_os = "tvos",
497 target_os = "watchos"
498))]
499#[doc(hidden)]
500#[derive(Debug, Copy, Clone, PartialEq, Eq)]
501pub enum ConsoleFun {
502 Printf = 0,
504 NSLog = 1,
506 OSLog = 2,
508}
509
510#[cfg(feature = "macros")]
512#[macro_export]
513macro_rules! xlog {
514 ($logger:expr, $level:expr, $tag:expr, $($arg:tt)+) => {{
515 let logger_ref = $logger;
516 let level = $level;
517 if logger_ref.is_enabled(level) {
518 let msg = format!($($arg)+);
519 logger_ref.write_with_meta(level, Some($tag), file!(), module_path!(), line!(), &msg);
520 }
521 }};
522}
523
524#[cfg(feature = "macros")]
526#[macro_export]
527macro_rules! xlog_debug {
528 ($logger:expr, $tag:expr, $($arg:tt)+) => {{
529 $crate::xlog!($logger, $crate::LogLevel::Debug, $tag, $($arg)+)
530 }};
531}
532
533#[cfg(feature = "macros")]
535#[macro_export]
536macro_rules! xlog_info {
537 ($logger:expr, $tag:expr, $($arg:tt)+) => {{
538 $crate::xlog!($logger, $crate::LogLevel::Info, $tag, $($arg)+)
539 }};
540}
541
542#[cfg(feature = "macros")]
544#[macro_export]
545macro_rules! xlog_warn {
546 ($logger:expr, $tag:expr, $($arg:tt)+) => {{
547 $crate::xlog!($logger, $crate::LogLevel::Warn, $tag, $($arg)+)
548 }};
549}
550
551#[cfg(feature = "macros")]
553#[macro_export]
554macro_rules! xlog_error {
555 ($logger:expr, $tag:expr, $($arg:tt)+) => {{
556 $crate::xlog!($logger, $crate::LogLevel::Error, $tag, $($arg)+)
557 }};
558}
559
560#[cfg(test)]
561mod tests {
562 use std::sync::atomic::{AtomicUsize, Ordering};
563 use std::sync::{Mutex, OnceLock};
564
565 use tempfile::TempDir;
566
567 use super::{CompressMode, LogLevel, Xlog, XlogConfig, XlogError};
568
569 static NEXT_PREFIX_ID: AtomicUsize = AtomicUsize::new(1);
570 static APPENDER_TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
571
572 fn unique_prefix(label: &str) -> String {
573 let id = NEXT_PREFIX_ID.fetch_add(1, Ordering::Relaxed);
574 format!("{label}-{}-{id}", std::process::id())
575 }
576
577 fn appender_test_lock() -> &'static Mutex<()> {
578 APPENDER_TEST_LOCK.get_or_init(|| Mutex::new(()))
579 }
580
581 struct AppenderCloseGuard;
582
583 impl Drop for AppenderCloseGuard {
584 fn drop(&mut self) {
585 Xlog::appender_close();
586 }
587 }
588
589 #[test]
590 fn init_reuses_same_name_prefix_and_applies_latest_level() {
591 let dir = TempDir::new().expect("tempdir");
592 let prefix = unique_prefix("reuse");
593 let cfg = XlogConfig::new(dir.path().display().to_string(), &prefix);
594
595 let first = Xlog::init(cfg.clone(), LogLevel::Info).expect("init first");
596 let second = Xlog::init(cfg, LogLevel::Debug).expect("init second");
597
598 assert_eq!(first.instance(), second.instance());
599 assert_eq!(first.level(), LogLevel::Debug);
600 }
601
602 #[test]
603 fn init_rejects_conflicting_config_for_same_name_prefix() {
604 let dir = TempDir::new().expect("tempdir");
605 let prefix = unique_prefix("conflict");
606 let cfg = XlogConfig::new(dir.path().display().to_string(), &prefix);
607 let _first = Xlog::init(cfg.clone(), LogLevel::Info).expect("init first");
608
609 let conflict_cfg = cfg.compress_mode(CompressMode::Zstd);
610 let err = match Xlog::init(conflict_cfg, LogLevel::Info) {
611 Ok(_) => panic!("must reject conflict"),
612 Err(err) => err,
613 };
614 assert!(matches!(
615 err,
616 XlogError::ConfigConflict { ref name_prefix } if name_prefix == &prefix
617 ));
618 }
619
620 #[test]
621 fn appender_open_rejects_conflicting_config_when_default_exists() {
622 let _lock = appender_test_lock().lock().expect("lock poisoned");
623 let _guard = AppenderCloseGuard;
624 Xlog::appender_close();
625
626 let dir1 = TempDir::new().expect("tempdir1");
627 let dir2 = TempDir::new().expect("tempdir2");
628 let cfg1 = XlogConfig::new(dir1.path().display().to_string(), unique_prefix("global-a"));
629 let cfg2 = XlogConfig::new(dir2.path().display().to_string(), unique_prefix("global-b"));
630
631 Xlog::appender_open(cfg1, LogLevel::Info).expect("open first");
632 let err = Xlog::appender_open(cfg2, LogLevel::Info).expect_err("must reject conflict");
633 assert!(matches!(err, XlogError::ConfigConflict { .. }));
634 }
635}