diff --git a/Cargo.lock b/Cargo.lock index 75bf5260f4a88d197a5c85186334ff41ee8f4add..0400103b8eacdf18ffe6ad92f1d5606b2f3d3176 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -443,6 +443,7 @@ name = "mquery" version = "0.2.0" dependencies = [ "dotenv", + "enum-as-inner", "prometheus-http-query", "reqwest", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 3bdc678143e05580a8a0a3681c7ee49d7c76cb14..08fef249d36b8d162e6b15d945510fea91028e02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +enum-as-inner = "0.6.0" prometheus-http-query = "0.8.0" reqwest = "0.11.22" url = "2.5.0" diff --git a/src/lib.rs b/src/lib.rs index baaef7194d09247dd69551013dcec6792cd2976d..44c6480f1a218d45aa484047f8c8f1f051e68363 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,12 @@ #![doc = include_str!("../README.md")] pub mod query; +pub mod result; -use prometheus_http_query::{response::PromqlResult, Client, Error}; +use prometheus_http_query::Client; use query::IntoQuery; use reqwest::header::HeaderValue; +use result::{IntoQryResult, QryResult}; use url::Url; /// The primary way to send queries and get deserialized responses from the metrics server @@ -65,7 +67,7 @@ impl QueryManager { /// The trait is implemented for `&str`, `String`, `&String`, and __mquery's__ /// internal types such as [`query::Metric`], [`query::Scalar`] and others that result /// from the query builder API. - pub async fn query<Q: IntoQuery>(&self, query: Q) -> Result<PromqlResult, Error> { + pub async fn query<Q: IntoQuery>(&self, query: Q) -> QryResult { let mut builder = self.client.query(query.into_query().as_ref()); if let Some((name, val)) = self.auth.get_header() { builder = builder.header(name, val); @@ -74,8 +76,8 @@ impl QueryManager { builder = builder.timeout(*timeout); } match self.method { - Method::Get => builder.get().await, - Method::Post => builder.post().await, + Method::Get => builder.get().await.into_qry_result(), + Method::Post => builder.post().await.into_qry_result(), } } @@ -91,7 +93,7 @@ impl QueryManager { start: i64, end: i64, step: f64, - ) -> Result<PromqlResult, Error> { + ) -> QryResult { let mut builder = self .client .query_range(query.into_query().as_ref(), start, end, step); @@ -102,8 +104,8 @@ impl QueryManager { builder = builder.timeout(*timeout); } match self.method { - Method::Get => builder.get().await, - Method::Post => builder.post().await, + Method::Get => builder.get().await.into_qry_result(), + Method::Post => builder.post().await.into_qry_result(), } } } diff --git a/src/result.rs b/src/result.rs new file mode 100644 index 0000000000000000000000000000000000000000..949d5ad80b4413991409e39c6e3157dbb064d6bd --- /dev/null +++ b/src/result.rs @@ -0,0 +1,233 @@ +//! `Data` and the `Error` types which are essentially clones of the types +//! with the same name from the `prometheus-http-query` crate with slight modifications +//! to suit the needs of `mqeury`. +//! +//! Another reason why this crate maintains its own types which are the same (almost) as the ones +//! provided by the other crate that is used internally, is to ensure that the public +//! API remains stable even if the internal crate API changes. + +use enum_as_inner::EnumAsInner; +use prometheus_http_query::{error, response}; +use std::fmt::{self, Display}; + +pub type QryResult = std::result::Result<Data, Error>; + +/// Internal trait used for converting the `prometheus-http-query` crate's query result into the internal result type +pub(crate) trait IntoQryResult { + fn into_qry_result(self) -> QryResult; +} + +impl IntoQryResult for Result<response::PromqlResult, error::Error> { + fn into_qry_result(self) -> QryResult { + self.map(|r| r.into()).map_err(|e| e.into()) + } +} + +/// A wrapper for possible result types of expression queries ([`Client::query`](crate::Client::query) and [`Client::query_range`](crate::Client::query_range)). +#[derive(Clone, Debug, EnumAsInner)] +pub enum Data { + Vector(Vec<response::InstantVector>), + Matrix(Vec<response::RangeVector>), + Scalar(response::Sample), +} + +impl From<response::PromqlResult> for Data { + fn from(value: response::PromqlResult) -> Self { + let (data, _) = value.into_inner(); + match data { + response::Data::Vector(v) => Data::Vector(v), + response::Data::Matrix(m) => Data::Matrix(m), + response::Data::Scalar(s) => Data::Scalar(s), + } + } +} + +impl Data { + /// This is a shortcut to check if the query returned any data at all regardless of the exact type. + pub fn is_empty(&self) -> bool { + match self { + Data::Vector(v) => v.is_empty(), + Data::Matrix(v) => v.is_empty(), + Data::Scalar(_) => false, + } + } +} + +/// A global error enum that contains all errors that are returned by this +/// library. Some errors are wrappers for errors from underlying libraries. +/// All errors (this enum as well as all contained structs) implement [`std::error::Error`]. +#[non_exhaustive] +#[derive(Debug, Clone)] +pub enum Error { + /// Wraps errors from the underlying [`reqwest::Client`] that cannot be mapped + /// to a more specific error type. Deserialization errors also fall into this + /// category. + Client(ClientError), + /// Occurs when Prometheus responds with e.g. HTTP 4xx (e.g. due to a syntax error in a PromQL query).<br> + /// Details on the error as reported by Prometheus are included in [`PrometheusError`]. + Prometheus(PrometheusError), + /// Occurs when the [`Client::series`](crate::Client::series) method is called with an empty set of + /// series [`Selector`](crate::selector::Selector)s. According to the Prometheus API description at least one + /// [`Selector`](crate::selector::Selector) must be provided. + EmptySeriesSelector, +} + +impl Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Client(e) => e.fmt(f), + Self::Prometheus(e) => e.fmt(f), + Self::EmptySeriesSelector => f.write_str("at least one series selector must be provided in order to query the series endpoint"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Client(e) => e.source(), + Self::Prometheus(p) => p.source(), + Self::EmptySeriesSelector => None, + } + } +} + +impl From<prometheus_http_query::Error> for Error { + fn from(value: prometheus_http_query::Error) -> Self { + match value { + prometheus_http_query::Error::Client(c) => Self::Client(c.into()), + prometheus_http_query::Error::Prometheus(p) => Self::Prometheus(p.into()), + prometheus_http_query::Error::EmptySeriesSelector => Self::EmptySeriesSelector, + _ => unreachable!(), + } + } +} + +/// This error is thrown when the JSON response's `status` field contains `error`.<br> +/// The error-related information from the JSON body is included in this error. +#[derive(Debug, Clone, PartialEq)] +pub struct PrometheusError { + pub(crate) error_type: PrometheusErrorType, + pub(crate) message: String, +} + +impl std::error::Error for PrometheusError {} + +impl fmt::Display for PrometheusError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}: {}", self.error_type, self.message) + } +} + +impl From<error::PrometheusError> for PrometheusError { + fn from(value: error::PrometheusError) -> Self { + let error_type = PrometheusErrorType::from(value.error_type()); + let message = value.message().to_owned(); + PrometheusError { + error_type, + message, + } + } +} + +impl PrometheusError { + /// Returns the parsed version of the error type that was given by the Prometheus API. + pub fn error_type(&self) -> PrometheusErrorType { + self.error_type + } + + /// Returns the error message that was given by the Prometheus API. + pub fn message(&self) -> &str { + &self.message + } + + pub fn is_timeout(&self) -> bool { + self.error_type == PrometheusErrorType::Timeout + } + + pub fn is_canceled(&self) -> bool { + self.error_type == PrometheusErrorType::Canceled + } + + pub fn is_execution(&self) -> bool { + self.error_type == PrometheusErrorType::Execution + } + + pub fn is_bad_data(&self) -> bool { + self.error_type == PrometheusErrorType::BadData + } + + pub fn is_internal(&self) -> bool { + self.error_type == PrometheusErrorType::Internal + } + + pub fn is_unavailable(&self) -> bool { + self.error_type == PrometheusErrorType::Unavailable + } + + pub fn is_not_found(&self) -> bool { + self.error_type == PrometheusErrorType::NotFound + } +} + +/// The parsed error type as returned by the Prometheus API. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum PrometheusErrorType { + Timeout, + Canceled, + Execution, + BadData, + Internal, + Unavailable, + NotFound, +} + +impl From<error::PrometheusErrorType> for PrometheusErrorType { + fn from(value: error::PrometheusErrorType) -> Self { + match value { + error::PrometheusErrorType::Timeout => PrometheusErrorType::Timeout, + error::PrometheusErrorType::Canceled => PrometheusErrorType::Canceled, + error::PrometheusErrorType::Execution => PrometheusErrorType::Execution, + error::PrometheusErrorType::BadData => PrometheusErrorType::BadData, + error::PrometheusErrorType::Internal => PrometheusErrorType::Internal, + error::PrometheusErrorType::Unavailable => PrometheusErrorType::Unavailable, + error::PrometheusErrorType::NotFound => PrometheusErrorType::NotFound, + _ => unreachable!(), + } + } +} + +impl fmt::Display for PrometheusErrorType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Timeout => f.write_str("timeout"), + Self::Canceled => f.write_str("canceled"), + Self::Execution => f.write_str("execution"), + Self::BadData => f.write_str("bad_data"), + Self::Internal => f.write_str("internal"), + Self::Unavailable => f.write_str("unavailable"), + Self::NotFound => f.write_str("not_found"), + } + } +} + +/// Is thrown when the [`Client`](crate::Client) or the underlying +/// [`reqwest::Error`] fail to build or execute a request. +#[derive(Debug, Clone)] +pub struct ClientError(String); + +impl From<prometheus_http_query::error::ClientError> for ClientError { + fn from(value: prometheus_http_query::error::ClientError) -> Self { + let message = value.to_string(); + ClientError(message) + } +} + +impl fmt::Display for ClientError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(&self.0) + } +} + +impl std::error::Error for ClientError {} diff --git a/tests/utils.rs b/tests/utils.rs index dbaa3a86db7e4f85e5c5b53bf574c6e2e51ef0e0..e686901e0bdb3f6f0d62e35b4217f3b5dd01760c 100644 --- a/tests/utils.rs +++ b/tests/utils.rs @@ -1,9 +1,8 @@ -use mquery::{query::IntoQuery, QueryManager}; -use prometheus_http_query::{response::PromqlResult, Error}; +use mquery::{query::IntoQuery, result::QryResult, QueryManager}; // local victoria metrics server pub static URL: &str = "http://localhost:8428"; -pub async fn send_query(query: impl IntoQuery) -> Result<PromqlResult, Error> { +pub async fn send_query(query: impl IntoQuery) -> QryResult { QueryManager::new(URL.parse().unwrap()).query(query).await }