Skip to main content

mars_xlog/
lib.rs

1//! Safe Rust wrapper for the Tencent Mars Xlog logging library.
2//!
3//! This crate owns the high-level Rust API used by platform bindings and
4//! direct Rust integrations. The default release surface is pure Rust and
5//! built on top of `mars-xlog-core`; optional metrics hooks stay feature-gated
6//! and out of the default public API.
7//!
8//! # Quick start
9//! ```
10//! use mars_xlog::{LogLevel, Xlog, XlogConfig};
11//!
12//! let cfg = XlogConfig::new("/tmp/xlog", "demo");
13//! let logger = Xlog::init(cfg, LogLevel::Info).expect("init xlog");
14//! logger.log(LogLevel::Info, None, "hello from rust");
15//! logger.flush(true);
16//! ```
17//!
18//! # Feature flags
19//! - `macros`: `xlog!` and level helpers that capture file/module/line.
20//! - `tracing`: `XlogLayer` for `tracing-subscriber`.
21//! - `metrics`: emits structured runtime metrics via the `metrics` crate.
22use 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/// Log severity levels supported by Mars Xlog.
33#[derive(Debug, Copy, Clone, PartialEq, Eq)]
34pub enum LogLevel {
35    /// Verbose diagnostic output.
36    Verbose,
37    /// Debug output for development and troubleshooting.
38    Debug,
39    /// Informational output for normal events.
40    Info,
41    /// Warning output for recoverable issues.
42    Warn,
43    /// Error output for failures that do not immediately abort the process.
44    Error,
45    /// Fatal output for unrecoverable failures.
46    Fatal,
47    /// Logging disabled.
48    None,
49}
50
51/// Controls whether logs are appended asynchronously or synchronously.
52#[derive(Debug, Copy, Clone, PartialEq, Eq)]
53pub enum AppenderMode {
54    /// Queue writes and persist them from the async worker path.
55    Async,
56    /// Write through the sync path owned by the caller.
57    Sync,
58}
59
60/// Compression algorithm used for log buffers/files.
61#[derive(Debug, Copy, Clone, PartialEq, Eq)]
62pub enum CompressMode {
63    /// Use zlib framing compatible with the historical xlog format.
64    Zlib,
65    /// Use zstd framing supported by the Rust implementation.
66    Zstd,
67}
68
69/// Result code returned by `Xlog::oneshot_flush`.
70#[derive(Debug, Copy, Clone, PartialEq, Eq)]
71pub enum FileIoAction {
72    /// No file action was taken.
73    None,
74    /// The requested file action completed successfully.
75    Success,
76    /// The requested file action was not needed.
77    Unnecessary,
78    /// Opening the source or destination file failed.
79    OpenFailed,
80    /// Reading a source file failed.
81    ReadFailed,
82    /// Writing a destination file failed.
83    WriteFailed,
84    /// Closing a file handle failed.
85    CloseFailed,
86    /// Removing a source file failed.
87    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/// Raw metadata carried by low-level wrappers (JNI/FFI parity path).
106///
107/// Semantics match Mars `XLoggerInfo`:
108/// - `pid/tid/maintid = -1` means "let backend fill runtime value".
109/// - `trace_log = true` enables Android console bypass behavior.
110#[derive(Debug, Copy, Clone, PartialEq, Eq)]
111pub struct RawLogMeta {
112    /// Process id override. Use `-1` to let the backend fill the runtime pid.
113    pub pid: i64,
114    /// Thread id override. Use `-1` to let the backend fill the runtime tid.
115    pub tid: i64,
116    /// Main thread id override. Use `-1` to let the backend fill the runtime value.
117    pub maintid: i64,
118    /// Whether Android `traceLog` console bypass behavior should be enabled.
119    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    /// Build explicit pid/tid/maintid metadata.
135    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    /// Enable Android `traceLog` console bypass for this entry.
145    pub const fn with_trace_log(mut self, trace_log: bool) -> Self {
146        self.trace_log = trace_log;
147        self
148    }
149}
150
151/// Errors returned by Xlog initialization helpers.
152#[derive(Debug, thiserror::Error)]
153pub enum XlogError {
154    #[error("log_dir and name_prefix must be non-empty")]
155    /// Required config fields such as `log_dir` or `name_prefix` were empty.
156    InvalidConfig,
157    #[error("logger `{name_prefix}` is already initialized with a different config")]
158    /// The requested `name_prefix` already exists but with a different config.
159    ConfigConflict {
160        /// Name prefix of the already-initialized logger instance.
161        name_prefix: String,
162    },
163    #[error("xlog initialization failed")]
164    /// Backend initialization failed.
165    InitFailed,
166}
167
168/// Configuration used to create an Xlog instance or open the global appender.
169#[derive(Debug, Clone, PartialEq, Eq)]
170pub struct XlogConfig {
171    /// Directory for log files. Must be non-empty.
172    pub log_dir: String,
173    /// Prefix for log file names and the instance name. Must be non-empty.
174    pub name_prefix: String,
175    /// Optional public key (hex string, 128 chars) enabling log encryption.
176    pub pub_key: Option<String>,
177    /// Optional cache directory for mmap buffers and temporary logs.
178    pub cache_dir: Option<String>,
179    /// Days to keep cached logs before moving them to `log_dir`.
180    pub cache_days: i32,
181    /// Appender mode (async or sync).
182    pub mode: AppenderMode,
183    /// Compression algorithm for log buffers/files.
184    pub compress_mode: CompressMode,
185    /// Compression level forwarded to the compressor.
186    pub compress_level: i32,
187}
188
189impl XlogConfig {
190    /// Create a config with required fields and sensible defaults.
191    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    /// Set the public key used to encrypt logs.
205    pub fn pub_key(mut self, key: impl Into<String>) -> Self {
206        self.pub_key = Some(key.into());
207        self
208    }
209
210    /// Set the optional cache directory for mmap buffers and temp files.
211    pub fn cache_dir(mut self, dir: impl Into<String>) -> Self {
212        self.cache_dir = Some(dir.into());
213        self
214    }
215
216    /// Set the number of days to keep cached logs before moving them.
217    pub fn cache_days(mut self, days: i32) -> Self {
218        self.cache_days = days;
219        self
220    }
221
222    /// Set the appender mode.
223    pub fn mode(mut self, mode: AppenderMode) -> Self {
224        self.mode = mode;
225        self
226    }
227
228    /// Set the compression algorithm.
229    pub fn compress_mode(mut self, mode: CompressMode) -> Self {
230        self.compress_mode = mode;
231        self
232    }
233
234    /// Set the compression level forwarded to the compressor.
235    pub fn compress_level(mut self, level: i32) -> Self {
236        self.compress_level = level;
237        self
238    }
239}
240
241/// Handle to a Mars Xlog instance.
242///
243/// Cloning the handle is cheap; the underlying instance is reference-counted
244/// and released when the last handle is dropped.
245#[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    /// Initialize or reuse a named Xlog instance (recommended entrypoint).
257    ///
258    /// Behavior is idempotent by `name_prefix`:
259    /// - If no live instance exists for `name_prefix`, a new instance is created.
260    /// - If a live instance exists with the same config, it is reused.
261    /// - If a live instance exists with a different config, returns
262    ///   [`XlogError::ConfigConflict`].
263    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    /// Look up an existing instance by name prefix.
279    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    /// Open the global/default appender.
291    ///
292    /// If already open with a different config, returns
293    /// [`XlogError::ConfigConflict`].
294    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    /// Returns the raw instance handle used by the underlying C++ library.
320    pub fn instance(&self) -> usize {
321        self.inner.backend.instance()
322    }
323
324    /// Returns `true` if logs at `level` are enabled for this instance.
325    pub fn is_enabled(&self, level: LogLevel) -> bool {
326        self.inner.backend.is_enabled(level)
327    }
328
329    /// Get the current log level for this instance.
330    pub fn level(&self) -> LogLevel {
331        self.inner.backend.level()
332    }
333
334    /// Set the minimum log level for this instance.
335    pub fn set_level(&self, level: LogLevel) {
336        self.inner.backend.set_level(level);
337    }
338
339    /// Switch between async and sync appender modes.
340    pub fn set_appender_mode(&self, mode: AppenderMode) {
341        self.inner.backend.set_appender_mode(mode);
342    }
343
344    /// Flush buffered logs for this instance.
345    pub fn flush(&self, sync: bool) {
346        self.inner.backend.flush(sync);
347    }
348
349    /// Enable or disable console logging for this instance (platform dependent).
350    pub fn set_console_log_open(&self, open: bool) {
351        self.inner.backend.set_console_log_open(open);
352    }
353
354    /// Set the max log file size in bytes for this instance (0 disables splitting).
355    pub fn set_max_file_size(&self, max_bytes: i64) {
356        self.inner.backend.set_max_file_size(max_bytes);
357    }
358
359    /// Set the max log file age in seconds for this instance before deletion/rotation.
360    pub fn set_max_alive_time(&self, alive_seconds: i64) {
361        self.inner.backend.set_max_alive_time(alive_seconds);
362    }
363
364    /// Log a message with caller file/line captured via `#[track_caller]`.
365    ///
366    /// Note: function name is not available here; use `xlog!` macro or
367    /// `write_with_meta` when you need full metadata.
368    #[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    /// Compatibility wrapper for older APIs. Prefer `log` or the macros.
378    #[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    /// Log with explicit metadata (file, function, line).
387    ///
388    /// Use this when callers already provide metadata (for example from JNI).
389    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    /// Log with explicit metadata and raw pid/tid/trace flags.
402    ///
403    /// This is mainly for low-level platform wrappers that already own thread
404    /// metadata (for example JNI side thread ids).
405    #[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    /// Write via the global/default appender with raw metadata.
431    ///
432    /// This mirrors the C++ `XloggerWrite(instance_ptr == 0, ...)` path.
433    #[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    /// Forward console output through `printf`.
503    Printf = 0,
504    /// Forward console output through `NSLog`.
505    NSLog = 1,
506    /// Forward console output through `os_log`.
507    OSLog = 2,
508}
509
510/// Log with explicit metadata captured by the macro call site.
511#[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/// Convenience macro for `LogLevel::Debug`.
525#[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/// Convenience macro for `LogLevel::Info`.
534#[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/// Convenience macro for `LogLevel::Warn`.
543#[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/// Convenience macro for `LogLevel::Error`.
552#[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}