Skip to main content

fakecloud_cloudwatch/
state.rs

1use std::collections::BTreeMap;
2use std::sync::Arc;
3
4use chrono::{DateTime, Utc};
5use parking_lot::RwLock;
6use serde::{Deserialize, Serialize};
7
8pub type SharedCloudWatchState = Arc<RwLock<CloudWatchAccounts>>;
9
10/// On-disk snapshot envelope for CloudWatch state.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct CloudWatchSnapshot {
13    pub schema_version: u32,
14    pub accounts: CloudWatchAccounts,
15}
16
17pub const CLOUDWATCH_SNAPSHOT_SCHEMA_VERSION: u32 = 1;
18
19#[derive(Debug, Default, Clone, Serialize, Deserialize)]
20pub struct CloudWatchAccounts {
21    pub accounts: BTreeMap<String, CloudWatchState>,
22}
23
24impl CloudWatchAccounts {
25    pub fn new() -> Self {
26        Self::default()
27    }
28
29    /// Deep-clone for snapshot serialization. `Clone` isn't derived
30    /// because the live state is held behind a `RwLock` and the rest of
31    /// the crate references it by reference — this helper makes the
32    /// intent explicit at the persistence boundary.
33    pub fn clone_for_snapshot(&self) -> CloudWatchAccounts {
34        CloudWatchAccounts {
35            accounts: self.accounts.clone(),
36        }
37    }
38
39    pub fn get_or_create(&mut self, account_id: &str) -> &mut CloudWatchState {
40        self.accounts
41            .entry(account_id.to_string())
42            .or_insert_with(|| CloudWatchState::new(account_id))
43    }
44
45    pub fn get(&self, account_id: &str) -> Option<&CloudWatchState> {
46        self.accounts.get(account_id)
47    }
48}
49
50#[derive(Debug, Default, Clone, Serialize, Deserialize)]
51pub struct CloudWatchState {
52    pub account_id: String,
53    /// region -> namespace -> Vec<MetricDatum>
54    pub metrics: BTreeMap<String, BTreeMap<String, Vec<MetricDatum>>>,
55    /// region -> alarm_name -> MetricAlarm
56    pub alarms: BTreeMap<String, BTreeMap<String, MetricAlarm>>,
57    /// region -> alarm_name -> CompositeAlarm
58    #[serde(default)]
59    pub composite_alarms: BTreeMap<String, BTreeMap<String, CompositeAlarm>>,
60    /// Dashboards keyed by name (CloudWatch dashboards are global per
61    /// account, not regional).
62    #[serde(default)]
63    pub dashboards: BTreeMap<String, Dashboard>,
64    /// region -> (namespace, metric, stat, dims) key -> AnomalyDetector
65    #[serde(default)]
66    pub anomaly_detectors: BTreeMap<String, BTreeMap<String, AnomalyDetector>>,
67    /// region -> rule_name -> InsightRule
68    #[serde(default)]
69    pub insight_rules: BTreeMap<String, BTreeMap<String, InsightRule>>,
70    /// region -> resource_arn -> Vec<ManagedRule>
71    #[serde(default)]
72    pub managed_rules: BTreeMap<String, BTreeMap<String, Vec<ManagedRule>>>,
73    /// region -> stream_name -> MetricStream
74    #[serde(default)]
75    pub metric_streams: BTreeMap<String, BTreeMap<String, MetricStream>>,
76    /// region -> rule_name -> AlarmMuteRule
77    #[serde(default)]
78    pub mute_rules: BTreeMap<String, BTreeMap<String, AlarmMuteRule>>,
79    /// region -> dataset_identifier -> Dataset (KMS key association)
80    #[serde(default)]
81    pub datasets: BTreeMap<String, BTreeMap<String, Dataset>>,
82    /// resource_arn -> tag_key -> tag_value (tags are account-global by ARN)
83    #[serde(default)]
84    pub tags: BTreeMap<String, BTreeMap<String, String>>,
85    /// OTel enrichment is on when true (per account).
86    #[serde(default)]
87    pub otel_enrichment_running: bool,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct Dashboard {
92    pub name: String,
93    pub arn: String,
94    pub body: String,
95    pub last_modified: DateTime<Utc>,
96    pub size_bytes: i64,
97}
98
99/// A CloudWatch dataset and its optional KMS key association. Datasets are
100/// referenced by an identifier; there is no Create API, so an entry is
101/// materialized the first time a KMS key is associated with an identifier.
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct Dataset {
104    pub id: String,
105    pub arn: String,
106    pub kms_key_arn: Option<String>,
107}
108
109impl CloudWatchState {
110    pub fn new(account_id: &str) -> Self {
111        Self {
112            account_id: account_id.to_string(),
113            ..Default::default()
114        }
115    }
116
117    pub fn metrics_in(&self, region: &str) -> Option<&BTreeMap<String, Vec<MetricDatum>>> {
118        self.metrics.get(region)
119    }
120
121    pub fn metrics_in_mut(&mut self, region: &str) -> &mut BTreeMap<String, Vec<MetricDatum>> {
122        self.metrics.entry(region.to_string()).or_default()
123    }
124
125    pub fn alarms_in(&self, region: &str) -> Option<&BTreeMap<String, MetricAlarm>> {
126        self.alarms.get(region)
127    }
128
129    pub fn alarms_in_mut(&mut self, region: &str) -> &mut BTreeMap<String, MetricAlarm> {
130        self.alarms.entry(region.to_string()).or_default()
131    }
132
133    pub fn composite_alarms_in(&self, region: &str) -> Option<&BTreeMap<String, CompositeAlarm>> {
134        self.composite_alarms.get(region)
135    }
136
137    pub fn composite_alarms_in_mut(
138        &mut self,
139        region: &str,
140    ) -> &mut BTreeMap<String, CompositeAlarm> {
141        self.composite_alarms.entry(region.to_string()).or_default()
142    }
143
144    pub fn anomaly_detectors_in(&self, region: &str) -> Option<&BTreeMap<String, AnomalyDetector>> {
145        self.anomaly_detectors.get(region)
146    }
147
148    pub fn anomaly_detectors_in_mut(
149        &mut self,
150        region: &str,
151    ) -> &mut BTreeMap<String, AnomalyDetector> {
152        self.anomaly_detectors
153            .entry(region.to_string())
154            .or_default()
155    }
156
157    pub fn insight_rules_in(&self, region: &str) -> Option<&BTreeMap<String, InsightRule>> {
158        self.insight_rules.get(region)
159    }
160
161    pub fn insight_rules_in_mut(&mut self, region: &str) -> &mut BTreeMap<String, InsightRule> {
162        self.insight_rules.entry(region.to_string()).or_default()
163    }
164
165    pub fn managed_rules_in(&self, region: &str) -> Option<&BTreeMap<String, Vec<ManagedRule>>> {
166        self.managed_rules.get(region)
167    }
168
169    pub fn managed_rules_in_mut(
170        &mut self,
171        region: &str,
172    ) -> &mut BTreeMap<String, Vec<ManagedRule>> {
173        self.managed_rules.entry(region.to_string()).or_default()
174    }
175
176    pub fn metric_streams_in(&self, region: &str) -> Option<&BTreeMap<String, MetricStream>> {
177        self.metric_streams.get(region)
178    }
179
180    pub fn metric_streams_in_mut(&mut self, region: &str) -> &mut BTreeMap<String, MetricStream> {
181        self.metric_streams.entry(region.to_string()).or_default()
182    }
183
184    pub fn mute_rules_in(&self, region: &str) -> Option<&BTreeMap<String, AlarmMuteRule>> {
185        self.mute_rules.get(region)
186    }
187
188    pub fn mute_rules_in_mut(&mut self, region: &str) -> &mut BTreeMap<String, AlarmMuteRule> {
189        self.mute_rules.entry(region.to_string()).or_default()
190    }
191
192    pub fn datasets_in(&self, region: &str) -> Option<&BTreeMap<String, Dataset>> {
193        self.datasets.get(region)
194    }
195
196    pub fn datasets_in_mut(&mut self, region: &str) -> &mut BTreeMap<String, Dataset> {
197        self.datasets.entry(region.to_string()).or_default()
198    }
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct MetricDatum {
203    pub metric_name: String,
204    pub dimensions: BTreeMap<String, String>,
205    pub timestamp: DateTime<Utc>,
206    pub value: Option<f64>,
207    pub statistic_values: Option<StatisticSet>,
208    pub unit: Option<String>,
209    pub storage_resolution: Option<i64>,
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct StatisticSet {
214    pub sample_count: f64,
215    pub sum: f64,
216    pub minimum: f64,
217    pub maximum: f64,
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct MetricAlarm {
222    pub alarm_name: String,
223    pub alarm_arn: String,
224    pub alarm_description: Option<String>,
225    pub actions_enabled: bool,
226    pub ok_actions: Vec<String>,
227    pub alarm_actions: Vec<String>,
228    pub insufficient_data_actions: Vec<String>,
229    pub state_value: AlarmState,
230    pub state_reason: String,
231    pub state_updated_timestamp: DateTime<Utc>,
232    pub metric_name: Option<String>,
233    pub namespace: Option<String>,
234    pub statistic: Option<String>,
235    pub extended_statistic: Option<String>,
236    pub dimensions: BTreeMap<String, String>,
237    pub period: Option<i64>,
238    pub unit: Option<String>,
239    pub evaluation_periods: i64,
240    pub datapoints_to_alarm: Option<i64>,
241    pub threshold: Option<f64>,
242    pub comparison_operator: String,
243    pub treat_missing_data: Option<String>,
244    pub evaluate_low_sample_count_percentile: Option<String>,
245    pub configuration_updated_timestamp: DateTime<Utc>,
246    pub alarm_configuration_updated_timestamp: DateTime<Utc>,
247}
248
249#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
250pub enum AlarmState {
251    Ok,
252    Alarm,
253    InsufficientData,
254}
255
256impl AlarmState {
257    pub fn as_str(&self) -> &'static str {
258        match self {
259            AlarmState::Ok => "OK",
260            AlarmState::Alarm => "ALARM",
261            AlarmState::InsufficientData => "INSUFFICIENT_DATA",
262        }
263    }
264
265    pub fn parse(s: &str) -> Option<Self> {
266        match s {
267            "OK" => Some(AlarmState::Ok),
268            "ALARM" => Some(AlarmState::Alarm),
269            "INSUFFICIENT_DATA" => Some(AlarmState::InsufficientData),
270            _ => None,
271        }
272    }
273}
274
275/// A composite alarm (defined by an `AlarmRule` expression over other alarms).
276#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct CompositeAlarm {
278    pub alarm_name: String,
279    pub alarm_arn: String,
280    pub alarm_description: Option<String>,
281    pub alarm_rule: String,
282    pub actions_enabled: bool,
283    pub ok_actions: Vec<String>,
284    pub alarm_actions: Vec<String>,
285    pub insufficient_data_actions: Vec<String>,
286    pub actions_suppressor: Option<String>,
287    pub actions_suppressor_wait_period: Option<i64>,
288    pub actions_suppressor_extension_period: Option<i64>,
289    pub state_value: AlarmState,
290    pub state_reason: String,
291    pub state_updated_timestamp: DateTime<Utc>,
292    pub alarm_configuration_updated_timestamp: DateTime<Utc>,
293}
294
295/// An anomaly detection model on a metric (single-metric or metric-math).
296#[derive(Debug, Clone, Serialize, Deserialize)]
297pub struct AnomalyDetector {
298    /// Stable key derived from namespace/metric/stat/dims (single) or a hash
299    /// of the metric-math expression so Put/Delete/Describe agree.
300    pub key: String,
301    pub namespace: Option<String>,
302    pub metric_name: Option<String>,
303    pub stat: Option<String>,
304    pub dimensions: BTreeMap<String, String>,
305    pub metric_math: bool,
306    pub state_value: String,
307}
308
309/// A Contributor Insights rule.
310#[derive(Debug, Clone, Serialize, Deserialize)]
311pub struct InsightRule {
312    pub name: String,
313    pub state: String,
314    pub schema: String,
315    pub definition: String,
316    pub managed: bool,
317    pub apply_on_transformed_logs: bool,
318}
319
320/// A managed Contributor Insights rule (template applied to a resource ARN).
321#[derive(Debug, Clone, Serialize, Deserialize)]
322pub struct ManagedRule {
323    pub template_name: String,
324    pub resource_arn: String,
325}
326
327/// A metric stream (control-plane config; no data-plane delivery).
328#[derive(Debug, Clone, Serialize, Deserialize)]
329pub struct MetricStream {
330    pub name: String,
331    pub arn: String,
332    pub firehose_arn: String,
333    pub role_arn: String,
334    pub output_format: String,
335    /// "running" or "stopped".
336    pub state: String,
337    pub include_filters: Vec<MetricStreamFilter>,
338    pub exclude_filters: Vec<MetricStreamFilter>,
339    pub include_linked_accounts_metrics: bool,
340    pub creation_date: DateTime<Utc>,
341    pub last_update_date: DateTime<Utc>,
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct MetricStreamFilter {
346    pub namespace: Option<String>,
347    pub metric_names: Vec<String>,
348}
349
350/// An alarm mute rule. `Rule` is a nested `Schedule` structure on the wire.
351#[derive(Debug, Clone, Serialize, Deserialize)]
352pub struct AlarmMuteRule {
353    pub name: String,
354    pub arn: String,
355    pub description: Option<String>,
356    pub schedule_expression: Option<String>,
357    pub schedule_duration: Option<String>,
358    pub schedule_timezone: Option<String>,
359    pub mute_target_alarm_names: Vec<String>,
360    pub start_date: Option<DateTime<Utc>>,
361    pub expire_date: Option<DateTime<Utc>>,
362    pub last_updated_timestamp: DateTime<Utc>,
363}