baryl 0.0.4

Public SDK for Baryl, a full-system emulation and introspection engine
Documentation
//! Logging from inside a component, into a file of its own.
//!
//! `#[component]` installs your sink for you, so writing
//! `baryl::logging::info!("...")` is all a component needs; the records land in
//! `<state_dir>/<name>.log`, where `<name>` is the name you gave the component.
//! Call [`init_file`] yourself only when you are not using the macro.
//!
//! Each library in a run has its own logger and its own file — a sink installed
//! in one reaches none of the others, so your component's records are yours
//! alone and nothing else is mixed in.
//!
//! Files only. Nothing here writes to stdout, which belongs to the guest.
//!
//! The `log` macros are re-exported, so `use baryl::logging::{info, warn}` is
//! enough and your crate need not depend on `log` itself.

use std::{ffi::CStr, os::raw::c_char, path::Path};

pub use log::{LevelFilter, debug, error, info, log, trace, warn};
use log4rs::{
    append::file::FileAppender,
    config::{Appender, Config, Root},
    encode::pattern::PatternEncoder,
};

/// Module, level, then message — what a record looks like when no pattern is
/// named.
pub const DEFAULT_PATTERN: &str = "{M} - {l} - {m}{n}";

/// The message and nothing else, for records that arrive already formatted.
pub const BARE_PATTERN: &str = "{m}{n}";

/// Install this library's log sink at `<state_dir>/<name>.log`.
///
/// `level` is `0` off, `1` error, `2` warn, `3` info, `4` debug, `5` trace.
/// Anything below 0 is off and anything above 5 is trace, so a level from a
/// config file cannot accidentally select the loudest filter.
///
/// Best-effort and infallible: a null `state_dir`, an unwritable path or a
/// logger already installed all leave the run going, with a line on stderr
/// where there is something to say. A run with no log file is quieter, not
/// broken.
///
/// Calling it twice does nothing the second time — the first sink stands.
pub fn init_file(state_dir: *const c_char, name: &str, level: i32) {
    init_file_with(state_dir, name, level, DEFAULT_PATTERN);
}

/// [`init_file`], with the line layout chosen.
///
/// Pass [`BARE_PATTERN`] when your records already carry everything they need
/// and you want nothing prepended.
pub fn init_file_with(state_dir: *const c_char, name: &str, level: i32, pattern: &str) {
    if let Err(e) = try_install(state_dir, name, level, pattern) {
        eprintln!("{name}: log init failed: {e}");
    }
}

/// Build the config and install the logger. A logger already installed is not
/// an error; the second call simply does nothing.
fn try_install(
    state_dir: *const c_char,
    name: &str,
    level: i32,
    pattern: &str,
) -> anyhow::Result<()> {
    if state_dir.is_null() {
        anyhow::bail!("no state directory");
    }
    // SAFETY: a live NUL-terminated path, written before any `init` reads it.
    let dir = unsafe { CStr::from_ptr(state_dir) }
        .to_string_lossy()
        .into_owned();
    let path = Path::new(&dir).join(format!("{name}.log"));
    let appender = FileAppender::builder()
        .encoder(Box::new(PatternEncoder::new(pattern)))
        .append(true)
        .build(&path)?;
    let config = Config::builder()
        .appender(Appender::builder().build("logfile", Box::new(appender)))
        .build(Root::builder().appender("logfile").build(level_of(level)))?;
    let _ = log4rs::init_config(config);
    Ok(())
}

/// `0=off .. 5=trace`, clamped at both ends.
fn level_of(level: i32) -> LevelFilter {
    match level {
        ..=0 => LevelFilter::Off,
        1 => LevelFilter::Error,
        2 => LevelFilter::Warn,
        3 => LevelFilter::Info,
        4 => LevelFilter::Debug,
        5.. => LevelFilter::Trace,
    }
}