From f4c97880ce9211b6dae1f5c2e683b33b6bea0f76 Mon Sep 17 00:00:00 2001 From: Evan Purkhiser Date: Fri, 13 Sep 2024 19:42:46 -0400 Subject: [PATCH] feat(http): Make checks using request configs This updates the http_checker to execute checks using the configurations in the `requets_{method,headers,body}` fields of the `CheckConfig`. --- src/checker/http_checker.rs | 85 +++++++++++++++++++++++++++------- src/producer/kafka_producer.rs | 10 ++-- src/types.rs | 1 + src/types/check_config.rs | 21 +-------- src/types/result.rs | 12 ++--- src/types/shared.rs | 34 ++++++++++++++ 6 files changed, 115 insertions(+), 48 deletions(-) create mode 100644 src/types/shared.rs diff --git a/src/checker/http_checker.rs b/src/checker/http_checker.rs index 13f256e..06eee77 100644 --- a/src/checker/http_checker.rs +++ b/src/checker/http_checker.rs @@ -1,5 +1,5 @@ use chrono::{TimeDelta, Utc}; -use reqwest::header::HeaderMap; +use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; use reqwest::{Client, ClientBuilder, Response}; use sentry::protocol::{SpanId, TraceId}; use std::error::Error; @@ -9,10 +9,7 @@ use uuid::Uuid; use crate::config_store::Tick; use crate::types::{ check_config::CheckConfig, - result::{ - CheckResult, CheckStatus, CheckStatusReason, CheckStatusReasonType, RequestInfo, - RequestType, - }, + result::{CheckResult, CheckStatus, CheckStatusReason, CheckStatusReasonType, RequestInfo}, }; use super::Checker; @@ -33,20 +30,34 @@ async fn do_request( client: &Client, check_config: &CheckConfig, sentry_trace: &str, -) -> (RequestType, Result) { +) -> Result { let timeout = check_config .timeout .to_std() .expect("Timeout duration could not be converted to std::time::Duration"); - let result = client - .get(check_config.url.as_str()) + let url = check_config.url.as_str(); + + let headers: HeaderMap = check_config + .request_headers + .clone() + .into_iter() + .filter_map(|(key, value)| { + // Try to convert key and value to HeaderName and HeaderValue + let header_name = HeaderName::try_from(key).ok()?; + let header_value = HeaderValue::from_str(&value).ok()?; + Some((header_name, header_value)) + }) + .collect(); + + client + .request(check_config.request_method.into(), url) .timeout(timeout) + .headers(headers) .header("sentry-trace", sentry_trace.to_owned()) + .body(check_config.request_body.to_owned()) .send() - .await; - - (RequestType::Get, result) + .await } /// Check if the request error is a DNS error. @@ -96,7 +107,7 @@ impl Checker for HttpChecker { let trace_header = format!("{}-{}-{}", trace_id, span_id, '0'); let start = Instant::now(); - let (request_type, response) = do_request(&self.client, config, &trace_header).await; + let response = do_request(&self.client, config, &trace_header).await; let duration = Some(TimeDelta::from_std(start.elapsed()).unwrap()); let status = if response.as_ref().is_ok_and(|r| r.status().is_success()) { @@ -112,7 +123,7 @@ impl Checker for HttpChecker { let request_info = Some(RequestInfo { http_status_code, - request_type, + request_type: config.request_method, }); let status_reason = match response { @@ -158,10 +169,13 @@ impl Checker for HttpChecker { #[cfg(test)] mod tests { + use std::collections::HashMap; + use crate::checker::Checker; use crate::config_store::Tick; use crate::types::check_config::CheckConfig; - use crate::types::result::{CheckStatus, CheckStatusReasonType, RequestType}; + use crate::types::result::{CheckStatus, CheckStatusReasonType}; + use crate::types::shared::RequestMethod; use super::{HttpChecker, UPTIME_USER_AGENT}; use chrono::{TimeDelta, Utc}; @@ -173,7 +187,7 @@ mod tests { } #[tokio::test] - async fn test_simple_get() { + async fn test_default_get() { let server = MockServer::start(); let checker = HttpChecker::new(); @@ -196,7 +210,46 @@ mod tests { assert_eq!(result.status, CheckStatus::Success); assert_eq!( result.request_info.as_ref().map(|i| i.request_type), - Some(RequestType::Get) + Some(RequestMethod::Get) + ); + + get_mock.assert(); + } + + #[tokio::test] + async fn test_configured_post() { + let server = MockServer::start(); + let checker = HttpChecker::new(); + + let get_mock = server.mock(|when, then| { + when.method(Method::POST) + .path("/no-head") + .header_exists("sentry-trace") + .body("{\"key\":\"value\"}") + .header("User-Agent", UPTIME_USER_AGENT.to_string()) + .header("Authorization", "Bearer my-token".to_string()) + .header("X-My-Custom-Header", "value".to_string()); + then.status(200); + }); + + let config = CheckConfig { + url: server.url("/no-head").to_string(), + request_method: RequestMethod::Post, + request_headers: HashMap::from([ + ("Authorization".to_string(), "Bearer my-token".to_string()), + ("X-My-Custom-Header".to_string(), "value".to_string()), + ]), + request_body: "{\"key\":\"value\"}".to_string(), + ..Default::default() + }; + + let tick = make_tick(); + let result = checker.check_url(&config, &tick).await; + + assert_eq!(result.status, CheckStatus::Success); + assert_eq!( + result.request_info.as_ref().map(|i| i.request_type), + Some(RequestMethod::Post) ); get_mock.assert(); diff --git a/src/producer/kafka_producer.rs b/src/producer/kafka_producer.rs index 1e82a1b..3fdc8a1 100644 --- a/src/producer/kafka_producer.rs +++ b/src/producer/kafka_producer.rs @@ -52,9 +52,11 @@ mod tests { use super::KafkaResultsProducer; use crate::{ producer::{ExtractCodeError, ResultsProducer}, - types::result::{ - CheckResult, CheckStatus, CheckStatusReason, CheckStatusReasonType, RequestInfo, - RequestType, + types::{ + result::{ + CheckResult, CheckStatus, CheckStatusReason, CheckStatusReasonType, RequestInfo, + }, + shared::RequestMethod, }, }; use chrono::{TimeDelta, Utc}; @@ -78,7 +80,7 @@ mod tests { actual_check_time: Utc::now(), duration: Some(TimeDelta::seconds(1)), request_info: Some(RequestInfo { - request_type: RequestType::Head, + request_type: RequestMethod::Get, http_status_code: Some(200), }), }; diff --git a/src/types.rs b/src/types.rs index bd411ef..5b6c11b 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,2 +1,3 @@ pub mod check_config; pub mod result; +pub mod shared; diff --git a/src/types/check_config.rs b/src/types/check_config.rs index e0e771c..fa3368b 100644 --- a/src/types/check_config.rs +++ b/src/types/check_config.rs @@ -8,6 +8,8 @@ use std::{ }; use uuid::Uuid; +use super::shared::RequestMethod; + const ONE_MINUTE: isize = 60; /// Valid intervals between the checks in seconds. @@ -25,25 +27,6 @@ pub enum CheckInterval { /// The largest check interval pub const MAX_CHECK_INTERVAL_SECS: usize = CheckInterval::SixtyMinutes as usize; -/// Request methods available for the check configuration. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)] -#[serde(rename_all = "UPPERCASE")] -pub enum RequestMethod { - Get, - Post, - Head, - Put, - Delete, - Patch, - Options, -} - -impl Default for RequestMethod { - fn default() -> Self { - Self::Get - } -} - /// The CheckConfig represents a configuration for a single check. #[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] diff --git a/src/types/result.rs b/src/types/result.rs index dcd9f73..8c0ef17 100644 --- a/src/types/result.rs +++ b/src/types/result.rs @@ -5,6 +5,8 @@ use serde_with::chrono; use serde_with::serde_as; use uuid::Uuid; +use super::shared::RequestMethod; + fn uuid_simple(uuid: &Uuid, serializer: S) -> Result where S: serde::Serializer, @@ -30,14 +32,6 @@ pub enum CheckStatusReasonType { Failure, } -/// The type of HTTP request used for the check -#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum RequestType { - Head, - Get, -} - /// Captures the reason for a check's given status #[derive(Debug, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] @@ -54,7 +48,7 @@ pub struct CheckStatusReason { #[serde(rename_all = "snake_case")] pub struct RequestInfo { /// The type of HTTP method used for the check - pub request_type: RequestType, + pub request_type: RequestMethod, /// The status code of the response. May be empty when the request did not receive a response /// whatsoever. diff --git a/src/types/shared.rs b/src/types/shared.rs new file mode 100644 index 0000000..0261e2a --- /dev/null +++ b/src/types/shared.rs @@ -0,0 +1,34 @@ +use serde::{Deserialize, Serialize}; + +/// Common requets methods. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum RequestMethod { + Get, + Post, + Head, + Put, + Delete, + Patch, + Options, +} + +impl Default for RequestMethod { + fn default() -> Self { + Self::Get + } +} + +impl From for reqwest::Method { + fn from(value: RequestMethod) -> Self { + match value { + RequestMethod::Get => reqwest::Method::GET, + RequestMethod::Post => reqwest::Method::POST, + RequestMethod::Head => reqwest::Method::HEAD, + RequestMethod::Put => reqwest::Method::PUT, + RequestMethod::Delete => reqwest::Method::DELETE, + RequestMethod::Patch => reqwest::Method::PATCH, + RequestMethod::Options => reqwest::Method::OPTIONS, + } + } +}