fakecloud-cloudwatch 0.44.0

AWS CloudWatch metrics + alarms implementation for FakeCloud
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
use std::collections::BTreeMap;
use std::sync::Arc;

use chrono::{DateTime, Utc};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};

pub type SharedCloudWatchState = Arc<RwLock<CloudWatchAccounts>>;

/// On-disk snapshot envelope for CloudWatch state.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CloudWatchSnapshot {
    pub schema_version: u32,
    pub accounts: CloudWatchAccounts,
}

pub const CLOUDWATCH_SNAPSHOT_SCHEMA_VERSION: u32 = 1;

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct CloudWatchAccounts {
    pub accounts: BTreeMap<String, CloudWatchState>,
}

impl CloudWatchAccounts {
    pub fn new() -> Self {
        Self::default()
    }

    /// Deep-clone for snapshot serialization. `Clone` isn't derived
    /// because the live state is held behind a `RwLock` and the rest of
    /// the crate references it by reference — this helper makes the
    /// intent explicit at the persistence boundary.
    pub fn clone_for_snapshot(&self) -> CloudWatchAccounts {
        CloudWatchAccounts {
            accounts: self.accounts.clone(),
        }
    }

    pub fn get_or_create(&mut self, account_id: &str) -> &mut CloudWatchState {
        self.accounts
            .entry(account_id.to_string())
            .or_insert_with(|| CloudWatchState::new(account_id))
    }

    pub fn get(&self, account_id: &str) -> Option<&CloudWatchState> {
        self.accounts.get(account_id)
    }
}

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct CloudWatchState {
    pub account_id: String,
    /// region -> namespace -> Vec<MetricDatum>
    pub metrics: BTreeMap<String, BTreeMap<String, Vec<MetricDatum>>>,
    /// region -> alarm_name -> MetricAlarm
    pub alarms: BTreeMap<String, BTreeMap<String, MetricAlarm>>,
    /// region -> alarm_name -> CompositeAlarm
    #[serde(default)]
    pub composite_alarms: BTreeMap<String, BTreeMap<String, CompositeAlarm>>,
    /// region -> alarm_name -> history items (newest appended last). Populated
    /// by PutMetricAlarm (ConfigurationUpdate), SetAlarmState (StateUpdate) and
    /// DeleteAlarms so DescribeAlarmHistory reflects real transitions.
    #[serde(default)]
    pub alarm_history: BTreeMap<String, BTreeMap<String, Vec<AlarmHistoryItem>>>,
    /// Dashboards keyed by name (CloudWatch dashboards are global per
    /// account, not regional).
    #[serde(default)]
    pub dashboards: BTreeMap<String, Dashboard>,
    /// region -> (namespace, metric, stat, dims) key -> AnomalyDetector
    #[serde(default)]
    pub anomaly_detectors: BTreeMap<String, BTreeMap<String, AnomalyDetector>>,
    /// region -> rule_name -> InsightRule
    #[serde(default)]
    pub insight_rules: BTreeMap<String, BTreeMap<String, InsightRule>>,
    /// region -> resource_arn -> Vec<ManagedRule>
    #[serde(default)]
    pub managed_rules: BTreeMap<String, BTreeMap<String, Vec<ManagedRule>>>,
    /// region -> stream_name -> MetricStream
    #[serde(default)]
    pub metric_streams: BTreeMap<String, BTreeMap<String, MetricStream>>,
    /// region -> rule_name -> AlarmMuteRule
    #[serde(default)]
    pub mute_rules: BTreeMap<String, BTreeMap<String, AlarmMuteRule>>,
    /// region -> dataset_identifier -> Dataset (KMS key association)
    #[serde(default)]
    pub datasets: BTreeMap<String, BTreeMap<String, Dataset>>,
    /// resource_arn -> tag_key -> tag_value (tags are account-global by ARN)
    #[serde(default)]
    pub tags: BTreeMap<String, BTreeMap<String, String>>,
    /// OTel enrichment is on when true (per account).
    #[serde(default)]
    pub otel_enrichment_running: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Dashboard {
    pub name: String,
    pub arn: String,
    pub body: String,
    pub last_modified: DateTime<Utc>,
    pub size_bytes: i64,
}

/// A CloudWatch dataset and its optional KMS key association. Datasets are
/// referenced by an identifier; there is no Create API, so an entry is
/// materialized the first time a KMS key is associated with an identifier.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Dataset {
    pub id: String,
    pub arn: String,
    pub kms_key_arn: Option<String>,
}

impl CloudWatchState {
    pub fn new(account_id: &str) -> Self {
        Self {
            account_id: account_id.to_string(),
            ..Default::default()
        }
    }

    pub fn metrics_in(&self, region: &str) -> Option<&BTreeMap<String, Vec<MetricDatum>>> {
        self.metrics.get(region)
    }

    pub fn metrics_in_mut(&mut self, region: &str) -> &mut BTreeMap<String, Vec<MetricDatum>> {
        self.metrics.entry(region.to_string()).or_default()
    }

    pub fn alarms_in(&self, region: &str) -> Option<&BTreeMap<String, MetricAlarm>> {
        self.alarms.get(region)
    }

    pub fn alarms_in_mut(&mut self, region: &str) -> &mut BTreeMap<String, MetricAlarm> {
        self.alarms.entry(region.to_string()).or_default()
    }

    pub fn composite_alarms_in(&self, region: &str) -> Option<&BTreeMap<String, CompositeAlarm>> {
        self.composite_alarms.get(region)
    }

    pub fn composite_alarms_in_mut(
        &mut self,
        region: &str,
    ) -> &mut BTreeMap<String, CompositeAlarm> {
        self.composite_alarms.entry(region.to_string()).or_default()
    }

    pub fn alarm_history_in(
        &self,
        region: &str,
    ) -> Option<&BTreeMap<String, Vec<AlarmHistoryItem>>> {
        self.alarm_history.get(region)
    }

    pub fn alarm_history_in_mut(
        &mut self,
        region: &str,
    ) -> &mut BTreeMap<String, Vec<AlarmHistoryItem>> {
        self.alarm_history.entry(region.to_string()).or_default()
    }

    pub fn anomaly_detectors_in(&self, region: &str) -> Option<&BTreeMap<String, AnomalyDetector>> {
        self.anomaly_detectors.get(region)
    }

    pub fn anomaly_detectors_in_mut(
        &mut self,
        region: &str,
    ) -> &mut BTreeMap<String, AnomalyDetector> {
        self.anomaly_detectors
            .entry(region.to_string())
            .or_default()
    }

    pub fn insight_rules_in(&self, region: &str) -> Option<&BTreeMap<String, InsightRule>> {
        self.insight_rules.get(region)
    }

    pub fn insight_rules_in_mut(&mut self, region: &str) -> &mut BTreeMap<String, InsightRule> {
        self.insight_rules.entry(region.to_string()).or_default()
    }

    pub fn managed_rules_in(&self, region: &str) -> Option<&BTreeMap<String, Vec<ManagedRule>>> {
        self.managed_rules.get(region)
    }

    pub fn managed_rules_in_mut(
        &mut self,
        region: &str,
    ) -> &mut BTreeMap<String, Vec<ManagedRule>> {
        self.managed_rules.entry(region.to_string()).or_default()
    }

    pub fn metric_streams_in(&self, region: &str) -> Option<&BTreeMap<String, MetricStream>> {
        self.metric_streams.get(region)
    }

    pub fn metric_streams_in_mut(&mut self, region: &str) -> &mut BTreeMap<String, MetricStream> {
        self.metric_streams.entry(region.to_string()).or_default()
    }

    pub fn mute_rules_in(&self, region: &str) -> Option<&BTreeMap<String, AlarmMuteRule>> {
        self.mute_rules.get(region)
    }

    pub fn mute_rules_in_mut(&mut self, region: &str) -> &mut BTreeMap<String, AlarmMuteRule> {
        self.mute_rules.entry(region.to_string()).or_default()
    }

    pub fn datasets_in(&self, region: &str) -> Option<&BTreeMap<String, Dataset>> {
        self.datasets.get(region)
    }

    pub fn datasets_in_mut(&mut self, region: &str) -> &mut BTreeMap<String, Dataset> {
        self.datasets.entry(region.to_string()).or_default()
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricDatum {
    pub metric_name: String,
    pub dimensions: BTreeMap<String, String>,
    pub timestamp: DateTime<Utc>,
    pub value: Option<f64>,
    pub statistic_values: Option<StatisticSet>,
    pub unit: Option<String>,
    pub storage_resolution: Option<i64>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StatisticSet {
    pub sample_count: f64,
    pub sum: f64,
    pub minimum: f64,
    pub maximum: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricAlarm {
    pub alarm_name: String,
    pub alarm_arn: String,
    pub alarm_description: Option<String>,
    pub actions_enabled: bool,
    pub ok_actions: Vec<String>,
    pub alarm_actions: Vec<String>,
    pub insufficient_data_actions: Vec<String>,
    pub state_value: AlarmState,
    pub state_reason: String,
    pub state_updated_timestamp: DateTime<Utc>,
    pub metric_name: Option<String>,
    pub namespace: Option<String>,
    pub statistic: Option<String>,
    pub extended_statistic: Option<String>,
    pub dimensions: BTreeMap<String, String>,
    pub period: Option<i64>,
    pub unit: Option<String>,
    pub evaluation_periods: i64,
    pub datapoints_to_alarm: Option<i64>,
    pub threshold: Option<f64>,
    pub comparison_operator: String,
    pub treat_missing_data: Option<String>,
    pub evaluate_low_sample_count_percentile: Option<String>,
    /// `ThresholdMetricId` — references the metric-math id that produces an
    /// anomaly-detection band (used with the `*UpperThreshold` /
    /// `*LowerThreshold` comparison operators instead of a static Threshold).
    #[serde(default)]
    pub threshold_metric_id: Option<String>,
    pub configuration_updated_timestamp: DateTime<Utc>,
    pub alarm_configuration_updated_timestamp: DateTime<Utc>,
    /// `Metrics` — the metric-math / cross-account alarm definition (a list of
    /// `MetricDataQuery`). Set instead of the single-metric fields when the
    /// alarm evaluates an expression or a metric in another account.
    #[serde(default)]
    pub metrics: Vec<AlarmMetricQuery>,
    /// Whether the current state was forced by `SetAlarmState`. AWS reverts a
    /// manual state on the next evaluation, but the emulator keeps it sticky
    /// (so it can be used to drive composite alarms) until real datapoints
    /// arrive — at which point evaluation clears this and data governs the
    /// state. It only suppresses the default missing-data INSUFFICIENT_DATA
    /// transition, never a real threshold crossing.
    #[serde(default)]
    pub state_manually_set: bool,
}

/// A single `MetricDataQuery` entry in a `PutMetricAlarm` `Metrics` list.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AlarmMetricQuery {
    pub id: String,
    #[serde(default)]
    pub metric_stat: Option<AlarmMetricStat>,
    #[serde(default)]
    pub expression: Option<String>,
    #[serde(default)]
    pub label: Option<String>,
    #[serde(default)]
    pub return_data: Option<bool>,
    #[serde(default)]
    pub account_id: Option<String>,
    #[serde(default)]
    pub period: Option<i64>,
}

/// The `MetricStat` of an [`AlarmMetricQuery`] (a metric plus how to aggregate
/// it).
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AlarmMetricStat {
    #[serde(default)]
    pub namespace: Option<String>,
    #[serde(default)]
    pub metric_name: Option<String>,
    #[serde(default)]
    pub dimensions: BTreeMap<String, String>,
    #[serde(default)]
    pub period: Option<i64>,
    #[serde(default)]
    pub stat: Option<String>,
    #[serde(default)]
    pub unit: Option<String>,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum AlarmState {
    Ok,
    Alarm,
    InsufficientData,
}

impl AlarmState {
    pub fn as_str(&self) -> &'static str {
        match self {
            AlarmState::Ok => "OK",
            AlarmState::Alarm => "ALARM",
            AlarmState::InsufficientData => "INSUFFICIENT_DATA",
        }
    }

    pub fn parse(s: &str) -> Option<Self> {
        match s {
            "OK" => Some(AlarmState::Ok),
            "ALARM" => Some(AlarmState::Alarm),
            "INSUFFICIENT_DATA" => Some(AlarmState::InsufficientData),
            _ => None,
        }
    }
}

/// A single alarm-history record returned by `DescribeAlarmHistory`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlarmHistoryItem {
    pub alarm_name: String,
    /// `MetricAlarm` or `CompositeAlarm`.
    pub alarm_type: String,
    pub timestamp: DateTime<Utc>,
    /// One of the `HistoryItemType` enum values (ConfigurationUpdate /
    /// StateUpdate / Action).
    pub history_item_type: String,
    pub history_summary: String,
    /// JSON blob (`HistoryData`) describing the transition.
    pub history_data: String,
}

/// A composite alarm (defined by an `AlarmRule` expression over other alarms).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompositeAlarm {
    pub alarm_name: String,
    pub alarm_arn: String,
    pub alarm_description: Option<String>,
    pub alarm_rule: String,
    pub actions_enabled: bool,
    pub ok_actions: Vec<String>,
    pub alarm_actions: Vec<String>,
    pub insufficient_data_actions: Vec<String>,
    pub actions_suppressor: Option<String>,
    pub actions_suppressor_wait_period: Option<i64>,
    pub actions_suppressor_extension_period: Option<i64>,
    pub state_value: AlarmState,
    pub state_reason: String,
    pub state_updated_timestamp: DateTime<Utc>,
    pub alarm_configuration_updated_timestamp: DateTime<Utc>,
}

/// An anomaly detection model on a metric (single-metric or metric-math).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnomalyDetector {
    /// Stable key derived from namespace/metric/stat/dims (single) or a hash
    /// of the metric-math expression so Put/Delete/Describe agree.
    pub key: String,
    pub namespace: Option<String>,
    pub metric_name: Option<String>,
    pub stat: Option<String>,
    pub dimensions: BTreeMap<String, String>,
    pub metric_math: bool,
    pub state_value: String,
    /// `Configuration` (`MetricTimezone` + `ExcludedTimeRanges`) supplied on
    /// PutAnomalyDetector; echoed back on DescribeAnomalyDetectors.
    #[serde(default)]
    pub configuration: Option<AnomalyDetectorConfiguration>,
    /// `MetricCharacteristics.PeriodicSpikes`.
    #[serde(default)]
    pub periodic_spikes: Option<bool>,
}

/// The `Configuration` of an anomaly detector.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AnomalyDetectorConfiguration {
    #[serde(default)]
    pub excluded_time_ranges: Vec<ExcludedTimeRange>,
    #[serde(default)]
    pub metric_timezone: Option<String>,
}

/// A single `ExcludedTimeRanges` entry (`Range`).
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ExcludedTimeRange {
    #[serde(default)]
    pub start_time: Option<String>,
    #[serde(default)]
    pub end_time: Option<String>,
}

/// A Contributor Insights rule.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InsightRule {
    pub name: String,
    pub state: String,
    pub schema: String,
    pub definition: String,
    pub managed: bool,
    pub apply_on_transformed_logs: bool,
}

/// A managed Contributor Insights rule (template applied to a resource ARN).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManagedRule {
    pub template_name: String,
    pub resource_arn: String,
}

/// A metric stream (control-plane config; no data-plane delivery).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricStream {
    pub name: String,
    pub arn: String,
    pub firehose_arn: String,
    pub role_arn: String,
    pub output_format: String,
    /// "running" or "stopped".
    pub state: String,
    pub include_filters: Vec<MetricStreamFilter>,
    pub exclude_filters: Vec<MetricStreamFilter>,
    pub include_linked_accounts_metrics: bool,
    pub creation_date: DateTime<Utc>,
    pub last_update_date: DateTime<Utc>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricStreamFilter {
    pub namespace: Option<String>,
    pub metric_names: Vec<String>,
}

/// An alarm mute rule. `Rule` is a nested `Schedule` structure on the wire.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlarmMuteRule {
    pub name: String,
    pub arn: String,
    pub description: Option<String>,
    pub schedule_expression: Option<String>,
    pub schedule_duration: Option<String>,
    pub schedule_timezone: Option<String>,
    pub mute_target_alarm_names: Vec<String>,
    pub start_date: Option<DateTime<Utc>>,
    pub expire_date: Option<DateTime<Utc>>,
    pub last_updated_timestamp: DateTime<Utc>,
}