sui_metric_checker/
lib.rs1use anyhow::anyhow;
4use chrono::{DateTime, Duration, NaiveDateTime, Utc};
5use humantime::parse_duration;
6use serde::Deserialize;
7use strum_macros::Display;
8
9pub mod query;
10
11#[derive(Debug, Display, Deserialize, PartialEq)]
12pub enum QueryType {
13    Instant,
15    Range {
17        start: String,
24        end: String,
25        step: f64,
27        percentile: u8,
30    },
31}
32
33#[derive(Debug, Display, Deserialize, PartialEq)]
34pub enum Condition {
35    Greater,
36    Equal,
37    Less,
38}
39
40#[derive(Debug, Deserialize, PartialEq)]
49pub struct QueryResultValidation {
50    pub threshold: f64,
52    pub failure_condition: Condition,
55}
56
57#[derive(Debug, Deserialize, PartialEq)]
58pub struct Query {
59    pub query: String,
61    #[serde(rename = "type")]
63    pub query_type: QueryType,
64    pub validate_result: Option<QueryResultValidation>,
67}
68
69#[derive(Debug, Deserialize)]
70pub struct Config {
71    pub queries: Vec<Query>,
72}
73
74pub trait NowProvider {
77    fn now() -> DateTime<Utc>;
78}
79
80pub struct UtcNowProvider;
81
82impl NowProvider for UtcNowProvider {
84    fn now() -> DateTime<Utc> {
85        Utc::now()
86    }
87}
88
89pub fn timestamp_string_to_unix_seconds<N: NowProvider>(
97    timestamp: &str,
98) -> Result<i64, anyhow::Error> {
99    if timestamp.starts_with("now") {
100        if let Some(relative_timestamp) = timestamp.strip_prefix("now-") {
101            let duration = parse_duration(relative_timestamp)?;
102            let now = N::now();
103            let new_datetime = now.checked_sub_signed(Duration::from_std(duration)?);
104
105            if let Some(datetime) = new_datetime {
106                return Ok(datetime.timestamp());
107            } else {
108                return Err(anyhow!("Unable to calculate time offset"));
109            }
110        }
111
112        return Ok(N::now().timestamp());
113    }
114
115    if let Ok(datetime) = NaiveDateTime::parse_from_str(timestamp, "%Y-%m-%d %H:%M:%S") {
116        let utc_datetime: DateTime<Utc> = DateTime::from_naive_utc_and_offset(datetime, Utc);
117        Ok(utc_datetime.timestamp())
118    } else {
119        Err(anyhow!("Invalid timestamp format"))
120    }
121}
122
123pub fn fails_threshold_condition(
124    queried_value: f64,
125    threshold: f64,
126    failure_condition: &Condition,
127) -> bool {
128    match failure_condition {
129        Condition::Greater => queried_value > threshold,
130        Condition::Equal => queried_value == threshold,
131        Condition::Less => queried_value < threshold,
132    }
133}
134
135fn unix_seconds_to_timestamp_string(unix_seconds: i64) -> String {
136    DateTime::from_timestamp(unix_seconds, 0)
137        .unwrap()
138        .to_string()
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use chrono::TimeZone;
145
146    struct MockNowProvider;
147
148    impl NowProvider for MockNowProvider {
149        fn now() -> DateTime<Utc> {
150            Utc.timestamp_opt(1628553600, 0).unwrap()
151        }
152    }
153
154    #[test]
155    fn test_parse_timestamp_string_to_unix_seconds() {
156        let timestamp = "2021-08-10 00:00:00";
157        let unix_seconds = timestamp_string_to_unix_seconds::<MockNowProvider>(timestamp).unwrap();
158        assert_eq!(unix_seconds, 1628553600);
159
160        let timestamp = "now";
161        let unix_seconds = timestamp_string_to_unix_seconds::<MockNowProvider>(timestamp).unwrap();
162        assert_eq!(unix_seconds, 1628553600);
163
164        let timestamp = "now-1h";
165        let unix_seconds = timestamp_string_to_unix_seconds::<MockNowProvider>(timestamp).unwrap();
166        assert_eq!(unix_seconds, 1628553600 - 3600);
167
168        let timestamp = "now-30m 10s";
169        let unix_seconds = timestamp_string_to_unix_seconds::<MockNowProvider>(timestamp).unwrap();
170        assert_eq!(unix_seconds, 1628553600 - 1810);
171    }
172
173    #[test]
174    fn test_unix_seconds_to_timestamp_string() {
175        let unix_seconds = 1628534400;
176        let timestamp = unix_seconds_to_timestamp_string(unix_seconds);
177        assert_eq!(timestamp, "2021-08-09 18:40:00 UTC");
178    }
179
180    #[test]
181    fn test_parse_config() {
182        let config = r#"
183            queries:
184              - query: 'max(current_epoch{network="testnet"})'
185                type: Instant
186
187              - query: 'histogram_quantile(0.50, sum by(le) (rate(round_latency{network="testnet"}[15m])))'
188                type: !Range 
189                  start: "now-1h"
190                  end: "now"
191                  step: 60.0
192                  percentile: 50
193                validate_result:
194                  threshold: 3.0
195                  failure_condition: Greater
196        "#;
197
198        let config: Config = serde_yaml::from_str(config).unwrap();
199
200        let expected_range_query = Query {
201            query: "histogram_quantile(0.50, sum by(le) (rate(round_latency{network=\"testnet\"}[15m])))".to_string(),
202            query_type: QueryType::Range {
203                start: "now-1h".to_string(),
204                end: "now".to_string(),
205                step: 60.0,
206                percentile: 50,
207            },
208            validate_result: Some(QueryResultValidation {
209                threshold: 3.0,
210                failure_condition: Condition::Greater,
211            }),
212        };
213
214        let expected_instant_query = Query {
215            query: "max(current_epoch{network=\"testnet\"})".to_string(),
216            query_type: QueryType::Instant,
217            validate_result: None,
218        };
219
220        let expected_queries = vec![expected_instant_query, expected_range_query];
221
222        assert_eq!(config.queries, expected_queries);
223    }
224}