diff --git a/examples/loglevel.rs b/examples/loglevel.rs new file mode 100644 index 0000000..bbe5cb4 --- /dev/null +++ b/examples/loglevel.rs @@ -0,0 +1,7 @@ +use std::str::FromStr; + +use log::LevelFilter; + +fn main() { + println!("{:?}", LevelFilter::from_str("INFO")) +} \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 86a6d37..7620d33 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,6 @@ use std::{collections::BTreeMap, env, fs::File, net::IpAddr, path::PathBuf, str::FromStr}; +use log::{debug, error}; use serde::Deserialize; #[derive(Debug, Deserialize)] @@ -10,15 +11,38 @@ pub struct Config { impl Config { pub fn load() -> anyhow::Result { let config_loc = env::var("DNS_CHECK_CONFIG") - .map_err(anyhow::Error::new) - .and_then(|path| PathBuf::from_str(path.as_str()).map_err(anyhow::Error::new)) + .map_err(|error| { + debug!(error:err; "couldn't get config location from environment variable"); + anyhow::Error::new(error) + }) + .and_then(|path| { + PathBuf::from_str(path.as_str()).map_err(|error| { + error!(error:err, path; "couldn't convert environment variable value to a path"); + anyhow::Error::new(error) + }) + }) .or_else(|_| -> anyhow::Result<_> { - // .or_else::<_, impl FnOnce(_) -> anyhow::Result<_>>(|_| { - let cfg_loc = xdg::BaseDirectories::with_prefix("dns-check")? - .place_config_file("config.yml")?; + let cfg_loc = xdg::BaseDirectories::with_prefix("dns-check") + .map_err(|error| { + error!(error:err; "xdg init error"); + error + })? + .place_config_file("config.yml") + .map_err(|error| { + error!(error:err; "error creating config file"); + error + })?; Ok(cfg_loc) })?; - let file = File::open(config_loc)?; - Ok(serde_yaml_ng::from_reader(file)?) + + debug!(config_loc:?; "opening config file"); + let file = File::open(&config_loc).map_err(|error| { + error!(error:err, config_loc:?; "error opening config file"); + error + })?; + Ok(serde_yaml_ng::from_reader(file).map_err(|error| { + error!(error:err, config_loc:?; "error parsing config file"); + error + })?) } } diff --git a/src/dig_response.rs b/src/dig_response.rs index dbfb44e..393013f 100644 --- a/src/dig_response.rs +++ b/src/dig_response.rs @@ -1,9 +1,15 @@ -use std::{net::IpAddr, process::Command, vec}; +use std::{net::IpAddr, process::Command, time::Instant, vec}; use anyhow::anyhow; +use log::{debug, error}; use serde::{Deserialize, Serialize}; -#[derive(Debug)] +/** + * The deserialized data from Dig's Answers section of the response + * + * See https://stackoverflow.com/questions/20297531/meaning-of-the-five-fields-of-the-answer-section-in-dig-query + */ +#[derive(Debug, Clone)] pub struct DnsResponse { pub domain: String, pub ttl: i32, @@ -48,27 +54,45 @@ impl DnsResponse { impl DigResponse { pub fn query(domain: impl AsRef) -> anyhow::Result { + let domain = domain.as_ref(); + let start = Instant::now(); let cmd = Command::new("dig") .arg("+yaml") - .arg(domain.as_ref()) - .output()?; + .arg(domain) + .output() + .map_err(|error| { + error!(domain:?, error:err; "error running dig command"); + error + })?; + let cmd_time = start.elapsed().as_millis(); + debug!(status:? = cmd.status, + stdout = String::from_utf8_lossy(&cmd.stdout), + stderr = String::from_utf8_lossy(&cmd.stderr), + cmd_time; + "dig command completed running" + ); if cmd.status.success() { Ok(serde_yaml_ng::from_str( - String::from_utf8(cmd.stdout.into_iter().skip(2).collect())?.as_str(), + String::from_utf8(cmd.stdout.into_iter().skip(2).collect()) + // we skip two here --------------------------^ + // because the dig output starts with "-\n" which fails to parse. + .map_err(|error| { + error!(domain:?, error:?; "couldn't convert command output to a string"); + error + })? + .as_str(), )?) } else { - Err(anyhow!( - "dig command failed:\n\tstatus: {:?}\n\tstdout: {}\n\tstderr: {}", - cmd.status, - String::from_utf8_lossy(&cmd.stdout), - String::from_utf8_lossy(&cmd.stderr) - )) + error!(status:? = cmd.status, + stdout = String::from_utf8_lossy(&cmd.stdout), + stderr = String::from_utf8_lossy(&cmd.stderr) + ; "dig command failed"); + Err(anyhow!("dig command failed")) } } + pub fn answers(&self) -> anyhow::Result> { let mut answers = vec![]; - // let DigResponseType::Message(ref message) = self.message; - // for answer in &message.response_message_data.answer_section { for answer in &self.message.response_message_data.answer_section { answers.push(DnsResponse::parse(answer)?); } @@ -76,12 +100,6 @@ impl DigResponse { } } -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(untagged)] -pub enum DigResponseType { - Message(DigResponseMessage), -} - #[derive(Debug, Clone, Deserialize, Serialize)] pub struct DigResponseMessage { pub r#type: String, diff --git a/src/main.rs b/src/main.rs index 24405df..9664302 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,18 @@ mod config; pub mod dig_response; -use std::{net::IpAddr, thread::sleep, time::Duration}; +use std::{env, net::IpAddr, str::FromStr, thread::sleep, time::Duration}; use config::Config; use dig_response::DigResponse; use anyhow::anyhow; +use log::{error, info, trace, warn, LevelFilter}; fn main() -> anyhow::Result<()> { + start_log(); let config = Config::load()?; - println!("starting DNS check for {} domains", config.domains.len()); + info!(domain_count=config.domains.len(); "starting DNS check"); for (expected_ip, domains) in config.domains.into_iter() { for domain in &domains { sleep(Duration::from_millis(250)); @@ -20,16 +22,38 @@ fn main() -> anyhow::Result<()> { Ok(()) } +/** + * Get the log level from the $DNS_CHECK_LOG_LEVEL environment variable, and + * fall back on `LevelFilter::Info` if the environment variable is not + * specified. + */ +fn start_log() { + let level = match env::var("DNS_CHECK_LOG_LEVEL") + .map_err(anyhow::Error::new) + .and_then(|lvl| LevelFilter::from_str(&lvl).map_err(anyhow::Error::new)) + { + Ok(level) => level, + Err(err) => { + warn!(err:?; "log level not specified or invalid"); + LevelFilter::Info + } + }; + femme::with_level(level); +} + fn run_dns_check(domain: impl AsRef, expected_ip: IpAddr) -> anyhow::Result<()> { let domain = domain.as_ref(); let result = DigResponse::query(domain)?; let addresses: Vec<_> = result.answers()?.iter().map(|a| a.address).collect(); for address in &addresses { if address == &expected_ip { - println!("found expected IP {address:?} for domain {domain}"); + info!(ip_address:? = address, domain:?; "found expected IP for domain"); return Ok(()); + } else { + trace!(checked_ip:? = address, expected_ip:?, domain:?; "address did not match expected IP"); } } + error!(actual_ips:?=addresses, expected_ip:?, domain:?; "expected IP not found in DNS results"); Err(anyhow!( "expected IP {expected_ip:?} not found in DNS results: {addresses:?}" ))