prometheus_http_query/
util.rs

1use crate::error::{Error, ParseUrlError};
2use mime::Mime;
3use reqwest::header::HeaderValue;
4use serde::Deserialize;
5use std::fmt;
6use url::Url;
7
8/// A helper enum to filter targets by state.
9#[derive(Debug)]
10pub enum TargetState {
11    Active,
12    Dropped,
13    Any,
14}
15
16impl fmt::Display for TargetState {
17    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18        match self {
19            Self::Active => f.write_str("active"),
20            Self::Dropped => f.write_str("dropped"),
21            Self::Any => f.write_str("any"),
22        }
23    }
24}
25
26/// A helper enum to represent possible target health states.
27#[derive(Debug, Copy, Clone, Deserialize, Eq, PartialEq)]
28pub enum TargetHealth {
29    #[serde(alias = "up")]
30    Up,
31    #[serde(alias = "down")]
32    Down,
33    #[serde(alias = "unknown")]
34    Unknown,
35}
36
37impl TargetHealth {
38    pub fn is_up(&self) -> bool {
39        *self == Self::Up
40    }
41
42    pub fn is_down(&self) -> bool {
43        *self == Self::Down
44    }
45
46    pub fn is_unknown(&self) -> bool {
47        *self == Self::Unknown
48    }
49}
50
51impl fmt::Display for TargetHealth {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        match self {
54            Self::Up => f.write_str("up"),
55            Self::Down => f.write_str("down"),
56            Self::Unknown => f.write_str("unknown"),
57        }
58    }
59}
60
61/// A helper enum to represent possible rule health states.
62#[derive(Debug, Copy, Clone, Deserialize, Eq, PartialEq)]
63pub enum RuleHealth {
64    #[serde(alias = "ok")]
65    Good,
66    #[serde(alias = "err")]
67    Bad,
68    #[serde(alias = "unknown")]
69    Unknown,
70}
71
72impl RuleHealth {
73    pub fn is_good(&self) -> bool {
74        *self == Self::Good
75    }
76
77    pub fn is_bad(&self) -> bool {
78        *self == Self::Bad
79    }
80
81    pub fn is_unknown(&self) -> bool {
82        *self == Self::Unknown
83    }
84}
85
86impl fmt::Display for RuleHealth {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        match self {
89            Self::Good => f.write_str("ok"),
90            Self::Bad => f.write_str("err"),
91            Self::Unknown => f.write_str("unknown"),
92        }
93    }
94}
95
96/// A helper type to represent possible rule health states.
97#[derive(Debug, Copy, Clone, Deserialize, Eq, PartialEq)]
98pub enum AlertState {
99    #[serde(alias = "inactive")]
100    Inactive,
101    #[serde(alias = "pending")]
102    Pending,
103    #[serde(alias = "firing")]
104    Firing,
105}
106
107impl AlertState {
108    pub fn is_inactive(&self) -> bool {
109        *self == Self::Inactive
110    }
111
112    pub fn is_pending(&self) -> bool {
113        *self == Self::Pending
114    }
115
116    pub fn is_firing(&self) -> bool {
117        *self == Self::Firing
118    }
119}
120
121impl fmt::Display for AlertState {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        match self {
124            Self::Inactive => f.write_str("inactive"),
125            Self::Pending => f.write_str("pending"),
126            Self::Firing => f.write_str("firing"),
127        }
128    }
129}
130
131/// A helper enum to filter rules by type.
132#[derive(Copy, Clone, Debug)]
133pub enum RuleKind {
134    Alerting,
135    Recording,
136}
137
138impl RuleKind {
139    pub(crate) fn to_query_param(&self) -> String {
140        match self {
141            Self::Alerting => String::from("alert"),
142            Self::Recording => String::from("record"),
143        }
144    }
145}
146
147impl fmt::Display for RuleKind {
148    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149        match self {
150            Self::Alerting => f.write_str("alerting"),
151            Self::Recording => f.write_str("recording"),
152        }
153    }
154}
155
156#[allow(clippy::enum_variant_names)]
157#[derive(Debug, Clone, PartialEq)]
158pub(crate) enum Label<'a> {
159    Equal((&'a str, &'a str)),
160    NotEqual((&'a str, &'a str)),
161    RegexEqual((&'a str, &'a str)),
162    RegexNotEqual((&'a str, &'a str)),
163}
164
165impl<'a> fmt::Display for Label<'a> {
166    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
167        match self {
168            Self::Equal((k, v)) => write!(f, "{}=\"{}\"", k, v),
169            Self::NotEqual((k, v)) => write!(f, "{}!=\"{}\"", k, v),
170            Self::RegexEqual((k, v)) => write!(f, "{}=~\"{}\"", k, v),
171            Self::RegexNotEqual((k, v)) => write!(f, "{}!~\"{}\"", k, v),
172        }
173    }
174}
175
176/// Create a base URL that is common to all queries from a string literal.
177/// The implementations are probably a little more complicated than they need
178/// to be as we need to allocate a new string during the URL construction
179/// to preserve a possibly existing path component in the source string.
180pub(crate) trait ToBaseUrl {
181    fn to_base_url(self) -> Result<Url, Error>;
182}
183
184impl ToBaseUrl for &str {
185    fn to_base_url(self) -> Result<Url, Error> {
186        Url::parse(self).map_err(|source| {
187            Error::ParseUrl(ParseUrlError {
188                message: "failed to build Prometheus server base URL",
189                source,
190            })
191        })
192    }
193}
194
195impl ToBaseUrl for String {
196    fn to_base_url(self) -> Result<Url, Error> {
197        Url::parse(&self).map_err(|source| {
198            Error::ParseUrl(ParseUrlError {
199                message: "failed to build Prometheus server base URL",
200                source,
201            })
202        })
203    }
204}
205
206pub(crate) fn build_final_url(mut url: Url, path: &str) -> Url {
207    let base_path = url.path();
208    match base_path {
209        "/" => return url.join(path).unwrap(),
210        _ => {
211            let p = format!("{}/{}", base_path, path);
212            url.set_path(&p);
213        }
214    }
215    url
216}
217
218pub(crate) fn is_json(v: Option<&HeaderValue>) -> bool {
219    match v
220        .and_then(|h| h.to_str().ok())
221        .and_then(|h| h.parse::<Mime>().ok())
222    {
223        Some(mime) => match (mime.type_(), mime.subtype()) {
224            (mime::APPLICATION, mime::JSON) => true,
225            _ => false,
226        },
227        None => false,
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use super::{build_final_url, is_json, ToBaseUrl};
234
235    #[test]
236    fn test_simple_str_to_url() {
237        let s = "http://127.0.0.1:9090";
238        let url = s.to_base_url().unwrap();
239        assert_eq!("http://127.0.0.1:9090/", url.as_str());
240    }
241
242    #[test]
243    fn test_proxied_str_to_url() {
244        let s = "http://proxy.example.com/prometheus";
245        let url = s.to_base_url().unwrap();
246        assert_eq!("http://proxy.example.com/prometheus", url.as_str());
247    }
248
249    #[test]
250    fn test_simple_string_to_url() {
251        let s = String::from("http://127.0.0.1:9090");
252        let url = s.to_base_url().unwrap();
253        assert_eq!("http://127.0.0.1:9090/", url.as_str());
254    }
255
256    #[test]
257    fn test_proxied_string_to_url() {
258        let s = String::from("http://proxy.example.com/prometheus");
259        let url = s.to_base_url().unwrap();
260        assert_eq!("http://proxy.example.com/prometheus", url.as_str());
261    }
262
263    #[test]
264    fn test_simple_url_finalization() {
265        let s = String::from("http://127.0.0.1:9090");
266        let url = s.to_base_url().unwrap();
267        let final_url = build_final_url(url, "api/v1/targets");
268        assert_eq!("http://127.0.0.1:9090/api/v1/targets", final_url.as_str());
269    }
270
271    #[test]
272    fn test_proxied_url_finalization() {
273        let s = String::from("http://proxy.example.com/prometheus");
274        let url = s.to_base_url().unwrap();
275        let final_url = build_final_url(url, "api/v1/targets");
276        assert_eq!(
277            "http://proxy.example.com/prometheus/api/v1/targets",
278            final_url.as_str()
279        );
280    }
281
282    #[test]
283    fn test_is_json() {
284        let header = reqwest::header::HeaderValue::from_static("application/json");
285        assert!(is_json(Some(&header)));
286    }
287
288    #[test]
289    fn test_is_json_with_charset() {
290        let header = reqwest::header::HeaderValue::from_static("application/json; charset=utf-8");
291        assert!(is_json(Some(&header)));
292    }
293}