Skip to main content

rusticity_core/
lambda.rs

1use crate::config::AwsConfig;
2use anyhow::Result;
3
4pub use aws_sdk_cloudwatch::types::Statistic;
5
6#[derive(Clone, Debug)]
7pub struct LambdaFunction {
8    pub name: String,
9    pub arn: String,
10    pub application: Option<String>,
11    pub description: String,
12    pub package_type: String,
13    pub runtime: String,
14    pub architecture: String,
15    pub code_size: i64,
16    pub code_sha256: String,
17    pub memory_mb: i32,
18    pub timeout_seconds: i32,
19    pub last_modified: String,
20    pub layers: Vec<LambdaLayer>,
21}
22
23#[derive(Clone, Debug)]
24pub struct LambdaLayer {
25    pub arn: String,
26    pub code_size: i64,
27}
28
29#[derive(Clone, Debug)]
30pub struct LambdaVersion {
31    pub version: String,
32    pub aliases: String,
33    pub description: String,
34    pub last_modified: String,
35    pub architecture: String,
36}
37
38#[derive(Clone, Debug)]
39pub struct LambdaAlias {
40    pub name: String,
41    pub versions: String,
42    pub description: String,
43}
44
45pub struct LambdaClient {
46    config: AwsConfig,
47}
48
49impl LambdaClient {
50    pub fn new(config: AwsConfig) -> Self {
51        Self { config }
52    }
53
54    pub async fn list_functions(&self) -> Result<Vec<LambdaFunction>> {
55        let client = self.config.lambda_client().await;
56
57        let mut functions = Vec::new();
58        let mut next_marker: Option<String> = None;
59
60        loop {
61            let mut request = client.list_functions().max_items(100);
62            if let Some(marker) = next_marker {
63                request = request.marker(marker);
64            }
65
66            let response = request.send().await?;
67
68            if let Some(funcs) = response.functions {
69                for func in funcs {
70                    // AWS returns last_modified in format: "2024-10-31T12:30:45.000+0000"
71                    let last_modified = func
72                        .last_modified
73                        .as_deref()
74                        .map(|s| {
75                            // Try parsing with timezone
76                            if let Ok(dt) =
77                                chrono::DateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.3f%z")
78                            {
79                                dt.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
80                            } else if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(s) {
81                                dt.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
82                            } else {
83                                s.to_string()
84                            }
85                        })
86                        .unwrap_or_default();
87
88                    let function_name = func.function_name.unwrap_or_default();
89
90                    // Extract application name from function name pattern
91                    // e.g., "storefront-studio-beta-api" -> "storefront-studio-beta"
92                    let application = function_name
93                        .rsplit_once('-')
94                        .map(|(prefix, _)| prefix.to_string());
95
96                    let layers = func
97                        .layers
98                        .unwrap_or_default()
99                        .into_iter()
100                        .map(|layer| LambdaLayer {
101                            arn: layer.arn.unwrap_or_default(),
102                            code_size: layer.code_size,
103                        })
104                        .collect();
105
106                    functions.push(LambdaFunction {
107                        name: function_name,
108                        arn: func.function_arn.unwrap_or_default(),
109                        application,
110                        description: func.description.unwrap_or_default(),
111                        package_type: func
112                            .package_type
113                            .map(|p| format!("{:?}", p))
114                            .unwrap_or_default(),
115                        runtime: func.runtime.map(|r| format!("{:?}", r)).unwrap_or_default(),
116                        architecture: func
117                            .architectures
118                            .and_then(|a| a.first().map(|arch| format!("{:?}", arch)))
119                            .unwrap_or_default(),
120                        code_size: func.code_size,
121                        code_sha256: func.code_sha256.unwrap_or_default(),
122                        memory_mb: func.memory_size.unwrap_or(0),
123                        timeout_seconds: func.timeout.unwrap_or(0),
124                        last_modified,
125                        layers,
126                    });
127                }
128            }
129
130            next_marker = response.next_marker;
131            if next_marker.is_none() {
132                break;
133            }
134        }
135
136        Ok(functions)
137    }
138
139    pub async fn list_applications(&self) -> Result<Vec<LambdaApplication>> {
140        let client = self.config.cloudformation_client().await;
141
142        let mut applications = Vec::new();
143        let mut next_token: Option<String> = None;
144
145        loop {
146            let mut request = client.list_stacks();
147            if let Some(token) = next_token {
148                request = request.next_token(token);
149            }
150
151            let response = request.send().await?;
152
153            if let Some(stacks) = response.stack_summaries {
154                for stack in stacks {
155                    // Only include active stacks
156                    let status = stack
157                        .stack_status
158                        .map(|s| format!("{:?}", s))
159                        .unwrap_or_default();
160                    if status.contains("DELETE") {
161                        continue;
162                    }
163
164                    applications.push(LambdaApplication {
165                        name: stack.stack_name.unwrap_or_default(),
166                        arn: stack.stack_id.unwrap_or_default(),
167                        description: stack.template_description.unwrap_or_default(),
168                        status,
169                        last_modified: stack
170                            .last_updated_time
171                            .or(stack.creation_time)
172                            .map(|dt| {
173                                let timestamp = dt.secs();
174                                let datetime = chrono::DateTime::from_timestamp(timestamp, 0)
175                                    .unwrap_or_default();
176                                datetime.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
177                            })
178                            .unwrap_or_default(),
179                    });
180                }
181            }
182
183            next_token = response.next_token;
184            if next_token.is_none() {
185                break;
186            }
187        }
188
189        Ok(applications)
190    }
191
192    pub async fn list_versions(&self, function_name: &str) -> Result<Vec<LambdaVersion>> {
193        let client = self.config.lambda_client().await;
194
195        let mut versions = Vec::new();
196        let mut next_marker: Option<String> = None;
197
198        loop {
199            let mut request = client
200                .list_versions_by_function()
201                .function_name(function_name)
202                .max_items(100);
203            if let Some(marker) = next_marker {
204                request = request.marker(marker);
205            }
206
207            let response = request.send().await?;
208
209            if let Some(vers) = response.versions {
210                for ver in vers {
211                    let last_modified = ver
212                        .last_modified
213                        .as_deref()
214                        .map(|s| {
215                            if let Ok(dt) =
216                                chrono::DateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.3f%z")
217                            {
218                                dt.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
219                            } else if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(s) {
220                                dt.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
221                            } else {
222                                s.to_string()
223                            }
224                        })
225                        .unwrap_or_default();
226
227                    versions.push(LambdaVersion {
228                        version: ver.version.unwrap_or_default(),
229                        aliases: String::new(), // Will be populated below
230                        description: ver.description.unwrap_or_default(),
231                        last_modified,
232                        architecture: ver
233                            .architectures
234                            .and_then(|a| a.first().map(|arch| format!("{:?}", arch)))
235                            .unwrap_or_default(),
236                    });
237                }
238            }
239
240            next_marker = response.next_marker;
241            if next_marker.is_none() {
242                break;
243            }
244        }
245
246        // Fetch aliases for all versions
247        let aliases_response = client
248            .list_aliases()
249            .function_name(function_name)
250            .send()
251            .await?;
252
253        if let Some(aliases) = aliases_response.aliases {
254            for alias in aliases {
255                let alias_name = alias.name.unwrap_or_default();
256
257                // Add alias to primary version
258                if let Some(version) = alias.function_version {
259                    if let Some(ver) = versions.iter_mut().find(|v| v.version == version) {
260                        if !ver.aliases.is_empty() {
261                            ver.aliases.push_str(", ");
262                        }
263                        ver.aliases.push_str(&alias_name);
264                    }
265                }
266
267                // Add alias to additional versions in routing config
268                if let Some(routing_config) = alias.routing_config {
269                    if let Some(additional_version_weights) =
270                        routing_config.additional_version_weights
271                    {
272                        for (version, _weight) in additional_version_weights {
273                            if let Some(ver) = versions.iter_mut().find(|v| v.version == version) {
274                                if !ver.aliases.is_empty() {
275                                    ver.aliases.push_str(", ");
276                                }
277                                ver.aliases.push_str(&alias_name);
278                            }
279                        }
280                    }
281                }
282            }
283        }
284
285        Ok(versions)
286    }
287}
288
289#[derive(Clone, Debug)]
290pub struct LambdaApplication {
291    pub name: String,
292    pub arn: String,
293    pub description: String,
294    pub status: String,
295    pub last_modified: String,
296}
297
298impl LambdaClient {
299    pub async fn list_aliases(&self, function_name: &str) -> Result<Vec<LambdaAlias>> {
300        let client = self.config.lambda_client().await;
301        let response = client
302            .list_aliases()
303            .function_name(function_name)
304            .send()
305            .await?;
306
307        let mut aliases = Vec::new();
308        if let Some(alias_list) = response.aliases {
309            for alias in alias_list {
310                let primary_version = alias.function_version.unwrap_or_default();
311
312                // Check for additional versions in routing config
313                let mut versions_str = primary_version.clone();
314                if let Some(routing_config) = alias.routing_config {
315                    if let Some(additional_version_weights) =
316                        routing_config.additional_version_weights
317                    {
318                        for (version, weight) in additional_version_weights {
319                            versions_str.push_str(&format!(
320                                ", {} ({}%)",
321                                version,
322                                (weight * 100.0) as i32
323                            ));
324                        }
325                    }
326                }
327
328                aliases.push(LambdaAlias {
329                    name: alias.name.unwrap_or_default(),
330                    versions: versions_str,
331                    description: alias.description.unwrap_or_default(),
332                });
333            }
334        }
335
336        Ok(aliases)
337    }
338
339    pub async fn get_invocations_metric(
340        &self,
341        function_name: &str,
342        resource: Option<&str>,
343    ) -> Result<Vec<(i64, f64)>> {
344        let cw_client = self.config.cloudwatch_client().await;
345
346        let end_time = aws_smithy_types::DateTime::from_secs(
347            std::time::SystemTime::now()
348                .duration_since(std::time::UNIX_EPOCH)?
349                .as_secs() as i64,
350        );
351        let start_time = aws_smithy_types::DateTime::from_secs(
352            std::time::SystemTime::now()
353                .duration_since(std::time::UNIX_EPOCH)?
354                .as_secs() as i64
355                - 3 * 3600,
356        );
357
358        let mut dimensions = vec![aws_sdk_cloudwatch::types::Dimension::builder()
359            .name("FunctionName")
360            .value(function_name)
361            .build()];
362
363        if let Some(res) = resource {
364            dimensions.push(
365                aws_sdk_cloudwatch::types::Dimension::builder()
366                    .name("Resource")
367                    .value(res)
368                    .build(),
369            );
370        }
371
372        let mut request = cw_client
373            .get_metric_statistics()
374            .namespace("AWS/Lambda")
375            .metric_name("Invocations")
376            .start_time(start_time)
377            .end_time(end_time)
378            .period(60)
379            .statistics(aws_sdk_cloudwatch::types::Statistic::Sum);
380
381        for dim in dimensions {
382            request = request.dimensions(dim);
383        }
384
385        let response = request.send().await?;
386
387        let mut data = Vec::new();
388        if let Some(datapoints) = response.datapoints {
389            for dp in datapoints {
390                if let (Some(timestamp), Some(value)) = (dp.timestamp, dp.sum) {
391                    data.push((timestamp.secs(), value));
392                }
393            }
394        }
395
396        data.sort_by_key(|&(timestamp, _)| timestamp);
397        Ok(data)
398    }
399
400    pub async fn get_duration_metric(
401        &self,
402        function_name: &str,
403        stat: aws_sdk_cloudwatch::types::Statistic,
404    ) -> Result<Vec<(i64, f64)>> {
405        let cw_client = self.config.cloudwatch_client().await;
406
407        let end_time = aws_smithy_types::DateTime::from_secs(
408            std::time::SystemTime::now()
409                .duration_since(std::time::UNIX_EPOCH)?
410                .as_secs() as i64,
411        );
412        let start_time = aws_smithy_types::DateTime::from_secs(
413            std::time::SystemTime::now()
414                .duration_since(std::time::UNIX_EPOCH)?
415                .as_secs() as i64
416                - 3 * 3600,
417        );
418
419        let dimension = aws_sdk_cloudwatch::types::Dimension::builder()
420            .name("FunctionName")
421            .value(function_name)
422            .build();
423
424        let response = cw_client
425            .get_metric_statistics()
426            .namespace("AWS/Lambda")
427            .metric_name("Duration")
428            .dimensions(dimension)
429            .start_time(start_time)
430            .end_time(end_time)
431            .period(60)
432            .statistics(stat.clone())
433            .send()
434            .await?;
435
436        let mut data = Vec::new();
437        if let Some(datapoints) = response.datapoints {
438            for dp in datapoints {
439                let value = match stat {
440                    aws_sdk_cloudwatch::types::Statistic::Minimum => dp.minimum,
441                    aws_sdk_cloudwatch::types::Statistic::Average => dp.average,
442                    aws_sdk_cloudwatch::types::Statistic::Maximum => dp.maximum,
443                    _ => None,
444                };
445                if let (Some(timestamp), Some(value)) = (dp.timestamp, value) {
446                    data.push((timestamp.secs(), value));
447                }
448            }
449        }
450
451        data.sort_by_key(|&(timestamp, _)| timestamp);
452        Ok(data)
453    }
454
455    pub async fn get_errors_metric(&self, function_name: &str) -> Result<Vec<(i64, f64)>> {
456        let cw_client = self.config.cloudwatch_client().await;
457
458        let end_time = aws_smithy_types::DateTime::from_secs(
459            std::time::SystemTime::now()
460                .duration_since(std::time::UNIX_EPOCH)?
461                .as_secs() as i64,
462        );
463        let start_time = aws_smithy_types::DateTime::from_secs(
464            std::time::SystemTime::now()
465                .duration_since(std::time::UNIX_EPOCH)?
466                .as_secs() as i64
467                - 3 * 3600,
468        );
469
470        let dimension = aws_sdk_cloudwatch::types::Dimension::builder()
471            .name("FunctionName")
472            .value(function_name)
473            .build();
474
475        let response = cw_client
476            .get_metric_statistics()
477            .namespace("AWS/Lambda")
478            .metric_name("Errors")
479            .dimensions(dimension)
480            .start_time(start_time)
481            .end_time(end_time)
482            .period(60)
483            .statistics(aws_sdk_cloudwatch::types::Statistic::Sum)
484            .send()
485            .await?;
486
487        let mut data = Vec::new();
488        if let Some(datapoints) = response.datapoints {
489            for dp in datapoints {
490                if let (Some(timestamp), Some(value)) = (dp.timestamp, dp.sum) {
491                    data.push((timestamp.secs(), value));
492                }
493            }
494        }
495
496        data.sort_by_key(|&(timestamp, _)| timestamp);
497        Ok(data)
498    }
499
500    pub async fn get_throttles_metric(&self, function_name: &str) -> Result<Vec<(i64, f64)>> {
501        let cw_client = self.config.cloudwatch_client().await;
502
503        let end_time = aws_smithy_types::DateTime::from_secs(
504            std::time::SystemTime::now()
505                .duration_since(std::time::UNIX_EPOCH)?
506                .as_secs() as i64,
507        );
508        let start_time = aws_smithy_types::DateTime::from_secs(
509            std::time::SystemTime::now()
510                .duration_since(std::time::UNIX_EPOCH)?
511                .as_secs() as i64
512                - 3 * 3600,
513        );
514
515        let dimension = aws_sdk_cloudwatch::types::Dimension::builder()
516            .name("FunctionName")
517            .value(function_name)
518            .build();
519
520        let response = cw_client
521            .get_metric_statistics()
522            .namespace("AWS/Lambda")
523            .metric_name("Throttles")
524            .dimensions(dimension)
525            .start_time(start_time)
526            .end_time(end_time)
527            .period(60)
528            .statistics(aws_sdk_cloudwatch::types::Statistic::Sum)
529            .send()
530            .await?;
531
532        let mut data = Vec::new();
533        if let Some(datapoints) = response.datapoints {
534            for dp in datapoints {
535                if let (Some(timestamp), Some(value)) = (dp.timestamp, dp.sum) {
536                    data.push((timestamp.secs(), value));
537                }
538            }
539        }
540
541        data.sort_by_key(|&(timestamp, _)| timestamp);
542        Ok(data)
543    }
544
545    pub async fn get_concurrent_executions_metric(
546        &self,
547        function_name: &str,
548    ) -> Result<Vec<(i64, f64)>> {
549        let cw_client = self.config.cloudwatch_client().await;
550
551        let end_time = aws_smithy_types::DateTime::from_secs(
552            std::time::SystemTime::now()
553                .duration_since(std::time::UNIX_EPOCH)?
554                .as_secs() as i64,
555        );
556        let start_time = aws_smithy_types::DateTime::from_secs(
557            std::time::SystemTime::now()
558                .duration_since(std::time::UNIX_EPOCH)?
559                .as_secs() as i64
560                - 3 * 3600,
561        );
562
563        let dimension = aws_sdk_cloudwatch::types::Dimension::builder()
564            .name("FunctionName")
565            .value(function_name)
566            .build();
567
568        let response = cw_client
569            .get_metric_statistics()
570            .namespace("AWS/Lambda")
571            .metric_name("ConcurrentExecutions")
572            .dimensions(dimension)
573            .start_time(start_time)
574            .end_time(end_time)
575            .period(60)
576            .statistics(aws_sdk_cloudwatch::types::Statistic::Maximum)
577            .send()
578            .await?;
579
580        let mut data = Vec::new();
581        if let Some(datapoints) = response.datapoints {
582            for dp in datapoints {
583                if let (Some(timestamp), Some(value)) = (dp.timestamp, dp.maximum) {
584                    data.push((timestamp.secs(), value));
585                }
586            }
587        }
588
589        data.sort_by_key(|&(timestamp, _)| timestamp);
590        Ok(data)
591    }
592
593    pub async fn get_recursive_invocations_dropped_metric(
594        &self,
595        function_name: &str,
596    ) -> Result<Vec<(i64, f64)>> {
597        let cw_client = self.config.cloudwatch_client().await;
598
599        let end_time = aws_smithy_types::DateTime::from_secs(
600            std::time::SystemTime::now()
601                .duration_since(std::time::UNIX_EPOCH)?
602                .as_secs() as i64,
603        );
604        let start_time = aws_smithy_types::DateTime::from_secs(
605            std::time::SystemTime::now()
606                .duration_since(std::time::UNIX_EPOCH)?
607                .as_secs() as i64
608                - 3 * 3600,
609        );
610
611        let dimension = aws_sdk_cloudwatch::types::Dimension::builder()
612            .name("FunctionName")
613            .value(function_name)
614            .build();
615
616        let response = cw_client
617            .get_metric_statistics()
618            .namespace("AWS/Lambda")
619            .metric_name("RecursiveInvocationsDropped")
620            .dimensions(dimension)
621            .start_time(start_time)
622            .end_time(end_time)
623            .period(60)
624            .statistics(aws_sdk_cloudwatch::types::Statistic::Sum)
625            .send()
626            .await?;
627
628        let mut data = Vec::new();
629        if let Some(datapoints) = response.datapoints {
630            for dp in datapoints {
631                if let (Some(timestamp), Some(value)) = (dp.timestamp, dp.sum) {
632                    data.push((timestamp.secs(), value));
633                }
634            }
635        }
636
637        data.sort_by_key(|&(timestamp, _)| timestamp);
638        Ok(data)
639    }
640
641    pub async fn get_async_event_age_metric(
642        &self,
643        function_name: &str,
644        stat: aws_sdk_cloudwatch::types::Statistic,
645    ) -> Result<Vec<(i64, f64)>> {
646        let cw_client = self.config.cloudwatch_client().await;
647
648        let end_time = aws_smithy_types::DateTime::from_secs(
649            std::time::SystemTime::now()
650                .duration_since(std::time::UNIX_EPOCH)?
651                .as_secs() as i64,
652        );
653        let start_time = aws_smithy_types::DateTime::from_secs(
654            std::time::SystemTime::now()
655                .duration_since(std::time::UNIX_EPOCH)?
656                .as_secs() as i64
657                - 3 * 3600,
658        );
659
660        let dimension = aws_sdk_cloudwatch::types::Dimension::builder()
661            .name("FunctionName")
662            .value(function_name)
663            .build();
664
665        let response = cw_client
666            .get_metric_statistics()
667            .namespace("AWS/Lambda")
668            .metric_name("AsyncEventAge")
669            .dimensions(dimension)
670            .start_time(start_time)
671            .end_time(end_time)
672            .period(60)
673            .statistics(stat.clone())
674            .send()
675            .await?;
676
677        let mut data = Vec::new();
678        if let Some(datapoints) = response.datapoints {
679            for dp in datapoints {
680                let value = match stat {
681                    aws_sdk_cloudwatch::types::Statistic::Minimum => dp.minimum,
682                    aws_sdk_cloudwatch::types::Statistic::Average => dp.average,
683                    aws_sdk_cloudwatch::types::Statistic::Maximum => dp.maximum,
684                    _ => None,
685                };
686                if let (Some(timestamp), Some(value)) = (dp.timestamp, value) {
687                    data.push((timestamp.secs(), value));
688                }
689            }
690        }
691
692        data.sort_by_key(|&(timestamp, _)| timestamp);
693        Ok(data)
694    }
695
696    pub async fn get_async_events_received_metric(
697        &self,
698        function_name: &str,
699    ) -> Result<Vec<(i64, f64)>> {
700        let cw_client = self.config.cloudwatch_client().await;
701
702        let end_time = aws_smithy_types::DateTime::from_secs(
703            std::time::SystemTime::now()
704                .duration_since(std::time::UNIX_EPOCH)?
705                .as_secs() as i64,
706        );
707        let start_time = aws_smithy_types::DateTime::from_secs(
708            std::time::SystemTime::now()
709                .duration_since(std::time::UNIX_EPOCH)?
710                .as_secs() as i64
711                - 3 * 3600,
712        );
713
714        let dimension = aws_sdk_cloudwatch::types::Dimension::builder()
715            .name("FunctionName")
716            .value(function_name)
717            .build();
718
719        let response = cw_client
720            .get_metric_statistics()
721            .namespace("AWS/Lambda")
722            .metric_name("AsyncEventsReceived")
723            .dimensions(dimension)
724            .start_time(start_time)
725            .end_time(end_time)
726            .period(60)
727            .statistics(aws_sdk_cloudwatch::types::Statistic::Sum)
728            .send()
729            .await?;
730
731        let mut data = Vec::new();
732        if let Some(datapoints) = response.datapoints {
733            for dp in datapoints {
734                if let (Some(timestamp), Some(value)) = (dp.timestamp, dp.sum) {
735                    data.push((timestamp.secs(), value));
736                }
737            }
738        }
739
740        data.sort_by_key(|&(timestamp, _)| timestamp);
741        Ok(data)
742    }
743
744    pub async fn get_async_events_dropped_metric(
745        &self,
746        function_name: &str,
747    ) -> Result<Vec<(i64, f64)>> {
748        let cw_client = self.config.cloudwatch_client().await;
749
750        let end_time = aws_smithy_types::DateTime::from_secs(
751            std::time::SystemTime::now()
752                .duration_since(std::time::UNIX_EPOCH)?
753                .as_secs() as i64,
754        );
755        let start_time = aws_smithy_types::DateTime::from_secs(
756            std::time::SystemTime::now()
757                .duration_since(std::time::UNIX_EPOCH)?
758                .as_secs() as i64
759                - 3 * 3600,
760        );
761
762        let dimension = aws_sdk_cloudwatch::types::Dimension::builder()
763            .name("FunctionName")
764            .value(function_name)
765            .build();
766
767        let response = cw_client
768            .get_metric_statistics()
769            .namespace("AWS/Lambda")
770            .metric_name("AsyncEventsDropped")
771            .dimensions(dimension)
772            .start_time(start_time)
773            .end_time(end_time)
774            .period(60)
775            .statistics(aws_sdk_cloudwatch::types::Statistic::Sum)
776            .send()
777            .await?;
778
779        let mut data = Vec::new();
780        if let Some(datapoints) = response.datapoints {
781            for dp in datapoints {
782                if let (Some(timestamp), Some(value)) = (dp.timestamp, dp.sum) {
783                    data.push((timestamp.secs(), value));
784                }
785            }
786        }
787
788        data.sort_by_key(|&(timestamp, _)| timestamp);
789        Ok(data)
790    }
791
792    pub async fn get_destination_delivery_failures_metric(
793        &self,
794        function_name: &str,
795    ) -> Result<Vec<(i64, f64)>> {
796        let cw_client = self.config.cloudwatch_client().await;
797
798        let end_time = aws_smithy_types::DateTime::from_secs(
799            std::time::SystemTime::now()
800                .duration_since(std::time::UNIX_EPOCH)?
801                .as_secs() as i64,
802        );
803        let start_time = aws_smithy_types::DateTime::from_secs(
804            std::time::SystemTime::now()
805                .duration_since(std::time::UNIX_EPOCH)?
806                .as_secs() as i64
807                - 3 * 3600,
808        );
809
810        let dimension = aws_sdk_cloudwatch::types::Dimension::builder()
811            .name("FunctionName")
812            .value(function_name)
813            .build();
814
815        let response = cw_client
816            .get_metric_statistics()
817            .namespace("AWS/Lambda")
818            .metric_name("DestinationDeliveryFailures")
819            .dimensions(dimension)
820            .start_time(start_time)
821            .end_time(end_time)
822            .period(60)
823            .statistics(aws_sdk_cloudwatch::types::Statistic::Sum)
824            .send()
825            .await?;
826
827        let mut data = Vec::new();
828        if let Some(datapoints) = response.datapoints {
829            for dp in datapoints {
830                if let (Some(timestamp), Some(value)) = (dp.timestamp, dp.sum) {
831                    data.push((timestamp.secs(), value));
832                }
833            }
834        }
835
836        data.sort_by_key(|&(timestamp, _)| timestamp);
837        Ok(data)
838    }
839
840    pub async fn get_dead_letter_errors_metric(
841        &self,
842        function_name: &str,
843    ) -> Result<Vec<(i64, f64)>> {
844        let cw_client = self.config.cloudwatch_client().await;
845
846        let end_time = aws_smithy_types::DateTime::from_secs(
847            std::time::SystemTime::now()
848                .duration_since(std::time::UNIX_EPOCH)?
849                .as_secs() as i64,
850        );
851        let start_time = aws_smithy_types::DateTime::from_secs(
852            std::time::SystemTime::now()
853                .duration_since(std::time::UNIX_EPOCH)?
854                .as_secs() as i64
855                - 3 * 3600,
856        );
857
858        let dimension = aws_sdk_cloudwatch::types::Dimension::builder()
859            .name("FunctionName")
860            .value(function_name)
861            .build();
862
863        let response = cw_client
864            .get_metric_statistics()
865            .namespace("AWS/Lambda")
866            .metric_name("DeadLetterErrors")
867            .dimensions(dimension)
868            .start_time(start_time)
869            .end_time(end_time)
870            .period(60)
871            .statistics(aws_sdk_cloudwatch::types::Statistic::Sum)
872            .send()
873            .await?;
874
875        let mut data = Vec::new();
876        if let Some(datapoints) = response.datapoints {
877            for dp in datapoints {
878                if let (Some(timestamp), Some(value)) = (dp.timestamp, dp.sum) {
879                    data.push((timestamp.secs(), value));
880                }
881            }
882        }
883
884        data.sort_by_key(|&(timestamp, _)| timestamp);
885        Ok(data)
886    }
887
888    pub async fn get_iterator_age_metric(&self, function_name: &str) -> Result<Vec<(i64, f64)>> {
889        let cw_client = self.config.cloudwatch_client().await;
890
891        let end_time = aws_smithy_types::DateTime::from_secs(
892            std::time::SystemTime::now()
893                .duration_since(std::time::UNIX_EPOCH)?
894                .as_secs() as i64,
895        );
896        let start_time = aws_smithy_types::DateTime::from_secs(
897            std::time::SystemTime::now()
898                .duration_since(std::time::UNIX_EPOCH)?
899                .as_secs() as i64
900                - 3 * 3600,
901        );
902
903        let dimension = aws_sdk_cloudwatch::types::Dimension::builder()
904            .name("FunctionName")
905            .value(function_name)
906            .build();
907
908        let response = cw_client
909            .get_metric_statistics()
910            .namespace("AWS/Lambda")
911            .metric_name("IteratorAge")
912            .dimensions(dimension)
913            .start_time(start_time)
914            .end_time(end_time)
915            .period(60)
916            .statistics(aws_sdk_cloudwatch::types::Statistic::Maximum)
917            .send()
918            .await?;
919
920        let mut data = Vec::new();
921        if let Some(datapoints) = response.datapoints {
922            for dp in datapoints {
923                if let (Some(timestamp), Some(value)) = (dp.timestamp, dp.maximum) {
924                    data.push((timestamp.secs(), value));
925                }
926            }
927        }
928
929        data.sort_by_key(|&(timestamp, _)| timestamp);
930        Ok(data)
931    }
932}
933
934#[cfg(test)]
935mod tests {
936    #[test]
937    fn test_application_extraction_from_function_name() {
938        // Test typical pattern: app-name-suffix
939        let name = "storefront-studio-beta-api";
940        let application = name.rsplit_once('-').map(|(prefix, _)| prefix).unwrap();
941        assert_eq!(application, "storefront-studio-beta");
942
943        // Test single dash
944        let name = "myapp-api";
945        let application = name.rsplit_once('-').map(|(prefix, _)| prefix).unwrap();
946        assert_eq!(application, "myapp");
947
948        // Test no dash
949        let name = "simplefunction";
950        let application = name.rsplit_once('-').map(|(prefix, _)| prefix);
951        assert_eq!(application, None);
952
953        // Test multiple dashes
954        let name = "my-complex-app-name-worker";
955        let application = name.rsplit_once('-').map(|(prefix, _)| prefix).unwrap();
956        assert_eq!(application, "my-complex-app-name");
957    }
958
959    #[test]
960    fn test_invocations_metric_data_structure() {
961        // Test that metric data is a vector of (timestamp, value) tuples
962        let data: Vec<(i64, f64)> =
963            vec![(1700000000, 10.0), (1700000060, 15.0), (1700000120, 20.0)];
964        assert_eq!(data.len(), 3);
965        assert_eq!(data[0].0, 1700000000);
966        assert_eq!(data[0].1, 10.0);
967    }
968
969    #[test]
970    fn test_invocations_metric_sorting() {
971        // Test that data should be sorted by timestamp
972        let mut data: Vec<(i64, f64)> =
973            vec![(1700000120, 20.0), (1700000000, 10.0), (1700000060, 15.0)];
974        data.sort_by_key(|&(timestamp, _)| timestamp);
975        assert_eq!(data[0].0, 1700000000);
976        assert_eq!(data[1].0, 1700000060);
977        assert_eq!(data[2].0, 1700000120);
978    }
979}