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    /// region -> alarm_name -> history items (newest appended last). Populated
61    /// by PutMetricAlarm (ConfigurationUpdate), SetAlarmState (StateUpdate) and
62    /// DeleteAlarms so DescribeAlarmHistory reflects real transitions.
63    #[serde(default)]
64    pub alarm_history: BTreeMap<String, BTreeMap<String, Vec<AlarmHistoryItem>>>,
65    /// Dashboards keyed by name (CloudWatch dashboards are global per
66    /// account, not regional).
67    #[serde(default)]
68    pub dashboards: BTreeMap<String, Dashboard>,
69    /// region -> (namespace, metric, stat, dims) key -> AnomalyDetector
70    #[serde(default)]
71    pub anomaly_detectors: BTreeMap<String, BTreeMap<String, AnomalyDetector>>,
72    /// region -> rule_name -> InsightRule
73    #[serde(default)]
74    pub insight_rules: BTreeMap<String, BTreeMap<String, InsightRule>>,
75    /// region -> resource_arn -> Vec<ManagedRule>
76    #[serde(default)]
77    pub managed_rules: BTreeMap<String, BTreeMap<String, Vec<ManagedRule>>>,
78    /// region -> stream_name -> MetricStream
79    #[serde(default)]
80    pub metric_streams: BTreeMap<String, BTreeMap<String, MetricStream>>,
81    /// region -> rule_name -> AlarmMuteRule
82    #[serde(default)]
83    pub mute_rules: BTreeMap<String, BTreeMap<String, AlarmMuteRule>>,
84    /// region -> dataset_identifier -> Dataset (KMS key association)
85    #[serde(default)]
86    pub datasets: BTreeMap<String, BTreeMap<String, Dataset>>,
87    /// resource_arn -> tag_key -> tag_value (tags are account-global by ARN)
88    #[serde(default)]
89    pub tags: BTreeMap<String, BTreeMap<String, String>>,
90    /// OTel enrichment is on when true (per account).
91    #[serde(default)]
92    pub otel_enrichment_running: bool,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct Dashboard {
97    pub name: String,
98    pub arn: String,
99    pub body: String,
100    pub last_modified: DateTime<Utc>,
101    pub size_bytes: i64,
102}
103
104/// A CloudWatch dataset and its optional KMS key association. Datasets are
105/// referenced by an identifier; there is no Create API, so an entry is
106/// materialized the first time a KMS key is associated with an identifier.
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct Dataset {
109    pub id: String,
110    pub arn: String,
111    pub kms_key_arn: Option<String>,
112}
113
114impl CloudWatchState {
115    pub fn new(account_id: &str) -> Self {
116        Self {
117            account_id: account_id.to_string(),
118            ..Default::default()
119        }
120    }
121
122    pub fn metrics_in(&self, region: &str) -> Option<&BTreeMap<String, Vec<MetricDatum>>> {
123        self.metrics.get(region)
124    }
125
126    pub fn metrics_in_mut(&mut self, region: &str) -> &mut BTreeMap<String, Vec<MetricDatum>> {
127        self.metrics.entry(region.to_string()).or_default()
128    }
129
130    pub fn alarms_in(&self, region: &str) -> Option<&BTreeMap<String, MetricAlarm>> {
131        self.alarms.get(region)
132    }
133
134    pub fn alarms_in_mut(&mut self, region: &str) -> &mut BTreeMap<String, MetricAlarm> {
135        self.alarms.entry(region.to_string()).or_default()
136    }
137
138    pub fn composite_alarms_in(&self, region: &str) -> Option<&BTreeMap<String, CompositeAlarm>> {
139        self.composite_alarms.get(region)
140    }
141
142    pub fn composite_alarms_in_mut(
143        &mut self,
144        region: &str,
145    ) -> &mut BTreeMap<String, CompositeAlarm> {
146        self.composite_alarms.entry(region.to_string()).or_default()
147    }
148
149    pub fn alarm_history_in(
150        &self,
151        region: &str,
152    ) -> Option<&BTreeMap<String, Vec<AlarmHistoryItem>>> {
153        self.alarm_history.get(region)
154    }
155
156    pub fn alarm_history_in_mut(
157        &mut self,
158        region: &str,
159    ) -> &mut BTreeMap<String, Vec<AlarmHistoryItem>> {
160        self.alarm_history.entry(region.to_string()).or_default()
161    }
162
163    pub fn anomaly_detectors_in(&self, region: &str) -> Option<&BTreeMap<String, AnomalyDetector>> {
164        self.anomaly_detectors.get(region)
165    }
166
167    pub fn anomaly_detectors_in_mut(
168        &mut self,
169        region: &str,
170    ) -> &mut BTreeMap<String, AnomalyDetector> {
171        self.anomaly_detectors
172            .entry(region.to_string())
173            .or_default()
174    }
175
176    pub fn insight_rules_in(&self, region: &str) -> Option<&BTreeMap<String, InsightRule>> {
177        self.insight_rules.get(region)
178    }
179
180    pub fn insight_rules_in_mut(&mut self, region: &str) -> &mut BTreeMap<String, InsightRule> {
181        self.insight_rules.entry(region.to_string()).or_default()
182    }
183
184    pub fn managed_rules_in(&self, region: &str) -> Option<&BTreeMap<String, Vec<ManagedRule>>> {
185        self.managed_rules.get(region)
186    }
187
188    pub fn managed_rules_in_mut(
189        &mut self,
190        region: &str,
191    ) -> &mut BTreeMap<String, Vec<ManagedRule>> {
192        self.managed_rules.entry(region.to_string()).or_default()
193    }
194
195    pub fn metric_streams_in(&self, region: &str) -> Option<&BTreeMap<String, MetricStream>> {
196        self.metric_streams.get(region)
197    }
198
199    pub fn metric_streams_in_mut(&mut self, region: &str) -> &mut BTreeMap<String, MetricStream> {
200        self.metric_streams.entry(region.to_string()).or_default()
201    }
202
203    pub fn mute_rules_in(&self, region: &str) -> Option<&BTreeMap<String, AlarmMuteRule>> {
204        self.mute_rules.get(region)
205    }
206
207    pub fn mute_rules_in_mut(&mut self, region: &str) -> &mut BTreeMap<String, AlarmMuteRule> {
208        self.mute_rules.entry(region.to_string()).or_default()
209    }
210
211    pub fn datasets_in(&self, region: &str) -> Option<&BTreeMap<String, Dataset>> {
212        self.datasets.get(region)
213    }
214
215    pub fn datasets_in_mut(&mut self, region: &str) -> &mut BTreeMap<String, Dataset> {
216        self.datasets.entry(region.to_string()).or_default()
217    }
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct MetricDatum {
222    pub metric_name: String,
223    pub dimensions: BTreeMap<String, String>,
224    pub timestamp: DateTime<Utc>,
225    pub value: Option<f64>,
226    pub statistic_values: Option<StatisticSet>,
227    pub unit: Option<String>,
228    pub storage_resolution: Option<i64>,
229}
230
231#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct StatisticSet {
233    pub sample_count: f64,
234    pub sum: f64,
235    pub minimum: f64,
236    pub maximum: f64,
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize)]
240pub struct MetricAlarm {
241    pub alarm_name: String,
242    pub alarm_arn: String,
243    pub alarm_description: Option<String>,
244    pub actions_enabled: bool,
245    pub ok_actions: Vec<String>,
246    pub alarm_actions: Vec<String>,
247    pub insufficient_data_actions: Vec<String>,
248    pub state_value: AlarmState,
249    pub state_reason: String,
250    pub state_updated_timestamp: DateTime<Utc>,
251    pub metric_name: Option<String>,
252    pub namespace: Option<String>,
253    pub statistic: Option<String>,
254    pub extended_statistic: Option<String>,
255    pub dimensions: BTreeMap<String, String>,
256    pub period: Option<i64>,
257    pub unit: Option<String>,
258    pub evaluation_periods: i64,
259    pub datapoints_to_alarm: Option<i64>,
260    pub threshold: Option<f64>,
261    pub comparison_operator: String,
262    pub treat_missing_data: Option<String>,
263    pub evaluate_low_sample_count_percentile: Option<String>,
264    /// `ThresholdMetricId` — references the metric-math id that produces an
265    /// anomaly-detection band (used with the `*UpperThreshold` /
266    /// `*LowerThreshold` comparison operators instead of a static Threshold).
267    #[serde(default)]
268    pub threshold_metric_id: Option<String>,
269    pub configuration_updated_timestamp: DateTime<Utc>,
270    pub alarm_configuration_updated_timestamp: DateTime<Utc>,
271    /// `Metrics` — the metric-math / cross-account alarm definition (a list of
272    /// `MetricDataQuery`). Set instead of the single-metric fields when the
273    /// alarm evaluates an expression or a metric in another account.
274    #[serde(default)]
275    pub metrics: Vec<AlarmMetricQuery>,
276}
277
278/// A single `MetricDataQuery` entry in a `PutMetricAlarm` `Metrics` list.
279#[derive(Debug, Clone, Serialize, Deserialize, Default)]
280pub struct AlarmMetricQuery {
281    pub id: String,
282    #[serde(default)]
283    pub metric_stat: Option<AlarmMetricStat>,
284    #[serde(default)]
285    pub expression: Option<String>,
286    #[serde(default)]
287    pub label: Option<String>,
288    #[serde(default)]
289    pub return_data: Option<bool>,
290    #[serde(default)]
291    pub account_id: Option<String>,
292    #[serde(default)]
293    pub period: Option<i64>,
294}
295
296/// The `MetricStat` of an [`AlarmMetricQuery`] (a metric plus how to aggregate
297/// it).
298#[derive(Debug, Clone, Serialize, Deserialize, Default)]
299pub struct AlarmMetricStat {
300    #[serde(default)]
301    pub namespace: Option<String>,
302    #[serde(default)]
303    pub metric_name: Option<String>,
304    #[serde(default)]
305    pub dimensions: BTreeMap<String, String>,
306    #[serde(default)]
307    pub period: Option<i64>,
308    #[serde(default)]
309    pub stat: Option<String>,
310    #[serde(default)]
311    pub unit: Option<String>,
312}
313
314#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
315pub enum AlarmState {
316    Ok,
317    Alarm,
318    InsufficientData,
319}
320
321impl AlarmState {
322    pub fn as_str(&self) -> &'static str {
323        match self {
324            AlarmState::Ok => "OK",
325            AlarmState::Alarm => "ALARM",
326            AlarmState::InsufficientData => "INSUFFICIENT_DATA",
327        }
328    }
329
330    pub fn parse(s: &str) -> Option<Self> {
331        match s {
332            "OK" => Some(AlarmState::Ok),
333            "ALARM" => Some(AlarmState::Alarm),
334            "INSUFFICIENT_DATA" => Some(AlarmState::InsufficientData),
335            _ => None,
336        }
337    }
338}
339
340/// A single alarm-history record returned by `DescribeAlarmHistory`.
341#[derive(Debug, Clone, Serialize, Deserialize)]
342pub struct AlarmHistoryItem {
343    pub alarm_name: String,
344    /// `MetricAlarm` or `CompositeAlarm`.
345    pub alarm_type: String,
346    pub timestamp: DateTime<Utc>,
347    /// One of the `HistoryItemType` enum values (ConfigurationUpdate /
348    /// StateUpdate / Action).
349    pub history_item_type: String,
350    pub history_summary: String,
351    /// JSON blob (`HistoryData`) describing the transition.
352    pub history_data: String,
353}
354
355/// A composite alarm (defined by an `AlarmRule` expression over other alarms).
356#[derive(Debug, Clone, Serialize, Deserialize)]
357pub struct CompositeAlarm {
358    pub alarm_name: String,
359    pub alarm_arn: String,
360    pub alarm_description: Option<String>,
361    pub alarm_rule: String,
362    pub actions_enabled: bool,
363    pub ok_actions: Vec<String>,
364    pub alarm_actions: Vec<String>,
365    pub insufficient_data_actions: Vec<String>,
366    pub actions_suppressor: Option<String>,
367    pub actions_suppressor_wait_period: Option<i64>,
368    pub actions_suppressor_extension_period: Option<i64>,
369    pub state_value: AlarmState,
370    pub state_reason: String,
371    pub state_updated_timestamp: DateTime<Utc>,
372    pub alarm_configuration_updated_timestamp: DateTime<Utc>,
373}
374
375/// An anomaly detection model on a metric (single-metric or metric-math).
376#[derive(Debug, Clone, Serialize, Deserialize)]
377pub struct AnomalyDetector {
378    /// Stable key derived from namespace/metric/stat/dims (single) or a hash
379    /// of the metric-math expression so Put/Delete/Describe agree.
380    pub key: String,
381    pub namespace: Option<String>,
382    pub metric_name: Option<String>,
383    pub stat: Option<String>,
384    pub dimensions: BTreeMap<String, String>,
385    pub metric_math: bool,
386    pub state_value: String,
387    /// `Configuration` (`MetricTimezone` + `ExcludedTimeRanges`) supplied on
388    /// PutAnomalyDetector; echoed back on DescribeAnomalyDetectors.
389    #[serde(default)]
390    pub configuration: Option<AnomalyDetectorConfiguration>,
391    /// `MetricCharacteristics.PeriodicSpikes`.
392    #[serde(default)]
393    pub periodic_spikes: Option<bool>,
394}
395
396/// The `Configuration` of an anomaly detector.
397#[derive(Debug, Clone, Serialize, Deserialize, Default)]
398pub struct AnomalyDetectorConfiguration {
399    #[serde(default)]
400    pub excluded_time_ranges: Vec<ExcludedTimeRange>,
401    #[serde(default)]
402    pub metric_timezone: Option<String>,
403}
404
405/// A single `ExcludedTimeRanges` entry (`Range`).
406#[derive(Debug, Clone, Serialize, Deserialize, Default)]
407pub struct ExcludedTimeRange {
408    #[serde(default)]
409    pub start_time: Option<String>,
410    #[serde(default)]
411    pub end_time: Option<String>,
412}
413
414/// A Contributor Insights rule.
415#[derive(Debug, Clone, Serialize, Deserialize)]
416pub struct InsightRule {
417    pub name: String,
418    pub state: String,
419    pub schema: String,
420    pub definition: String,
421    pub managed: bool,
422    pub apply_on_transformed_logs: bool,
423}
424
425/// A managed Contributor Insights rule (template applied to a resource ARN).
426#[derive(Debug, Clone, Serialize, Deserialize)]
427pub struct ManagedRule {
428    pub template_name: String,
429    pub resource_arn: String,
430}
431
432/// A metric stream (control-plane config; no data-plane delivery).
433#[derive(Debug, Clone, Serialize, Deserialize)]
434pub struct MetricStream {
435    pub name: String,
436    pub arn: String,
437    pub firehose_arn: String,
438    pub role_arn: String,
439    pub output_format: String,
440    /// "running" or "stopped".
441    pub state: String,
442    pub include_filters: Vec<MetricStreamFilter>,
443    pub exclude_filters: Vec<MetricStreamFilter>,
444    pub include_linked_accounts_metrics: bool,
445    pub creation_date: DateTime<Utc>,
446    pub last_update_date: DateTime<Utc>,
447}
448
449#[derive(Debug, Clone, Serialize, Deserialize)]
450pub struct MetricStreamFilter {
451    pub namespace: Option<String>,
452    pub metric_names: Vec<String>,
453}
454
455/// An alarm mute rule. `Rule` is a nested `Schedule` structure on the wire.
456#[derive(Debug, Clone, Serialize, Deserialize)]
457pub struct AlarmMuteRule {
458    pub name: String,
459    pub arn: String,
460    pub description: Option<String>,
461    pub schedule_expression: Option<String>,
462    pub schedule_duration: Option<String>,
463    pub schedule_timezone: Option<String>,
464    pub mute_target_alarm_names: Vec<String>,
465    pub start_date: Option<DateTime<Utc>>,
466    pub expire_date: Option<DateTime<Utc>>,
467    pub last_updated_timestamp: DateTime<Utc>,
468}