insomnilog 0.2.0

An asynchronous Rust logging library that never sleeps
Documentation
//! Process-wide backend lifecycle: [`start`], [`shutdown`], and their guards.

use std::sync::Arc;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, Ordering};

use crate::backend;
use crate::level::LogLevel;
use crate::logger::Logger;
use crate::sink::Sink;

/// Panic message produced by [`get_sink`] / [`create_sink`] (and the
/// macros, in later steps) when called before [`start`].
const NOT_STARTED_PANIC: &str = "insomnilog: call insomnilog::start() before using the logger";

/// Error returned by [`start`] when the backend has already been initialised
/// in this process.
///
/// `start` is conceptually one-shot per process: once called, subsequent calls
/// — including those after [`shutdown`] — return this error rather than
/// silently spawning a fresh backend with possibly different options.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AlreadyStarted;

impl core::fmt::Display for AlreadyStarted {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.write_str("insomnilog backend has already been started")
    }
}

impl std::error::Error for AlreadyStarted {}

/// RAII guard returned from [`start`]. Tears the backend down on drop.
///
/// Bind it for the lifetime you want logging to be alive — typically
/// `let _guard = insomnilog::start(opts)?;` near the top of `main`. The
/// `#[must_use]` attribute makes the compiler warn on `let _ = start(...)`,
/// which would drop the guard immediately.
#[must_use = "ShutdownGuard drops the backend immediately if not bound; \
              bind it with `let _guard = ...` for the desired lifetime"]
pub struct ShutdownGuard {
    /// Private field so callers cannot construct this type directly.
    _private: (),
}

impl Drop for ShutdownGuard {
    fn drop(&mut self) {
        shutdown();
    }
}

/// Latch flipped by [`start`] to detect repeat initialisation.
///
/// Kept separate from [`BACKEND`] because `OnceLock::set` consumes its
/// argument: relying on the lock alone would force us to spawn a worker
/// thread before learning that initialisation is illegal. The latch keeps
/// the spawn out of the failing branch.
static STARTED: AtomicBool = AtomicBool::new(false);

/// Process-wide backend, initialised exactly once by [`start`]. Subsequent
/// reads via [`shutdown`] go through `OnceLock::get`.
static BACKEND: OnceLock<backend::Backend> = OnceLock::new();

/// Initialises the process-wide backend.
///
/// On success returns a [`ShutdownGuard`] whose `Drop` tears the backend
/// down (drain → join in later migration steps; just join for now). Bind
/// the guard in `main` for the desired lifetime of the logging system.
///
/// # Errors
///
/// Returns [`AlreadyStarted`] if `start` has already been called in this
/// process. There is no automatic restart.
///
/// # Panics
///
/// Panics if the backend worker thread cannot be spawned (the OS refused
/// to create a new thread).
pub fn start(options: backend::BackendOptions) -> Result<ShutdownGuard, AlreadyStarted> {
    if STARTED
        .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
        .is_err()
    {
        return Err(AlreadyStarted);
    }
    let backend = backend::Backend::start(options);
    if BACKEND.set(backend).is_err() {
        unreachable!("BACKEND set after STARTED CAS won — no other thread can have set it");
    }
    Ok(ShutdownGuard { _private: () })
}

/// Returns the process-wide backend.
///
/// # Panics
///
/// Panics if [`start`] has not been called in this process.
pub fn get_backend() -> &'static backend::Backend {
    BACKEND.get().expect(NOT_STARTED_PANIC)
}

/// Drains and tears the backend down.
///
/// Idempotent: safe to call multiple times, before or after a
/// [`ShutdownGuard`] drops, and even if [`start`] has not been called (in
/// which case it is a no-op).
pub fn shutdown() {
    if let Some(backend) = BACKEND.get() {
        backend.shutdown();
    }
}

/// Returns the sink registered under `name`, if any.
///
/// # Panics
///
/// Panics if [`start`] has not been called in this process.
#[must_use]
pub fn get_sink(name: &str) -> Option<Arc<dyn Sink>> {
    BACKEND.get().expect(NOT_STARTED_PANIC).get_sink(name)
}

/// Registers `sink` under `name`.
///
/// The error carries the existing `Arc` so the caller can inspect or compare
/// it.
///
/// # Errors
///
/// Returns <code>Err([`backend::SinkAlreadyRegistered`])</code> if a sink is
/// already registered under `name`.
///
/// # Panics
///
/// Panics if [`start`] has not been called in this process.
pub fn register_sink(
    name: &str,
    sink: Arc<dyn Sink>,
) -> Result<(), backend::SinkAlreadyRegistered> {
    BACKEND
        .get()
        .expect(NOT_STARTED_PANIC)
        .register_sink(name, sink)
}

/// Returns the logger registered under `name`, if any.
///
/// # Panics
///
/// Panics if [`start`] has not been called in this process.
#[must_use]
pub fn get_logger(name: &str) -> Option<Arc<Logger>> {
    BACKEND.get().expect(NOT_STARTED_PANIC).get_logger(name)
}

/// Creates a new logger under `name` with the given `sinks` and `level`.
///
/// **`level` gates emission for every sink.** The producer-side check against
/// `level` runs on the macro hot path before any sink sees the record,
/// so a sink configured *more permissive* than this logger never receives the
/// difference. The effective level for any sink is
/// `max(logger.level, sink.level)`. To get more output, lower this `level`,
/// not the sink's.
///
/// # Errors
///
/// Returns <code>Err([crate::LoggerAlreadyRegistered])</code> if a logger is already
/// registered under `name`. The error carries the existing `Arc<Logger>`.
///
/// # Panics
///
/// Panics if [`start`] has not been called in this process.
pub fn create_logger(
    name: &str,
    sinks: Vec<Arc<dyn Sink>>,
    level: LogLevel,
) -> Result<Arc<Logger>, crate::LoggerAlreadyRegistered> {
    BACKEND
        .get()
        .expect(NOT_STARTED_PANIC)
        .create_logger(name, sinks, level)
}

/// Eagerly registers the calling thread with the backend, paying the queue
/// allocation and context push upfront rather than on the first log call.
///
/// Idempotent: a second call from the same thread is a no-op because the
/// thread-local producer slot is already populated. A subsequent log macro call
/// on the same thread also reuses the slot and does not register a second context.
///
/// # Panics
///
/// - Panics if [`start`] has not been called.
/// - Panics if called from the backend worker thread (would create infinite
///   dispatch loop).
#[cfg_attr(feature = "rtsan", rtsan_standalone::blocking)]
pub fn preallocate_thread() {
    crate::frontend::ensure_producer_registered();
}