//
// Syd: rock-solid application kernel
// src/syslog.rs: syslog(2) interface
//
// Copyright (c) 2023, 2024, 2025 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

// syslog.rs
//
// A self-contained Rust module that implements a kernel-like syslog(2)
// interface on top of a ring buffer from `ringbuf`, with multi-producer
// and multi-consumer semantics, using exactly one `parking_lot::RwLock`
// to synchronize concurrent producers (writers) and consumers
// (readers).
//
// SECURITY & CONCURRENCY DISCLAIMER:
//   - The ring buffer is shared among multiple producer threads (which
//   take a write lock) and multiple consumer threads (which typically
//   take a read lock). Certain consumer operations that modify the ring
//   (like CLEAR, READ_CLEAR) require upgradable or exclusive write
//   locks. We minimize lock hold times.
//   - Once locked (via `lock()`), the ring buffer is freed and any
//   subsequent ring-based operations return EPERM. Writes to the main
//   fd continue. Writes to the console fd do NOT continue after locked.
//
// If the "log" feature is not enabled, we provide stubs that return ENOSYS.

use btoi::btoi;
use memchr::arch::all::is_equal;

/// Kernel log levels (KERN_*).
#[derive(Copy, Clone, Debug)]
pub enum LogLevel {
    /// KERN_EMERG
    Emergent = 0,
    /// KERN_ALERT
    Alert = 1,
    /// KERN_CRIT
    Crit = 2,
    /// KERN_ERR
    Err = 3,
    /// KERN_WARNING
    Warn = 4,
    /// KERN_NOTICE
    Notice = 5,
    /// KERN_INFO
    Info = 6,
    /// KERN_DEBUG
    Debug = 7,
}

impl LogLevel {
    /// Converts `LogLevel` to a number.
    pub fn as_u8(self) -> u8 {
        self as u8
    }

    /// Converts `LogLevel` to a bytestring.
    pub fn as_bytes(self) -> &'static [u8] {
        match self {
            Self::Emergent => b"emerg",
            Self::Alert => b"alert",
            Self::Crit => b"crit",
            Self::Err => b"error",
            Self::Warn => b"warn",
            Self::Notice => b"notice",
            Self::Info => b"info",
            Self::Debug => b"debug",
        }
    }
}

impl From<u8> for LogLevel {
    fn from(level: u8) -> Self {
        let level = level.clamp(Self::Emergent.as_u8(), Self::Debug.as_u8());

        if level == Self::Emergent.as_u8() {
            Self::Emergent
        } else if level == Self::Alert.as_u8() {
            Self::Alert
        } else if level == Self::Crit.as_u8() {
            Self::Crit
        } else if level == Self::Err.as_u8() {
            Self::Err
        } else if level == Self::Warn.as_u8() {
            Self::Warn
        } else if level == Self::Notice.as_u8() {
            Self::Notice
        } else if level == Self::Info.as_u8() {
            Self::Info
        } else {
            Self::Debug
        }
    }
}

impl From<i64> for LogLevel {
    #[expect(clippy::cast_possible_truncation)]
    #[expect(clippy::cast_sign_loss)]
    fn from(level: i64) -> Self {
        (level.clamp(Self::Emergent.as_u8().into(), Self::Debug.as_u8().into()) as u8).into()
    }
}

/// Translate a string log level from environment to `LogLevel`
/// or fallback to the given default `LogLevel`.
pub fn parse_loglevel(level: &[u8], default: LogLevel) -> LogLevel {
    let level = level.trim_ascii();
    if level.is_empty() {
        default
    } else if let Ok(level) = btoi::<i64>(level) {
        level.into()
    } else if is_equal(level, b"emerg") {
        LogLevel::Emergent
    } else if is_equal(level, b"alert") {
        LogLevel::Alert
    } else if is_equal(level, b"crit") {
        LogLevel::Crit
    } else if is_equal(level, b"error") {
        LogLevel::Err
    } else if is_equal(level, b"warn") {
        LogLevel::Warn
    } else if is_equal(level, b"notice") {
        LogLevel::Notice
    } else if is_equal(level, b"info") {
        LogLevel::Info
    } else if is_equal(level, b"debug") {
        LogLevel::Debug
    } else {
        default
    }
}

//
// Syslog action constants
//

/// Close the log.  Currently a NOP.
pub const SYSLOG_ACTION_CLOSE: libc::c_int = 0;

/// Open the log.  Currently a NOP.
pub const SYSLOG_ACTION_OPEN: libc::c_int = 1;

/// Read from the log. The call waits until the kernel log buffer
/// is nonempty, and then reads at most len bytes into the buffer
/// pointed to by bufp. The call returns the number of bytes read.
/// Bytes read from the log disappear from the log buffer: the
/// information can be read only once. This is the function
/// executed by the kernel when a user program reads /proc/kmsg.
pub const SYSLOG_ACTION_READ: libc::c_int = 2;

/// Read all messages remaining in the ring buffer, placing them in
/// the buffer pointed to by bufp.  The call reads the last len
/// bytes from the log buffer (nondestructively), but will not read
/// more than was written into the buffer since the last "clear ring
/// buffer" command (see command 5 below)).  The call returns the
/// number of bytes read.
pub const SYSLOG_ACTION_READ_ALL: libc::c_int = 3;

/// Read and clear all messages remaining in the ring buffer.
/// The call does precisely the same as for a type of 3, but
/// also executes the "clear ring buffer" command.
pub const SYSLOG_ACTION_READ_CLEAR: libc::c_int = 4;

/// The call executes just the "clear ring buffer" command.
/// The bufp and len arguments are ignored.
///
/// This command does not really clear the ring buffer.
/// Rather, it sets a kernel bookkeeping variable that
/// determines the results returned by commands 3
/// (SYSLOG_ACTION_READ_ALL) and 4 (SYSLOG_ACTION_READ_CLEAR).
/// This command has no effect on commands 2
/// (SYSLOG_ACTION_READ) and 9 (SYSLOG_ACTION_SIZE_UNREAD).
pub const SYSLOG_ACTION_CLEAR: libc::c_int = 5;

/// The command saves the current value of console_loglevel
/// and then sets console_loglevel to
/// minimum_console_loglevel, so that no messages are printed
/// to the console.  Before Linux 2.6.32, the command simply
/// sets console_loglevel to minimum_console_loglevel.  See
/// the discussion of /proc/sys/kernel/printk, below.
///
/// The bufp and len arguments are ignored.
pub const SYSLOG_ACTION_CONSOLE_OFF: libc::c_int = 6;

/// If a previous SYSLOG_ACTION_CONSOLE_OFF command has been
/// performed, this command restores console_loglevel to the
/// value that was saved by that command.  Before Linux
/// 2.6.32, this command simply sets console_loglevel to
/// default_console_loglevel.  See the discussion of
/// /proc/sys/kernel/printk, below.
///
/// The bufp and len arguments are ignored.
pub const SYSLOG_ACTION_CONSOLE_ON: libc::c_int = 7;

/// The call sets console_loglevel to the value given in len,
/// which must be an integer between 1 and 8 (inclusive).  The
/// kernel silently enforces a minimum value of
/// minimum_console_loglevel for len.  See the log level
/// section for details.  The bufp argument is ignored.
pub const SYSLOG_ACTION_CONSOLE_LEVEL: libc::c_int = 8;

/// The call returns the number of bytes currently available
/// to be read from the kernel log buffer via command 2
/// (SYSLOG_ACTION_READ).  The bufp and len arguments are
/// ignored.
pub const SYSLOG_ACTION_SIZE_UNREAD: libc::c_int = 9;

/// This command returns the total size of the kernel log
/// buffer.  The bufp and len arguments are ignored.
pub const SYSLOG_ACTION_SIZE_BUFFER: libc::c_int = 10;

// LOG-ENABLED IMPLEMENTATION
#[cfg(feature = "log")]
mod syslog_enabled {
    use std::{
        io::{BufWriter, Write},
        mem::MaybeUninit,
        os::fd::BorrowedFd,
        sync::{
            atomic::{AtomicBool, AtomicU8, Ordering},
            OnceLock, RwLock,
        },
    };

    use nix::{
        errno::Errno,
        time::{clock_gettime, ClockId},
    };
    use ringbuf::{
        storage::{Array, Heap},
        traits::*,
        wrap::caching::Caching,
        Arc, SharedRb,
    };

    use crate::{config::SYSLOG_STACK_SIZE, log::LockedWriter, syslog::*};

    // Store ring data in an enum to handle "heap" vs "static" capacity.
    // Store Arc<SharedRb<...>> plus Caching wrappers to fix all trait bounds.
    enum RbMode {
        Heap {
            shared: Arc<SharedRb<Heap<u8>>>,
            prod: Caching<Arc<SharedRb<Heap<u8>>>, true, false>,
            cons: Caching<Arc<SharedRb<Heap<u8>>>, false, true>,
        },
        Static {
            shared: Arc<SharedRb<Array<u8, SYSLOG_STACK_SIZE>>>,
            prod: Caching<Arc<SharedRb<Array<u8, SYSLOG_STACK_SIZE>>>, true, false>,
            cons: Caching<Arc<SharedRb<Array<u8, SYSLOG_STACK_SIZE>>>, false, true>,
        },
    }

    // The main ring data behind the RwLock. Freed after locked==true.
    struct RingData {
        mode: RbMode,
    }

    /// Syslog: multi-producer, multi-consumer ring protected by RwLock.
    ///
    ///  - Many producers each do ring_lock.write() to push logs.
    ///  - Many consumers do ring_lock.read() to read logs.
    ///  - read_clear or clear requires write lock.
    ///  - If locked => ring is freed => ring ops => EPERM.
    ///    We still write to fd + host syslog after locking.
    pub struct Syslog {
        ring_lock: RwLock<Option<RingData>>,

        // Ring is freed when it's locked.
        locked: AtomicBool,

        // Log level.
        level: AtomicU8,
    }

    // SAFETY: The ringbuf uses interior mutability, but SharedRb + Arc
    // is thread-safe. So we can allow Syslog to be Sync:
    unsafe impl Sync for Syslog {}

    impl Syslog {
        /// Creates a multi-producer, multi-consumer Syslog.
        /// - If `use_stack == true`, uses a SharedRb<Array<u8, SYSLOG_STACK_SIZE>>.
        /// - Otherwise, uses a SharedRb<Heap<u8>> of capacity `capacity`.
        pub fn new(capacity: usize, level: LogLevel, use_stack: bool) -> Self {
            let ring_data = if use_stack {
                // Static array-based ring
                let shared = Arc::new(SharedRb::<Array<u8, SYSLOG_STACK_SIZE>>::default());
                let prod = Caching::new(Arc::clone(&shared));
                let cons = Caching::new(Arc::clone(&shared));
                Some(RingData {
                    mode: RbMode::Static { shared, prod, cons },
                })
            } else {
                // Heap-based ring
                // ringbuf expects capacity > 0
                // (the caller is presumably ensuring capacity > 0 if not stack).
                let shared = Arc::new(SharedRb::<Heap<u8>>::new(capacity));
                let prod = Caching::new(Arc::clone(&shared));
                let cons = Caching::new(Arc::clone(&shared));
                Some(RingData {
                    mode: RbMode::Heap { shared, prod, cons },
                })
            };

            Syslog {
                ring_lock: RwLock::new(ring_data),
                locked: AtomicBool::new(false),
                level: AtomicU8::new(level as u8),
            }
        }

        /// Write a log message at `level`.
        /// - Always writes the raw message to the `fd` (if present).
        /// - If not locked, also writes the formatted ring message to the ring,
        ///   under a short write lock.
        ///
        /// If locked => no ring operations, but still writes to fd + host syslog.
        pub fn write_log(&self, level: LogLevel, msg: &str, msg_pretty: Option<&str>) {
            if level.as_u8() > self.loglevel() {
                // Return immediately if the level is not enabled.
                return;
            }

            // Always attempt to write message to log fd, append a new line to the message.
            // Use formatting as necessary. We take a OFD write lock here.
            let msg = msg_pretty.unwrap_or(msg);
            if let Some(fd) = Self::logfd() {
                if let Ok(mut writer) = LockedWriter::new(fd).map(BufWriter::new) {
                    let _ = writer.write_all(msg.as_bytes());
                    let _ = writer.write_all(b"\n");
                }
            }

            // If locked => skip ring.
            if self.is_locked() {
                return;
            }

            // Build ring message, use formatting as necessary.
            let ring_str = self.format_ring_message(level, msg);

            // Acquire write lock for ring push.
            {
                let mut guard = self
                    .ring_lock
                    .write()
                    .unwrap_or_else(|err| err.into_inner());
                if self.locked.load(Ordering::SeqCst) {
                    // ring locked in meantime => skip
                } else if let Some(ring_data) = guard.as_mut() {
                    match &mut ring_data.mode {
                        RbMode::Heap { prod, .. } => {
                            let _ = prod.push_slice(ring_str.as_bytes());
                        }
                        RbMode::Static { prod, .. } => {
                            let _ = prod.push_slice(ring_str.as_bytes());
                        }
                    }
                }
            }
        }

        /// syslog(2)-like interface. Returns Ok((count, data)) on success,
        /// or Err(errno) on error.
        ///
        /// # Errors:
        /// - EPERM if locked (// ring is freed, cannot proceed).
        ///
        /// Multiple consumers can do read locks, but if they need to mutate
        /// ring (like CLEAR), they do an exclusive lock.
        #[expect(clippy::type_complexity)]
        pub fn syslog(
            &self,
            action: libc::c_int,
            len: usize,
        ) -> Result<(usize, Option<Vec<u8>>), Errno> {
            if self.is_locked() {
                // ring is locked => EPERM
                // ring is freed, no ring ops allowed.
                return Err(Errno::EPERM);
            }

            match action {
                SYSLOG_ACTION_CLOSE | SYSLOG_ACTION_OPEN => Ok((0, None)),

                SYSLOG_ACTION_READ => {
                    if len == 0 {
                        return Ok((0, None));
                    }
                    // We'll do a write lock to gather data and then to pop.
                    let mut write_guard = self.ring_lock.try_write().or(Err(Errno::EINTR))?;
                    let ring_data = match write_guard.as_mut() {
                        None => {
                            // ring is None => locked/freed in between.
                            return Ok((0, None));
                        }
                        Some(ring_data) => ring_data,
                    };
                    Ok(self.read_and_consume(ring_data, len))
                }

                SYSLOG_ACTION_READ_ALL => {
                    if len == 0 {
                        return Ok((0, None));
                    }
                    // read lock, gather data, do not consume.
                    let read_guard = self.ring_lock.try_read().or(Err(Errno::EINTR))?;
                    let ring_data = match read_guard.as_ref() {
                        None => {
                            // ring freed.
                            return Ok((0, None));
                        }
                        Some(ring_data) => ring_data,
                    };
                    Ok(self.read_all_no_consume(ring_data, len))
                }

                SYSLOG_ACTION_READ_CLEAR => {
                    if len == 0 {
                        return Ok((0, None));
                    }
                    // exclusive lock, read data & pop.
                    let mut write_guard = self.ring_lock.try_write().or(Err(Errno::EINTR))?;
                    let ring_data = match write_guard.as_mut() {
                        None => {
                            // ring is None => locked/freed in between.
                            return Ok((0, None));
                        }
                        Some(ring_data) => ring_data,
                    };
                    let (count, data_vec) = self.read_all_no_consume_mut(ring_data, len);
                    if count > 0 {
                        self.pop_count(ring_data, count);
                    }
                    Ok((count, data_vec))
                }

                SYSLOG_ACTION_CLEAR => {
                    // exclusive lock, skip entire ring.
                    let mut write_guard = self.ring_lock.try_write().or(Err(Errno::EINTR))?;
                    let ring_data = match write_guard.as_mut() {
                        None => {
                            // ring is None => locked/freed in between.
                            return Ok((0, None));
                        }
                        Some(ring_data) => ring_data,
                    };
                    self.skip_all(ring_data);
                    Ok((0, None))
                }

                SYSLOG_ACTION_CONSOLE_OFF => {
                    self.set_loglevel(LogLevel::Emergent.as_u8());
                    Ok((0, None))
                }

                SYSLOG_ACTION_CONSOLE_ON => {
                    self.set_loglevel(LogLevel::Warn.as_u8());
                    Ok((0, None))
                }

                SYSLOG_ACTION_CONSOLE_LEVEL => {
                    let level: u8 = len.try_into().or(Err(Errno::EINVAL))?;
                    self.set_loglevel(level);
                    Ok((0, None))
                }

                SYSLOG_ACTION_SIZE_UNREAD => {
                    let read_guard = self.ring_lock.try_read().or(Err(Errno::EINTR))?;
                    let ring_data = match read_guard.as_ref() {
                        None => {
                            // ring freed.
                            return Ok((0, None));
                        }
                        Some(ring_data) => ring_data,
                    };
                    let unread = self.ring_unread(ring_data);
                    Ok((unread, None))
                }

                SYSLOG_ACTION_SIZE_BUFFER => {
                    let read_guard = self.ring_lock.try_read().or(Err(Errno::EINTR))?;
                    let ring_data = match read_guard.as_ref() {
                        None => {
                            // ring freed.
                            return Ok((0, None));
                        }
                        Some(ring_data) => ring_data,
                    };
                    let cap = self.ring_capacity(ring_data);
                    Ok((cap, None))
                }

                _ => {
                    // invalid action
                    Err(Errno::EINVAL)
                }
            }
        }

        /// Return `Some(BorrowedFd)` if logging is enabled, `None` otherwise.
        pub fn logfd() -> Option<BorrowedFd<'static>> {
            let fd = crate::log::LOG_FD.load(Ordering::Relaxed);
            if fd >= 0 {
                // SAFETY: `LOG_FD` is valid for process lifetime.
                Some(unsafe { BorrowedFd::borrow_raw(fd) })
            } else {
                None
            }
        }

        /// Returns the current console log level (0..7).
        pub fn loglevel(&self) -> u8 {
            self.level.load(Ordering::SeqCst)
        }

        /// Sets console log level, clamped to [0..7].
        pub fn set_loglevel(&self, level: u8) {
            let lv = level.clamp(LogLevel::Emergent.as_u8(), LogLevel::Debug.as_u8());
            self.level.store(lv, Ordering::SeqCst);
        }

        /// Returns true if syslog is locked.
        pub fn is_locked(&self) -> bool {
            self.locked.load(Ordering::SeqCst)
        }

        /// Lock the syslog, freeing ring data. Return true if we locked now,
        /// false if already locked previously.
        pub fn lock(&self) -> bool {
            // Attempt to set locked from false->true
            if self
                .locked
                .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
                .is_ok()
            {
                // Freed ring
                let mut guard = self
                    .ring_lock
                    .write()
                    .unwrap_or_else(|err| err.into_inner());
                *guard = None; // ring data freed.
                true
            } else {
                false
            }
        }

        // HELPER: Format "<LEVEL>[   12.345678] msg\n"
        fn format_ring_message(&self, level: LogLevel, msg: &str) -> String {
            #[expect(clippy::cast_precision_loss)]
            let now = match clock_gettime(ClockId::CLOCK_BOOTTIME) {
                Ok(ts) => ts.tv_sec() as f64 + (ts.tv_nsec() as f64 / 1_000_000_000.0),
                Err(_) => 0.0,
            };
            format!("<{}>[{:12.6}] {}\n", level.as_u8(), now, msg)
        }

        // HELPER: read and consume ring data.
        fn read_and_consume(
            &self,
            ring_data: &mut RingData,
            len: usize,
        ) -> (usize, Option<Vec<u8>>) {
            // Read data.
            let (count, out) = self.peek_and_copy(ring_data, len);
            if count == 0 {
                return (0, None);
            }

            // Now pop them.
            self.pop_count(ring_data, count);

            (count, Some(out))
        }

        // HELPER: read-all without consumption
        fn read_all_no_consume(
            &self,
            ring_data: &RingData,
            len: usize,
        ) -> (usize, Option<Vec<u8>>) {
            let (count, out) = self.peek_and_copy(ring_data, len);
            if count == 0 {
                (0, None)
            } else {
                (count, Some(out))
            }
        }

        // For read_clear, same approach but do it under same exclusive lock:
        fn read_all_no_consume_mut(
            &self,
            ring_data: &mut RingData,
            len: usize,
        ) -> (usize, Option<Vec<u8>>) {
            let (count, out) = self.peek_and_copy_mut(ring_data, len);
            if count == 0 {
                (0, None)
            } else {
                (count, Some(out))
            }
        }

        // HELPER: skip all ring content.
        fn skip_all(&self, ring_data: &mut RingData) {
            match &mut ring_data.mode {
                RbMode::Heap { cons, .. } => {
                    let to_skip = cons.occupied_len();
                    if to_skip > 0 {
                        let mut scratch = vec![MaybeUninit::<u8>::uninit(); to_skip];
                        let _ = cons.pop_slice_uninit(&mut scratch);
                    }
                }
                RbMode::Static { cons, .. } => {
                    let to_skip = cons.occupied_len();
                    if to_skip > 0 {
                        let mut scratch = vec![MaybeUninit::<u8>::uninit(); to_skip];
                        let _ = cons.pop_slice_uninit(&mut scratch);
                    }
                }
            }
        }

        // HELPER: pop 'count' items from ring.
        fn pop_count(&self, ring_data: &mut RingData, count: usize) {
            if count == 0 {
                return;
            }
            match &mut ring_data.mode {
                RbMode::Heap { cons, .. } => {
                    let mut scratch = vec![MaybeUninit::<u8>::uninit(); count];
                    let _ = cons.pop_slice_uninit(&mut scratch);
                }
                RbMode::Static { cons, .. } => {
                    let mut scratch = vec![MaybeUninit::<u8>::uninit(); count];
                    let _ = cons.pop_slice_uninit(&mut scratch);
                }
            }
        }

        // HELPER: read (peek) up to `len` items from ring into a Vec<u8>.
        // Does not consume them from the ring.
        fn peek_and_copy(&self, ring_data: &RingData, len: usize) -> (usize, Vec<u8>) {
            match &ring_data.mode {
                RbMode::Heap { cons, .. } => {
                    let rlen = cons.occupied_len().min(len);
                    if rlen == 0 {
                        return (0, Vec::new());
                    }
                    let mut tmp = vec![MaybeUninit::<u8>::uninit(); rlen];
                    let actual = cons.peek_slice_uninit(&mut tmp);
                    let mut out = Vec::with_capacity(actual);
                    for item in tmp.iter().take(actual) {
                        // SAFETY: ring data wrote these items.
                        out.push(unsafe { item.assume_init() });
                    }
                    (actual, out)
                }
                RbMode::Static { cons, .. } => {
                    let rlen = cons.occupied_len().min(len);
                    if rlen == 0 {
                        return (0, Vec::new());
                    }
                    let mut tmp = vec![MaybeUninit::<u8>::uninit(); rlen];
                    let actual = cons.peek_slice_uninit(&mut tmp);
                    let mut out = Vec::with_capacity(actual);
                    for item in tmp.iter().take(actual) {
                        // SAFETY: ring data wrote these items.
                        out.push(unsafe { item.assume_init() });
                    }
                    (actual, out)
                }
            }
        }

        // same but ring_data is mutable reference.
        fn peek_and_copy_mut(&self, ring_data: &mut RingData, len: usize) -> (usize, Vec<u8>) {
            match &mut ring_data.mode {
                RbMode::Heap { cons, .. } => {
                    let rlen = cons.occupied_len().min(len);
                    if rlen == 0 {
                        return (0, Vec::new());
                    }
                    let mut tmp = vec![MaybeUninit::<u8>::uninit(); rlen];
                    let actual = cons.peek_slice_uninit(&mut tmp);
                    let mut out = Vec::with_capacity(actual);
                    for item in tmp.iter().take(actual) {
                        // SAFETY: ring data wrote these items.
                        out.push(unsafe { item.assume_init() });
                    }
                    (actual, out)
                }
                RbMode::Static { cons, .. } => {
                    let rlen = cons.occupied_len().min(len);
                    if rlen == 0 {
                        return (0, Vec::new());
                    }
                    let mut tmp = vec![MaybeUninit::<u8>::uninit(); rlen];
                    let actual = cons.peek_slice_uninit(&mut tmp);
                    let mut out = Vec::with_capacity(actual);
                    for item in tmp.iter().take(actual) {
                        // SAFETY: ring data wrote these items.
                        out.push(unsafe { item.assume_init() });
                    }
                    (actual, out)
                }
            }
        }

        // HELPER: read the current unread length (no pop).
        fn ring_unread(&self, ring_data: &RingData) -> usize {
            match &ring_data.mode {
                RbMode::Heap { cons, .. } => cons.occupied_len(),
                RbMode::Static { cons, .. } => cons.occupied_len(),
            }
        }

        // HELPER: get ring capacity.
        fn ring_capacity(&self, ring_data: &RingData) -> usize {
            match &ring_data.mode {
                RbMode::Heap { shared, .. } => shared.capacity().get(),
                RbMode::Static { shared, .. } => shared.capacity().get(),
            }
        }
    }

    /// Global syslog instance.
    pub static SYSLOG_INSTANCE: OnceLock<Syslog> = OnceLock::new();

    /// Initialize the global `Syslog` instance.
    pub fn init_global_syslog(
        capacity: usize,
        level: LogLevel,
        use_stack: bool,
    ) -> Result<(), Errno> {
        if !use_stack && capacity == 0 {
            // Cannot create a heap-based syslog with zero capacity!
            return Err(Errno::EINVAL);
        }

        SYSLOG_INSTANCE
            .set(Syslog::new(capacity, level, use_stack))
            .or(Err(Errno::EAGAIN))
    }

    /// Returns the global `Syslog` instance.
    pub fn global_syslog() -> Option<&'static Syslog> {
        SYSLOG_INSTANCE.get()
    }
} // end of syslog_enabled

// PUBLIC RE-EXPORTS
#[cfg(feature = "log")]
pub use syslog_enabled::*;

/// Returns current log level of the global `Syslog`.
#[cfg(feature = "log")]
#[inline(always)]
pub fn current_loglevel() -> u8 {
    global_syslog().map(|sys| sys.loglevel()).unwrap_or(4)
}

/// This still parses log level from SYD_LOG,
/// so dry run with SYD_LOG=debug works,
/// even if the log feature is disabled.
#[cfg(not(feature = "log"))]
#[inline(always)]
pub fn current_loglevel() -> u8 {
    use std::{os::unix::ffi::OsStrExt, sync::LazyLock};

    static LOGLEVEL: LazyLock<u8> = LazyLock::new(|| {
        std::env::var_os(crate::config::ENV_LOG)
            .map(|val| parse_loglevel(val.as_os_str().as_bytes(), LogLevel::Warn))
            .unwrap_or(LogLevel::Warn)
            .as_u8()
    });

    *LOGLEVEL
}

/// Returns whether given log level is enabled.
#[macro_export]
macro_rules! log_enabled {
    ($level:expr) => {
        $crate::syslog::current_loglevel() >= $level.as_u8()
    };
}
