myc

Self-custodial remote signer for Radroots apps
git clone https://radroots.dev/git/myc.git
Log | Files | Refs | README | LICENSE

commit 7fb16206b66e2aab72609ff21cb4d3294baafebf
parent 3f811b7b0e31ebbfabaa773274a4ded27a70873c
Author: triesap <tyson@radroots.org>
Date:   Mon, 23 Mar 2026 02:03:04 +0000

logging: add myc runtime log smoke coverage

- add dated log path coverage for myc logging options
- add a myc run smoke test that waits for a non-empty dated log file
- generate isolated temp env and identity fixtures for runtime logging verification
- keep coverage aligned with the canonical daily service log layout

Diffstat:
MCargo.lock | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/logging.rs | 46++++++++++++++++++++++++++++++++++++++++++++--
Atests/logging_run.rs | 109+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 259 insertions(+), 2 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -51,6 +51,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] name = "anstream" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -313,6 +322,19 @@ dependencies = [ ] [[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] name = "cipher" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -418,6 +440,12 @@ dependencies = [ ] [[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -779,6 +807,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] name = "icu_collections" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1395,6 +1447,7 @@ dependencies = [ name = "radroots-log" version = "0.1.0-alpha.1" dependencies = [ + "chrono", "thiserror 1.0.69", "tracing", "tracing-appender", @@ -2432,12 +2485,65 @@ dependencies = [ ] [[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/src/logging.rs b/src/logging.rs @@ -1,6 +1,6 @@ -use radroots_log::{LogFileLayout, LoggingOptions}; use crate::config::MycLoggingConfig; use crate::error::MycError; +use radroots_log::{LogFileLayout, LoggingOptions}; pub fn init_logging(config: &MycLoggingConfig) -> Result<(), MycError> { radroots_log::init_logging(LoggingOptions { @@ -15,6 +15,7 @@ pub fn init_logging(config: &MycLoggingConfig) -> Result<(), MycError> { #[cfg(test)] mod tests { + use radroots_log::{LogFileLayout, LoggingOptions}; use std::path::PathBuf; use crate::config::MycConfig; @@ -36,7 +37,48 @@ MYC_TRANSPORT_CONNECT_TIMEOUT_SECS=10 ) .expect("config"); - assert_eq!(config.logging.output_dir, Some(PathBuf::from("/tmp/myc-logs"))); + assert_eq!( + config.logging.output_dir, + Some(PathBuf::from("/tmp/myc-logs")) + ); assert!(!config.logging.stdout); } + + #[test] + fn logging_options_resolve_real_dated_file_path() { + let config = MycConfig::from_env_str( + r#" +MYC_LOGGING_FILTER=info,myc=debug +MYC_LOGGING_OUTPUT_DIR=/tmp/myc-logs +MYC_LOGGING_STDOUT=false +MYC_PATHS_STATE_DIR=/tmp/myc +MYC_PATHS_SIGNER_IDENTITY_PATH=/tmp/signer.json +MYC_PATHS_USER_IDENTITY_PATH=/tmp/user.json +MYC_DISCOVERY_ENABLED=false +MYC_TRANSPORT_ENABLED=false +MYC_TRANSPORT_CONNECT_TIMEOUT_SECS=10 + "#, + ) + .expect("config"); + + let path = LoggingOptions { + dir: config.logging.output_dir.clone(), + file_name: "log".to_owned(), + stdout: config.logging.stdout, + default_level: Some(config.logging.filter.clone()), + file_layout: LogFileLayout::DatedFileName, + } + .resolved_current_log_file_path() + .expect("resolved log path"); + + assert_eq!( + path.parent(), + Some(PathBuf::from("/tmp/myc-logs").as_path()) + ); + assert!( + path.file_name() + .and_then(|value| value.to_str()) + .is_some_and(|value| value.ends_with(".log")) + ); + } } diff --git a/tests/logging_run.rs b/tests/logging_run.rs @@ -0,0 +1,109 @@ +use radroots_identity::RadrootsIdentity; +use radroots_log::{LogFileLayout, LoggingOptions}; +use std::path::Path; +use std::process::{Child, Command, Stdio}; +use std::thread; +use std::time::{Duration, Instant}; + +fn write_test_identity(path: &Path, secret_key: &str) { + RadrootsIdentity::from_secret_key_str(secret_key) + .expect("identity from secret") + .save_json(path) + .expect("write identity"); +} + +fn wait_for_non_empty_log(path: &Path, timeout: Duration) -> Result<String, String> { + let deadline = Instant::now() + timeout; + while Instant::now() < deadline { + match std::fs::read_to_string(path) { + Ok(contents) if !contents.trim().is_empty() => return Ok(contents), + Ok(_) => {} + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(error) => return Err(format!("failed to read log file: {error}")), + } + thread::sleep(Duration::from_millis(100)); + } + Err(format!( + "timed out waiting for non-empty log file at {}", + path.display() + )) +} + +fn kill_child(child: &mut Child) { + let _ = child.kill(); + let _ = child.wait(); +} + +#[test] +fn myc_run_writes_non_empty_dated_log_file() { + let temp = tempfile::tempdir().expect("tempdir"); + let state_dir = temp.path().join("state"); + let logs_dir = temp.path().join("logs"); + let signer_path = temp.path().join("signer.json"); + let user_path = temp.path().join("user.json"); + let env_path = temp.path().join("myc.env"); + + write_test_identity( + signer_path.as_path(), + "1111111111111111111111111111111111111111111111111111111111111111", + ); + write_test_identity( + user_path.as_path(), + "2222222222222222222222222222222222222222222222222222222222222222", + ); + + std::fs::write( + &env_path, + format!( + "MYC_SERVICE_INSTANCE_NAME=myc-test\n\ +MYC_LOGGING_FILTER=info,myc=info\n\ +MYC_LOGGING_OUTPUT_DIR={}\n\ +MYC_LOGGING_STDOUT=false\n\ +MYC_PATHS_STATE_DIR={}\n\ +MYC_PATHS_SIGNER_IDENTITY_PATH={}\n\ +MYC_PATHS_USER_IDENTITY_PATH={}\n\ +MYC_DISCOVERY_ENABLED=false\n\ +MYC_TRANSPORT_ENABLED=false\n\ +MYC_TRANSPORT_CONNECT_TIMEOUT_SECS=10\n", + logs_dir.display(), + state_dir.display(), + signer_path.display(), + user_path.display(), + ), + ) + .expect("write env"); + + let expected_log_path = LoggingOptions { + dir: Some(logs_dir.clone()), + file_name: "log".to_owned(), + stdout: false, + default_level: Some("info,myc=info".to_owned()), + file_layout: LogFileLayout::DatedFileName, + } + .resolved_current_log_file_path() + .expect("resolved current log path"); + + let mut child = Command::new(env!("CARGO_BIN_EXE_myc")) + .arg("--env-file") + .arg(&env_path) + .arg("run") + .stdout(Stdio::null()) + .stderr(Stdio::piped()) + .spawn() + .expect("spawn myc"); + + let contents = match wait_for_non_empty_log(expected_log_path.as_path(), Duration::from_secs(5)) + { + Ok(contents) => contents, + Err(error) => { + kill_child(&mut child); + panic!("{error}"); + } + }; + + kill_child(&mut child); + + assert!(expected_log_path.exists()); + assert!(contents.contains("logging initialized")); + assert!(contents.contains("myc runtime bootstrapped")); +}