Skip to main content

fakecloud_cloudwatch/
introspection.rs

1//! CloudWatch admin introspection helpers consumed by
2//! `/_fakecloud/cloudwatch/*` routes.
3//!
4//! These read the in-memory state cross-account/region and produce
5//! assertion-friendly rows. They intentionally bypass IAM — admin
6//! endpoints never authenticate.
7
8use chrono::{DateTime, Utc};
9
10use crate::state::{MetricDatum, SharedCloudWatchState};
11
12/// A single name/value metric dimension pair.
13#[derive(Debug, Clone)]
14pub struct DimensionRow {
15    pub name: String,
16    pub value: String,
17}
18
19/// One alarm (metric or composite) flattened for introspection.
20#[derive(Debug, Clone)]
21pub struct AlarmRow {
22    pub account_id: String,
23    pub region: String,
24    pub name: String,
25    /// "metric" or "composite".
26    pub kind: &'static str,
27    /// OK / ALARM / INSUFFICIENT_DATA.
28    pub state: String,
29    pub state_reason: String,
30    pub state_updated_timestamp: Option<DateTime<Utc>>,
31    pub actions_enabled: bool,
32    pub alarm_actions: Vec<String>,
33    pub ok_actions: Vec<String>,
34    pub insufficient_data_actions: Vec<String>,
35    // metric-only
36    pub namespace: Option<String>,
37    pub metric_name: Option<String>,
38    pub threshold: Option<f64>,
39    pub comparison_operator: Option<String>,
40    // composite-only
41    pub alarm_rule: Option<String>,
42}
43
44/// Latest datapoint summary for a metric series.
45#[derive(Debug, Clone)]
46pub struct LatestDatapoint {
47    pub timestamp: DateTime<Utc>,
48    pub value: Option<f64>,
49    pub unit: Option<String>,
50}
51
52/// One unique metric series (account, region, namespace, metric, dims).
53#[derive(Debug, Clone)]
54pub struct MetricRow {
55    pub account_id: String,
56    pub region: String,
57    pub namespace: String,
58    pub metric_name: String,
59    pub dimensions: Vec<DimensionRow>,
60    pub datapoint_count: usize,
61    pub latest: Option<LatestDatapoint>,
62}
63
64fn dims_to_rows(dims: &std::collections::BTreeMap<String, String>) -> Vec<DimensionRow> {
65    dims.iter()
66        .map(|(name, value)| DimensionRow {
67            name: name.clone(),
68            value: value.clone(),
69        })
70        .collect()
71}
72
73/// Flatten every metric alarm and composite alarm across all accounts and
74/// regions into one sorted list.
75pub fn list_all_alarms(state: &SharedCloudWatchState) -> Vec<AlarmRow> {
76    let accounts = state.read();
77    let mut rows: Vec<AlarmRow> = Vec::new();
78    for (account_id, acct) in &accounts.accounts {
79        for (region, alarms) in &acct.alarms {
80            for (name, a) in alarms {
81                rows.push(AlarmRow {
82                    account_id: account_id.clone(),
83                    region: region.clone(),
84                    name: name.clone(),
85                    kind: "metric",
86                    state: a.state_value.as_str().to_string(),
87                    state_reason: a.state_reason.clone(),
88                    state_updated_timestamp: Some(a.state_updated_timestamp),
89                    actions_enabled: a.actions_enabled,
90                    alarm_actions: a.alarm_actions.clone(),
91                    ok_actions: a.ok_actions.clone(),
92                    insufficient_data_actions: a.insufficient_data_actions.clone(),
93                    namespace: a.namespace.clone(),
94                    metric_name: a.metric_name.clone(),
95                    threshold: a.threshold,
96                    comparison_operator: Some(a.comparison_operator.clone()),
97                    alarm_rule: None,
98                });
99            }
100        }
101        for (region, alarms) in &acct.composite_alarms {
102            for (name, a) in alarms {
103                rows.push(AlarmRow {
104                    account_id: account_id.clone(),
105                    region: region.clone(),
106                    name: name.clone(),
107                    kind: "composite",
108                    state: a.state_value.as_str().to_string(),
109                    state_reason: a.state_reason.clone(),
110                    state_updated_timestamp: Some(a.state_updated_timestamp),
111                    actions_enabled: a.actions_enabled,
112                    alarm_actions: a.alarm_actions.clone(),
113                    ok_actions: a.ok_actions.clone(),
114                    insufficient_data_actions: a.insufficient_data_actions.clone(),
115                    namespace: None,
116                    metric_name: None,
117                    threshold: None,
118                    comparison_operator: None,
119                    alarm_rule: Some(a.alarm_rule.clone()),
120                });
121            }
122        }
123    }
124    rows.sort_by(|a, b| {
125        a.account_id
126            .cmp(&b.account_id)
127            .then_with(|| a.region.cmp(&b.region))
128            .then_with(|| a.name.cmp(&b.name))
129    });
130    rows
131}
132
133/// Collapse stored metric data points into one row per unique
134/// (account, region, namespace, metricName, dimensions) series, with a
135/// datapoint count and the most-recent datapoint.
136pub fn list_all_metrics(state: &SharedCloudWatchState) -> Vec<MetricRow> {
137    let accounts = state.read();
138    let mut rows: Vec<MetricRow> = Vec::new();
139    for (account_id, acct) in &accounts.accounts {
140        // region -> namespace -> Vec<MetricDatum>
141        for (region, by_namespace) in &acct.metrics {
142            for (namespace, data) in by_namespace {
143                // Group this namespace's data by (metric_name, dimensions).
144                type SeriesKey = (String, Vec<(String, String)>);
145                let mut groups: std::collections::BTreeMap<SeriesKey, Vec<&MetricDatum>> =
146                    std::collections::BTreeMap::new();
147                for d in data {
148                    let dim_key: Vec<(String, String)> = d
149                        .dimensions
150                        .iter()
151                        .map(|(k, v)| (k.clone(), v.clone()))
152                        .collect();
153                    groups
154                        .entry((d.metric_name.clone(), dim_key))
155                        .or_default()
156                        .push(d);
157                }
158                for ((metric_name, _), series) in groups {
159                    let dimensions = series
160                        .first()
161                        .map(|d| dims_to_rows(&d.dimensions))
162                        .unwrap_or_default();
163                    let latest =
164                        series
165                            .iter()
166                            .max_by_key(|d| d.timestamp)
167                            .map(|d| LatestDatapoint {
168                                timestamp: d.timestamp,
169                                value: d.value,
170                                unit: d.unit.clone(),
171                            });
172                    rows.push(MetricRow {
173                        account_id: account_id.clone(),
174                        region: region.clone(),
175                        namespace: namespace.clone(),
176                        metric_name,
177                        dimensions,
178                        datapoint_count: series.len(),
179                        latest,
180                    });
181                }
182            }
183        }
184    }
185    rows.sort_by(|a, b| {
186        a.account_id
187            .cmp(&b.account_id)
188            .then_with(|| a.region.cmp(&b.region))
189            .then_with(|| a.namespace.cmp(&b.namespace))
190            .then_with(|| a.metric_name.cmp(&b.metric_name))
191            .then_with(|| a.dimensions.len().cmp(&b.dimensions.len()))
192    });
193    rows
194}