diff --git a/Cargo.lock b/Cargo.lock index 4dba285..1408959 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,10 +17,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + [[package]] name = "apache_prometheus_exporter" version = "0.1.0" dependencies = [ + "anyhow", "hyper", "notify", "path-slash", diff --git a/Cargo.toml b/Cargo.toml index 06ab33c..02f48ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ lto = true codegen-units = 1 [dependencies] +anyhow = "1.0.75" hyper = { version = "0.14.27", default-features = false, features = ["http1", "server", "runtime"] } notify = { version = "6.1.1", default-features = false, features = ["macos_kqueue"] } path-slash = "0.2.1" diff --git a/src/logs/log_file_pattern.rs b/src/logs/log_file_pattern.rs index a9b545a..3685454 100644 --- a/src/logs/log_file_pattern.rs +++ b/src/logs/log_file_pattern.rs @@ -1,9 +1,9 @@ -use std::{env, io}; -use std::env::VarError; use std::fs::DirEntry; +use std::io; use std::io::ErrorKind; use std::path::{Path, PathBuf}; +use anyhow::{anyhow, bail, Result}; use path_slash::PathExt; /// Reads and parses an environment variable that determines the path and file name pattern of log files. @@ -13,34 +13,22 @@ use path_slash::PathExt; /// 1. A simple path to a file. /// 2. A path with a wildcard anywhere in the file name. /// 3. A path with a standalone wildcard component (i.e. no prefix or suffix in the folder name). -pub fn parse_log_file_pattern_from_env(variable_name: &str) -> Result<LogFilePattern, String> { - match env::var(variable_name) { - Ok(str) => { - let pattern_str = Path::new(&str).to_slash().ok_or(format!("Environment variable {} contains an invalid path.", variable_name))?; - parse_log_file_pattern_from_str(&pattern_str) - } - Err(err) => match err { - VarError::NotPresent => Err(format!("Environment variable {} must be set.", variable_name)), - VarError::NotUnicode(_) => Err(format!("Environment variable {} contains invalid characters.", variable_name)) - } - } -} - -fn parse_log_file_pattern_from_str(pattern_str: &str) -> Result<LogFilePattern, String> { - if pattern_str.trim().is_empty() { - return Err(String::from("Path is empty.")); +pub fn parse_log_file_pattern_from_str(pattern: &str) -> Result<LogFilePattern> { + let pattern = Path::new(pattern).to_slash().ok_or_else(|| anyhow!("Path is invalid"))?; + if pattern.trim().is_empty() { + bail!("Path is empty"); } - if let Some((left, right)) = pattern_str.split_once('*') { + if let Some((left, right)) = pattern.split_once('*') { parse_log_file_pattern_split_on_wildcard(left, right) } else { - Ok(LogFilePattern::WithoutWildcard(pattern_str.to_string())) + Ok(LogFilePattern::WithoutWildcard(pattern.to_string())) } } -fn parse_log_file_pattern_split_on_wildcard(left: &str, right: &str) -> Result<LogFilePattern, String> { +fn parse_log_file_pattern_split_on_wildcard(left: &str, right: &str) -> Result<LogFilePattern> { if left.contains('*') || right.contains('*') { - return Err(String::from("Path has too many wildcards.")); + bail!("Path has too many wildcards"); } if left.ends_with('/') && right.starts_with('/') { @@ -51,7 +39,7 @@ fn parse_log_file_pattern_split_on_wildcard(left: &str, right: &str) -> Result<L } if right.contains('/') { - return Err(String::from("Path has a folder wildcard with a prefix or suffix.")); + bail!("Path has a folder wildcard with a prefix or suffix"); } if let Some((folder_path, file_name_prefix)) = left.rsplit_once('/') { @@ -122,10 +110,7 @@ impl LogFilePattern { } fn search_without_wildcard(path_str: &String) -> Result<Vec<LogFilePath>, io::Error> { - let path = Path::new(path_str); - let is_valid = path.is_file() || matches!(path.parent(), Some(parent) if parent.is_dir()); - - if is_valid { + if Path::new(path_str).is_file() { Ok(vec![LogFilePath::with_empty_label(path_str)]) } else { Err(io::Error::from(ErrorKind::NotFound)) @@ -182,23 +167,23 @@ mod tests { #[test] fn empty_path() { - assert!(matches!(parse_log_file_pattern_from_str(""), Err(err) if err == "Path is empty.")); - assert!(matches!(parse_log_file_pattern_from_str(" "), Err(err) if err == "Path is empty.")); + assert!(matches!(parse_log_file_pattern_from_str(""), Err(err) if err.to_string() == "Path is empty")); + assert!(matches!(parse_log_file_pattern_from_str(" "), Err(err) if err.to_string() == "Path is empty")); } #[test] fn too_many_wildcards() { - assert!(matches!(parse_log_file_pattern_from_str("/path/*/to/files/*.log"), Err(err) if err == "Path has too many wildcards.")); + assert!(matches!(parse_log_file_pattern_from_str("/path/*/to/files/*.log"), Err(err) if err.to_string() == "Path has too many wildcards")); } #[test] fn folder_wildcard_with_prefix_not_supported() { - assert!(matches!(parse_log_file_pattern_from_str("/path/*abc/to/files/access.log"), Err(err) if err == "Path has a folder wildcard with a prefix or suffix.")); + assert!(matches!(parse_log_file_pattern_from_str("/path/*abc/to/files/access.log"), Err(err) if err.to_string() == "Path has a folder wildcard with a prefix or suffix")); } #[test] fn folder_wildcard_with_suffix_not_supported() { - assert!(matches!(parse_log_file_pattern_from_str("/path/abc*/to/files/access.log"), Err(err) if err == "Path has a folder wildcard with a prefix or suffix.")); + assert!(matches!(parse_log_file_pattern_from_str("/path/abc*/to/files/access.log"), Err(err) if err.to_string() == "Path has a folder wildcard with a prefix or suffix")); } #[test] diff --git a/src/logs/log_file_watcher.rs b/src/logs/log_file_watcher.rs index 1dab437..5e3439f 100644 --- a/src/logs/log_file_watcher.rs +++ b/src/logs/log_file_watcher.rs @@ -2,6 +2,7 @@ use std::cmp::max; use std::path::PathBuf; use std::sync::Arc; +use anyhow::{anyhow, bail, Context, Result}; use notify::{Event, EventKind}; use notify::event::{CreateKind, ModifyKind}; use tokio::fs::File; @@ -50,10 +51,9 @@ impl LogWatcherConfiguration { self.files.push((path, metadata)); } - pub async fn start(self, metrics: &Metrics) -> bool { + pub async fn start(self, metrics: &Metrics) -> Result<()> { if self.files.is_empty() { - println!("[LogWatcher] No log files provided."); - return false; + bail!("No log files provided"); } println!("[LogWatcher] Watching {} access log file(s) and {} error log file(s).", self.count_files_of_kind(LogFileKind::Access), self.count_files_of_kind(LogFileKind::Error)); @@ -73,32 +73,16 @@ impl LogWatcherConfiguration { prepared_files.push(PreparedFile { path, metadata, fs_event_receiver }); } - let fs_watcher = match FsWatcher::new(fs_callbacks) { - Ok(fs_watcher) => fs_watcher, - Err(e) => { - println!("[LogWatcher] Error creating filesystem watcher: {}", e); - return false; - } - }; + let fs_watcher = FsWatcher::new(fs_callbacks).context("Could not create filesystem watcher")?; for file in &prepared_files { let file_path = &file.path; if !file_path.is_absolute() { - println!("[LogWatcher] Error creating filesystem watcher, path is not absolute: {}", file_path.to_string_lossy()); - return false; + bail!("Path is not absolute: {}", file_path.to_string_lossy()); } - let parent_path = if let Some(parent) = file_path.parent() { - parent - } else { - println!("[LogWatcher] Error creating filesystem watcher for parent directory of file \"{}\", parent directory does not exist", file_path.to_string_lossy()); - return false; - }; - - if let Err(e) = fs_watcher.watch(parent_path).await { - println!("[LogWatcher] Error creating filesystem watcher for directory \"{}\": {}", parent_path.to_string_lossy(), e); - return false; - } + let parent_path = file_path.parent().ok_or_else(|| anyhow!("Path has no parent: {}", file_path.to_string_lossy()))?; + fs_watcher.watch(parent_path).await.with_context(|| format!("Could not create filesystem watcher for directory: {}", parent_path.to_string_lossy()))?; } let fs_watcher = Arc::new(fs_watcher); @@ -108,15 +92,13 @@ impl LogWatcherConfiguration { let _ = metrics.requests_total.get_or_create(&label_set); let _ = metrics.errors_total.get_or_create(&label_set); - let log_watcher = match LogWatcher::create(file.path, file.metadata, metrics.clone(), Arc::clone(&fs_watcher), file.fs_event_receiver).await { - Some(log_watcher) => log_watcher, - None => return false, - }; + let log_watcher = LogWatcher::create(file.path.clone(), file.metadata, metrics.clone(), Arc::clone(&fs_watcher), file.fs_event_receiver); + let log_watcher = log_watcher.await.with_context(|| format!("Could not watch log file: {}", file.path.to_string_lossy()))?; tokio::spawn(log_watcher.watch()); } - true + Ok(()) } } @@ -127,14 +109,10 @@ struct LogWatcher { } impl LogWatcher { - async fn create(path: PathBuf, metadata: LogFileMetadata, metrics: Metrics, fs_watcher: Arc<FsWatcher>, fs_event_receiver: Receiver<Event>) -> Option<Self> { - let state = match LogWatchingState::initialize(path.clone(), fs_watcher).await { - Some(state) => state, - None => return None, - }; - + async fn create(path: PathBuf, metadata: LogFileMetadata, metrics: Metrics, fs_watcher: Arc<FsWatcher>, fs_event_receiver: Receiver<Event>) -> Result<Self> { + let state = LogWatchingState::initialize(path.clone(), fs_watcher).await?; let processor = LogLineProcessor { path, metadata, metrics }; - Some(LogWatcher { state, processor, fs_event_receiver }) + Ok(LogWatcher { state, processor, fs_event_receiver }) } async fn watch(mut self) { @@ -176,8 +154,11 @@ impl LogWatcher { } self.state = match self.state.reinitialize().await { - Some(state) => state, - None => break 'read_loop, + Ok(state) => state, + Err(e) => { + println!("Could not re-watch log file \"{}\": {}", path.to_string_lossy(), e); + break 'read_loop; + } }; continue 'read_loop; @@ -222,24 +203,16 @@ struct LogWatchingState { impl LogWatchingState { const DEFAULT_BUFFER_CAPACITY: usize = 1024 * 4; - async fn initialize(path: PathBuf, fs_watcher: Arc<FsWatcher>) -> Option<LogWatchingState> { - if let Err(e) = fs_watcher.watch(&path).await { - println!("[LogWatcher] Error creating filesystem watcher for file \"{}\": {}", path.to_string_lossy(), e); - return None; - } + async fn initialize(path: PathBuf, fs_watcher: Arc<FsWatcher>) -> Result<LogWatchingState> { + fs_watcher.watch(&path).await.context("Could not create filesystem watcher")?; - let lines = match File::open(&path).await { - Ok(file) => BufReader::with_capacity(Self::DEFAULT_BUFFER_CAPACITY, file).lines(), - Err(e) => { - println!("[LogWatcher] Error opening file \"{}\": {}", path.to_string_lossy(), e); - return None; - } - }; + let file = File::open(&path).await.context("Could not open file")?; + let lines = BufReader::with_capacity(Self::DEFAULT_BUFFER_CAPACITY, file).lines(); - Some(LogWatchingState { path, lines, fs_watcher }) + Ok(LogWatchingState { path, lines, fs_watcher }) } - async fn reinitialize(self) -> Option<LogWatchingState> { + async fn reinitialize(self) -> Result<LogWatchingState> { LogWatchingState::initialize(self.path, self.fs_watcher).await } } diff --git a/src/logs/mod.rs b/src/logs/mod.rs index ee5a320..a497704 100644 --- a/src/logs/mod.rs +++ b/src/logs/mod.rs @@ -1,6 +1,11 @@ +use std::env; +use std::env::VarError; + +use anyhow::{anyhow, bail, Context, Result}; + use log_file_watcher::{LogFileKind, LogWatcherConfiguration}; -use crate::logs::log_file_pattern::{LogFilePath, parse_log_file_pattern_from_env}; +use crate::logs::log_file_pattern::{LogFilePath, parse_log_file_pattern_from_str}; use crate::metrics::Metrics; mod access_log_parser; @@ -8,36 +13,27 @@ mod filesystem_watcher; mod log_file_pattern; mod log_file_watcher; -pub fn find_log_files(environment_variable_name: &str, log_kind: &str) -> Option<Vec<LogFilePath>> { - let log_file_pattern = match parse_log_file_pattern_from_env(environment_variable_name) { - Ok(pattern) => pattern, - Err(error) => { - println!("Error: {}", error); - return None; - } - }; +pub fn find_log_files(environment_variable_name: &str, log_kind: &str) -> Result<Vec<LogFilePath>> { + let log_file_pattern_str = env::var(environment_variable_name).map_err(|err| match err { + VarError::NotPresent => anyhow!("Environment variable {} must be set", environment_variable_name), + VarError::NotUnicode(_) => anyhow!("Environment variable {} contains invalid characters", environment_variable_name) + })?; - let log_files = match log_file_pattern.search() { - Ok(files) => files, - Err(error) => { - println!("Error searching {} files: {}", log_kind, error); - return None; - } - }; + let log_file_pattern = parse_log_file_pattern_from_str(&log_file_pattern_str).with_context(|| format!("Could not parse pattern: {}", log_file_pattern_str))?; + let log_files = log_file_pattern.search().with_context(|| format!("Could not search files: {}", log_file_pattern_str))?; if log_files.is_empty() { - println!("Found no matching {} files.", log_kind); - return None; + bail!("No files match pattern: {}", log_file_pattern_str); } for log_file in &log_files { println!("Found {} file: {} (label \"{}\")", log_kind, log_file.path.display(), log_file.label); } - Some(log_files) + Ok(log_files) } -pub async fn start_log_watcher(access_log_files: Vec<LogFilePath>, error_log_files: Vec<LogFilePath>, metrics: Metrics) -> bool { +pub async fn start_log_watcher(access_log_files: Vec<LogFilePath>, error_log_files: Vec<LogFilePath>, metrics: Metrics) -> Result<()> { let mut watcher = LogWatcherConfiguration::new(); for log_file in access_log_files.into_iter() { diff --git a/src/main.rs b/src/main.rs index de107b1..98a1d22 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,9 @@ use std::env; use std::net::{IpAddr, SocketAddr}; -use std::process::ExitCode; use std::str::FromStr; use std::sync::Mutex; +use anyhow::{anyhow, Context}; use tokio::signal; use crate::metrics::Metrics; @@ -17,49 +17,22 @@ const ACCESS_LOG_FILE_PATTERN: &str = "ACCESS_LOG_FILE_PATTERN"; const ERROR_LOG_FILE_PATTERN: &str = "ERROR_LOG_FILE_PATTERN"; #[tokio::main(flavor = "current_thread")] -async fn main() -> ExitCode { +async fn main() -> anyhow::Result<()> { let host = env::var("HTTP_HOST").unwrap_or(String::from("127.0.0.1")); - let bind_ip = match IpAddr::from_str(&host) { - Ok(addr) => addr, - Err(_) => { - println!("Invalid HTTP host: {}", host); - return ExitCode::FAILURE; - } - }; + let bind_ip = IpAddr::from_str(&host).map_err(|_| anyhow!("Invalid HTTP host: {}", host))?; println!("Initializing exporter..."); - let access_log_files = match logs::find_log_files(ACCESS_LOG_FILE_PATTERN, "access log") { - Some(files) => files, - None => return ExitCode::FAILURE, - }; - - let error_log_files = match logs::find_log_files(ERROR_LOG_FILE_PATTERN, "error log") { - Some(files) => files, - None => return ExitCode::FAILURE, - }; - - let server = match WebServer::try_bind(SocketAddr::new(bind_ip, 9240)) { - Some(server) => server, - None => return ExitCode::FAILURE - }; + let access_log_files = logs::find_log_files(ACCESS_LOG_FILE_PATTERN, "access log").context("Could not find access log files")?; + let error_log_files = logs::find_log_files(ERROR_LOG_FILE_PATTERN, "error log").context("Could not find error log files")?; + let server = WebServer::try_bind(SocketAddr::new(bind_ip, 9240)).context("Could not configure web server")?; let (metrics_registry, metrics) = Metrics::new(); - if !logs::start_log_watcher(access_log_files, error_log_files, metrics).await { - return ExitCode::FAILURE; - } - + logs::start_log_watcher(access_log_files, error_log_files, metrics).await.context("Could not start watching logs")?; tokio::spawn(server.serve(Mutex::new(metrics_registry))); - match signal::ctrl_c().await { - Ok(_) => { - println!("Received CTRL-C, shutting down..."); - ExitCode::SUCCESS - } - Err(e) => { - println!("Error registering CTRL-C handler: {}", e); - ExitCode::FAILURE - } - } + signal::ctrl_c().await.with_context(|| "Could not register CTRL-C handler")?; + println!("Received CTRL-C, shutting down..."); + Ok(()) } diff --git a/src/web/mod.rs b/src/web/mod.rs index 5fb2048..12a9234 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -2,6 +2,7 @@ use std::net::SocketAddr; use std::sync::{Arc, Mutex}; use std::time::Duration; +use anyhow::Context; use hyper::{Body, Error, Method, Request, Response, Server, StatusCode}; use hyper::http::Result; use hyper::server::Builder; @@ -19,23 +20,17 @@ pub struct WebServer { impl WebServer { //noinspection HttpUrlsUsage - pub fn try_bind(addr: SocketAddr) -> Option<WebServer> { + pub fn try_bind(addr: SocketAddr) -> anyhow::Result<WebServer> { println!("[WebServer] Starting web server on {0} with metrics endpoint: http://{0}/metrics", addr); - let builder = match Server::try_bind(&addr) { - Ok(builder) => builder, - Err(e) => { - println!("[WebServer] Could not bind to {}: {}", addr, e); - return None; - } - }; + let builder = Server::try_bind(&addr).with_context(|| format!("Could not bind to {}", addr))?; let builder = builder.tcp_keepalive(Some(Duration::from_secs(60))); let builder = builder.http1_only(true); let builder = builder.http1_keepalive(true); let builder = builder.http1_max_buf_size(MAX_BUFFER_SIZE); let builder = builder.http1_header_read_timeout(Duration::from_secs(10)); - Some(WebServer { builder }) + Ok(WebServer { builder }) } pub async fn serve(self, metrics_registry: Mutex<Registry>) {