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:
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);
}
}