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