1use time::OffsetDateTime;
4
5use crate::models::monitor::{Data, Measurement};
6
7#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, strum::Display, sqlx::Type)]
9#[serde(rename_all = "lowercase")]
10#[strum(serialize_all = "lowercase")]
11#[sqlx(type_name = "region", rename_all = "lowercase")]
12pub enum Region {
13 Europe,
14 America,
15 Asia,
16 Australia,
17}
18
19#[derive(
21 Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, sqlx::Type,
22)]
23#[serde(rename_all = "lowercase")]
24#[sqlx(type_name = "target", rename_all = "lowercase")]
25pub enum Target {
26 Monitor,
27 #[serde(rename = "monitor:test")]
28 #[sqlx(rename = "monitor:test")]
29 MonitorTest,
30 Heartbeat,
31 #[serde(rename = "heartbeat:test")]
32 #[sqlx(rename = "heartbeat:test")]
33 HeartbeatTest,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, sqlx::Type)]
37#[sqlx(type_name = "system_metric")]
38pub enum SystemMetricName {
39 #[serde(rename = "monitor.up")]
44 #[sqlx(rename = "monitor.up")]
45 MonitorUp,
46
47 #[serde(rename = "heartbeat.up")]
52 #[sqlx(rename = "heartbeat.up")]
53 HeartbeatUp,
54
55 #[serde(rename = "monitor.ssl.days_left")]
59 #[sqlx(rename = "monitor.ssl.days_left")]
60 MonitorSslDaysLeft,
61
62 #[serde(rename = "monitor.domain.days_left")]
66 #[sqlx(rename = "monitor.domain.days_left")]
67 MonitorDomainDaysLeft,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, sqlx::Type)]
76#[serde(rename_all = "snake_case")]
77#[sqlx(type_name = "error_type")]
78#[sqlx(rename_all = "snake_case")]
79pub enum ErrorType {
80 DnsError,
82
83 Timeout,
85
86 StatusMismatch,
88
89 KeywordNotFound,
91
92 NetworkError,
94
95 Unknown,
97}
98
99#[derive(Debug)]
102pub struct SystemMetric {
103 pub timestamp: OffsetDateTime,
105
106 pub target_id: i64,
108
109 pub target: Target,
111
112 pub name: SystemMetricName,
114
115 pub value: f32,
117
118 pub error: MetricError,
120}
121
122#[derive(Debug)]
124pub struct MetricError {
125 pub r#type: Option<ErrorType>,
127
128 pub details: Option<String>,
130}
131
132impl From<&Measurement> for SystemMetric {
133 fn from(measurement: &Measurement) -> Self {
134 let make = |name: SystemMetricName, value: f32| SystemMetric {
135 target: Target::Monitor,
136 target_id: measurement.monitor_id,
137 timestamp: measurement.timestamp,
138 name,
139 value,
140 error: measurement
141 .error
142 .as_ref()
143 .map(Into::into)
144 .unwrap_or_default(),
145 };
146
147 if measurement.error.is_some() {
148 make(SystemMetricName::MonitorUp, 0f32)
149 } else {
150 match measurement.data.as_ref() {
151 Some(Data::Ping(_)) => make(SystemMetricName::MonitorUp, 1f32),
152 Some(Data::Http(_)) => make(SystemMetricName::MonitorUp, 1f32),
153 _ => unimplemented!("Measurement data is not implemented"),
154 }
155 }
156 }
157}
158
159impl Default for MetricError {
160 fn default() -> Self {
161 Self {
162 r#type: None,
163 details: None,
164 }
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use std::hash::Hash;
171
172 use rstest::rstest;
173 use serde_json;
174 use static_assertions::assert_impl_all;
175 use time::OffsetDateTime;
176
177 use super::*;
178 use crate::collectors::monitor::errors::{CollectorError, PingError};
179 use crate::models::common::Region;
180 use crate::models::monitor::measurement::{HttpData, PingData};
181
182 assert_impl_all!(Target: PartialEq, Eq, Hash);
183
184 assert_impl_all!(SystemMetricName: Clone, Copy);
185 assert_impl_all!(ErrorType: Clone, Copy);
186
187 assert_impl_all!(Region: Clone, Copy);
188 assert_impl_all!(Region: serde::Serialize, serde::Deserialize<'static>);
189
190 #[test]
191 fn test_region_to_string() {
192 let cases: Vec<(Region, &str)> = vec![
193 (Region::Europe, "europe"),
194 (Region::America, "america"),
195 (Region::Asia, "asia"),
196 (Region::Australia, "australia"),
197 ];
198
199 for (region, expected) in cases {
200 assert_eq!(region.to_string(), expected);
201 }
202 }
203
204 #[rstest]
205 #[case(SystemMetricName::MonitorUp, "monitor.up")]
206 #[case(SystemMetricName::HeartbeatUp, "heartbeat.up")]
207 #[case(SystemMetricName::MonitorSslDaysLeft, "monitor.ssl.days_left")]
208 #[case(SystemMetricName::MonitorDomainDaysLeft, "monitor.domain.days_left")]
209 fn test_metric_name_serialization(#[case] metric: SystemMetricName, #[case] expected_str: &str) {
210 let json = serde_json::to_string(&metric).unwrap();
211 assert_eq!(json, format!("\"{}\"", expected_str));
212
213 let parsed: SystemMetricName = serde_json::from_str(&json).unwrap();
214 assert_eq!(parsed, metric);
215 }
216
217 #[rstest]
218 #[case(ErrorType::DnsError, "dns_error")]
219 #[case(ErrorType::Timeout, "timeout")]
220 #[case(ErrorType::StatusMismatch, "status_mismatch")]
221 #[case(ErrorType::KeywordNotFound, "keyword_not_found")]
222 #[case(ErrorType::NetworkError, "network_error")]
223 #[case(ErrorType::Unknown, "unknown")]
224 fn test_error_type_serialization(#[case] value: ErrorType, #[case] expected_str: &str) {
225 let json = serde_json::to_string(&value).unwrap();
226 assert_eq!(json, format!("\"{}\"", expected_str));
227
228 let parsed: ErrorType = serde_json::from_str(&json).unwrap();
229 assert_eq!(parsed, value);
230 }
231
232 #[rstest]
233 #[case(
234 Measurement {
235 monitor_id: 1,
236 timestamp: OffsetDateTime::now_utc(),
237 data: Some(Data::Ping(PingData { dns: 10.0, rtt: 20.0 })),
238 error: None,
239 },
240 (SystemMetricName::MonitorUp, 1.0)
241 )]
242 #[case(
243 Measurement {
244 monitor_id: 2,
245 timestamp: OffsetDateTime::now_utc(),
246 data: Some(Data::Http(HttpData {
247 dns: 15.0, tcp: 25.0, tls: 35.0, ttfb: 45.0, transfer: 55.0
248 })),
249 error: None,
250 },
251 (SystemMetricName::MonitorUp, 1.0)
252 )]
253 #[case(
254 Measurement {
255 monitor_id: 3,
256 timestamp: OffsetDateTime::now_utc(),
257 data: None,
258 error: Some(CollectorError::Ping(PingError::Timeout { timeout: 5 })),
259 },
260 (SystemMetricName::MonitorUp, 0f32)
261 )]
262 fn test_measurement_to_metrics(
263 #[case] measurement: Measurement,
264 #[case] expected: (SystemMetricName, f32),
265 ) {
266 let (expected_name, expected_value) = expected;
267 let metric: SystemMetric = (&measurement).into();
268
269 assert_eq!(metric.name, expected_name);
270 assert_eq!(metric.value, expected_value);
271 }
272
273 #[test]
274 fn test_metric_error_default() {
275 let value = MetricError::default();
276
277 assert!(value.r#type.is_none());
278 assert!(value.details.is_none());
279 }
280}