Skip to main content

rusticity_core/
sqs.rs

1use crate::config::AwsConfig;
2use crate::metrics::{MetricQuery, MetricReader};
3use anyhow::Result;
4use aws_sdk_cloudwatch::types::Statistic;
5
6#[derive(Clone, Debug)]
7pub struct SqsQueue {
8    pub name: String,
9    pub url: String,
10    pub queue_type: String,
11    pub created_timestamp: String,
12    pub messages_available: String,
13    pub messages_in_flight: String,
14    pub encryption: String,
15    pub content_based_deduplication: String,
16    pub last_modified_timestamp: String,
17    pub visibility_timeout: String,
18    pub message_retention_period: String,
19    pub maximum_message_size: String,
20    pub delivery_delay: String,
21    pub receive_message_wait_time: String,
22    pub high_throughput_fifo: String,
23    pub deduplication_scope: String,
24    pub fifo_throughput_limit: String,
25    pub dead_letter_queue: String,
26    pub messages_delayed: String,
27    pub redrive_allow_policy: String,
28    pub redrive_policy: String,
29    pub redrive_task_id: String,
30    pub redrive_task_start_time: String,
31    pub redrive_task_status: String,
32    pub redrive_task_percent: String,
33    pub redrive_task_destination: String,
34}
35
36#[derive(Clone, Debug)]
37pub struct LambdaTrigger {
38    pub uuid: String,
39    pub arn: String,
40    pub status: String,
41    pub last_modified: String,
42}
43
44#[derive(Clone, Debug)]
45pub struct EventBridgePipe {
46    pub name: String,
47    pub status: String,
48    pub target: String,
49    pub last_modified: String,
50}
51
52#[derive(Clone, Debug)]
53pub struct QueueTag {
54    pub key: String,
55    pub value: String,
56}
57
58pub struct SqsClient {
59    config: AwsConfig,
60}
61
62impl SqsClient {
63    pub fn new(config: AwsConfig) -> Self {
64        Self { config }
65    }
66
67    pub async fn list_lambda_triggers(&self, queue_arn: &str) -> Result<Vec<LambdaTrigger>> {
68        let lambda_client = self.config.lambda_client();
69
70        let response = lambda_client
71            .list_event_source_mappings()
72            .event_source_arn(queue_arn)
73            .send()
74            .await?;
75
76        let mut triggers = Vec::new();
77        if let Some(mappings) = response.event_source_mappings {
78            for mapping in mappings {
79                triggers.push(LambdaTrigger {
80                    uuid: mapping.uuid.unwrap_or_default(),
81                    arn: mapping.function_arn.unwrap_or_default(),
82                    status: mapping
83                        .state
84                        .map(|s| s.as_str().to_string())
85                        .unwrap_or_else(|| "Unknown".to_string()),
86                    last_modified: mapping
87                        .last_modified
88                        .map(|dt| dt.secs().to_string())
89                        .unwrap_or_default(),
90                });
91            }
92        }
93
94        Ok(triggers)
95    }
96
97    pub async fn list_queues(&self, prefix: &str) -> Result<Vec<SqsQueue>> {
98        let client = self.config.sqs_client();
99
100        let mut request = client.list_queues();
101        if !prefix.is_empty() {
102            request = request.queue_name_prefix(prefix);
103        }
104
105        let response = request.send().await?;
106        let mut queues = Vec::new();
107
108        if let Some(urls) = response.queue_urls {
109            for url in urls {
110                let attrs_response = client
111                    .get_queue_attributes()
112                    .queue_url(&url)
113                    .attribute_names(aws_sdk_sqs::types::QueueAttributeName::All)
114                    .send()
115                    .await?;
116
117                let attrs = attrs_response.attributes.unwrap_or_default();
118
119                let name = url.split('/').next_back().unwrap_or(&url).to_string();
120                let queue_type = if name.ends_with(".fifo") {
121                    "FIFO".to_string()
122                } else {
123                    "Standard".to_string()
124                };
125
126                queues.push(SqsQueue {
127                    name,
128                    url: url.clone(),
129                    queue_type,
130                    created_timestamp: attrs
131                        .get(&aws_sdk_sqs::types::QueueAttributeName::CreatedTimestamp)
132                        .map(|v| v.to_string())
133                        .unwrap_or_default(),
134                    messages_available: attrs
135                        .get(&aws_sdk_sqs::types::QueueAttributeName::ApproximateNumberOfMessages)
136                        .map(|v| v.to_string())
137                        .unwrap_or_else(|| "0".to_string()),
138                    messages_in_flight: attrs
139                        .get(&aws_sdk_sqs::types::QueueAttributeName::ApproximateNumberOfMessagesNotVisible)
140                        .map(|v| v.to_string())
141                        .unwrap_or_else(|| "0".to_string()),
142                    encryption: if let Some(kms_key) = attrs.get(&aws_sdk_sqs::types::QueueAttributeName::KmsMasterKeyId) {
143                        if kms_key.is_empty() {
144                            "SSE-SQS".to_string()
145                        } else {
146                            "SSE-KMS".to_string()
147                        }
148                    } else if attrs.contains_key(&aws_sdk_sqs::types::QueueAttributeName::SqsManagedSseEnabled) {
149                        "SSE-SQS".to_string()
150                    } else {
151                        "-".to_string()
152                    },
153                    content_based_deduplication: attrs
154                        .get(&aws_sdk_sqs::types::QueueAttributeName::ContentBasedDeduplication)
155                        .map(|v| if v == "true" { "Enabled" } else { "Disabled" })
156                        .unwrap_or("Disabled")
157                        .to_string(),
158                    last_modified_timestamp: attrs
159                        .get(&aws_sdk_sqs::types::QueueAttributeName::LastModifiedTimestamp)
160                        .map(|v| v.to_string())
161                        .unwrap_or_default(),
162                    visibility_timeout: attrs
163                        .get(&aws_sdk_sqs::types::QueueAttributeName::VisibilityTimeout)
164                        .map(|v| v.to_string())
165                        .unwrap_or_default(),
166                    message_retention_period: attrs
167                        .get(&aws_sdk_sqs::types::QueueAttributeName::MessageRetentionPeriod)
168                        .map(|v| v.to_string())
169                        .unwrap_or_default(),
170                    maximum_message_size: attrs
171                        .get(&aws_sdk_sqs::types::QueueAttributeName::MaximumMessageSize)
172                        .map(|v| format!("{} bytes", v))
173                        .unwrap_or_default(),
174                    delivery_delay: attrs
175                        .get(&aws_sdk_sqs::types::QueueAttributeName::DelaySeconds)
176                        .map(|v| v.to_string())
177                        .unwrap_or_default(),
178                    receive_message_wait_time: attrs
179                        .get(&aws_sdk_sqs::types::QueueAttributeName::ReceiveMessageWaitTimeSeconds)
180                        .map(|v| v.to_string())
181                        .unwrap_or_default(),
182                    high_throughput_fifo: attrs
183                        .get(&aws_sdk_sqs::types::QueueAttributeName::FifoThroughputLimit)
184                        .map(|v| if v == "perMessageGroupId" { "Disabled" } else { "Enabled" })
185                        .unwrap_or("-")
186                        .to_string(),
187                    deduplication_scope: attrs
188                        .get(&aws_sdk_sqs::types::QueueAttributeName::DeduplicationScope)
189                        .map(|v| v.to_string())
190                        .unwrap_or_else(|| "-".to_string()),
191                    fifo_throughput_limit: attrs
192                        .get(&aws_sdk_sqs::types::QueueAttributeName::FifoThroughputLimit)
193                        .map(|v| v.to_string())
194                        .unwrap_or_else(|| "-".to_string()),
195                    dead_letter_queue: attrs
196                        .get(&aws_sdk_sqs::types::QueueAttributeName::RedrivePolicy)
197                        .and_then(|policy| {
198                            // Parse JSON to extract deadLetterTargetArn
199                            serde_json::from_str::<serde_json::Value>(policy)
200                                .ok()
201                                .and_then(|v| v.get("deadLetterTargetArn").and_then(|arn| arn.as_str()).map(|s| {
202                                    // Extract queue name from ARN
203                                    s.split(':').next_back().unwrap_or(s).to_string()
204                                }))
205                        })
206                        .unwrap_or_else(|| "-".to_string()),
207                    messages_delayed: attrs
208                        .get(&aws_sdk_sqs::types::QueueAttributeName::ApproximateNumberOfMessagesDelayed)
209                        .map(|v| v.to_string())
210                        .unwrap_or_else(|| "0".to_string()),
211                    redrive_allow_policy: attrs
212                        .get(&aws_sdk_sqs::types::QueueAttributeName::RedriveAllowPolicy)
213                        .map(|policy| {
214                            // Parse JSON to extract redrivePermission
215                            serde_json::from_str::<serde_json::Value>(policy)
216                                .ok()
217                                .and_then(|v| v.get("redrivePermission").and_then(|p| p.as_str()).map(String::from))
218                                .unwrap_or_else(|| policy.to_string())
219                        })
220                        .unwrap_or_else(|| "-".to_string()),
221                    redrive_policy: attrs
222                        .get(&aws_sdk_sqs::types::QueueAttributeName::RedrivePolicy)
223                        .map(|v| v.to_string())
224                        .unwrap_or_else(|| "".to_string()),
225                    // TODO: Fetch actual redrive task data using ListMessageMoveTasksCommand
226                    redrive_task_id: "-".to_string(),
227                    redrive_task_start_time: "-".to_string(),
228                    redrive_task_status: "-".to_string(),
229                    redrive_task_percent: "-".to_string(),
230                    redrive_task_destination: "-".to_string(),
231                });
232            }
233        }
234
235        queues.sort_by(|a, b| a.name.cmp(&b.name));
236        Ok(queues)
237    }
238
239    pub async fn list_message_move_tasks(
240        &self,
241        queue_url: &str,
242    ) -> Result<(String, String, String, String, String)> {
243        let client = self.config.sqs_client();
244
245        match client
246            .list_message_move_tasks()
247            .source_arn(queue_url)
248            .send()
249            .await
250        {
251            Ok(response) => {
252                let results = response.results();
253                if let Some(task) = results.first() {
254                    let task_id = task.task_handle().unwrap_or("-").to_string();
255
256                    let start_time = chrono::DateTime::from_timestamp(task.started_timestamp(), 0)
257                        .map(|dt| format!("{} (UTC)", dt.format("%Y-%m-%d %H:%M:%S")))
258                        .unwrap_or_else(|| "-".to_string());
259
260                    let status = format!("{:?}", task.status());
261
262                    let moved = task.approximate_number_of_messages_moved();
263                    let total = task.approximate_number_of_messages_to_move().unwrap_or(0);
264                    let percent = if total > 0 {
265                        format!("{}%", (moved * 100) / total)
266                    } else {
267                        "-".to_string()
268                    };
269
270                    let destination = task.destination_arn().unwrap_or("-").to_string();
271
272                    return Ok((task_id, start_time, status, percent, destination));
273                }
274            }
275            Err(_) => {
276                // No active redrive tasks or error fetching
277            }
278        }
279
280        Ok((
281            "-".to_string(),
282            "-".to_string(),
283            "-".to_string(),
284            "-".to_string(),
285            "-".to_string(),
286        ))
287    }
288
289    pub async fn get_queue_arn(&self, queue_url: &str) -> Result<String> {
290        let client = self.config.sqs_client();
291
292        let response = client
293            .get_queue_attributes()
294            .queue_url(queue_url)
295            .attribute_names(aws_sdk_sqs::types::QueueAttributeName::QueueArn)
296            .send()
297            .await?;
298
299        let arn = response
300            .attributes()
301            .and_then(|attrs| attrs.get(&aws_sdk_sqs::types::QueueAttributeName::QueueArn))
302            .map(|v| v.to_string())
303            .unwrap_or_default();
304
305        Ok(arn)
306    }
307
308    pub async fn list_pipes(&self, queue_arn: &str) -> Result<Vec<EventBridgePipe>> {
309        let pipes_client = self.config.pipes_client();
310
311        let response = pipes_client
312            .list_pipes()
313            .source_prefix(queue_arn)
314            .send()
315            .await?;
316
317        let mut pipes = Vec::new();
318        if let Some(pipe_list) = response.pipes {
319            for pipe in pipe_list {
320                pipes.push(EventBridgePipe {
321                    name: pipe.name.unwrap_or_default(),
322                    status: pipe
323                        .current_state
324                        .map(|s| s.as_str().to_string())
325                        .unwrap_or_default(),
326                    target: pipe.target.unwrap_or_default(),
327                    last_modified: pipe
328                        .last_modified_time
329                        .map(|dt| dt.secs().to_string())
330                        .unwrap_or_default(),
331                });
332            }
333        }
334
335        Ok(pipes)
336    }
337
338    pub async fn list_tags(&self, queue_arn: &str) -> Result<Vec<QueueTag>> {
339        let client = self.config.sqs_client();
340
341        let response = client.list_queue_tags().queue_url(queue_arn).send().await?;
342
343        let mut tags = Vec::new();
344        if let Some(tag_map) = response.tags {
345            for (key, value) in tag_map {
346                tags.push(QueueTag { key, value });
347            }
348        }
349
350        Ok(tags)
351    }
352
353    pub async fn get_queue_metrics(&self, queue_name: &str) -> Result<Vec<(i64, f64)>> {
354        let query = MetricQuery::new(
355            "AWS/SQS",
356            "ApproximateAgeOfOldestMessage",
357            Statistic::Maximum,
358        )
359        .dimension("QueueName", queue_name);
360        MetricReader::new(self.config.cloudwatch_client())
361            .recent(query, 60)
362            .await
363    }
364
365    pub async fn get_queue_delayed_metrics(&self, queue_name: &str) -> Result<Vec<(i64, f64)>> {
366        let query = MetricQuery::new(
367            "AWS/SQS",
368            "ApproximateNumberOfMessagesDelayed",
369            Statistic::Average,
370        )
371        .dimension("QueueName", queue_name);
372        MetricReader::new(self.config.cloudwatch_client())
373            .recent(query, 60)
374            .await
375    }
376
377    pub async fn get_queue_not_visible_metrics(&self, queue_name: &str) -> Result<Vec<(i64, f64)>> {
378        let query = MetricQuery::new(
379            "AWS/SQS",
380            "ApproximateNumberOfMessagesNotVisible",
381            Statistic::Average,
382        )
383        .dimension("QueueName", queue_name);
384        MetricReader::new(self.config.cloudwatch_client())
385            .recent(query, 60)
386            .await
387    }
388
389    pub async fn get_queue_visible_metrics(&self, queue_name: &str) -> Result<Vec<(i64, f64)>> {
390        let query = MetricQuery::new(
391            "AWS/SQS",
392            "ApproximateNumberOfMessagesVisible",
393            Statistic::Average,
394        )
395        .dimension("QueueName", queue_name);
396        MetricReader::new(self.config.cloudwatch_client())
397            .recent(query, 60)
398            .await
399    }
400
401    pub async fn get_queue_empty_receives_metrics(
402        &self,
403        queue_name: &str,
404    ) -> Result<Vec<(i64, f64)>> {
405        let query = MetricQuery::new("AWS/SQS", "NumberOfEmptyReceives", Statistic::Sum)
406            .dimension("QueueName", queue_name);
407        MetricReader::new(self.config.cloudwatch_client())
408            .recent(query, 60)
409            .await
410    }
411
412    pub async fn get_queue_messages_deleted_metrics(
413        &self,
414        queue_name: &str,
415    ) -> Result<Vec<(i64, f64)>> {
416        let query = MetricQuery::new("AWS/SQS", "NumberOfMessagesDeleted", Statistic::Sum)
417            .dimension("QueueName", queue_name);
418        MetricReader::new(self.config.cloudwatch_client())
419            .recent(query, 60)
420            .await
421    }
422
423    pub async fn get_queue_messages_received_metrics(
424        &self,
425        queue_name: &str,
426    ) -> Result<Vec<(i64, f64)>> {
427        let query = MetricQuery::new("AWS/SQS", "NumberOfMessagesReceived", Statistic::Sum)
428            .dimension("QueueName", queue_name);
429        MetricReader::new(self.config.cloudwatch_client())
430            .recent(query, 60)
431            .await
432    }
433
434    pub async fn get_queue_messages_sent_metrics(
435        &self,
436        queue_name: &str,
437    ) -> Result<Vec<(i64, f64)>> {
438        let query = MetricQuery::new("AWS/SQS", "NumberOfMessagesSent", Statistic::Sum)
439            .dimension("QueueName", queue_name);
440        MetricReader::new(self.config.cloudwatch_client())
441            .recent(query, 60)
442            .await
443    }
444
445    pub async fn get_queue_sent_message_size_metrics(
446        &self,
447        queue_name: &str,
448    ) -> Result<Vec<(i64, f64)>> {
449        let query = MetricQuery::new("AWS/SQS", "SentMessageSize", Statistic::Average)
450            .dimension("QueueName", queue_name);
451        MetricReader::new(self.config.cloudwatch_client())
452            .recent(query, 60)
453            .await
454    }
455}