Add structured logging
This commit is contained in:
parent
f55484be15
commit
0207e365c7
7
examples/loglevel.rs
Normal file
7
examples/loglevel.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use log::LevelFilter;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("{:?}", LevelFilter::from_str("INFO"))
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use std::{collections::BTreeMap, env, fs::File, net::IpAddr, path::PathBuf, str::FromStr};
|
use std::{collections::BTreeMap, env, fs::File, net::IpAddr, path::PathBuf, str::FromStr};
|
||||||
|
|
||||||
|
use log::{debug, error};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
@ -10,15 +11,38 @@ pub struct Config {
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn load() -> anyhow::Result<Self> {
|
pub fn load() -> anyhow::Result<Self> {
|
||||||
let config_loc = env::var("DNS_CHECK_CONFIG")
|
let config_loc = env::var("DNS_CHECK_CONFIG")
|
||||||
.map_err(anyhow::Error::new)
|
.map_err(|error| {
|
||||||
.and_then(|path| PathBuf::from_str(path.as_str()).map_err(anyhow::Error::new))
|
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(|_| -> anyhow::Result<_> {
|
||||||
// .or_else::<_, impl FnOnce(_) -> anyhow::Result<_>>(|_| {
|
let cfg_loc = xdg::BaseDirectories::with_prefix("dns-check")
|
||||||
let cfg_loc = xdg::BaseDirectories::with_prefix("dns-check")?
|
.map_err(|error| {
|
||||||
.place_config_file("config.yml")?;
|
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)
|
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
|
||||||
|
})?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,15 @@
|
||||||
use std::{net::IpAddr, process::Command, vec};
|
use std::{net::IpAddr, process::Command, time::Instant, vec};
|
||||||
|
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
|
use log::{debug, error};
|
||||||
use serde::{Deserialize, Serialize};
|
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 struct DnsResponse {
|
||||||
pub domain: String,
|
pub domain: String,
|
||||||
pub ttl: i32,
|
pub ttl: i32,
|
||||||
|
|
@ -48,27 +54,45 @@ impl DnsResponse {
|
||||||
|
|
||||||
impl DigResponse {
|
impl DigResponse {
|
||||||
pub fn query(domain: impl AsRef<str>) -> anyhow::Result<Self> {
|
pub fn query(domain: impl AsRef<str>) -> anyhow::Result<Self> {
|
||||||
|
let domain = domain.as_ref();
|
||||||
|
let start = Instant::now();
|
||||||
let cmd = Command::new("dig")
|
let cmd = Command::new("dig")
|
||||||
.arg("+yaml")
|
.arg("+yaml")
|
||||||
.arg(domain.as_ref())
|
.arg(domain)
|
||||||
.output()?;
|
.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() {
|
if cmd.status.success() {
|
||||||
Ok(serde_yaml_ng::from_str(
|
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 {
|
} else {
|
||||||
Err(anyhow!(
|
error!(status:? = cmd.status,
|
||||||
"dig command failed:\n\tstatus: {:?}\n\tstdout: {}\n\tstderr: {}",
|
stdout = String::from_utf8_lossy(&cmd.stdout),
|
||||||
cmd.status,
|
stderr = String::from_utf8_lossy(&cmd.stderr)
|
||||||
String::from_utf8_lossy(&cmd.stdout),
|
; "dig command failed");
|
||||||
String::from_utf8_lossy(&cmd.stderr)
|
Err(anyhow!("dig command failed"))
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn answers(&self) -> anyhow::Result<Vec<DnsResponse>> {
|
pub fn answers(&self) -> anyhow::Result<Vec<DnsResponse>> {
|
||||||
let mut answers = vec![];
|
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 {
|
for answer in &self.message.response_message_data.answer_section {
|
||||||
answers.push(DnsResponse::parse(answer)?);
|
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)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
pub struct DigResponseMessage {
|
pub struct DigResponseMessage {
|
||||||
pub r#type: String,
|
pub r#type: String,
|
||||||
|
|
|
||||||
30
src/main.rs
30
src/main.rs
|
|
@ -1,16 +1,18 @@
|
||||||
mod config;
|
mod config;
|
||||||
pub mod dig_response;
|
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 config::Config;
|
||||||
use dig_response::DigResponse;
|
use dig_response::DigResponse;
|
||||||
|
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
|
use log::{error, info, trace, warn, LevelFilter};
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
|
start_log();
|
||||||
let config = Config::load()?;
|
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 (expected_ip, domains) in config.domains.into_iter() {
|
||||||
for domain in &domains {
|
for domain in &domains {
|
||||||
sleep(Duration::from_millis(250));
|
sleep(Duration::from_millis(250));
|
||||||
|
|
@ -20,16 +22,38 @@ fn main() -> anyhow::Result<()> {
|
||||||
Ok(())
|
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<str>, expected_ip: IpAddr) -> anyhow::Result<()> {
|
fn run_dns_check(domain: impl AsRef<str>, expected_ip: IpAddr) -> anyhow::Result<()> {
|
||||||
let domain = domain.as_ref();
|
let domain = domain.as_ref();
|
||||||
let result = DigResponse::query(domain)?;
|
let result = DigResponse::query(domain)?;
|
||||||
let addresses: Vec<_> = result.answers()?.iter().map(|a| a.address).collect();
|
let addresses: Vec<_> = result.answers()?.iter().map(|a| a.address).collect();
|
||||||
for address in &addresses {
|
for address in &addresses {
|
||||||
if address == &expected_ip {
|
if address == &expected_ip {
|
||||||
println!("found expected IP {address:?} for domain {domain}");
|
info!(ip_address:? = address, domain:?; "found expected IP for domain");
|
||||||
return Ok(());
|
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!(
|
Err(anyhow!(
|
||||||
"expected IP {expected_ip:?} not found in DNS results: {addresses:?}"
|
"expected IP {expected_ip:?} not found in DNS results: {addresses:?}"
|
||||||
))
|
))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue