diff --git a/Cargo.lock b/Cargo.lock index 1895f1d..4224e37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,6 +21,17 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d25d88fd6b8041580a654f9d0c581a047baee2b3efee13275f2fc392fc75034" +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi 0.3.9", +] + [[package]] name = "autocfg" version = "1.0.1" @@ -115,6 +126,19 @@ dependencies = [ "cfg-if 0.1.10", ] +[[package]] +name = "env_logger" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54532e3223c5af90a6a757c90b5c5521564b07e5e7a958681bcd2afad421cdcd" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + [[package]] name = "fnv" version = "1.0.7" @@ -326,6 +350,12 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" +[[package]] +name = "humantime" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c1ad908cc71012b7bea4d0c53ba96a8cba9962f048fa68d143376143d863b7a" + [[package]] name = "hyper" version = "0.13.8" @@ -561,8 +591,10 @@ version = "0.1.0" dependencies = [ "anyhow", "dotenv", + "env_logger", "futures", "hyper", + "log", "prometheus", "reqwest", "serde", @@ -1052,6 +1084,15 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "termcolor" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.21" @@ -1367,6 +1408,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 14be3e3..8d5705d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,5 @@ hyper = "0.13" prometheus = "0.10" futures = "0.3" anyhow = "1.0" +log = "0.4" +env_logger = { version = "0.8", features = ["termcolor", "humantime"] } diff --git a/src/collectors/github_rate_limit.rs b/src/collectors/github_rate_limit.rs index 060d633..cdca957 100644 --- a/src/collectors/github_rate_limit.rs +++ b/src/collectors/github_rate_limit.rs @@ -1,6 +1,8 @@ use prometheus::{core::Collector, IntGauge, Opts}; use crate::Config; +use anyhow::{Context, Error, Result}; +use log::{debug, error}; use reqwest::header::{ACCEPT, AUTHORIZATION, USER_AGENT}; use reqwest::{Client, Method, Request}; use std::collections::HashMap; @@ -24,7 +26,7 @@ enum GithubReqBuilder { } impl GithubReqBuilder { - fn build_request(&self, client: &Client, token: &str) -> Result { + fn build_request(&self, client: &Client, token: &str) -> Result { let rb = match self { Self::User => client.request(Method::GET, GH_API_USER_ENDPOINT), Self::RateLimit => client.request(Method::GET, GH_API_RATE_LIMIT_ENDPOINT), @@ -37,49 +39,50 @@ impl GithubReqBuilder { .header(AUTHORIZATION, format!("{} {}", "token", token)) .header(ACCEPT, "application/vnd.github.v3+json") .build() + .map_err(Error::from) } } #[derive(Clone)] pub struct GitHubRateLimit { - descriptions: Vec, users: Vec, } impl GitHubRateLimit { - pub async fn new(config: &Config) -> Self { + pub async fn new(config: &Config) -> Result { let tokens: Vec = config .gh_rate_limit_tokens .split(',') .map(|v| v.trim().to_string()) .collect(); - let users = Self::get_users_for_tokens(tokens).await; - let descriptions = Vec::new(); + let users = Self::get_users_for_tokens(tokens) + .await + .context("Unable to get usernames for rate limit stats")?; - let rv = Self { - users, - descriptions, - }; + let rv = Self { users }; let refresh_rate = config.gh_rate_limit_stats_cache_refresh; let mut rv2 = rv.clone(); tokio::spawn(async move { loop { - rv2.update_stats().await; + if let Err(e) = rv2.update_stats().await { + error!("{:#?}", e); + } + tokio::time::delay_for(Duration::from_secs(refresh_rate)).await; } }); - rv + Ok(rv) } - async fn get_users_for_tokens(tokens: Vec) -> Vec { + async fn get_users_for_tokens(tokens: Vec) -> Result, Error> { let ns = String::from("monitorbot_github_rate_limit"); let mut rv: Vec = Vec::new(); for token in tokens.into_iter() { let ns2 = ns.clone(); - let username = GitHubRateLimit::get_github_api_username(&token).await; + let username = GitHubRateLimit::get_github_api_username(&token).await?; let user_future = tokio::task::spawn_blocking(move || { let rate_limit = IntGauge::with_opts( Opts::new("limit", "Rate limit.") @@ -119,50 +122,45 @@ impl GitHubRateLimit { rv.push(user); } - rv + Ok(rv) } - async fn get_github_api_username(token: &str) -> String { + async fn get_github_api_username(token: &str) -> Result { #[derive(serde::Deserialize)] struct GithubUser { pub login: String, } let client = reqwest::Client::new(); - let req = GithubReqBuilder::User - .build_request(&client, &token) - .unwrap(); - let u = client - .execute(req) - .await - .unwrap() - .json::() - .await - .unwrap(); + let req = GithubReqBuilder::User.build_request(&client, &token)?; + let u = client.execute(req).await?.json::().await?; - u.login + Ok(u.login) } - async fn update_stats(&mut self) { + async fn update_stats(&mut self) -> Result<(), Error> { + debug!("Updating rate limit stats"); + #[derive(Debug, serde::Deserialize)] struct GithubRateLimit { pub rate: HashMap, } let client = reqwest::Client::new(); - - //FIXME: we will (might?) need a RWLock on users structure for u in self.users.iter_mut() { let req = GithubReqBuilder::RateLimit .build_request(&client, &u.token) - .unwrap(); - let mut data = client + .context("Unable to build request to update stats")?; + + let response = client .execute(req) .await - .unwrap() + .context("Unable to execute request to update stats")?; + + let mut data = response .json::() .await - .unwrap(); + .context("Unable to deserialize rate limit stats")?; let remaining = data.rate.remove("remaining").unwrap_or(0); let limit = data.rate.remove("limit").unwrap_or(0); @@ -172,12 +170,15 @@ impl GitHubRateLimit { u.reset.set(reset as i64); u.limit.set(limit as i64); } + + Ok(()) } } impl Collector for GitHubRateLimit { fn desc(&self) -> std::vec::Vec<&prometheus::core::Desc> { - self.descriptions.iter().collect() + // descriptions are being defined in the initialization of the metrics options + Vec::default() } fn collect(&self) -> std::vec::Vec { diff --git a/src/collectors/mod.rs b/src/collectors/mod.rs index bf16541..f6ba43c 100644 --- a/src/collectors/mod.rs +++ b/src/collectors/mod.rs @@ -1,12 +1,18 @@ mod github_rate_limit; + pub use crate::collectors::github_rate_limit::GitHubRateLimit; use crate::MetricProvider; -use futures::FutureExt; +use anyhow::{Error, Result}; +use futures::TryFutureExt; +use log::info; // register collectors for metrics gathering -pub async fn register_collectors(p: &MetricProvider) -> Result<(), prometheus::Error> { +pub async fn register_collectors(p: &MetricProvider) -> Result<(), Error> { GitHubRateLimit::new(&p.config) - .map(|rl| p.register_collector(rl)) + .and_then(|rl| async { + info!("Registering GitHubRateLimit collector"); + p.register_collector(rl) + }) .await } diff --git a/src/lib.rs b/src/lib.rs index 62f210e..dd4f1bb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,15 +2,18 @@ pub mod collectors; mod config; + pub use config::Config; use prometheus::core::Collector; use prometheus::{Encoder, Registry}; +use anyhow::{Error, Result}; use futures::future; use futures::task::{Context, Poll}; use hyper::service::Service; use hyper::{Body, Method, Request, Response, StatusCode}; +use log::{debug, error}; #[derive(Clone, Debug)] pub struct MetricProvider { @@ -24,15 +27,19 @@ impl MetricProvider { Self { register, config } } - fn register_collector( - &self, - collector: impl Collector + 'static, - ) -> Result<(), prometheus::Error> { - self.register.register(Box::new(collector)) + fn register_collector(&self, collector: impl Collector + 'static) -> Result<(), Error> { + self.register + .register(Box::new(collector)) + .map_err(Error::from) } - fn gather_with_encoder(&self, encoder: impl Encoder, buf: &mut BUF) { - encoder.encode(&self.register.gather(), buf).unwrap(); + fn gather_with_encoder(&self, encoder: impl Encoder, buf: &mut BUF) -> Result<(), Error> + where + BUF: std::io::Write, + { + encoder + .encode(&self.register.gather(), buf) + .map_err(Error::from) } pub fn into_service(self) -> MetricProviderFactory { @@ -50,17 +57,26 @@ impl Service> for MetricProvider { } fn call(&mut self, req: Request) -> Self::Future { + debug!("New Request to endpoint {}", req.uri().path()); + let output = match (req.method(), req.uri().path()) { // Metrics handler (&Method::GET, "/metrics") => { let encoder = prometheus::TextEncoder::new(); let mut buffer = Vec::::new(); - self.gather_with_encoder(encoder, &mut buffer); - - Response::builder() - .status(StatusCode::OK) - .body(Body::from(buffer)) - .unwrap() + match self.gather_with_encoder(encoder, &mut buffer) { + Ok(_) => Response::builder() + .status(StatusCode::OK) + .body(Body::from(buffer)) + .unwrap(), + Err(e) => { + error!("{:?}", e); + Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::empty()) + .unwrap() + } + } } // All other paths and methods _ => Response::builder() diff --git a/src/main.rs b/src/main.rs index 923c563..f4bb25d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,14 @@ +use anyhow::{bail, Error, Result}; use hyper::Server; +use log::info; use monitorbot::Config; use monitorbot::{collectors::register_collectors, MetricProvider}; use std::net::SocketAddr; #[tokio::main] -async fn main() -> Result<(), Box> { +async fn main() -> Result<(), Error> { dotenv::dotenv().ok(); + env_logger::init(); let config = Config::from_env()?; let port = config.port; @@ -13,15 +16,14 @@ async fn main() -> Result<(), Box> { let provider = MetricProvider::new(config); if let Err(e) = register_collectors(&provider).await { - eprintln!("Unable to register collectors: {:#?}", e); - return Ok(()); + bail!("(Registering collectors) {}", e) } let server = Server::bind(&addr).serve(provider.into_service()); - println!("Server listening on port {}", port); + info!("Server listening on port: {}", port); if let Err(e) = server.await { - eprintln!("Server error: {}", e); + bail!("(Hyper server error) {}", e); } Ok(())