myc

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

commit 3f811b7b0e31ebbfabaa773274a4ded27a70873c
parent 448da4402cbe1cfea53951490cba55b8a9ac456c
Author: triesap <tyson@radroots.org>
Date:   Sun, 22 Mar 2026 20:29:28 +0000

logging: use shared canonical service logger

- replace the bespoke tracing setup with radroots-log using the dated filename layout
- add explicit myc env settings for log output directory and stdout mirroring
- keep the deployment example aligned to /var/log/radroots/services/myc while honoring the local machine env
- validate cargo check cargo test and live file logging at logs/services/local/myc/2026-03-22.log

Diffstat:
M.env.example | 2+-
MCargo.lock | 1+
MCargo.toml | 3++-
Msrc/config.rs | 32++++++++++++++++++++++++++++++--
Msrc/logging.rs | 54+++++++++++++++++++++++++++++-------------------------
5 files changed, 63 insertions(+), 29 deletions(-)

diff --git a/.env.example b/.env.example @@ -1,6 +1,6 @@ MYC_SERVICE_INSTANCE_NAME=myc MYC_LOGGING_FILTER=info,myc=info -MYC_LOGGING_OUTPUT_DIR=/var/log/myc +MYC_LOGGING_OUTPUT_DIR=/var/log/radroots/services/myc MYC_LOGGING_STDOUT=true MYC_PATHS_STATE_DIR=/var/lib/myc diff --git a/Cargo.lock b/Cargo.lock @@ -1035,6 +1035,7 @@ dependencies = [ "futures-util", "nostr", "radroots-identity", + "radroots-log", "radroots-nostr", "radroots-nostr-connect", "radroots-nostr-signer", diff --git a/Cargo.toml b/Cargo.toml @@ -17,6 +17,7 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } clap = { version = "4.5", features = ["derive"] } nostr = { version = "0.44.2", features = ["nip04", "nip44"] } radroots-identity = { path = "../lib/crates/identity" } +radroots-log = { path = "../lib/crates/log" } radroots-nostr = { path = "../lib/crates/nostr", features = ["client", "events"] } radroots-nostr-connect = { path = "../lib/crates/nostr-connect" } radroots-nostr-signer = { path = "../lib/crates/nostr-signer" } @@ -25,7 +26,7 @@ serde_json = "1.0" thiserror = "2.0" tokio = { version = "1.48", features = ["macros", "net", "rt-multi-thread", "sync", "time"] } tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } url = "2.5" [dev-dependencies] diff --git a/src/config.rs b/src/config.rs @@ -34,6 +34,8 @@ pub struct MycServiceConfig { #[serde(default, deny_unknown_fields)] pub struct MycLoggingConfig { pub filter: String, + pub output_dir: Option<PathBuf>, + pub stdout: bool, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -123,6 +125,8 @@ impl Default for MycLoggingConfig { fn default() -> Self { Self { filter: "info,myc=info".to_owned(), + output_dir: None, + stdout: true, } } } @@ -240,6 +244,14 @@ impl MycConfig { } })?; + if let Some(output_dir) = self.logging.output_dir.as_ref() { + if output_dir.as_os_str().is_empty() { + return Err(MycError::InvalidConfig( + "logging.output_dir must not be empty when set".to_owned(), + )); + } + } + if self.paths.state_dir.as_os_str().is_empty() { return Err(MycError::InvalidConfig( "paths.state_dir must not be empty".to_owned(), @@ -376,6 +388,12 @@ fn apply_env_entry( match key { "MYC_SERVICE_INSTANCE_NAME" => config.service.instance_name = value.to_owned(), "MYC_LOGGING_FILTER" => config.logging.filter = value.to_owned(), + "MYC_LOGGING_OUTPUT_DIR" => { + config.logging.output_dir = parse_optional_path_env(value); + } + "MYC_LOGGING_STDOUT" => { + config.logging.stdout = parse_bool_env(key, value, path, line_number)?; + } "MYC_PATHS_STATE_DIR" => config.paths.state_dir = PathBuf::from(value), "MYC_PATHS_SIGNER_IDENTITY_PATH" => { config.paths.signer_identity_path = PathBuf::from(value); @@ -725,6 +743,8 @@ mod tests { let config = MycConfig::default(); assert_eq!(config.service.instance_name, "myc"); assert_eq!(config.logging.filter, "info,myc=info"); + assert_eq!(config.logging.output_dir, None); + assert!(config.logging.stdout); assert_eq!(config.paths.state_dir, PathBuf::from("var")); assert_eq!( config.paths.signer_identity_path, @@ -759,6 +779,8 @@ mod tests { r#" MYC_SERVICE_INSTANCE_NAME=myc-dev MYC_LOGGING_FILTER=debug,myc=trace +MYC_LOGGING_OUTPUT_DIR=/tmp/myc-logs +MYC_LOGGING_STDOUT=false MYC_PATHS_STATE_DIR=/tmp/myc MYC_PATHS_SIGNER_IDENTITY_PATH=/tmp/myc-identity.json MYC_PATHS_USER_IDENTITY_PATH=/tmp/myc-user.json @@ -788,6 +810,8 @@ MYC_TRANSPORT_RELAYS=wss://relay.example.com,wss://relay2.example.com assert_eq!(config.service.instance_name, "myc-dev"); assert_eq!(config.logging.filter, "debug,myc=trace"); + assert_eq!(config.logging.output_dir, Some(PathBuf::from("/tmp/myc-logs"))); + assert!(!config.logging.stdout); assert_eq!(config.paths.state_dir, PathBuf::from("/tmp/myc")); assert_eq!( config.paths.signer_identity_path, @@ -938,12 +962,16 @@ MYC_UNKNOWN=nope assert!(config.discovery.enabled); assert_eq!( config.discovery.domain.as_deref(), - Some("localhost") + Some("myc.radroots.org") ); assert_eq!(config.discovery.handler_identifier, "myc"); assert_eq!( + config.logging.output_dir, + Some(PathBuf::from("/var/log/radroots/services/myc")) + ); + assert_eq!( config.discovery.nip05_output_path, - Some(PathBuf::from("var/public/.well-known/nostr.json")) + Some(PathBuf::from("/var/lib/myc/public/.well-known/nostr.json")) ); } } diff --git a/src/logging.rs b/src/logging.rs @@ -1,38 +1,42 @@ -use tracing_subscriber::EnvFilter; - +use radroots_log::{LogFileLayout, LoggingOptions}; use crate::config::MycLoggingConfig; use crate::error::MycError; -pub fn build_env_filter(filter: &str) -> Result<EnvFilter, MycError> { - EnvFilter::try_new(filter).map_err(|source| MycError::InvalidLogFilter { - filter: filter.to_owned(), - source, - }) -} - pub fn init_logging(config: &MycLoggingConfig) -> Result<(), MycError> { - let filter = build_env_filter(&config.filter)?; - let subscriber = tracing_subscriber::fmt() - .with_env_filter(filter) - .with_target(true) - .finish(); - - tracing::subscriber::set_global_default(subscriber) - .map_err(|_| MycError::LoggingAlreadyInitialized) + radroots_log::init_logging(LoggingOptions { + dir: config.output_dir.clone(), + file_name: "log".to_owned(), + stdout: config.stdout, + default_level: Some(config.filter.clone()), + file_layout: LogFileLayout::DatedFileName, + }) + .map_err(|source| MycError::InvalidOperation(format!("failed to initialize logging: {source}"))) } #[cfg(test)] mod tests { - use super::*; + use std::path::PathBuf; - #[test] - fn build_env_filter_accepts_valid_filter() { - assert!(build_env_filter("info,myc=debug").is_ok()); - } + use crate::config::MycConfig; #[test] - fn build_env_filter_rejects_invalid_filter() { - let err = build_env_filter("info,myc=[").expect_err("invalid filter"); - assert!(err.to_string().contains("invalid log filter")); + fn config_parses_logging_output_dir_and_stdout() { + 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"); + + assert_eq!(config.logging.output_dir, Some(PathBuf::from("/tmp/myc-logs"))); + assert!(!config.logging.stdout); } }