Skip to main content

limon_core/models/
common.rs

1//! A module containing a set of common models.
2
3use time::OffsetDateTime;
4
5use crate::models::monitor::{Data, Measurement};
6
7/// Represents the supported geographical regions for monitoring.
8#[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/// Represents the target of system metric.
20#[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  /// Base availability metric `monitor.up`
40  ///
41  /// Indicates the availability of the monitor at the time of measurement
42  /// (1 - was available, 0 - was not).
43  #[serde(rename = "monitor.up")]
44  #[sqlx(rename = "monitor.up")]
45  MonitorUp,
46
47  /// Base availability metric `heartbeat.up`
48  ///
49  /// Indicates the availability of the heartbeat
50  /// (0 - was available, other - was not).
51  #[serde(rename = "heartbeat.up")]
52  #[sqlx(rename = "heartbeat.up")]
53  HeartbeatUp,
54
55  /// Expiration metric `monitor.ssl.days_left`
56  ///
57  /// Indicates how many days remain until the certificate expires.
58  #[serde(rename = "monitor.ssl.days_left")]
59  #[sqlx(rename = "monitor.ssl.days_left")]
60  MonitorSslDaysLeft,
61
62  /// Expiration metric `monitor.domain.days_left`
63  ///
64  /// Indicates how many days remain until the domain name expires.
65  #[serde(rename = "monitor.domain.days_left")]
66  #[sqlx(rename = "monitor.domain.days_left")]
67  MonitorDomainDaysLeft,
68}
69
70/// Defines error codes describing the outcome of a monitor execution.
71///
72/// These codes are used to classify the known reason of an error.
73/// [`ErrorType::Unknown`] represents an unclassified error for which
74/// detailed information should be preserved.
75#[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  /// A DNS resolution error occurred.
81  DnsError,
82
83  /// The operation exceeded the configured time limit.
84  Timeout,
85
86  /// The received response status did not match the expected value.
87  StatusMismatch,
88
89  /// The expected keyword was not found in the response body.
90  KeywordNotFound,
91
92  /// A network-level error occurred while establishing a connection.
93  NetworkError,
94
95  /// An unknown or unclassified error.
96  Unknown,
97}
98
99/// Represents a single metric event produced by a target at
100/// a concrete point in time.
101#[derive(Debug)]
102pub struct SystemMetric {
103  /// Unix timestamp when the metric was created.
104  pub timestamp: OffsetDateTime,
105
106  /// Unique identifier of the target that produced this metric.
107  pub target_id: i64,
108
109  /// Name of the target that produced this metric.
110  pub target: Target,
111
112  /// The name of metric being reported.
113  pub name: SystemMetricName,
114
115  /// Numeric value of the metric.
116  pub value: f32,
117
118  /// Type of error associated with this metric.
119  pub error: MetricError,
120}
121
122/// Represents the presence or absence of an error during metric collection.
123#[derive(Debug)]
124pub struct MetricError {
125  /// Type of error by default is None.
126  pub r#type: Option<ErrorType>,
127
128  /// Optional detailed description of the error.
129  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}