From 381879724ae51e1e3349b73dc70e1ad955dbfa6d Mon Sep 17 00:00:00 2001 From: Markus Kohlhase Date: Fri, 7 Jun 2024 00:15:08 +0200 Subject: [PATCH] Log loaded configuration at startup (#529) --- Cargo.lock | 1 + Cargo.toml | 1 + .../src/usecases/send_update_reminders.rs | 2 +- src/config/mod.rs | 168 ++++++++++++++---- src/config/raw.rs | 1 + src/main.rs | 59 ++++-- src/recurring_reminder.rs | 2 +- 7 files changed, 187 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ef69b0d2..d1d9059a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2266,6 +2266,7 @@ dependencies = [ "ofdb-webserver", "rand", "serde", + "thiserror", "time", "tokio", "toml", diff --git a/Cargo.toml b/Cargo.toml index ec0ecc5d..3273e6b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,7 @@ duration-str = { version = "0.7.1", default-features = false, features = ["serde env_logger = "0.11.3" log = "0.4.21" serde = { version = "1.0.203", features = ["derive"] } +thiserror = "1.0.61" time = "0.3.36" tokio = "1.38.0" toml = "0.8.14" diff --git a/ofdb-core/src/usecases/send_update_reminders.rs b/ofdb-core/src/usecases/send_update_reminders.rs index 9ab7d066..efaa7e71 100644 --- a/ofdb-core/src/usecases/send_update_reminders.rs +++ b/ofdb-core/src/usecases/send_update_reminders.rs @@ -89,7 +89,7 @@ where Ok(()) } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum RecipientRole { Owner, Scout, diff --git a/src/config/mod.rs b/src/config/mod.rs index 7943ef25..0fbf5a53 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,20 +1,20 @@ -use anyhow::{anyhow, Result}; -use ofdb_core::usecases::RecipientRole; -use ofdb_entities::email::EmailAddress; use std::{ collections::HashSet, - env, fs, - io::ErrorKind, + fmt, fs, io, path::{Path, PathBuf}, time::Duration, }; -mod raw; +use anyhow::anyhow; +use thiserror::Error; -const DEFAULT_CONFIG_FILE_NAME: &str = "openfairdb.toml"; +use ofdb_core::usecases::RecipientRole; +use ofdb_entities::email::EmailAddress; -const ENV_NAME_DB_URL: &str = "DATABASE_URL"; +mod raw; +#[derive(Debug)] +#[cfg_attr(test, derive(PartialEq))] pub struct Config { pub db: Db, pub entries: Entries, @@ -24,33 +24,59 @@ pub struct Config { pub reminders: Reminders, } +#[derive(Debug, Error)] +pub enum LoadError { + #[error("Config file not found")] + NotFound, + + #[error(transparent)] + Toml(#[from] toml::de::Error), + + #[error(transparent)] + Other(#[from] anyhow::Error), +} + impl Config { - pub fn try_load_from_file_or_default>(file_path: Option

) -> Result { - let file_path: &Path = file_path.as_ref().map(|p| p.as_ref()).unwrap_or_else(|| { - log::info!("No configuration file specified. load {DEFAULT_CONFIG_FILE_NAME}"); - Path::new(DEFAULT_CONFIG_FILE_NAME) - }); + pub fn try_load_from_file>(file_path: P) -> Result { + let raw_config = try_load_raw_config_from_file(file_path)?; + let cfg = Self::try_from(raw_config)?; + Ok(cfg) + } - let raw_config = match fs::read_to_string(file_path) { - Ok(cfg_string) => toml::from_str(&cfg_string)?, - Err(err) => match err.kind() { - ErrorKind::NotFound => { + pub fn try_load_from_file_or_default>(file_path: P) -> anyhow::Result { + match Self::try_load_from_file(file_path.as_ref()) { + Ok(cfg) => Ok(cfg), + Err(err) => match err { + LoadError::NotFound => { log::info!( - "{DEFAULT_CONFIG_FILE_NAME} not found => load default configuration." + "Configuration file {} not found: load default configuration.", + file_path.as_ref().display() ); - Ok(raw::Config::default()) + Ok(Self::default()) } - _ => Err(err), - }?, - }; - let mut cfg = Self::try_from(raw_config)?; - if let Ok(db_url) = env::var(ENV_NAME_DB_URL) { - cfg.db.conn_sqlite = db_url; + _ => Err(err.into()), + }, } - Ok(cfg) } } +impl Default for Config { + fn default() -> Self { + Self::try_from(raw::Config::default()).expect("default config") + } +} + +fn try_load_raw_config_from_file>(file_path: P) -> Result { + let cfg_string = fs::read_to_string(file_path).map_err(|err| match err.kind() { + io::ErrorKind::NotFound => LoadError::NotFound, + _ => LoadError::Other(err.into()), + })?; + let raw_config = toml::from_str(&cfg_string)?; + Ok(raw_config) +} + +#[derive(Debug)] +#[cfg_attr(test, derive(PartialEq))] pub struct Db { /// SQLite connection pub conn_sqlite: String, @@ -59,28 +85,48 @@ pub struct Db { pub index_dir: Option, } +#[derive(Debug)] +#[cfg_attr(test, derive(PartialEq))] pub struct Geocoding { pub gateway: Option, } +#[cfg_attr(test, derive(PartialEq))] pub enum GeocodingGateway { OpenCage { api_key: String }, } +impl fmt::Debug for GeocodingGateway { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + GeocodingGateway::OpenCage { api_key: _ } => { + f.debug_struct("OpenCage").field("api_key", &"***").finish() + } + } + } +} + +#[derive(Debug)] +#[cfg_attr(test, derive(PartialEq))] pub struct Entries { pub accepted_licenses: HashSet, } +#[derive(Debug)] +#[cfg_attr(test, derive(PartialEq))] pub struct WebServer { pub protect_with_captcha: bool, pub enable_cors: bool, } +#[derive(Debug)] +#[cfg_attr(test, derive(PartialEq))] pub struct Email { pub gateway: Option, } #[derive(Clone)] +#[cfg_attr(test, derive(PartialEq))] pub enum EmailGateway { MailGun { api_base_url: String, // TODO: use url::Url @@ -98,6 +144,37 @@ pub enum EmailGateway { }, } +impl fmt::Debug for EmailGateway { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + EmailGateway::MailGun { + api_base_url, + api_key: _, + domain, + sender_address, + } => f + .debug_struct("MailGun") + .field("api_base_url", &api_base_url) + .field("api_key", &"***") + .field("domain", &domain) + .field("sender_address", &sender_address) + .finish(), + + EmailGateway::Sendmail { sender_address } => f + .debug_struct("Sendmail") + .field("sender_address", &sender_address) + .finish(), + + EmailGateway::EmailToJsonFile { dir } => f + .debug_struct("EmailToJsonFile") + .field("dir", &dir) + .finish(), + } + } +} + +#[derive(Debug)] +#[cfg_attr(test, derive(PartialEq))] pub struct Reminders { pub task_interval_time: Duration, pub send_max: u32, @@ -108,17 +185,21 @@ pub struct Reminders { pub token_expire_in: Duration, } +#[derive(Debug)] +#[cfg_attr(test, derive(PartialEq))] pub struct ScoutReminders { pub not_updated_for: Duration, } +#[derive(Debug)] +#[cfg_attr(test, derive(PartialEq))] pub struct OwnerReminders { pub not_updated_for: Duration, } impl TryFrom for Config { type Error = anyhow::Error; - fn try_from(from: raw::Config) -> Result { + fn try_from(from: raw::Config) -> anyhow::Result { let raw::Config { db, geocoding, @@ -241,7 +322,7 @@ impl TryFrom for Config { let send_bcc = if let Some(bcc) = send_bcc { bcc.into_iter() .map(|a| a.parse::()) - .collect::, _>>()? + .collect::, _>>()? } else { vec![] }; @@ -312,7 +393,34 @@ mod tests { #[test] fn load_default_config() { - let file: Option<&Path> = None; - let _: Config = Config::try_load_from_file_or_default(file).unwrap(); + let file = Path::new(""); + let cfg = Config::try_load_from_file_or_default(file).unwrap(); + assert_eq!(cfg, Config::default()); + assert!(cfg.reminders.send_to.is_empty()); + assert!(cfg.reminders.send_bcc.is_empty()); + } + + #[test] + fn hide_api_key_of_geo_gateway() { + let x = GeocodingGateway::OpenCage { + api_key: "123".to_string(), + }; + let d = format!("{x:?}"); + assert_eq!(r#"OpenCage { api_key: "***" }"#, d); + } + + #[test] + fn hide_api_key_of_mailgun_gateway() { + let x = EmailGateway::MailGun { + api_base_url: "x".to_string(), + domain: "y".to_string(), + sender_address: "z@example.com".parse().unwrap(), + api_key: "123".to_string(), + }; + let d = format!("{x:?}"); + assert_eq!( + r#"MailGun { api_base_url: "x", api_key: "***", domain: "y", sender_address: EmailAddress { address: "z@example.com", display_name: None } }"#, + d + ); } } diff --git a/src/config/raw.rs b/src/config/raw.rs index b2895952..6ef045d9 100644 --- a/src/config/raw.rs +++ b/src/config/raw.rs @@ -228,6 +228,7 @@ mod tests { assert!(cfg.task_interval_time.is_some()); assert!(cfg.send_max.is_some()); assert!(cfg.send_to.is_none()); + assert!(cfg.send_bcc.is_none()); assert!(cfg.scouts.is_some()); assert!(cfg.owners.is_some()); assert!(cfg.token_expire_in.is_some()); diff --git a/src/main.rs b/src/main.rs index 6f92f2ab..a497a742 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,19 +24,23 @@ where { let events = repo.all_events_chronologically()?; for mut e in events { - if let Some(ref mut loc) = e.location { - if let Some(ref addr) = loc.address { - if let Some((lat, lng)) = geo.resolve_address_lat_lng(addr) { - if let Ok(pos) = MapPoint::try_from_lat_lng_deg(lat, lng) { - if pos.is_valid() { - if let Err(err) = repo.update_event(&e) { - log::warn!("Failed to update location of event {}: {}", e.id, err); - } else { - log::info!("Updated location of event {}", e.id); - } - } - } - } + let Some(ref mut loc) = e.location else { + continue; + }; + let Some(ref addr) = loc.address else { + continue; + }; + let Some((lat, lng)) = geo.resolve_address_lat_lng(addr) else { + continue; + }; + let Ok(pos) = MapPoint::try_from_lat_lng_deg(lat, lng) else { + continue; + }; + if pos.is_valid() { + if let Err(err) = repo.update_event(&e) { + log::warn!("Failed to update location of event {}: {err}", e.id); + } else { + log::info!("Updated location of event {}", e.id); } } } @@ -59,13 +63,38 @@ enum Command { FixEventAddressLocation, } +const ENV_NAME_DB_URL: &str = "DATABASE_URL"; +const ENV_NAME_RUST_LOG: &str = "RUST_LOG"; + +const DEFAULT_CONFIG_FILE_NAME: &str = "openfairdb.toml"; + +const FALLBACK_RUST_LOG_CONFIG: &str = "info,tantivy=warn"; + #[tokio::main] pub async fn main() -> anyhow::Result<()> { - env_logger::init(); dotenv().ok(); + if env::var(ENV_NAME_RUST_LOG) == Err(env::VarError::NotPresent) { + env::set_var(ENV_NAME_RUST_LOG, FALLBACK_RUST_LOG_CONFIG); + } + env_logger::init(); + let args = Args::parse(); - let cfg = config::Config::try_load_from_file_or_default(args.config_file)?; + let mut cfg = match args.config_file { + Some(file_path) => config::Config::try_load_from_file(file_path)?, + None => { + log::info!("No configuration file specified: load {DEFAULT_CONFIG_FILE_NAME}"); + config::Config::try_load_from_file_or_default(DEFAULT_CONFIG_FILE_NAME)? + } + }; + + if let Ok(db_url) = env::var(ENV_NAME_DB_URL) { + log::info!("Use DB connection {db_url} defined by {ENV_NAME_DB_URL}"); + cfg.db.conn_sqlite = db_url; + } + + log::info!("Start server with the following config:\n {cfg:#?}"); + let config::Db { conn_sqlite, conn_pool_size, diff --git a/src/recurring_reminder.rs b/src/recurring_reminder.rs index 4e7087d1..527789b6 100644 --- a/src/recurring_reminder.rs +++ b/src/recurring_reminder.rs @@ -12,7 +12,7 @@ pub async fn run( cfg: config::Reminders, ) { if cfg.send_to.is_empty() { - log::info!("Do not send recurring reminders"); + log::info!("No recipient defined in `send_to`: do not send recurring reminders"); return; }