iota_metric_checker/
lib.rs1use 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 Instant,
17 Range {
19 start: String,
26 end: String,
27 step: f64,
29 percentile: u8,
32 },
33}
34
35#[derive(Debug, Display, Deserialize, PartialEq)]
36pub enum Condition {
37 Greater,
38 Equal,
39 Less,
40}
41
42#[derive(Debug, Deserialize, PartialEq)]
51pub struct QueryResultValidation {
52 pub threshold: f64,
54 pub failure_condition: Condition,
57}
58
59#[derive(Debug, Deserialize, PartialEq)]
60pub struct Query {
61 pub query: String,
63 #[serde(rename = "type")]
65 pub query_type: QueryType,
66 pub validate_result: Option<QueryResultValidation>,
69}
70
71#[derive(Debug, Deserialize)]
72pub struct Config {
73 pub queries: Vec<Query>,
74}
75
76pub trait NowProvider {
79 fn now() -> DateTime<Utc>;
80}
81
82pub struct UtcNowProvider;
83
84impl NowProvider for UtcNowProvider {
86 fn now() -> DateTime<Utc> {
87 Utc::now()
88 }
89}
90
91pub 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}