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}