1use anyhow::Result;
4use aws_sdk_cloudwatch::{
5 operation::get_metric_statistics::builders::GetMetricStatisticsFluentBuilder,
6 types::{Datapoint, Dimension, Statistic},
7 Client,
8};
9use aws_smithy_types::DateTime;
10use std::time::{SystemTime, UNIX_EPOCH};
11
12pub struct MetricQuery<'a> {
13 pub namespace: &'a str,
14 pub name: &'a str,
15 pub statistic: Statistic,
16 pub dimensions: Vec<Dimension>,
17}
18impl<'a> MetricQuery<'a> {
19 pub fn new(namespace: &'a str, name: &'a str, statistic: Statistic) -> Self {
20 Self {
21 namespace,
22 name,
23 statistic,
24 dimensions: Vec::new(),
25 }
26 }
27 pub fn dimension(mut self, name: &str, value: &str) -> Self {
28 self.dimensions
29 .push(Dimension::builder().name(name).value(value).build());
30 self
31 }
32}
33
34#[derive(Debug, Clone)]
35pub struct MetricWindow {
36 pub start: DateTime,
37 pub end: DateTime,
38 pub period: i32,
39}
40impl MetricWindow {
41 pub fn last_three_hours(period: i32) -> Result<Self> {
42 let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64;
43 Ok(Self {
44 start: DateTime::from_secs(now - 3 * 3600),
45 end: DateTime::from_secs(now),
46 period,
47 })
48 }
49}
50
51pub struct MetricReader {
52 client: Client,
53}
54impl MetricReader {
55 pub fn new(client: Client) -> Self {
56 Self { client }
57 }
58 fn request(
59 &self,
60 query: &MetricQuery<'_>,
61 window: MetricWindow,
62 ) -> GetMetricStatisticsFluentBuilder {
63 self.client
64 .get_metric_statistics()
65 .namespace(query.namespace)
66 .metric_name(query.name)
67 .statistics(query.statistic.clone())
68 .set_dimensions(Some(query.dimensions.clone()))
69 .start_time(window.start)
70 .end_time(window.end)
71 .period(window.period)
72 }
73 pub async fn read(
74 &self,
75 query: MetricQuery<'_>,
76 window: MetricWindow,
77 ) -> Result<Vec<(i64, f64)>> {
78 let response = self.request(&query, window).send().await?;
79 Ok(series(response.datapoints(), &query.statistic))
80 }
81 pub async fn recent(&self, query: MetricQuery<'_>, period: i32) -> Result<Vec<(i64, f64)>> {
82 self.read(query, MetricWindow::last_three_hours(period)?)
83 .await
84 }
85}
86
87fn series(datapoints: &[Datapoint], statistic: &Statistic) -> Vec<(i64, f64)> {
88 let mut points: Vec<_> = datapoints
89 .iter()
90 .filter_map(|point| {
91 let timestamp = point.timestamp()?.secs();
92 let value = match statistic {
93 Statistic::Sum => point.sum(),
94 Statistic::Minimum => point.minimum(),
95 Statistic::Maximum => point.maximum(),
96 Statistic::SampleCount => point.sample_count(),
97 _ => point.average(),
98 }?;
99 Some((timestamp, value))
100 })
101 .collect();
102 points.sort_by_key(|(time, _)| *time);
103 points
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use crate::config::AwsConfig;
110
111 #[test]
112 fn request_preserves_namespace_metric_dimensions_period_and_explicit_window() {
113 let reader =
114 MetricReader::new(AwsConfig::dummy(Some("us-east-2".into())).cloudwatch_client());
115 let query = MetricQuery::new("AWS/Lambda", "Invocations", Statistic::Sum)
116 .dimension("FunctionName", "function")
117 .dimension("Resource", "function:alias");
118 let request = reader.request(
119 &query,
120 MetricWindow {
121 start: DateTime::from_secs(100),
122 end: DateTime::from_secs(200),
123 period: 60,
124 },
125 );
126 let input = request.as_input();
127 assert_eq!(input.get_namespace().as_deref(), Some("AWS/Lambda"));
128 assert_eq!(input.get_metric_name().as_deref(), Some("Invocations"));
129 assert_eq!(input.get_statistics().as_ref().unwrap(), &[Statistic::Sum]);
130 assert_eq!(input.get_dimensions().as_ref().unwrap().len(), 2);
131 assert_eq!(
132 input.get_dimensions().as_ref().unwrap()[1].value(),
133 Some("function:alias")
134 );
135 assert_eq!(input.get_period(), &Some(60));
136 assert_eq!(input.get_start_time(), &Some(DateTime::from_secs(100)));
137 assert_eq!(input.get_end_time(), &Some(DateTime::from_secs(200)));
138 }
139
140 #[test]
141 fn recent_windows_use_one_clock_sample_and_preserve_period() {
142 for period in [60, 300] {
143 let window = MetricWindow::last_three_hours(period).unwrap();
144 assert_eq!(window.end.secs() - window.start.secs(), 10800);
145 assert_eq!(window.period, period);
146 }
147 }
148
149 #[test]
150 fn series_selects_each_statistic_sorts_and_does_not_invent_missing_values() {
151 let complete = |time| {
152 Datapoint::builder()
153 .timestamp(DateTime::from_secs(time))
154 .average(1.0)
155 .sum(2.0)
156 .minimum(0.0)
157 .maximum(3.0)
158 .sample_count(4.0)
159 .build()
160 };
161 let points = [
162 complete(20),
163 Datapoint::builder().average(99.0).build(),
164 Datapoint::builder()
165 .timestamp(DateTime::from_secs(15))
166 .build(),
167 complete(10),
168 ];
169 for (stat, expected) in [
170 (Statistic::Average, 1.0),
171 (Statistic::Sum, 2.0),
172 (Statistic::Minimum, 0.0),
173 (Statistic::Maximum, 3.0),
174 (Statistic::SampleCount, 4.0),
175 ] {
176 assert_eq!(series(&points, &stat), [(10, expected), (20, expected)]);
177 assert!(series(&[], &stat).is_empty());
178 }
179 }
180}