mechutil 0.8.6

Utility structures and functions for mechatronics applications.
Documentation
//! One-call service logger setup for autocore binaries.
//!
//! Every long-running autocore process (the server and each module) needs the
//! same three things wired together, in the same order, every time:
//!
//!   1. a per-run log file in the shared log dir, keeping the newest N runs
//!      ([`crate::run_log`]),
//!   2. a hard size cap on that file so a single run can never fill the disk
//!      ([`RotatingFileWriter`]),
//!   3. a `simplelog` sink at `Info` (plus a terminal tee in debug builds).
//!
//! Historically each binary open-coded all three, and the size cap (step 2)
//! was simply forgotten everywhere — `RotatingFileWriter` existed but was wired
//! into nothing, so run files grew without bound (multi-GB files that filled
//! field disks). [`init_service_logger`] makes the capped path the *only* path:
//! call it once at startup and the cap can't be left out.
//!
//! # Memory
//!
//! This is disk-bounded, not RAM-bounded: [`RotatingFileWriter`] streams each
//! record straight to the file and holds only a byte counter in memory. There
//! is deliberately no in-RAM ring buffer — these targets are memory
//! constrained, so the FIFO behaviour lives on disk (rotate to one `.old`
//! backup), not in a heap buffer.
//!
//! # Example
//!
//! ```no_run
//! // In a module's `main`, service path only:
//! match mechutil::init_service_logger("autocore-ni", 7) {
//!     Ok(info) => println!("autocore-ni log file: {:?}", info.path),
//!     Err(e) => {
//!         eprintln!("FATAL: cannot open log file: {e}");
//!         std::process::exit(1);
//!     }
//! }
//! ```

use std::io;
use std::path::PathBuf;

use simplelog::{
    ColorChoice, CombinedLogger, Config, LevelFilter, TermLogger, TerminalMode, WriteLogger,
};

use crate::rotating_file_writer::RotatingFileWriter;
use crate::run_log::{open_default_run_log, RunLog};

/// Size threshold (bytes) at which a run's log file rotates to its single
/// `.old` backup. With one backup kept, the worst-case on-disk footprint for a
/// run is ~2x this value, so 2.5 MB keeps each run at or under ~5 MB total —
/// the agreed ceiling. Anything past the most recent ~5 MB of a run is dropped
/// (oldest first), which is the intended FIFO behaviour: nobody reads gigabytes
/// of a single run anyway.
pub const MAX_RUN_BYTES: u64 = 2_500_000;

/// File log level. Kept at `Info`: the file is the operator's record, so it
/// should hold startup, state changes, warnings and errors — but not per-sample
/// or per-message chatter. (Hot-path logging belongs at `Debug`/`Trace`, which
/// this filter drops in release.)
pub const FILE_LEVEL: LevelFilter = LevelFilter::Info;

/// What [`init_service_logger`] set up, for the caller to report. The file
/// handle is owned by the installed logger, so it is intentionally not exposed.
pub struct ServiceLog {
    /// Full path of this run's (size-capped) log file.
    pub path: PathBuf,
    /// Older run logs pruned to honor the run-count retention limit.
    pub pruned: Vec<PathBuf>,
}

/// Open this run's size-capped log file and install it as the global logger.
///
/// Combines [`open_default_run_log`] (shared dir + keep newest `keep` runs),
/// [`RotatingFileWriter`] (rotate at [`MAX_RUN_BYTES`], one `.old` backup), and
/// `simplelog`. In debug builds a `Debug`-level terminal tee is added; in
/// release the capped file is the only sink.
///
/// Returns an error if the log file can't be opened or a global logger is
/// already installed; callers decide whether that is fatal (the server and
/// modules exit) or recoverable (fall back to a terminal-only logger).
pub fn init_service_logger(base_name: &str, keep: usize) -> io::Result<ServiceLog> {
    let RunLog { path, file, pruned } = open_default_run_log(base_name, keep)?;

    // The hard size cap that makes a runaway log impossible. Streams to disk;
    // no RAM buffering.
    let writer = RotatingFileWriter::new(path.clone(), file, MAX_RUN_BYTES);

    let install = if cfg!(debug_assertions) {
        CombinedLogger::init(vec![
            TermLogger::new(
                LevelFilter::Debug,
                Config::default(),
                TerminalMode::Mixed,
                ColorChoice::Auto,
            ),
            WriteLogger::new(FILE_LEVEL, Config::default(), writer),
        ])
    } else {
        WriteLogger::init(FILE_LEVEL, Config::default(), writer)
    };
    install.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;

    Ok(ServiceLog { path, pruned })
}