1use async_trait::async_trait;
2use http::StatusCode;
3use serde_json::{json, Value};
4
5use std::sync::Arc;
6
7use tokio::sync::Mutex as AsyncMutex;
8
9use fakecloud_core::delivery::DeliveryBus;
10use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
11use fakecloud_persistence::SnapshotStore;
12
13use crate::state::{LogsSnapshot, SharedLogsState, LOGS_SNAPSHOT_SCHEMA_VERSION};
14
15mod anomaly;
16mod deliveries;
17mod destinations;
18mod exports;
19mod filters;
20mod groups;
21mod misc;
22mod policies;
23mod queries;
24mod streams;
25mod syslog;
26mod tags;
27
28fn is_read_only_action(action: &str) -> bool {
31 matches!(
32 action,
33 "DescribeLogGroups"
34 | "DescribeLogStreams"
35 | "GetLogEvents"
36 | "FilterLogEvents"
37 | "ListTagsLogGroup"
38 | "ListTagsForResource"
39 | "DescribeSubscriptionFilters"
40 | "DescribeMetricFilters"
41 | "DescribeResourcePolicies"
42 | "DescribeDestinations"
43 | "GetQueryResults"
44 | "DescribeQueries"
45 | "DescribeExportTasks"
46 | "GetDeliveryDestination"
47 | "DescribeDeliveryDestinations"
48 | "GetDeliveryDestinationPolicy"
49 | "GetDeliverySource"
50 | "DescribeDeliverySources"
51 | "GetDelivery"
52 | "DescribeDeliveries"
53 | "DescribeQueryDefinitions"
54 | "DescribeAccountPolicies"
55 | "GetDataProtectionPolicy"
56 | "DescribeIndexPolicies"
57 | "DescribeFieldIndexes"
58 | "GetTransformer"
59 | "TestTransformer"
60 | "GetLogAnomalyDetector"
61 | "ListLogAnomalyDetectors"
62 | "GetLogGroupFields"
63 | "TestMetricFilter"
64 | "GetLogRecord"
65 | "ListAnomalies"
66 | "DescribeImportTasks"
67 | "DescribeImportTaskBatches"
68 | "GetIntegration"
69 | "ListIntegrations"
70 | "GetLookupTable"
71 | "DescribeLookupTables"
72 | "GetScheduledQuery"
73 | "GetScheduledQueryHistory"
74 | "ListScheduledQueries"
75 | "StartLiveTail"
76 | "ListLogGroups"
77 | "ListLogGroupsForQuery"
78 | "ListAggregateLogGroupSummaries"
79 | "GetLogObject"
80 | "GetLogFields"
81 | "ListSourcesForS3TableIntegration"
82 | "DescribeConfigurationTemplates"
83 | "ListSyslogConfigurations"
84 | "GetExportedData"
85 )
86}
87
88pub struct LogsService {
89 state: SharedLogsState,
90 delivery_bus: Arc<DeliveryBus>,
91 snapshot_store: Option<Arc<dyn SnapshotStore>>,
92 snapshot_lock: Arc<AsyncMutex<()>>,
93}
94
95impl LogsService {
96 pub fn new(state: SharedLogsState, delivery_bus: Arc<DeliveryBus>) -> Self {
97 Self {
98 state,
99 delivery_bus,
100 snapshot_store: None,
101 snapshot_lock: Arc::new(AsyncMutex::new(())),
102 }
103 }
104
105 pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
106 self.snapshot_store = Some(store);
107 self
108 }
109
110 async fn save_snapshot(&self) {
114 save_logs_snapshot(
115 &self.state,
116 self.snapshot_store.clone(),
117 &self.snapshot_lock,
118 )
119 .await;
120 }
121
122 pub fn snapshot_hook(&self) -> Option<fakecloud_persistence::SnapshotHook> {
127 let store = self.snapshot_store.clone()?;
128 let state = self.state.clone();
129 let lock = self.snapshot_lock.clone();
130 Some(Arc::new(move || {
131 let state = state.clone();
132 let store = store.clone();
133 let lock = lock.clone();
134 Box::pin(async move {
135 save_logs_snapshot(&state, Some(store), &lock).await;
136 })
137 }))
138 }
139}
140
141pub async fn save_logs_snapshot(
147 state: &SharedLogsState,
148 store: Option<Arc<dyn SnapshotStore>>,
149 lock: &AsyncMutex<()>,
150) {
151 let Some(store) = store else {
152 return;
153 };
154 let _guard = lock.lock().await;
155 let snapshot = LogsSnapshot {
156 schema_version: LOGS_SNAPSHOT_SCHEMA_VERSION,
157 accounts: Some(state.read().clone()),
158 state: None,
159 };
160 let join = tokio::task::spawn_blocking(move || -> std::io::Result<()> {
161 let bytes = serde_json::to_vec(&snapshot)
162 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
163 store.save(&bytes)
164 })
165 .await;
166 match join {
167 Ok(Ok(())) => {}
168 Ok(Err(err)) => tracing::error!(%err, "failed to write logs snapshot"),
169 Err(err) => tracing::error!(%err, "logs snapshot task panicked"),
170 }
171}
172
173#[async_trait]
174impl AwsService for LogsService {
175 fn service_name(&self) -> &str {
176 "logs"
177 }
178
179 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
180 let mutates = !is_read_only_action(req.action.as_str());
181 let result = match req.action.as_str() {
182 "CreateLogGroup" => self.create_log_group(&req),
183 "DeleteLogGroup" => self.delete_log_group(&req),
184 "DescribeLogGroups" => self.describe_log_groups(&req),
185 "CreateLogStream" => self.create_log_stream(&req),
186 "DeleteLogStream" => self.delete_log_stream(&req),
187 "DescribeLogStreams" => self.describe_log_streams(&req),
188 "PutLogEvents" => self.put_log_events(&req),
189 "GetLogEvents" => self.get_log_events(&req),
190 "FilterLogEvents" => self.filter_log_events(&req),
191 "TagLogGroup" => self.tag_log_group(&req),
192 "UntagLogGroup" => self.untag_log_group(&req),
193 "ListTagsLogGroup" => self.list_tags_log_group(&req),
194 "TagResource" => self.tag_resource(&req),
195 "UntagResource" => self.untag_resource(&req),
196 "ListTagsForResource" => self.list_tags_for_resource(&req),
197 "PutRetentionPolicy" => self.put_retention_policy(&req),
198 "DeleteRetentionPolicy" => self.delete_retention_policy(&req),
199 "PutSubscriptionFilter" => self.put_subscription_filter(&req),
200 "DescribeSubscriptionFilters" => self.describe_subscription_filters(&req),
201 "DeleteSubscriptionFilter" => self.delete_subscription_filter(&req),
202 "PutMetricFilter" => self.put_metric_filter(&req),
203 "DescribeMetricFilters" => self.describe_metric_filters(&req),
204 "DeleteMetricFilter" => self.delete_metric_filter(&req),
205 "PutResourcePolicy" => self.put_resource_policy(&req),
206 "DescribeResourcePolicies" => self.describe_resource_policies(&req),
207 "DeleteResourcePolicy" => self.delete_resource_policy(&req),
208 "PutDestination" => self.put_destination(&req),
209 "DescribeDestinations" => self.describe_destinations(&req),
210 "DeleteDestination" => self.delete_destination(&req),
211 "PutDestinationPolicy" => self.put_destination_policy(&req),
212 "StartQuery" => self.start_query(&req),
213 "GetQueryResults" => self.get_query_results(&req),
214 "DescribeQueries" => self.describe_queries(&req),
215 "CreateExportTask" => self.create_export_task(&req),
216 "DescribeExportTasks" => self.describe_export_tasks(&req),
217 "CancelExportTask" => self.cancel_export_task(&req),
218 "PutDeliveryDestination" => self.put_delivery_destination(&req),
219 "GetDeliveryDestination" => self.get_delivery_destination(&req),
220 "DescribeDeliveryDestinations" => self.describe_delivery_destinations(&req),
221 "DeleteDeliveryDestination" => self.delete_delivery_destination(&req),
222 "PutDeliveryDestinationPolicy" => self.put_delivery_destination_policy(&req),
223 "GetDeliveryDestinationPolicy" => self.get_delivery_destination_policy(&req),
224 "DeleteDeliveryDestinationPolicy" => self.delete_delivery_destination_policy(&req),
225 "PutDeliverySource" => self.put_delivery_source(&req),
226 "GetDeliverySource" => self.get_delivery_source(&req),
227 "DescribeDeliverySources" => self.describe_delivery_sources(&req),
228 "DeleteDeliverySource" => self.delete_delivery_source(&req),
229 "CreateDelivery" => self.create_delivery(&req),
230 "GetDelivery" => self.get_delivery(&req),
231 "DescribeDeliveries" => self.describe_deliveries(&req),
232 "DeleteDelivery" => self.delete_delivery(&req),
233 "AssociateKmsKey" => self.associate_kms_key(&req),
234 "DisassociateKmsKey" => self.disassociate_kms_key(&req),
235 "PutQueryDefinition" => self.put_query_definition(&req),
236 "DescribeQueryDefinitions" => self.describe_query_definitions(&req),
237 "DeleteQueryDefinition" => self.delete_query_definition(&req),
238 "PutAccountPolicy" => self.put_account_policy(&req),
239 "DescribeAccountPolicies" => self.describe_account_policies(&req),
240 "DeleteAccountPolicy" => self.delete_account_policy(&req),
241 "PutDataProtectionPolicy" => self.put_data_protection_policy(&req),
242 "GetDataProtectionPolicy" => self.get_data_protection_policy(&req),
243 "DeleteDataProtectionPolicy" => self.delete_data_protection_policy(&req),
244 "PutIndexPolicy" => self.put_index_policy(&req),
245 "DescribeIndexPolicies" => self.describe_index_policies(&req),
246 "DeleteIndexPolicy" => self.delete_index_policy(&req),
247 "DescribeFieldIndexes" => self.describe_field_indexes(&req),
248 "PutTransformer" => self.put_transformer(&req),
249 "GetTransformer" => self.get_transformer(&req),
250 "DeleteTransformer" => self.delete_transformer(&req),
251 "TestTransformer" => self.test_transformer(&req),
252 "CreateLogAnomalyDetector" => self.create_log_anomaly_detector(&req),
253 "GetLogAnomalyDetector" => self.get_log_anomaly_detector(&req),
254 "DeleteLogAnomalyDetector" => self.delete_log_anomaly_detector(&req),
255 "ListLogAnomalyDetectors" => self.list_log_anomaly_detectors(&req),
256 "UpdateLogAnomalyDetector" => self.update_log_anomaly_detector(&req),
257 "GetLogGroupFields" => self.get_log_group_fields(&req),
258 "TestMetricFilter" => self.test_metric_filter(&req),
259 "StopQuery" => self.stop_query(&req),
260 "PutLogGroupDeletionProtection" => self.put_log_group_deletion_protection(&req),
261 "GetLogRecord" => self.get_log_record(&req),
262 "ListAnomalies" => self.list_anomalies(&req),
263 "UpdateAnomaly" => self.update_anomaly(&req),
264 "CreateImportTask" => self.create_import_task(&req),
265 "DescribeImportTasks" => self.describe_import_tasks(&req),
266 "DescribeImportTaskBatches" => self.describe_import_task_batches(&req),
267 "CancelImportTask" => self.cancel_import_task(&req),
268 "PutIntegration" => self.put_integration(&req),
269 "GetIntegration" => self.get_integration(&req),
270 "DeleteIntegration" => self.delete_integration(&req),
271 "ListIntegrations" => self.list_integrations(&req),
272 "CreateLookupTable" => self.create_lookup_table(&req),
273 "GetLookupTable" => self.get_lookup_table(&req),
274 "DescribeLookupTables" => self.describe_lookup_tables(&req),
275 "DeleteLookupTable" => self.delete_lookup_table(&req),
276 "UpdateLookupTable" => self.update_lookup_table(&req),
277 "CreateScheduledQuery" => self.create_scheduled_query(&req),
278 "GetScheduledQuery" => self.get_scheduled_query(&req),
279 "GetScheduledQueryHistory" => self.get_scheduled_query_history(&req),
280 "ListScheduledQueries" => self.list_scheduled_queries(&req),
281 "DeleteScheduledQuery" => self.delete_scheduled_query(&req),
282 "UpdateScheduledQuery" => self.update_scheduled_query(&req),
283 "StartLiveTail" => self.start_live_tail(&req),
284 "ListLogGroups" => self.list_log_groups(&req),
285 "ListLogGroupsForQuery" => self.list_log_groups_for_query(&req),
286 "ListAggregateLogGroupSummaries" => self.list_aggregate_log_group_summaries(&req),
287 "PutBearerTokenAuthentication" => self.put_bearer_token_authentication(&req),
288 "GetLogObject" => self.get_log_object(&req),
289 "GetLogFields" => self.get_log_fields(&req),
290 "AssociateSourceToS3TableIntegration" => {
291 self.associate_source_to_s3_table_integration(&req)
292 }
293 "ListSourcesForS3TableIntegration" => self.list_sources_for_s3_table_integration(&req),
294 "DisassociateSourceFromS3TableIntegration" => {
295 self.disassociate_source_from_s3_table_integration(&req)
296 }
297 "UpdateDeliveryConfiguration" => self.update_delivery_configuration(&req),
298 "DescribeConfigurationTemplates" => self.describe_configuration_templates(&req),
299 "PutSyslogConfiguration" => self.put_syslog_configuration(&req),
300 "ListSyslogConfigurations" => self.list_syslog_configurations(&req),
301 "DeleteSyslogConfiguration" => self.delete_syslog_configuration(&req),
302 "GetExportedData" => self.get_exported_data(&req),
304 _ => Err(AwsServiceError::action_not_implemented("logs", &req.action)),
305 };
306 if mutates && matches!(result.as_ref(), Ok(resp) if resp.status.is_success()) {
307 self.save_snapshot().await;
308 }
309 result
310 }
311
312 fn supported_actions(&self) -> &[&str] {
313 SUPPORTED_ACTIONS
314 }
315}
316
317const SUPPORTED_ACTIONS: &[&str] = &[
318 "CreateLogGroup",
319 "DeleteLogGroup",
320 "DescribeLogGroups",
321 "CreateLogStream",
322 "DeleteLogStream",
323 "DescribeLogStreams",
324 "PutLogEvents",
325 "GetLogEvents",
326 "FilterLogEvents",
327 "TagLogGroup",
328 "UntagLogGroup",
329 "ListTagsLogGroup",
330 "TagResource",
331 "UntagResource",
332 "ListTagsForResource",
333 "PutRetentionPolicy",
334 "DeleteRetentionPolicy",
335 "PutSubscriptionFilter",
336 "DescribeSubscriptionFilters",
337 "DeleteSubscriptionFilter",
338 "PutMetricFilter",
339 "DescribeMetricFilters",
340 "DeleteMetricFilter",
341 "PutResourcePolicy",
342 "DescribeResourcePolicies",
343 "DeleteResourcePolicy",
344 "PutDestination",
345 "DescribeDestinations",
346 "DeleteDestination",
347 "PutDestinationPolicy",
348 "StartQuery",
349 "GetQueryResults",
350 "DescribeQueries",
351 "CreateExportTask",
352 "DescribeExportTasks",
353 "CancelExportTask",
354 "PutDeliveryDestination",
355 "GetDeliveryDestination",
356 "DescribeDeliveryDestinations",
357 "DeleteDeliveryDestination",
358 "PutDeliveryDestinationPolicy",
359 "GetDeliveryDestinationPolicy",
360 "DeleteDeliveryDestinationPolicy",
361 "PutDeliverySource",
362 "GetDeliverySource",
363 "DescribeDeliverySources",
364 "DeleteDeliverySource",
365 "CreateDelivery",
366 "GetDelivery",
367 "DescribeDeliveries",
368 "DeleteDelivery",
369 "AssociateKmsKey",
370 "DisassociateKmsKey",
371 "PutQueryDefinition",
372 "DescribeQueryDefinitions",
373 "DeleteQueryDefinition",
374 "PutAccountPolicy",
375 "DescribeAccountPolicies",
376 "DeleteAccountPolicy",
377 "PutDataProtectionPolicy",
378 "GetDataProtectionPolicy",
379 "DeleteDataProtectionPolicy",
380 "PutIndexPolicy",
381 "DescribeIndexPolicies",
382 "DeleteIndexPolicy",
383 "DescribeFieldIndexes",
384 "PutTransformer",
385 "GetTransformer",
386 "DeleteTransformer",
387 "TestTransformer",
388 "CreateLogAnomalyDetector",
389 "GetLogAnomalyDetector",
390 "DeleteLogAnomalyDetector",
391 "ListLogAnomalyDetectors",
392 "UpdateLogAnomalyDetector",
393 "GetLogGroupFields",
394 "TestMetricFilter",
395 "StopQuery",
396 "PutLogGroupDeletionProtection",
397 "GetLogRecord",
398 "ListAnomalies",
399 "UpdateAnomaly",
400 "CreateImportTask",
401 "DescribeImportTasks",
402 "DescribeImportTaskBatches",
403 "CancelImportTask",
404 "PutIntegration",
405 "GetIntegration",
406 "DeleteIntegration",
407 "ListIntegrations",
408 "CreateLookupTable",
409 "GetLookupTable",
410 "DescribeLookupTables",
411 "DeleteLookupTable",
412 "UpdateLookupTable",
413 "CreateScheduledQuery",
414 "GetScheduledQuery",
415 "GetScheduledQueryHistory",
416 "ListScheduledQueries",
417 "DeleteScheduledQuery",
418 "UpdateScheduledQuery",
419 "StartLiveTail",
420 "ListLogGroups",
421 "ListLogGroupsForQuery",
422 "ListAggregateLogGroupSummaries",
423 "PutBearerTokenAuthentication",
424 "GetLogObject",
425 "GetLogFields",
426 "AssociateSourceToS3TableIntegration",
427 "ListSourcesForS3TableIntegration",
428 "DisassociateSourceFromS3TableIntegration",
429 "UpdateDeliveryConfiguration",
430 "DescribeConfigurationTemplates",
431 "PutSyslogConfiguration",
432 "ListSyslogConfigurations",
433 "DeleteSyslogConfiguration",
434];
435
436fn require_str<'a>(body: &'a Value, field: &str) -> Result<&'a str, AwsServiceError> {
437 body[field].as_str().ok_or_else(|| {
438 AwsServiceError::aws_error(
439 StatusCode::BAD_REQUEST,
440 "InvalidParameterException",
441 format!("{field} is required"),
442 )
443 })
444}
445
446fn dd_config_json(config: &std::collections::BTreeMap<String, String>) -> Value {
449 let mut m: serde_json::Map<String, Value> =
450 config.iter().map(|(k, v)| (k.clone(), json!(v))).collect();
451 m.entry("destinationResourceArn".to_string())
452 .or_insert_with(|| json!(""));
453 Value::Object(m)
454}
455
456pub fn infer_delivery_destination_type(destination_arn: Option<&String>) -> String {
460 let arn = destination_arn.map(String::as_str).unwrap_or("");
461 let service = arn.split(':').nth(2).unwrap_or("");
462 match service {
463 "s3" => "S3",
464 "firehose" => "FH",
465 "xray" => "XRAY",
466 _ => "CWL",
467 }
468 .to_string()
469}
470
471fn generate_sequence_token() -> String {
472 use std::time::{SystemTime, UNIX_EPOCH};
473 let nanos = SystemTime::now()
474 .duration_since(UNIX_EPOCH)
475 .unwrap_or_default()
476 .as_nanos();
477 format!("{:038}", nanos % 10u128.pow(38))
479}
480
481fn validation_error(field: &str, value: &str, constraint: &str) -> AwsServiceError {
482 AwsServiceError::aws_error(
483 StatusCode::BAD_REQUEST,
484 "InvalidParameterException",
485 format!(
486 "1 validation error detected: Value '{value}' at '{field}' failed to satisfy constraint: {constraint}"
487 ),
488 )
489}
490
491fn resolve_log_group_name(
494 log_group_name: Option<&str>,
495 resource_identifier: Option<&str>,
496) -> Result<String, AwsServiceError> {
497 if let Some(identifier) = resource_identifier {
498 if identifier.starts_with("arn:") {
499 extract_log_group_from_arn(identifier).ok_or_else(|| {
500 AwsServiceError::aws_error(
501 StatusCode::BAD_REQUEST,
502 "InvalidParameterException",
503 format!("Invalid ARN: {identifier}"),
504 )
505 })
506 } else {
507 Ok(identifier.to_string())
508 }
509 } else if let Some(name) = log_group_name {
510 Ok(name.to_string())
511 } else {
512 Err(AwsServiceError::aws_error(
513 StatusCode::BAD_REQUEST,
514 "InvalidParameterException",
515 "Either logGroupName or resourceIdentifier is required",
516 ))
517 }
518}
519
520pub(crate) fn extract_log_group_from_arn(arn: &str) -> Option<String> {
522 let parts: Vec<&str> = arn.splitn(7, ':').collect();
524 if parts.len() >= 7 && parts[5] == "log-group" {
525 let name = parts[6].strip_suffix(":*").unwrap_or(parts[6]);
526 Some(name.to_string())
527 } else {
528 None
529 }
530}
531
532fn matches_filter_pattern(pattern: &str, message: &str) -> bool {
540 let pattern = pattern.trim();
541
542 if pattern.is_empty() {
544 return true;
545 }
546
547 if (pattern.starts_with('{') && pattern.ends_with('}')) || pattern.starts_with('[') {
551 return crate::filter_pattern::matches(pattern, message);
552 }
553
554 if pattern.starts_with('"') && pattern.ends_with('"') && pattern.len() >= 2 {
556 let inner = &pattern[1..pattern.len() - 1];
557 let unescaped = inner.replace("\\\"", "\"");
559 return message.contains(&unescaped);
560 }
561
562 let terms = parse_filter_terms(pattern);
564 terms.iter().all(|term| message.contains(term.as_str()))
565}
566
567fn parse_filter_terms(pattern: &str) -> Vec<String> {
569 let mut terms = Vec::new();
570 let mut chars = pattern.chars().peekable();
571
572 while chars.peek().is_some() {
573 while chars.peek().is_some_and(|c| c.is_whitespace()) {
575 chars.next();
576 }
577
578 if chars.peek().is_none() {
579 break;
580 }
581
582 if chars.peek() == Some(&'"') {
583 chars.next(); let mut term = String::new();
586 loop {
587 match chars.next() {
588 Some('\\') => {
589 if let Some(c) = chars.next() {
590 term.push(c);
591 }
592 }
593 Some('"') => break,
594 Some(c) => term.push(c),
595 None => break,
596 }
597 }
598 terms.push(term);
599 } else {
600 let mut term = String::new();
602 while chars.peek().is_some_and(|c| !c.is_whitespace()) {
603 term.push(chars.next().unwrap());
604 }
605 if !term.is_empty() {
606 terms.push(term);
607 }
608 }
609 }
610
611 terms
612}
613
614#[cfg(test)]
615pub(crate) mod test_helpers {
616 use super::*;
617 use bytes::Bytes;
618 use fakecloud_core::delivery::DeliveryBus;
619 use http::{HeaderMap, Method};
620 use std::collections::HashMap;
621 use std::sync::Arc;
622
623 pub fn make_service() -> LogsService {
624 let state = Arc::new(parking_lot::RwLock::new(
625 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
626 ));
627 let delivery_bus = Arc::new(DeliveryBus::new());
628 LogsService::new(state, delivery_bus)
629 }
630
631 pub fn make_request(
632 action: &str,
633 body: serde_json::Value,
634 ) -> fakecloud_core::service::AwsRequest {
635 fakecloud_core::service::AwsRequest {
636 service: "logs".to_string(),
637 action: action.to_string(),
638 region: "us-east-1".to_string(),
639 account_id: "123456789012".to_string(),
640 request_id: "test-request-id".to_string(),
641 headers: HeaderMap::new(),
642 query_params: HashMap::new(),
643 body: Bytes::from(serde_json::to_vec(&body).unwrap()),
644 body_stream: parking_lot::Mutex::new(None),
645 path_segments: vec![],
646 raw_path: "/".to_string(),
647 raw_query: String::new(),
648 method: Method::POST,
649 is_query_protocol: false,
650 access_key_id: None,
651 principal: None,
652 }
653 }
654
655 pub fn create_group(svc: &LogsService, name: &str) {
656 let req = make_request(
657 "CreateLogGroup",
658 serde_json::json!({ "logGroupName": name }),
659 );
660 svc.create_log_group(&req).unwrap();
661 }
662
663 pub fn create_stream(svc: &LogsService, group: &str, stream: &str) {
664 let req = make_request(
665 "CreateLogStream",
666 serde_json::json!({ "logGroupName": group, "logStreamName": stream }),
667 );
668 svc.create_log_stream(&req).unwrap();
669 }
670
671 pub fn put_events(svc: &LogsService, group: &str, stream: &str, messages: &[&str]) {
672 let now = chrono::Utc::now().timestamp_millis();
673 let events: Vec<serde_json::Value> = messages
674 .iter()
675 .enumerate()
676 .map(|(i, msg)| serde_json::json!({ "timestamp": now + i as i64, "message": msg }))
677 .collect();
678 let req = make_request(
679 "PutLogEvents",
680 serde_json::json!({
681 "logGroupName": group,
682 "logStreamName": stream,
683 "logEvents": events,
684 }),
685 );
686 svc.put_log_events(&req).unwrap();
687 }
688
689 pub fn put_events_at(
690 svc: &LogsService,
691 group: &str,
692 stream: &str,
693 messages: &[&str],
694 timestamp: i64,
695 ) {
696 let events: Vec<serde_json::Value> = messages
697 .iter()
698 .enumerate()
699 .map(
700 |(i, msg)| serde_json::json!({ "timestamp": timestamp + i as i64, "message": msg }),
701 )
702 .collect();
703 let req = make_request(
704 "PutLogEvents",
705 serde_json::json!({
706 "logGroupName": group,
707 "logStreamName": stream,
708 "logEvents": events,
709 }),
710 );
711 svc.put_log_events(&req).unwrap();
712 }
713
714 pub fn put_retention(svc: &LogsService, group: &str, days: i32) {
715 let req = make_request(
716 "PutRetentionPolicy",
717 serde_json::json!({ "logGroupName": group, "retentionInDays": days }),
718 );
719 svc.put_retention_policy(&req).unwrap();
720 }
721
722 #[test]
726 fn array_filter_pattern_matches_positionally() {
727 assert!(matches_filter_pattern("[w1, w2, w3]", "some log message"));
729 assert!(!matches_filter_pattern(
731 "[w1=ERROR, w2, w3]",
732 "INFO log message"
733 ));
734 assert!(!matches_filter_pattern("[w1, w2, w3]", "a b c d"));
736 }
737
738 #[test]
740 fn json_filter_pattern_supports_or() {
741 let msg = r#"{"level":"ERROR","code":500}"#;
742 assert!(matches_filter_pattern(
743 "{ $.level = \"WARN\" || $.code = 500 }",
744 msg
745 ));
746 assert!(!matches_filter_pattern(
747 "{ $.level = \"WARN\" || $.code = 200 }",
748 msg
749 ));
750 }
751
752 #[test]
754 fn snapshot_hook_is_none_without_store() {
755 let svc = make_service();
756 assert!(svc.snapshot_hook().is_none());
757 }
758
759 #[tokio::test]
763 async fn snapshot_hook_fires_with_store() {
764 let store: Arc<dyn fakecloud_persistence::SnapshotStore> =
765 Arc::new(fakecloud_persistence::MemorySnapshotStore::new());
766 let svc = make_service().with_snapshot_store(store);
767 let hook = svc
768 .snapshot_hook()
769 .expect("hook present when a store is set");
770 hook().await;
772 }
773}