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    /// resource_arn -> tag_key -> tag_value (tags are account-global by ARN)
80    #[serde(default)]
81    pub tags: BTreeMap<String, BTreeMap<String, String>>,
82    /// OTel enrichment is on when true (per account).
83    #[serde(default)]
84    pub otel_enrichment_running: bool,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct Dashboard {
89    pub name: String,
90    pub arn: String,
91    pub body: String,
92    pub last_modified: DateTime<Utc>,
93    pub size_bytes: i64,
94}
95
96impl CloudWatchState {
97    pub fn new(account_id: &str) -> Self {
98        Self {
99            account_id: account_id.to_string(),
100            ..Default::default()
101        }
102    }
103
104    pub fn metrics_in(&self, region: &str) -> Option<&BTreeMap<String, Vec<MetricDatum>>> {
105        self.metrics.get(region)
106    }
107
108    pub fn metrics_in_mut(&mut self, region: &str) -> &mut BTreeMap<String, Vec<MetricDatum>> {
109        self.metrics.entry(region.to_string()).or_default()
110    }
111
112    pub fn alarms_in(&self, region: &str) -> Option<&BTreeMap<String, MetricAlarm>> {
113        self.alarms.get(region)
114    }
115
116    pub fn alarms_in_mut(&mut self, region: &str) -> &mut BTreeMap<String, MetricAlarm> {
117        self.alarms.entry(region.to_string()).or_default()
118    }
119
120    pub fn composite_alarms_in(&self, region: &str) -> Option<&BTreeMap<String, CompositeAlarm>> {
121        self.composite_alarms.get(region)
122    }
123
124    pub fn composite_alarms_in_mut(
125        &mut self,
126        region: &str,
127    ) -> &mut BTreeMap<String, CompositeAlarm> {
128        self.composite_alarms.entry(region.to_string()).or_default()
129    }
130
131    pub fn anomaly_detectors_in(&self, region: &str) -> Option<&BTreeMap<String, AnomalyDetector>> {
132        self.anomaly_detectors.get(region)
133    }
134
135    pub fn anomaly_detectors_in_mut(
136        &mut self,
137        region: &str,
138    ) -> &mut BTreeMap<String, AnomalyDetector> {
139        self.anomaly_detectors
140            .entry(region.to_string())
141            .or_default()
142    }
143
144    pub fn insight_rules_in(&self, region: &str) -> Option<&BTreeMap<String, InsightRule>> {
145        self.insight_rules.get(region)
146    }
147
148    pub fn insight_rules_in_mut(&mut self, region: &str) -> &mut BTreeMap<String, InsightRule> {
149        self.insight_rules.entry(region.to_string()).or_default()
150    }
151
152    pub fn managed_rules_in(&self, region: &str) -> Option<&BTreeMap<String, Vec<ManagedRule>>> {
153        self.managed_rules.get(region)
154    }
155
156    pub fn managed_rules_in_mut(
157        &mut self,
158        region: &str,
159    ) -> &mut BTreeMap<String, Vec<ManagedRule>> {
160        self.managed_rules.entry(region.to_string()).or_default()
161    }
162
163    pub fn metric_streams_in(&self, region: &str) -> Option<&BTreeMap<String, MetricStream>> {
164        self.metric_streams.get(region)
165    }
166
167    pub fn metric_streams_in_mut(&mut self, region: &str) -> &mut BTreeMap<String, MetricStream> {
168        self.metric_streams.entry(region.to_string()).or_default()
169    }
170
171    pub fn mute_rules_in(&self, region: &str) -> Option<&BTreeMap<String, AlarmMuteRule>> {
172        self.mute_rules.get(region)
173    }
174
175    pub fn mute_rules_in_mut(&mut self, region: &str) -> &mut BTreeMap<String, AlarmMuteRule> {
176        self.mute_rules.entry(region.to_string()).or_default()
177    }
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct MetricDatum {
182    pub metric_name: String,
183    pub dimensions: BTreeMap<String, String>,
184    pub timestamp: DateTime<Utc>,
185    pub value: Option<f64>,
186    pub statistic_values: Option<StatisticSet>,
187    pub unit: Option<String>,
188    pub storage_resolution: Option<i64>,
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct StatisticSet {
193    pub sample_count: f64,
194    pub sum: f64,
195    pub minimum: f64,
196    pub maximum: f64,
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct MetricAlarm {
201    pub alarm_name: String,
202    pub alarm_arn: String,
203    pub alarm_description: Option<String>,
204    pub actions_enabled: bool,
205    pub ok_actions: Vec<String>,
206    pub alarm_actions: Vec<String>,
207    pub insufficient_data_actions: Vec<String>,
208    pub state_value: AlarmState,
209    pub state_reason: String,
210    pub state_updated_timestamp: DateTime<Utc>,
211    pub metric_name: Option<String>,
212    pub namespace: Option<String>,
213    pub statistic: Option<String>,
214    pub extended_statistic: Option<String>,
215    pub dimensions: BTreeMap<String, String>,
216    pub period: Option<i64>,
217    pub unit: Option<String>,
218    pub evaluation_periods: i64,
219    pub datapoints_to_alarm: Option<i64>,
220    pub threshold: Option<f64>,
221    pub comparison_operator: String,
222    pub treat_missing_data: Option<String>,
223    pub evaluate_low_sample_count_percentile: Option<String>,
224    pub configuration_updated_timestamp: DateTime<Utc>,
225    pub alarm_configuration_updated_timestamp: DateTime<Utc>,
226}
227
228#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
229pub enum AlarmState {
230    Ok,
231    Alarm,
232    InsufficientData,
233}
234
235impl AlarmState {
236    pub fn as_str(&self) -> &'static str {
237        match self {
238            AlarmState::Ok => "OK",
239            AlarmState::Alarm => "ALARM",
240            AlarmState::InsufficientData => "INSUFFICIENT_DATA",
241        }
242    }
243
244    pub fn parse(s: &str) -> Option<Self> {
245        match s {
246            "OK" => Some(AlarmState::Ok),
247            "ALARM" => Some(AlarmState::Alarm),
248            "INSUFFICIENT_DATA" => Some(AlarmState::InsufficientData),
249            _ => None,
250        }
251    }
252}
253
254/// A composite alarm (defined by an `AlarmRule` expression over other alarms).
255#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct CompositeAlarm {
257    pub alarm_name: String,
258    pub alarm_arn: String,
259    pub alarm_description: Option<String>,
260    pub alarm_rule: String,
261    pub actions_enabled: bool,
262    pub ok_actions: Vec<String>,
263    pub alarm_actions: Vec<String>,
264    pub insufficient_data_actions: Vec<String>,
265    pub actions_suppressor: Option<String>,
266    pub actions_suppressor_wait_period: Option<i64>,
267    pub actions_suppressor_extension_period: Option<i64>,
268    pub state_value: AlarmState,
269    pub state_reason: String,
270    pub state_updated_timestamp: DateTime<Utc>,
271    pub alarm_configuration_updated_timestamp: DateTime<Utc>,
272}
273
274/// An anomaly detection model on a metric (single-metric or metric-math).
275#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct AnomalyDetector {
277    /// Stable key derived from namespace/metric/stat/dims (single) or a hash
278    /// of the metric-math expression so Put/Delete/Describe agree.
279    pub key: String,
280    pub namespace: Option<String>,
281    pub metric_name: Option<String>,
282    pub stat: Option<String>,
283    pub dimensions: BTreeMap<String, String>,
284    pub metric_math: bool,
285    pub state_value: String,
286}
287
288/// A Contributor Insights rule.
289#[derive(Debug, Clone, Serialize, Deserialize)]
290pub struct InsightRule {
291    pub name: String,
292    pub state: String,
293    pub schema: String,
294    pub definition: String,
295    pub managed: bool,
296    pub apply_on_transformed_logs: bool,
297}
298
299/// A managed Contributor Insights rule (template applied to a resource ARN).
300#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct ManagedRule {
302    pub template_name: String,
303    pub resource_arn: String,
304}
305
306/// A metric stream (control-plane config; no data-plane delivery).
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct MetricStream {
309    pub name: String,
310    pub arn: String,
311    pub firehose_arn: String,
312    pub role_arn: String,
313    pub output_format: String,
314    /// "running" or "stopped".
315    pub state: String,
316    pub include_filters: Vec<MetricStreamFilter>,
317    pub exclude_filters: Vec<MetricStreamFilter>,
318    pub include_linked_accounts_metrics: bool,
319    pub creation_date: DateTime<Utc>,
320    pub last_update_date: DateTime<Utc>,
321}
322
323#[derive(Debug, Clone, Serialize, Deserialize)]
324pub struct MetricStreamFilter {
325    pub namespace: Option<String>,
326    pub metric_names: Vec<String>,
327}
328
329/// An alarm mute rule. `Rule` is a nested `Schedule` structure on the wire.
330#[derive(Debug, Clone, Serialize, Deserialize)]
331pub struct AlarmMuteRule {
332    pub name: String,
333    pub arn: String,
334    pub description: Option<String>,
335    pub schedule_expression: Option<String>,
336    pub schedule_duration: Option<String>,
337    pub schedule_timezone: Option<String>,
338    pub mute_target_alarm_names: Vec<String>,
339    pub start_date: Option<DateTime<Utc>>,
340    pub expire_date: Option<DateTime<Utc>>,
341    pub last_updated_timestamp: DateTime<Utc>,
342}