Skip to main content

fakecloud_logs/
state.rs

1use std::collections::BTreeMap;
2use std::sync::Arc;
3
4use parking_lot::RwLock;
5
6pub type SharedLogsState = Arc<RwLock<fakecloud_core::multi_account::MultiAccountState<LogsState>>>;
7
8impl fakecloud_core::multi_account::AccountState for LogsState {
9    fn new_for_account(account_id: &str, region: &str, _endpoint: &str) -> Self {
10        Self::new(account_id, region)
11    }
12}
13
14/// JSON object keys must be strings, so serialize
15/// `HashMap<(String,String), AccountPolicy>` as a list of
16/// `[policy_name, policy_type, policy]` tuples.
17mod account_policy_map_serde {
18    use super::AccountPolicy;
19    use serde::{Deserialize, Deserializer, Serialize, Serializer};
20    use std::collections::BTreeMap;
21
22    pub fn serialize<S: Serializer>(
23        map: &BTreeMap<(String, String), AccountPolicy>,
24        s: S,
25    ) -> Result<S::Ok, S::Error> {
26        let entries: Vec<(&String, &String, &AccountPolicy)> = map
27            .iter()
28            .map(|((name, kind), p)| (name, kind, p))
29            .collect();
30        entries.serialize(s)
31    }
32
33    pub fn deserialize<'de, D: Deserializer<'de>>(
34        d: D,
35    ) -> Result<BTreeMap<(String, String), AccountPolicy>, D::Error> {
36        let entries: Vec<(String, String, AccountPolicy)> = Vec::deserialize(d)?;
37        Ok(entries
38            .into_iter()
39            .map(|(name, kind, p)| ((name, kind), p))
40            .collect())
41    }
42}
43
44#[derive(Clone, serde::Serialize, serde::Deserialize)]
45pub struct LogsState {
46    pub account_id: String,
47    pub region: String,
48    pub log_groups: BTreeMap<String, LogGroup>,
49    pub metric_filters: Vec<MetricFilter>,
50    pub resource_policies: BTreeMap<String, ResourcePolicy>,
51    pub destinations: BTreeMap<String, Destination>,
52    pub queries: BTreeMap<String, QueryInfo>,
53    pub export_tasks: Vec<ExportTask>,
54    pub delivery_destinations: BTreeMap<String, DeliveryDestination>,
55    pub delivery_sources: BTreeMap<String, DeliverySource>,
56    pub deliveries: BTreeMap<String, Delivery>,
57    pub query_definitions: BTreeMap<String, QueryDefinition>,
58    /// Account policies keyed by (policy_name, policy_type)
59    #[serde(with = "account_policy_map_serde")]
60    pub account_policies: BTreeMap<(String, String), AccountPolicy>,
61    /// Anomaly detectors keyed by detector ARN
62    pub anomaly_detectors: BTreeMap<String, AnomalyDetector>,
63    /// Import tasks keyed by import ID
64    pub import_tasks: BTreeMap<String, ImportTask>,
65    /// Integrations keyed by integration name
66    pub integrations: BTreeMap<String, Integration>,
67    /// Lookup tables keyed by ARN
68    pub lookup_tables: BTreeMap<String, LookupTable>,
69    /// Scheduled queries keyed by identifier (ARN)
70    pub scheduled_queries: BTreeMap<String, ScheduledQuery>,
71    /// S3 table integration sources keyed by integration ARN -> list of source identifiers
72    pub s3_table_sources: BTreeMap<String, Vec<String>>,
73    /// Bearer token authentication flag per log group
74    pub bearer_token_auth: BTreeMap<String, bool>,
75    /// Internal export storage: keyed by "bucket/prefix/..." path, value is exported data.
76    /// Used by CreateExportTask and delivery pipeline when direct S3 access is unavailable.
77    pub export_storage: BTreeMap<String, Vec<u8>>,
78    /// Detected log anomalies keyed by anomaly id. Populated via the
79    /// `/_fakecloud/logs/anomalies/inject` admin endpoint and surfaced
80    /// through ListAnomalies / UpdateAnomaly.
81    #[serde(default)]
82    pub anomalies: BTreeMap<String, LogAnomaly>,
83}
84
85#[derive(Clone, serde::Serialize, serde::Deserialize)]
86pub struct LogAnomaly {
87    pub anomaly_id: String,
88    pub anomaly_detector_arn: String,
89    pub log_group_arn_list: Vec<String>,
90    pub pattern_id: String,
91    pub pattern_string: String,
92    pub first_seen: i64,
93    pub last_seen: i64,
94    pub priority: String,
95    pub state: String,
96    pub suppressed: bool,
97}
98
99impl LogsState {
100    pub fn new(account_id: &str, region: &str) -> Self {
101        Self {
102            account_id: account_id.to_string(),
103            region: region.to_string(),
104            log_groups: BTreeMap::new(),
105            metric_filters: Vec::new(),
106            resource_policies: BTreeMap::new(),
107            destinations: BTreeMap::new(),
108            queries: BTreeMap::new(),
109            export_tasks: Vec::new(),
110            delivery_destinations: BTreeMap::new(),
111            delivery_sources: BTreeMap::new(),
112            deliveries: BTreeMap::new(),
113            query_definitions: BTreeMap::new(),
114            account_policies: BTreeMap::new(),
115            anomaly_detectors: BTreeMap::new(),
116            import_tasks: BTreeMap::new(),
117            integrations: BTreeMap::new(),
118            lookup_tables: BTreeMap::new(),
119            scheduled_queries: BTreeMap::new(),
120            s3_table_sources: BTreeMap::new(),
121            bearer_token_auth: BTreeMap::new(),
122            export_storage: BTreeMap::new(),
123            anomalies: BTreeMap::new(),
124        }
125    }
126
127    pub fn reset(&mut self) {
128        self.log_groups.clear();
129        self.metric_filters.clear();
130        self.resource_policies.clear();
131        self.destinations.clear();
132        self.queries.clear();
133        self.export_tasks.clear();
134        self.delivery_destinations.clear();
135        self.delivery_sources.clear();
136        self.deliveries.clear();
137        self.query_definitions.clear();
138        self.account_policies.clear();
139        self.anomaly_detectors.clear();
140        self.import_tasks.clear();
141        self.integrations.clear();
142        self.lookup_tables.clear();
143        self.scheduled_queries.clear();
144        self.s3_table_sources.clear();
145        self.bearer_token_auth.clear();
146        self.export_storage.clear();
147        self.anomalies.clear();
148    }
149}
150
151#[derive(Clone, serde::Serialize, serde::Deserialize)]
152pub struct LogGroup {
153    pub name: String,
154    pub arn: String,
155    pub creation_time: i64,
156    pub retention_in_days: Option<i32>,
157    pub kms_key_id: Option<String>,
158    pub tags: BTreeMap<String, String>,
159    pub log_streams: BTreeMap<String, LogStream>,
160    pub stored_bytes: i64,
161    pub subscription_filters: Vec<SubscriptionFilter>,
162    pub data_protection_policy: Option<DataProtectionPolicy>,
163    pub index_policies: Vec<IndexPolicy>,
164    pub transformer: Option<Transformer>,
165    pub deletion_protection: bool,
166    /// `STANDARD` (default), `INFREQUENT_ACCESS`, or `DELIVERY`. Set at
167    /// creation time via `CreateLogGroup`'s `logGroupClass` parameter.
168    /// Tracked here so `DescribeLogGroups` round-trips it correctly.
169    pub log_group_class: Option<String>,
170}
171
172#[derive(Clone, serde::Serialize, serde::Deserialize)]
173pub struct LogStream {
174    pub name: String,
175    pub arn: String,
176    pub creation_time: i64,
177    pub first_event_timestamp: Option<i64>,
178    pub last_event_timestamp: Option<i64>,
179    pub last_ingestion_time: Option<i64>,
180    pub upload_sequence_token: String,
181    pub events: Vec<LogEvent>,
182}
183
184#[derive(Clone, serde::Serialize, serde::Deserialize)]
185pub struct LogEvent {
186    pub timestamp: i64,
187    pub message: String,
188    pub ingestion_time: i64,
189}
190
191#[derive(Clone, serde::Serialize, serde::Deserialize)]
192pub struct SubscriptionFilter {
193    pub filter_name: String,
194    pub log_group_name: String,
195    pub filter_pattern: String,
196    pub destination_arn: String,
197    pub role_arn: Option<String>,
198    pub distribution: String,
199    pub creation_time: i64,
200}
201
202#[derive(Clone, serde::Serialize, serde::Deserialize)]
203pub struct MetricFilter {
204    pub filter_name: String,
205    pub filter_pattern: String,
206    pub log_group_name: String,
207    pub metric_transformations: Vec<MetricTransformation>,
208    pub creation_time: i64,
209}
210
211#[derive(Clone, serde::Serialize, serde::Deserialize)]
212pub struct MetricTransformation {
213    pub metric_name: String,
214    pub metric_namespace: String,
215    pub metric_value: String,
216    pub default_value: Option<f64>,
217    /// CloudWatch unit for the published metric. AWS always reports it on
218    /// DescribeMetricFilters, defaulting to `None` when unset, and the
219    /// Terraform `aws_cloudwatch_log_metric_filter` resource asserts on it.
220    #[serde(default)]
221    pub unit: Option<String>,
222}
223
224#[derive(Clone, serde::Serialize, serde::Deserialize)]
225pub struct ResourcePolicy {
226    pub policy_name: String,
227    pub policy_document: String,
228    pub last_updated_time: i64,
229}
230
231#[derive(Clone, serde::Serialize, serde::Deserialize)]
232pub struct Destination {
233    pub destination_name: String,
234    pub target_arn: String,
235    pub role_arn: String,
236    pub arn: String,
237    pub access_policy: Option<String>,
238    pub creation_time: i64,
239    pub tags: BTreeMap<String, String>,
240}
241
242#[derive(Clone, serde::Serialize, serde::Deserialize)]
243pub struct QueryInfo {
244    pub query_id: String,
245    pub log_group_name: String,
246    /// Every log group / identifier referenced by this query, used by
247    /// `ListLogGroupsForQuery`. Always includes `log_group_name` plus any
248    /// names from `logGroupNames` / identifiers from `logGroupIdentifiers`
249    /// passed at start time.
250    #[serde(default)]
251    pub log_group_identifiers: Vec<String>,
252    pub query_string: String,
253    pub start_time: i64,
254    pub end_time: i64,
255    pub status: String,
256    pub create_time: i64,
257}
258
259#[derive(Clone, serde::Serialize, serde::Deserialize)]
260pub struct ExportTask {
261    pub task_id: String,
262    pub task_name: Option<String>,
263    pub log_group_name: String,
264    pub log_stream_name_prefix: Option<String>,
265    pub from_time: i64,
266    pub to_time: i64,
267    pub destination: String,
268    pub destination_prefix: String,
269    pub status_code: String,
270    pub status_message: String,
271    #[serde(default)]
272    pub creation_time: i64,
273    #[serde(default)]
274    pub completion_time: Option<i64>,
275}
276
277#[derive(Clone, serde::Serialize, serde::Deserialize)]
278pub struct DeliveryDestination {
279    pub name: String,
280    pub arn: String,
281    pub output_format: Option<String>,
282    pub delivery_destination_configuration: BTreeMap<String, String>,
283    /// `CWL`/`S3`/`FH`/`XRAY` — derived from the destination resource ARN when
284    /// the caller does not specify it. AWS always reports it on read.
285    #[serde(default)]
286    pub delivery_destination_type: String,
287    pub tags: BTreeMap<String, String>,
288    pub delivery_destination_policy: Option<String>,
289}
290
291#[derive(Clone, serde::Serialize, serde::Deserialize)]
292pub struct DeliverySource {
293    pub name: String,
294    pub arn: String,
295    pub resource_arns: Vec<String>,
296    pub service: String,
297    pub log_type: String,
298    pub tags: BTreeMap<String, String>,
299    #[serde(default)]
300    pub created_at: i64,
301}
302
303#[derive(Clone, serde::Serialize, serde::Deserialize)]
304pub struct Delivery {
305    pub id: String,
306    pub delivery_source_name: String,
307    pub delivery_destination_arn: String,
308    pub delivery_destination_type: String,
309    pub arn: String,
310    pub tags: BTreeMap<String, String>,
311    #[serde(default)]
312    pub field_delimiter: Option<String>,
313    #[serde(default)]
314    pub record_fields: Vec<String>,
315    #[serde(default)]
316    pub s3_delivery_configuration: Option<serde_json::Value>,
317    #[serde(default)]
318    pub created_at: i64,
319}
320
321#[derive(Clone, serde::Serialize, serde::Deserialize)]
322pub struct QueryDefinition {
323    pub query_definition_id: String,
324    pub name: String,
325    pub query_string: String,
326    pub log_group_names: Vec<String>,
327    pub last_modified: i64,
328}
329
330#[derive(Clone, serde::Serialize, serde::Deserialize)]
331pub struct AccountPolicy {
332    pub policy_name: String,
333    pub policy_type: String,
334    pub policy_document: String,
335    pub scope: Option<String>,
336    pub selection_criteria: Option<String>,
337    pub account_id: String,
338    pub last_updated_time: i64,
339}
340
341#[derive(Clone, serde::Serialize, serde::Deserialize)]
342pub struct DataProtectionPolicy {
343    pub policy_document: String,
344    pub last_updated_time: i64,
345}
346
347#[derive(Clone, serde::Serialize, serde::Deserialize)]
348pub struct IndexPolicy {
349    pub policy_name: String,
350    pub policy_document: String,
351    pub last_updated_time: i64,
352}
353
354#[derive(Clone, serde::Serialize, serde::Deserialize)]
355pub struct Transformer {
356    pub transformer_config: serde_json::Value,
357    pub creation_time: i64,
358    pub last_modified_time: i64,
359}
360
361#[derive(Clone, serde::Serialize, serde::Deserialize)]
362pub struct AnomalyDetector {
363    pub detector_name: String,
364    pub arn: String,
365    pub log_group_arn_list: Vec<String>,
366    pub evaluation_frequency: Option<String>,
367    pub filter_pattern: Option<String>,
368    pub anomaly_visibility_time: Option<i64>,
369    pub creation_time: i64,
370    pub last_modified_time: i64,
371    pub enabled: bool,
372    #[serde(default)]
373    pub tags: BTreeMap<String, String>,
374}
375
376#[derive(Clone, serde::Serialize, serde::Deserialize)]
377pub struct ImportTask {
378    pub import_id: String,
379    pub import_source_arn: String,
380    pub import_role_arn: String,
381    pub log_group_name: Option<String>,
382    pub status: String,
383    pub creation_time: i64,
384}
385
386#[derive(Clone, serde::Serialize, serde::Deserialize)]
387pub struct Integration {
388    pub integration_name: String,
389    pub integration_type: String,
390    pub resource_config: serde_json::Value,
391    pub status: String,
392    pub creation_time: i64,
393}
394
395#[derive(Clone, serde::Serialize, serde::Deserialize)]
396pub struct LookupTable {
397    pub lookup_table_name: String,
398    pub arn: String,
399    pub table_body: String,
400    pub creation_time: i64,
401    pub last_modified_time: i64,
402}
403
404#[derive(Clone, serde::Serialize, serde::Deserialize)]
405pub struct ScheduledQuery {
406    pub name: String,
407    pub arn: String,
408    pub query_string: String,
409    pub query_language: String,
410    pub schedule_expression: String,
411    pub execution_role_arn: String,
412    pub status: String,
413    pub creation_time: i64,
414    pub last_modified_time: i64,
415}
416
417/// On-disk snapshot envelope for CloudWatch Logs state. Versioned so
418/// format changes fail loudly on upgrade.
419#[derive(Clone, serde::Serialize, serde::Deserialize)]
420pub struct LogsSnapshot {
421    pub schema_version: u32,
422    #[serde(default)]
423    pub accounts: Option<fakecloud_core::multi_account::MultiAccountState<LogsState>>,
424    #[serde(default)]
425    pub state: Option<LogsState>,
426}
427
428pub const LOGS_SNAPSHOT_SCHEMA_VERSION: u32 = 2;
429
430#[cfg(test)]
431mod tests {
432    use super::*;
433
434    #[test]
435    fn new_initializes_empty() {
436        let state = LogsState::new("123456789012", "us-east-1");
437        assert_eq!(state.account_id, "123456789012");
438        assert_eq!(state.region, "us-east-1");
439        assert!(state.log_groups.is_empty());
440        assert!(state.queries.is_empty());
441    }
442
443    #[test]
444    fn reset_clears_state() {
445        let mut state = LogsState::new("123456789012", "us-east-1");
446        state.bearer_token_auth.insert("g".to_string(), true);
447        state.reset();
448        assert!(state.bearer_token_auth.is_empty());
449    }
450}