use libc::c_int;
use std::sync::Arc;
mod backend;
#[cfg(feature = "tracing")]
mod tracing_layer;
#[cfg(feature = "tracing")]
pub use tracing_layer::{XlogLayer, XlogLayerConfig, XlogLayerHandle};
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum LogLevel {
Verbose,
Debug,
Info,
Warn,
Error,
Fatal,
None,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum AppenderMode {
Async,
Sync,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum CompressMode {
Zlib,
Zstd,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum FileIoAction {
None,
Success,
Unnecessary,
OpenFailed,
ReadFailed,
WriteFailed,
CloseFailed,
RemoveFailed,
}
impl From<c_int> for FileIoAction {
fn from(value: c_int) -> Self {
match value {
1 => FileIoAction::Success,
2 => FileIoAction::Unnecessary,
3 => FileIoAction::OpenFailed,
4 => FileIoAction::ReadFailed,
5 => FileIoAction::WriteFailed,
6 => FileIoAction::CloseFailed,
7 => FileIoAction::RemoveFailed,
_ => FileIoAction::None,
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct RawLogMeta {
pub pid: i64,
pub tid: i64,
pub maintid: i64,
pub trace_log: bool,
}
impl Default for RawLogMeta {
fn default() -> Self {
Self {
pid: -1,
tid: -1,
maintid: -1,
trace_log: false,
}
}
}
impl RawLogMeta {
pub const fn new(pid: i64, tid: i64, maintid: i64) -> Self {
Self {
pid,
tid,
maintid,
trace_log: false,
}
}
pub const fn with_trace_log(mut self, trace_log: bool) -> Self {
self.trace_log = trace_log;
self
}
}
#[derive(Debug, thiserror::Error)]
pub enum XlogError {
#[error("log_dir and name_prefix must be non-empty")]
InvalidConfig,
#[error("logger `{name_prefix}` is already initialized with a different config")]
ConfigConflict {
name_prefix: String,
},
#[error("xlog initialization failed")]
InitFailed,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct XlogConfig {
pub log_dir: String,
pub name_prefix: String,
pub pub_key: Option<String>,
pub cache_dir: Option<String>,
pub cache_days: i32,
pub mode: AppenderMode,
pub compress_mode: CompressMode,
pub compress_level: i32,
}
impl XlogConfig {
pub fn new(log_dir: impl Into<String>, name_prefix: impl Into<String>) -> Self {
Self {
log_dir: log_dir.into(),
name_prefix: name_prefix.into(),
pub_key: None,
cache_dir: None,
cache_days: 0,
mode: AppenderMode::Async,
compress_mode: CompressMode::Zlib,
compress_level: 6,
}
}
pub fn pub_key(mut self, key: impl Into<String>) -> Self {
self.pub_key = Some(key.into());
self
}
pub fn cache_dir(mut self, dir: impl Into<String>) -> Self {
self.cache_dir = Some(dir.into());
self
}
pub fn cache_days(mut self, days: i32) -> Self {
self.cache_days = days;
self
}
pub fn mode(mut self, mode: AppenderMode) -> Self {
self.mode = mode;
self
}
pub fn compress_mode(mut self, mode: CompressMode) -> Self {
self.compress_mode = mode;
self
}
pub fn compress_level(mut self, level: i32) -> Self {
self.compress_level = level;
self
}
}
#[derive(Clone)]
pub struct Xlog {
inner: Arc<Inner>,
}
struct Inner {
backend: Arc<dyn backend::XlogBackend>,
name_prefix: String,
}
impl Xlog {
pub fn init(config: XlogConfig, level: LogLevel) -> Result<Self, XlogError> {
Self::new(config, level)
}
#[doc(hidden)]
pub fn new(config: XlogConfig, level: LogLevel) -> Result<Self, XlogError> {
let backend = backend::provider().new_instance(&config, level)?;
Ok(Self {
inner: Arc::new(Inner {
backend,
name_prefix: config.name_prefix,
}),
})
}
pub fn get(name_prefix: &str) -> Option<Self> {
let backend = backend::provider().get_instance(name_prefix)?;
Some(Self {
inner: Arc::new(Inner {
backend,
name_prefix: name_prefix.to_string(),
}),
})
}
#[doc(hidden)]
pub fn appender_open(config: XlogConfig, level: LogLevel) -> Result<(), XlogError> {
backend::provider().appender_open(&config, level)
}
#[doc(hidden)]
pub fn appender_close() {
backend::provider().appender_close();
}
#[doc(hidden)]
pub fn flush_all(sync: bool) {
backend::provider().flush_all(sync);
}
#[cfg(any(
target_os = "ios",
target_os = "macos",
target_os = "tvos",
target_os = "watchos"
))]
#[doc(hidden)]
pub fn set_console_fun(fun: ConsoleFun) {
backend::provider().set_console_fun(fun);
}
pub fn instance(&self) -> usize {
self.inner.backend.instance()
}
pub fn is_enabled(&self, level: LogLevel) -> bool {
self.inner.backend.is_enabled(level)
}
pub fn level(&self) -> LogLevel {
self.inner.backend.level()
}
pub fn set_level(&self, level: LogLevel) {
self.inner.backend.set_level(level);
}
pub fn set_appender_mode(&self, mode: AppenderMode) {
self.inner.backend.set_appender_mode(mode);
}
pub fn flush(&self, sync: bool) {
self.inner.backend.flush(sync);
}
pub fn set_console_log_open(&self, open: bool) {
self.inner.backend.set_console_log_open(open);
}
pub fn set_max_file_size(&self, max_bytes: i64) {
self.inner.backend.set_max_file_size(max_bytes);
}
pub fn set_max_alive_time(&self, alive_seconds: i64) {
self.inner.backend.set_max_alive_time(alive_seconds);
}
#[track_caller]
pub fn log(&self, level: LogLevel, tag: Option<&str>, msg: impl AsRef<str>) {
if !self.is_enabled(level) {
return;
}
let loc = std::panic::Location::caller();
self.write_with_meta(level, tag, loc.file(), "", loc.line(), msg.as_ref());
}
#[track_caller]
pub fn write(&self, level: LogLevel, tag: Option<&str>, msg: &str) {
if !self.is_enabled(level) {
return;
}
self.write_with_meta(level, tag, "", "", 0, msg);
}
pub fn write_with_meta(
&self,
level: LogLevel,
tag: Option<&str>,
file: &str,
func: &str,
line: u32,
msg: &str,
) {
self.write_with_meta_raw(level, tag, file, func, line, msg, RawLogMeta::default());
}
#[allow(clippy::too_many_arguments)]
pub fn write_with_meta_raw(
&self,
level: LogLevel,
tag: Option<&str>,
file: &str,
func: &str,
line: u32,
msg: &str,
raw_meta: RawLogMeta,
) {
if !self.is_enabled(level) {
return;
}
self.inner.backend.write_with_meta(
level,
tag.unwrap_or(&self.inner.name_prefix),
file,
func,
line,
msg,
raw_meta,
);
}
#[doc(hidden)]
pub fn appender_write_with_meta_raw(
level: LogLevel,
tag: Option<&str>,
file: &str,
func: &str,
line: u32,
msg: &str,
raw_meta: RawLogMeta,
) {
if !backend::provider().global_is_enabled(level) {
return;
}
backend::provider().write_global_with_meta(
level,
tag.unwrap_or(""),
file,
func,
line,
msg,
raw_meta,
);
}
#[doc(hidden)]
pub fn current_log_path() -> Option<String> {
backend::provider().current_log_path()
}
#[doc(hidden)]
pub fn current_log_cache_path() -> Option<String> {
backend::provider().current_log_cache_path()
}
#[doc(hidden)]
pub fn filepaths_from_timespan(timespan: i32, prefix: &str) -> Vec<String> {
backend::provider().filepaths_from_timespan(timespan, prefix)
}
#[doc(hidden)]
pub fn make_logfile_name(timespan: i32, prefix: &str) -> Vec<String> {
backend::provider().make_logfile_name(timespan, prefix)
}
#[doc(hidden)]
pub fn oneshot_flush(config: XlogConfig) -> Result<FileIoAction, XlogError> {
backend::provider().oneshot_flush(&config)
}
#[doc(hidden)]
pub fn dump(buffer: &[u8]) -> String {
backend::provider().dump(buffer)
}
#[doc(hidden)]
pub fn memory_dump(buffer: &[u8]) -> String {
backend::provider().memory_dump(buffer)
}
}
#[cfg(any(
target_os = "ios",
target_os = "macos",
target_os = "tvos",
target_os = "watchos"
))]
#[doc(hidden)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum ConsoleFun {
Printf = 0,
NSLog = 1,
OSLog = 2,
}
#[cfg(feature = "macros")]
#[macro_export]
macro_rules! xlog {
($logger:expr, $level:expr, $tag:expr, $($arg:tt)+) => {{
let logger_ref = $logger;
let level = $level;
if logger_ref.is_enabled(level) {
let msg = format!($($arg)+);
logger_ref.write_with_meta(level, Some($tag), file!(), module_path!(), line!(), &msg);
}
}};
}
#[cfg(feature = "macros")]
#[macro_export]
macro_rules! xlog_debug {
($logger:expr, $tag:expr, $($arg:tt)+) => {{
$crate::xlog!($logger, $crate::LogLevel::Debug, $tag, $($arg)+)
}};
}
#[cfg(feature = "macros")]
#[macro_export]
macro_rules! xlog_info {
($logger:expr, $tag:expr, $($arg:tt)+) => {{
$crate::xlog!($logger, $crate::LogLevel::Info, $tag, $($arg)+)
}};
}
#[cfg(feature = "macros")]
#[macro_export]
macro_rules! xlog_warn {
($logger:expr, $tag:expr, $($arg:tt)+) => {{
$crate::xlog!($logger, $crate::LogLevel::Warn, $tag, $($arg)+)
}};
}
#[cfg(feature = "macros")]
#[macro_export]
macro_rules! xlog_error {
($logger:expr, $tag:expr, $($arg:tt)+) => {{
$crate::xlog!($logger, $crate::LogLevel::Error, $tag, $($arg)+)
}};
}
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Mutex, OnceLock};
use tempfile::TempDir;
use super::{CompressMode, LogLevel, Xlog, XlogConfig, XlogError};
static NEXT_PREFIX_ID: AtomicUsize = AtomicUsize::new(1);
static APPENDER_TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
fn unique_prefix(label: &str) -> String {
let id = NEXT_PREFIX_ID.fetch_add(1, Ordering::Relaxed);
format!("{label}-{}-{id}", std::process::id())
}
fn appender_test_lock() -> &'static Mutex<()> {
APPENDER_TEST_LOCK.get_or_init(|| Mutex::new(()))
}
struct AppenderCloseGuard;
impl Drop for AppenderCloseGuard {
fn drop(&mut self) {
Xlog::appender_close();
}
}
#[test]
fn init_reuses_same_name_prefix_and_applies_latest_level() {
let dir = TempDir::new().expect("tempdir");
let prefix = unique_prefix("reuse");
let cfg = XlogConfig::new(dir.path().display().to_string(), &prefix);
let first = Xlog::init(cfg.clone(), LogLevel::Info).expect("init first");
let second = Xlog::init(cfg, LogLevel::Debug).expect("init second");
assert_eq!(first.instance(), second.instance());
assert_eq!(first.level(), LogLevel::Debug);
}
#[test]
fn init_rejects_conflicting_config_for_same_name_prefix() {
let dir = TempDir::new().expect("tempdir");
let prefix = unique_prefix("conflict");
let cfg = XlogConfig::new(dir.path().display().to_string(), &prefix);
let _first = Xlog::init(cfg.clone(), LogLevel::Info).expect("init first");
let conflict_cfg = cfg.compress_mode(CompressMode::Zstd);
let err = match Xlog::init(conflict_cfg, LogLevel::Info) {
Ok(_) => panic!("must reject conflict"),
Err(err) => err,
};
assert!(matches!(
err,
XlogError::ConfigConflict { ref name_prefix } if name_prefix == &prefix
));
}
#[test]
fn appender_open_rejects_conflicting_config_when_default_exists() {
let _lock = appender_test_lock().lock().expect("lock poisoned");
let _guard = AppenderCloseGuard;
Xlog::appender_close();
let dir1 = TempDir::new().expect("tempdir1");
let dir2 = TempDir::new().expect("tempdir2");
let cfg1 = XlogConfig::new(dir1.path().display().to_string(), unique_prefix("global-a"));
let cfg2 = XlogConfig::new(dir2.path().display().to_string(), unique_prefix("global-b"));
Xlog::appender_open(cfg1, LogLevel::Info).expect("open first");
let err = Xlog::appender_open(cfg2, LogLevel::Info).expect_err("must reject conflict");
assert!(matches!(err, XlogError::ConfigConflict { .. }));
}
}