iota_metric_checker/
lib.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use anyhow::anyhow;
6use chrono::{DateTime, Duration, NaiveDateTime, Utc};
7use humantime::parse_duration;
8use serde::Deserialize;
9use strum::Display;
10
11pub mod query;
12
13#[derive(Debug, Display, Deserialize, PartialEq)]
14pub enum QueryType {
15    // Checks the last instant value of the query.
16    Instant,
17    // Checks the median value of the query over time.
18    Range {
19        // Both start & end accepts specific time formats
20        //  - "%Y-%m-%d %H:%M:%S" (UTC)
21        // Or relative time + offset, i.e.
22        //  - "now"
23        //  - "now-1h"
24        //  - "now-30m 10s"
25        start: String,
26        end: String,
27        // Query resolution step width as float number of seconds
28        step: f64,
29        // The result of the query is the percentile of the data points.
30        // Valid values are [1, 100].
31        percentile: u8,
32    },
33}
34
35#[derive(Debug, Display, Deserialize, PartialEq)]
36pub enum Condition {
37    Greater,
38    Equal,
39    Less,
40}
41
42// Used to specify validation rules for query result e.g.
43//
44// validate_result:
45//   threshold: 10
46//   failure_condition: Greater
47//
48// Program will report error if queried value is greater than 10, otherwise
49// no error will be reported.
50#[derive(Debug, Deserialize, PartialEq)]
51pub struct QueryResultValidation {
52    // Threshold to report error on
53    pub threshold: f64,
54    // Program will report error if threshold violates condition specified by this
55    // field.
56    pub failure_condition: Condition,
57}
58
59#[derive(Debug, Deserialize, PartialEq)]
60pub struct Query {
61    // PromQL query to exeute
62    pub query: String,
63    // Type of query to execute - Instant or Range.
64    #[serde(rename = "type")]
65    pub query_type: QueryType,
66    // Optional validation rules for the query result, otherwise the query result
67    // is just to be printed in debug logs.
68    pub validate_result: Option<QueryResultValidation>,
69}
70
71#[derive(Debug, Deserialize)]
72pub struct Config {
73    pub queries: Vec<Query>,
74}
75
76// Used to  mock now() in tests and use consistent now() return values across
77// queries in performance checks.
78pub trait NowProvider {
79    fn now() -> DateTime<Utc>;
80}
81
82pub struct UtcNowProvider;
83
84// Basic implementation of NowProvider that returns current time in UTC.
85impl NowProvider for UtcNowProvider {
86    fn now() -> DateTime<Utc> {
87        Utc::now()
88    }
89}
90
91// Convert timestamp string to unix seconds.
92// Accepts the following time formats
93//  - "%Y-%m-%d %H:%M:%S" (UTC)
94// Or relative time + offset, i.e.
95//  - "now"
96//  - "now-1h"
97//  - "now-30m 10s"
98pub fn timestamp_string_to_unix_seconds<N: NowProvider>(
99    timestamp: &str,
100) -> Result<i64, anyhow::Error> {
101    if timestamp.starts_with("now") {
102        if let Some(relative_timestamp) = timestamp.strip_prefix("now-") {
103            let duration = parse_duration(relative_timestamp)?;
104            let now = N::now();
105            let new_datetime = now.checked_sub_signed(Duration::from_std(duration)?);
106
107            if let Some(datetime) = new_datetime {
108                return Ok(datetime.timestamp());
109            } else {
110                return Err(anyhow!("Unable to calculate time offset"));
111            }
112        }
113
114        return Ok(N::now().timestamp());
115    }
116
117    if let Ok(datetime) = NaiveDateTime::parse_from_str(timestamp, "%Y-%m-%d %H:%M:%S") {
118        let utc_datetime: DateTime<Utc> = DateTime::from_naive_utc_and_offset(datetime, Utc);
119        Ok(utc_datetime.timestamp())
120    } else {
121        Err(anyhow!("Invalid timestamp format"))
122    }
123}
124
125pub fn fails_threshold_condition(
126    queried_value: f64,
127    threshold: f64,
128    failure_condition: &Condition,
129) -> bool {
130    match failure_condition {
131        Condition::Greater => queried_value > threshold,
132        Condition::Equal => queried_value == threshold,
133        Condition::Less => queried_value < threshold,
134    }
135}
136
137fn unix_seconds_to_timestamp_string(unix_seconds: i64) -> String {
138    DateTime::from_timestamp(unix_seconds, 0)
139        .unwrap()
140        .to_string()
141}
142
143#[cfg(test)]
144mod tests {
145    use chrono::TimeZone;
146
147    use super::*;
148
149    struct MockNowProvider;
150
151    impl NowProvider for MockNowProvider {
152        fn now() -> DateTime<Utc> {
153            Utc.timestamp_opt(1628553600, 0).unwrap()
154        }
155    }
156
157    #[test]
158    fn test_parse_timestamp_string_to_unix_seconds() {
159        let timestamp = "2021-08-10 00:00:00";
160        let unix_seconds = timestamp_string_to_unix_seconds::<MockNowProvider>(timestamp).unwrap();
161        assert_eq!(unix_seconds, 1628553600);
162
163        let timestamp = "now";
164        let unix_seconds = timestamp_string_to_unix_seconds::<MockNowProvider>(timestamp).unwrap();
165        assert_eq!(unix_seconds, 1628553600);
166
167        let timestamp = "now-1h";
168        let unix_seconds = timestamp_string_to_unix_seconds::<MockNowProvider>(timestamp).unwrap();
169        assert_eq!(unix_seconds, 1628553600 - 3600);
170
171        let timestamp = "now-30m 10s";
172        let unix_seconds = timestamp_string_to_unix_seconds::<MockNowProvider>(timestamp).unwrap();
173        assert_eq!(unix_seconds, 1628553600 - 1810);
174    }
175
176    #[test]
177    fn test_unix_seconds_to_timestamp_string() {
178        let unix_seconds = 1628534400;
179        let timestamp = unix_seconds_to_timestamp_string(unix_seconds);
180        assert_eq!(timestamp, "2021-08-09 18:40:00 UTC");
181    }
182
183    #[test]
184    fn test_parse_config() {
185        let config = r#"
186            queries:
187              - query: 'max(current_epoch{network="testnet"})'
188                type: Instant
189              - query: 'histogram_quantile(0.50, sum by(le) (rate(round_latency{network="testnet"}[15m])))'
190                type: 
191                  Range:
192                    start: "now-1h"
193                    end: "now"
194                    step: 60.0
195                    percentile: 50
196                validate_result:
197                  threshold: 3.0
198                  failure_condition: Greater
199        "#;
200
201        let config: Config = serde_yaml::from_str(config).unwrap();
202
203        let expected_range_query = Query {
204            query: "histogram_quantile(0.50, sum by(le) (rate(round_latency{network=\"testnet\"}[15m])))".to_string(),
205            query_type: QueryType::Range {
206                start: "now-1h".to_string(),
207                end: "now".to_string(),
208                step: 60.0,
209                percentile: 50,
210            },
211            validate_result: Some(QueryResultValidation {
212                threshold: 3.0,
213                failure_condition: Condition::Greater,
214            }),
215        };
216
217        let expected_instant_query = Query {
218            query: "max(current_epoch{network=\"testnet\"})".to_string(),
219            query_type: QueryType::Instant,
220            validate_result: None,
221        };
222
223        let expected_queries = vec![expected_instant_query, expected_range_query];
224
225        assert_eq!(config.queries, expected_queries);
226    }
227}