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