1use crate::config::AwsConfig;
2use anyhow::Result;
3
4pub struct AlarmsClient {
5 config: AwsConfig,
6}
7
8#[derive(Debug, Clone)]
10pub struct SubMetric {
11 pub id: String,
12 pub label: String,
13 pub expression: Option<String>,
14 pub namespace: Option<String>,
15 pub metric_name: Option<String>,
16 pub dimensions: Vec<(String, String)>,
17 pub period: i32,
18 pub stat: String,
19 pub return_data: bool,
20}
21
22#[derive(Debug, Clone)]
24pub struct AlarmData {
25 pub name: String,
26 pub state: String,
27 pub state_updated: String,
28 pub description: String,
29 pub metric_name: String,
30 pub namespace: String,
31 pub statistic: String,
32 pub period: u32,
33 pub comparison: String,
34 pub threshold: f64,
35 pub actions_enabled: bool,
36 pub state_reason: String,
37 pub resource: String,
38 pub dimensions: String,
39 pub expression: String,
40 pub alarm_type: String,
41 pub cross_account: String,
42 pub alarm_arn: String,
43 pub datapoints_to_alarm: u32,
44 pub evaluation_periods: u32,
45 pub treat_missing_data: String,
46 pub evaluate_low_sample_percentile: String,
47 pub sub_metrics: Vec<SubMetric>,
49}
50
51pub struct MetricStatsRequest<'a> {
53 pub namespace: &'a str,
54 pub metric_name: &'a str,
55 pub statistic: &'a str,
56 pub period: i32,
57 pub dimensions: &'a [(String, String)],
58 pub start_time: chrono::DateTime<chrono::Utc>,
59 pub end_time: chrono::DateTime<chrono::Utc>,
60}
61
62impl AlarmsClient {
63 pub fn new(config: AwsConfig) -> Self {
64 Self { config }
65 }
66
67 pub async fn list_alarms(&self) -> Result<Vec<AlarmData>> {
68 let client = self.config.cloudwatch_client();
69 let resp = client.describe_alarms().send().await?;
70 let mut alarms = Vec::new();
71
72 for alarm in resp.metric_alarms() {
73 let name = alarm.alarm_name().unwrap_or("").to_string();
74 let state = alarm
75 .state_value()
76 .map(|s| s.as_str())
77 .unwrap_or("INSUFFICIENT_DATA")
78 .to_string();
79 let state_updated = alarm
80 .state_updated_timestamp()
81 .map(|t| {
82 let dt = chrono::DateTime::parse_from_rfc3339(&t.to_string())
83 .unwrap_or_else(|_| chrono::Utc::now().into());
84 dt.format("%Y-%m-%d %H:%M:%S").to_string()
85 })
86 .unwrap_or_default();
87 let description = alarm.alarm_description().unwrap_or("").to_string();
88 let metric_name = alarm.metric_name().unwrap_or("").to_string();
89 let namespace = alarm.namespace().unwrap_or("").to_string();
90 let statistic = alarm
91 .statistic()
92 .map(|s| s.as_str())
93 .unwrap_or("")
94 .to_string();
95 let period = alarm.period().unwrap_or(0) as u32;
96 let comparison = alarm
97 .comparison_operator()
98 .map(|c| c.as_str())
99 .unwrap_or("")
100 .to_string();
101 let threshold = alarm.threshold().unwrap_or(0.0);
102 let actions_enabled = alarm.actions_enabled().unwrap_or(false);
103 let state_reason = alarm.state_reason().unwrap_or("").to_string();
104 let resource = alarm
105 .dimensions()
106 .iter()
107 .map(|d| format!("{}={}", d.name().unwrap_or(""), d.value().unwrap_or("")))
108 .collect::<Vec<_>>()
109 .join(", ");
110 let dimensions = resource.clone();
111 let expression = if !alarm.metrics().is_empty() {
112 "Expression".to_string()
113 } else {
114 String::new()
115 };
116 let alarm_type = if !alarm.metrics().is_empty() {
117 "Metric math"
118 } else {
119 "Metric"
120 }
121 .to_string();
122 let alarm_arn = alarm.alarm_arn().unwrap_or("").to_string();
123 let datapoints_to_alarm = alarm.datapoints_to_alarm().unwrap_or(0) as u32;
124 let evaluation_periods = alarm.evaluation_periods().unwrap_or(0) as u32;
125 let treat_missing_data = alarm
126 .treat_missing_data()
127 .map(|s| s.to_string())
128 .unwrap_or_default();
129 let evaluate_low_sample_percentile = alarm
130 .evaluate_low_sample_count_percentile()
131 .unwrap_or("")
132 .to_string();
133
134 let sub_metrics: Vec<SubMetric> = alarm
136 .metrics()
137 .iter()
138 .map(|mq| {
139 let id = mq.id().unwrap_or("").to_string();
140 let label = mq.label().unwrap_or("").to_string();
141 let expr = mq.expression().map(|s| s.to_string());
142 let (ns, mn, dims, period, stat) = if let Some(ms) = mq.metric_stat() {
143 let ns = ms
144 .metric()
145 .and_then(|m| m.namespace())
146 .unwrap_or("")
147 .to_string();
148 let mn = ms
149 .metric()
150 .and_then(|m| m.metric_name())
151 .unwrap_or("")
152 .to_string();
153 let dims: Vec<(String, String)> = ms
154 .metric()
155 .map(|m| {
156 m.dimensions()
157 .iter()
158 .map(|d| {
159 (
160 d.name().unwrap_or("").to_string(),
161 d.value().unwrap_or("").to_string(),
162 )
163 })
164 .collect()
165 })
166 .unwrap_or_default();
167 let period = ms.period().unwrap_or(60);
168 let stat = ms.stat().unwrap_or("Average").to_string();
169 (Some(ns), Some(mn), dims, period, stat)
170 } else {
171 let period = mq.period().unwrap_or(60);
173 (None, None, vec![], period, String::new())
174 };
175 SubMetric {
176 id,
177 label,
178 expression: expr,
179 namespace: ns,
180 metric_name: mn,
181 dimensions: dims,
182 period,
183 stat,
184 return_data: mq.return_data().unwrap_or(false),
185 }
186 })
187 .collect();
188
189 alarms.push(AlarmData {
190 name,
191 state,
192 state_updated,
193 description,
194 metric_name,
195 namespace,
196 statistic,
197 period,
198 comparison,
199 threshold,
200 actions_enabled,
201 state_reason,
202 resource,
203 dimensions,
204 expression,
205 alarm_type,
206 cross_account: String::new(),
207 alarm_arn,
208 datapoints_to_alarm,
209 evaluation_periods,
210 treat_missing_data,
211 evaluate_low_sample_percentile,
212 sub_metrics,
213 });
214 }
215
216 Ok(alarms)
217 }
218
219 pub async fn get_metric_math_data(
222 &self,
223 sub_metrics: &[SubMetric],
224 start_time: chrono::DateTime<chrono::Utc>,
225 end_time: chrono::DateTime<chrono::Utc>,
226 ) -> Result<Vec<(i64, f64)>> {
227 use aws_sdk_cloudwatch::types::{Dimension, Metric, MetricDataQuery, MetricStat};
228
229 let client = self.config.cloudwatch_client();
230
231 let default_period: Option<i32> = sub_metrics
233 .iter()
234 .filter(|sm| sm.period > 0)
235 .map(|sm| sm.period)
236 .next();
237
238 let visible_id: Option<String> = sub_metrics
242 .iter()
243 .find(|sm| sm.return_data)
244 .map(|sm| sm.id.clone())
245 .or_else(|| {
246 sub_metrics
247 .iter()
248 .find(|sm| sm.expression.is_some())
249 .map(|sm| sm.id.clone())
250 });
251
252 let queries: Vec<MetricDataQuery> = sub_metrics
254 .iter()
255 .filter_map(|sm| {
256 let force_return = visible_id.as_deref() == Some(sm.id.as_str());
257 let mut b = MetricDataQuery::builder().id(&sm.id);
258 b = b.return_data(sm.return_data || force_return);
259 if let Some(expr) = &sm.expression {
260 b = b.expression(expr);
261 let is_insights_query = expr.trim_start().to_uppercase().starts_with("SELECT");
265 let p = if sm.period > 0 {
266 sm.period
267 } else if is_insights_query {
268 default_period.unwrap_or(300)
269 } else {
270 default_period.unwrap_or(60)
271 };
272 b = b.period(p);
273 } else if let (Some(ns), Some(mn)) = (&sm.namespace, &sm.metric_name) {
274 if ns.is_empty() || mn.is_empty() {
275 return None;
276 }
277 let mut metric_b = Metric::builder().namespace(ns).metric_name(mn);
278 for (k, v) in &sm.dimensions {
279 metric_b =
280 metric_b.dimensions(Dimension::builder().name(k).value(v).build());
281 }
282 let metric = metric_b.build();
283 let ms = MetricStat::builder()
284 .metric(metric)
285 .period(sm.period)
286 .stat(&sm.stat)
287 .build();
288 b = b.metric_stat(ms);
289 } else {
290 return None;
291 }
292 Some(b.build())
293 })
294 .collect();
295
296 if queries.is_empty() {
297 return Ok(vec![]);
298 }
299
300 let mut req = client
301 .get_metric_data()
302 .start_time(aws_sdk_cloudwatch::primitives::DateTime::from_millis(
303 start_time.timestamp_millis(),
304 ))
305 .end_time(aws_sdk_cloudwatch::primitives::DateTime::from_millis(
306 end_time.timestamp_millis(),
307 ));
308 for q in queries {
309 req = req.metric_data_queries(q);
310 }
311
312 let resp = req.send().await?;
313
314 let result = resp
316 .metric_data_results()
317 .iter()
318 .find(|r| visible_id.as_deref() == r.id())
319 .or_else(|| {
320 resp.metric_data_results().iter().find(|r| {
321 sub_metrics
322 .iter()
323 .any(|sm| sm.return_data && sm.id == r.id().unwrap_or(""))
324 })
325 })
326 .or_else(|| resp.metric_data_results().first());
327
328 let mut data: Vec<(i64, f64)> = match result {
329 Some(r) => r
330 .timestamps()
331 .iter()
332 .zip(r.values().iter())
333 .map(|(ts, val)| (ts.as_secs_f64() as i64, *val))
334 .collect(),
335 None => vec![],
336 };
337 data.sort_by_key(|(ts, _)| *ts);
338 Ok(data)
339 }
340
341 pub async fn get_metric_statistics(
342 &self,
343 req: MetricStatsRequest<'_>,
344 ) -> Result<Vec<(i64, f64)>> {
345 use aws_sdk_cloudwatch::types::Dimension;
346 let client = self.config.cloudwatch_client();
347 let MetricStatsRequest {
348 namespace,
349 metric_name,
350 statistic,
351 period,
352 dimensions,
353 start_time,
354 end_time,
355 } = req;
356
357 let mut cw_req = client
358 .get_metric_statistics()
359 .namespace(namespace)
360 .metric_name(metric_name)
361 .statistics(aws_sdk_cloudwatch::types::Statistic::from(statistic))
362 .period(period)
363 .start_time(aws_sdk_cloudwatch::primitives::DateTime::from_millis(
364 start_time.timestamp_millis(),
365 ))
366 .end_time(aws_sdk_cloudwatch::primitives::DateTime::from_millis(
367 end_time.timestamp_millis(),
368 ));
369
370 for (name, value) in dimensions {
371 cw_req = cw_req.dimensions(Dimension::builder().name(name).value(value).build());
372 }
373
374 let resp = cw_req.send().await?;
375
376 let mut data: Vec<(i64, f64)> = resp
377 .datapoints()
378 .iter()
379 .filter_map(|dp| {
380 let timestamp = dp.timestamp()?.as_secs_f64() as i64;
381 let value = match statistic {
382 "Average" => dp.average(),
383 "Sum" => dp.sum(),
384 "Minimum" => dp.minimum(),
385 "Maximum" => dp.maximum(),
386 "SampleCount" => dp.sample_count(),
387 _ => dp.average(),
388 }?;
389 Some((timestamp, value))
390 })
391 .collect();
392
393 data.sort_by_key(|(ts, _)| *ts);
394 Ok(data)
395 }
396}