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