1use std::collections::HashMap;
4use std::sync::Arc;
5
6use futures_util::Stream;
7use reqwest::Method;
8use serde::{Deserialize, Serialize};
9use serde_json::{Map, Value};
10
11use crate::client::{CallOptions, Client};
12use crate::common::{string_enum, MessageResponse};
13use crate::error::Result;
14use crate::pagination::{auto_page, paginate, ListParams, Page, PageFetcher};
15use crate::query::QueryBuilder;
16use crate::resources::escape;
17
18const PATH: &str = "/v2/alertRules";
19
20string_enum! {
21 AlertSeverity {
23 INFO => "info",
25 WARNING => "warning",
27 CRITICAL => "critical",
29 }
30}
31
32string_enum! {
33 AlertDomain {
38 RTC => "rtc",
40 AGENT => "agent",
42 }
43}
44
45string_enum! {
46 AlertRuleStatus {
48 ACTIVE => "active",
50 PAUSED => "paused",
52 }
53}
54
55string_enum! {
56 AlertConditionOperator {
58 GREATER_THAN => ">",
60 LESS_THAN => "<",
62 EQUAL => "=",
64 NOT_EQUAL => "!=",
66 }
67}
68
69string_enum! {
70 AlertMatchType {
72 AT_LEAST_ONCE => "at_least_once",
74 ALL_THE_TIME => "all_the_time",
76 ON_AVERAGE => "on_average",
78 IN_TOTAL => "in_total",
80 LAST => "last",
82 }
83}
84
85string_enum! {
86 AlertReduceTo {
88 LAST => "last",
90 SUM => "sum",
92 AVG => "avg",
94 MIN => "min",
96 MAX => "max",
98 }
99}
100
101#[derive(Debug, Clone, Default, Serialize, Deserialize)]
105#[serde(rename_all = "camelCase")]
106pub struct AbsentDataAlert {
107 pub enabled: bool,
109 #[serde(skip_serializing_if = "Option::is_none")]
111 pub for_seconds: Option<u32>,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct AlertRuleFilter {
117 pub category: String,
119 pub condition: String,
121 pub value: Value,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
127#[serde(rename_all = "camelCase")]
128pub struct AlertRuleGroupBy {
129 pub key: String,
131 #[serde(skip_serializing_if = "Option::is_none")]
133 pub data_type: Option<String>,
134 #[serde(skip_serializing_if = "Option::is_none")]
136 pub key_type: Option<String>,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
141#[serde(rename_all = "camelCase")]
142pub struct AlertRuleHaving {
143 pub column_name: String,
145 pub operator: String,
147 pub value: f64,
149}
150
151#[derive(Debug, Clone, Default, Serialize, Deserialize)]
153#[serde(rename_all = "camelCase")]
154pub struct AlertRuleQuery {
155 pub metric: String,
157 #[serde(skip_serializing_if = "Option::is_none")]
159 pub metric_category: Option<String>,
160 #[serde(skip_serializing_if = "Option::is_none")]
163 pub time_aggregation: Option<String>,
164 #[serde(skip_serializing_if = "Option::is_none")]
166 pub space_aggregation: Option<String>,
167 #[serde(skip_serializing_if = "Option::is_none")]
169 pub data_type: Option<String>,
170 #[serde(skip_serializing_if = "Option::is_none")]
172 pub step_interval_seconds: Option<u32>,
173 #[serde(default, skip_serializing_if = "Vec::is_empty")]
175 pub filter: Vec<AlertRuleFilter>,
176 #[serde(default, skip_serializing_if = "Vec::is_empty")]
178 pub group_by: Vec<AlertRuleGroupBy>,
179 #[serde(default, skip_serializing_if = "Vec::is_empty")]
181 pub having: Vec<AlertRuleHaving>,
182}
183
184impl AlertRuleQuery {
185 pub fn metric(metric: impl Into<String>) -> Self {
187 Self {
188 metric: metric.into(),
189 ..Default::default()
190 }
191 }
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
196#[serde(rename_all = "camelCase")]
197pub struct AlertRuleCondition {
198 pub operator: AlertConditionOperator,
200 pub threshold: f64,
202 #[serde(skip_serializing_if = "Option::is_none")]
204 pub time_aggregation: Option<String>,
205 #[serde(skip_serializing_if = "Option::is_none")]
207 pub space_aggregation: Option<String>,
208 #[serde(skip_serializing_if = "Option::is_none")]
210 pub minimum_session_gate: Option<f64>,
211 #[serde(skip_serializing_if = "Option::is_none")]
213 pub unit: Option<String>,
214 #[serde(skip_serializing_if = "Option::is_none")]
217 pub match_type: Option<AlertMatchType>,
218 #[serde(skip_serializing_if = "Option::is_none")]
220 pub require_min_points: Option<bool>,
221 #[serde(skip_serializing_if = "Option::is_none")]
223 pub required_num_points: Option<u32>,
224 #[serde(skip_serializing_if = "Option::is_none")]
226 pub frequency_seconds: Option<u32>,
227}
228
229impl AlertRuleCondition {
230 pub fn new(operator: AlertConditionOperator, threshold: f64) -> Self {
232 Self {
233 operator,
234 threshold,
235 time_aggregation: None,
236 space_aggregation: None,
237 minimum_session_gate: None,
238 unit: None,
239 match_type: None,
240 require_min_points: None,
241 required_num_points: None,
242 frequency_seconds: None,
243 }
244 }
245}
246
247#[derive(Debug, Clone, Default, Serialize, Deserialize)]
249#[serde(rename_all = "camelCase")]
250pub struct AlertRuleEvaluation {
251 #[serde(skip_serializing_if = "Option::is_none")]
253 pub frequency_seconds: Option<u32>,
254 #[serde(skip_serializing_if = "Option::is_none")]
256 pub window_seconds: Option<u32>,
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize)]
263#[serde(rename_all = "camelCase")]
264pub struct AlertRuleConfig {
265 pub query: AlertRuleQuery,
267 pub condition: AlertRuleCondition,
269 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
271 pub labels: HashMap<String, String>,
272 #[serde(skip_serializing_if = "Option::is_none")]
274 pub evaluation: Option<AlertRuleEvaluation>,
275 #[serde(skip_serializing_if = "Option::is_none")]
277 pub absent_data_alert: Option<AbsentDataAlert>,
278 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
280 pub additional_queries: HashMap<String, AlertRuleQuery>,
281 #[serde(skip_serializing_if = "Option::is_none")]
283 pub formula: Option<String>,
284 #[serde(skip_serializing_if = "Option::is_none")]
286 pub reduce_to: Option<AlertReduceTo>,
287}
288
289impl AlertRuleConfig {
290 pub fn new(query: AlertRuleQuery, condition: AlertRuleCondition) -> Self {
292 Self {
293 query,
294 condition,
295 labels: HashMap::new(),
296 evaluation: None,
297 absent_data_alert: None,
298 additional_queries: HashMap::new(),
299 formula: None,
300 reduce_to: None,
301 }
302 }
303}
304
305#[derive(Debug, Clone, Deserialize)]
309#[serde(rename_all = "camelCase")]
310pub struct AlertRule {
311 pub id: Option<String>,
313 #[serde(default, deserialize_with = "crate::common::null_to_default")]
315 pub alert_rule_id: String,
316 pub name: Option<String>,
318 pub severity: Option<AlertSeverity>,
320 pub description: Option<String>,
322 pub summary: Option<String>,
324 pub domain: Option<AlertDomain>,
326 pub rule_config: Option<AlertRuleConfig>,
328 #[serde(default, deserialize_with = "crate::common::null_to_default")]
330 pub notification_channel_ids: Vec<String>,
331 pub status: Option<AlertRuleStatus>,
333 pub version: Option<i64>,
335 pub last_triggered: Option<String>,
337 pub created_at: Option<String>,
339 pub updated_at: Option<String>,
341 #[serde(flatten)]
343 pub extra: Map<String, Value>,
344}
345
346#[derive(Debug, Clone, Serialize)]
350#[serde(rename_all = "camelCase")]
351pub struct CreateAlertRuleParams {
352 pub name: String,
354 pub severity: AlertSeverity,
356 pub rule_config: AlertRuleConfig,
358 #[serde(skip_serializing_if = "Option::is_none")]
360 pub description: Option<String>,
361 #[serde(skip_serializing_if = "Option::is_none")]
363 pub summary: Option<String>,
364 #[serde(skip_serializing_if = "Option::is_none")]
367 pub enabled: Option<bool>,
368 #[serde(skip_serializing_if = "Option::is_none")]
370 pub domain: Option<AlertDomain>,
371 #[serde(skip_serializing_if = "Option::is_none")]
373 pub time_aggregation: Option<String>,
374 #[serde(skip_serializing_if = "Vec::is_empty")]
376 pub notification_channel_ids: Vec<String>,
377}
378
379impl CreateAlertRuleParams {
380 pub fn new(
382 name: impl Into<String>,
383 severity: AlertSeverity,
384 rule_config: AlertRuleConfig,
385 ) -> Self {
386 Self {
387 name: name.into(),
388 severity,
389 rule_config,
390 description: None,
391 summary: None,
392 enabled: None,
393 domain: None,
394 time_aggregation: None,
395 notification_channel_ids: Vec::new(),
396 }
397 }
398}
399
400#[derive(Debug, Clone, Default, Serialize)]
402#[serde(rename_all = "camelCase")]
403pub struct UpdateAlertRuleParams {
404 #[serde(skip_serializing_if = "Option::is_none")]
406 pub name: Option<String>,
407 #[serde(skip_serializing_if = "Option::is_none")]
409 pub severity: Option<AlertSeverity>,
410 #[serde(skip_serializing_if = "Option::is_none")]
412 pub rule_config: Option<AlertRuleConfig>,
413 #[serde(skip_serializing_if = "Option::is_none")]
415 pub description: Option<String>,
416 #[serde(skip_serializing_if = "Option::is_none")]
418 pub summary: Option<String>,
419 #[serde(skip_serializing_if = "Option::is_none")]
421 pub enabled: Option<bool>,
422 #[serde(skip_serializing_if = "Option::is_none")]
424 pub domain: Option<AlertDomain>,
425 #[serde(skip_serializing_if = "Option::is_none")]
427 pub time_aggregation: Option<String>,
428 #[serde(skip_serializing_if = "Vec::is_empty")]
430 pub notification_channel_ids: Vec<String>,
431}
432
433#[derive(Debug, Clone, Default)]
435pub struct ListAlertRulesParams {
436 pub page: Option<u32>,
438 pub per_page: Option<u32>,
440 pub cursor: Option<String>,
442 pub region: Option<String>,
444 pub status: Option<AlertRuleStatus>,
446 pub metric_category: Option<String>,
448 pub domain: Option<AlertDomain>,
451}
452
453impl ListAlertRulesParams {
454 fn pagination(&self) -> ListParams {
455 ListParams {
456 page: self.page,
457 per_page: self.per_page,
458 cursor: self.cursor.clone(),
459 }
460 }
461}
462
463#[derive(Debug, Clone, Copy)]
467pub struct AlertRulesResource<'a> {
468 client: &'a Client,
469}
470
471impl<'a> AlertRulesResource<'a> {
472 pub(crate) fn new(client: &'a Client) -> Self {
473 Self { client }
474 }
475
476 pub async fn create(&self, params: CreateAlertRuleParams) -> Result<AlertRule> {
478 self.client
479 .wrapped(Method::POST, PATH, "alertRule", CallOptions::json(¶ms)?)
480 .await
481 }
482
483 pub async fn list(&self, params: ListAlertRulesParams) -> Result<Page<AlertRule>> {
485 paginate(self.fetcher(¶ms), ¶ms.pagination(), "data", None).await
486 }
487
488 pub fn list_stream(
490 &self,
491 params: ListAlertRulesParams,
492 ) -> impl Stream<Item = Result<AlertRule>> + Send {
493 auto_page(self.fetcher(¶ms), params.pagination(), "data", None)
494 }
495
496 pub async fn get(&self, alert_rule_id: &str) -> Result<AlertRule> {
498 let path = format!("{PATH}/{}", escape(alert_rule_id));
499 self.client
500 .wrapped(Method::GET, &path, "alertRule", CallOptions::new())
501 .await
502 }
503
504 pub async fn update(
506 &self,
507 alert_rule_id: &str,
508 params: UpdateAlertRuleParams,
509 ) -> Result<AlertRule> {
510 let path = format!("{PATH}/{}", escape(alert_rule_id));
511 self.client
512 .wrapped(Method::PUT, &path, "alertRule", CallOptions::json(¶ms)?)
513 .await
514 }
515
516 pub async fn delete(&self, alert_rule_id: &str) -> Result<MessageResponse> {
518 let path = format!("{PATH}/{}", escape(alert_rule_id));
519 self.client
520 .json(Method::DELETE, &path, CallOptions::new())
521 .await
522 }
523
524 fn fetcher(&self, params: &ListAlertRulesParams) -> PageFetcher {
525 let client = self.client.clone();
526 let params = params.clone();
527 Arc::new(move |page, per_page| {
528 let client = client.clone();
529 let params = params.clone();
530 Box::pin(async move {
531 let query = QueryBuilder::new()
532 .opt("page", page)
533 .opt("perPage", per_page)
534 .opt_str("region", params.region.as_deref())
535 .opt_str(
536 "status",
537 params.status.as_ref().map(AlertRuleStatus::as_str),
538 )
539 .opt_str("metricCategory", params.metric_category.as_deref())
540 .opt_str("domain", params.domain.as_ref().map(AlertDomain::as_str))
541 .into_pairs();
542 client
543 .json::<Value>(Method::GET, PATH, CallOptions::new().query(query))
544 .await
545 })
546 })
547 }
548}
549
550#[cfg(test)]
551mod tests {
552 use super::*;
553 use serde_json::json;
554
555 #[test]
556 fn create_serializes_the_typed_config() {
557 let params = CreateAlertRuleParams::new(
558 "High bitrate",
559 AlertSeverity::WARNING,
560 AlertRuleConfig::new(
561 AlertRuleQuery::metric("stat_consumer_video_bitrate"),
562 AlertRuleCondition::new(AlertConditionOperator::GREATER_THAN, 2500.0),
563 ),
564 );
565 assert_eq!(
566 serde_json::to_value(¶ms).unwrap(),
567 json!({
568 "name": "High bitrate",
569 "severity": "warning",
570 "ruleConfig": {
571 "query": {"metric": "stat_consumer_video_bitrate"},
572 "condition": {"operator": ">", "threshold": 2500.0},
573 },
574 })
575 );
576 }
577
578 #[test]
579 fn an_unknown_severity_on_a_response_does_not_fail() {
580 let rule: AlertRule = serde_json::from_value(json!({
583 "alertRuleId": "ar-1",
584 "severity": "catastrophic",
585 "notificationChannelIds": null,
586 }))
587 .unwrap();
588 assert_eq!(rule.severity.unwrap().as_str(), "catastrophic");
589 assert!(rule.notification_channel_ids.is_empty());
590 }
591}