lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit 4b00c138694dabbfab83b6c56adf43f4333ca515
parent bf25385982a2d8cf6f0f1d6471fb8221d7c16700
Author: triesap <tyson@radroots.org>
Date:   Fri, 17 Jul 2026 22:00:08 +0000

log: support structured stderr output

- add stderr as an explicit canonical logging sink
- preserve stdout for machine-readable command results
- validate stderr-only structured configurations
- document structured console sink behavior

Diffstat:
Mcrates/log/README | 2+-
Mcrates/log/src/init.rs | 24++++++++++++++++++++++--
Mcrates/log/src/options.rs | 2++
3 files changed, 25 insertions(+), 3 deletions(-)

diff --git a/crates/log/README b/crates/log/README @@ -8,7 +8,7 @@ primitives for the `radroots` core libraries. * basic `log_info`, `log_error`, and `log_debug` helpers available across builds; * typed error and result definitions for logging initialization paths; - * `std` initializers for default, structured stdout, and file-backed tracing; + * `std` initializers for default, structured stdout/stderr, and file-backed tracing; * service, run, and environment identity on every structured JSON event; * fail-closed initialization when a different global configuration is active; * size-bounded files with deterministic retention and private file modes; diff --git a/crates/log/src/init.rs b/crates/log/src/init.rs @@ -51,10 +51,14 @@ pub fn init_logging(opts: LoggingOptions) -> Result<()> { let stdout_layer = opts .also_stdout() .then(|| fmt::layer().with_writer(std::io::stdout).with_target(false)); + let stderr_layer = opts + .stderr + .then(|| fmt::layer().with_writer(std::io::stderr).with_target(false)); tracing_subscriber::registry() .with(filter) .with(file_layer) .with(stdout_layer) + .with(stderr_layer) .try_init()?; } LogFormat::Json => { @@ -71,14 +75,21 @@ pub fn init_logging(opts: LoggingOptions) -> Result<()> { }); let stdout_layer = opts.also_stdout().then(|| { fmt::layer() - .event_format(JsonEventFormatter::new(identity)) + .event_format(JsonEventFormatter::new(identity.clone())) .with_writer(std::io::stdout) .with_ansi(false) }); + let stderr_layer = opts.stderr.then(|| { + fmt::layer() + .event_format(JsonEventFormatter::new(identity)) + .with_writer(std::io::stderr) + .with_ansi(false) + }); tracing_subscriber::registry() .with(filter) .with(file_layer) .with(stdout_layer) + .with(stderr_layer) .try_init()?; } } @@ -90,6 +101,7 @@ pub fn init_logging(opts: LoggingOptions) -> Result<()> { info!( file_enabled = file_path.is_some(), stdout_enabled = opts.also_stdout(), + stderr_enabled = opts.stderr, "logging initialized" ); Ok(()) @@ -153,7 +165,7 @@ fn build_file_writer( } fn validate_options(opts: &LoggingOptions) -> Result<()> { - if opts.dir.is_none() && !opts.stdout { + if opts.dir.is_none() && !opts.stdout && !opts.stderr { return Err(Error::Msg( "logging requires at least one configured output".to_owned(), )); @@ -269,6 +281,14 @@ mod tests { } #[test] + fn stderr_is_a_valid_standalone_sink() { + let mut options = LoggingOptions::json_service("service", "run-1", "production"); + options.stdout = false; + options.stderr = true; + assert!(validate_options(&options).is_ok()); + } + + #[test] fn repeated_initialization_reuses_only_an_identical_configuration() { let active = LoggingOptions::json_service("service", "run-1", "test"); assert!(initialization_decision(Some(&active), &active).expect("reuse")); diff --git a/crates/log/src/options.rs b/crates/log/src/options.rs @@ -58,6 +58,7 @@ pub struct LoggingOptions { pub dir: Option<PathBuf>, pub file_name: String, pub stdout: bool, + pub stderr: bool, pub default_level: Option<String>, pub file_layout: LogFileLayout, pub format: LogFormat, @@ -106,6 +107,7 @@ impl Default for LoggingOptions { dir: None, file_name: "radroots.log".into(), stdout: true, + stderr: false, default_level: None, file_layout: LogFileLayout::PrefixedDate, format: LogFormat::Compact,