Skip to main content

rusticity_core/
lambda.rs

1use crate::config::AwsConfig;
2use crate::metrics::{MetricQuery, MetricReader};
3use anyhow::Result;
4
5pub use aws_sdk_cloudwatch::types::Statistic;
6
7#[derive(Clone, Debug)]
8pub struct LambdaFunction {
9    pub name: String,
10    pub arn: String,
11    pub application: Option<String>,
12    pub description: String,
13    pub package_type: String,
14    pub runtime: String,
15    pub architecture: String,
16    pub code_size: i64,
17    pub code_sha256: String,
18    pub memory_mb: i32,
19    pub timeout_seconds: i32,
20    pub last_modified: String,
21    pub layers: Vec<LambdaLayer>,
22}
23
24#[derive(Clone, Debug)]
25pub struct LambdaLayer {
26    pub arn: String,
27    pub code_size: i64,
28}
29
30#[derive(Clone, Debug)]
31pub struct LambdaVersion {
32    pub version: String,
33    pub aliases: String,
34    pub description: String,
35    pub last_modified: String,
36    pub architecture: String,
37}
38
39#[derive(Clone, Debug)]
40pub struct LambdaAlias {
41    pub name: String,
42    pub versions: String,
43    pub description: String,
44}
45
46pub struct LambdaClient {
47    config: AwsConfig,
48}
49
50impl LambdaClient {
51    pub fn new(config: AwsConfig) -> Self {
52        Self { config }
53    }
54
55    pub async fn list_functions(&self) -> Result<Vec<LambdaFunction>> {
56        let client = self.config.lambda_client();
57
58        let mut functions = Vec::new();
59        let mut next_marker: Option<String> = None;
60
61        loop {
62            let mut request = client.list_functions().max_items(100);
63            if let Some(marker) = next_marker {
64                request = request.marker(marker);
65            }
66
67            let response = request.send().await?;
68
69            if let Some(funcs) = response.functions {
70                for func in funcs {
71                    // AWS returns last_modified in format: "2024-10-31T12:30:45.000+0000"
72                    let last_modified = func
73                        .last_modified
74                        .as_deref()
75                        .map(|s| {
76                            // Try parsing with timezone
77                            if let Ok(dt) =
78                                chrono::DateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.3f%z")
79                            {
80                                dt.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
81                            } else if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(s) {
82                                dt.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
83                            } else {
84                                s.to_string()
85                            }
86                        })
87                        .unwrap_or_default();
88
89                    let function_name = func.function_name.unwrap_or_default();
90
91                    // Extract application name from function name pattern
92                    // e.g., "storefront-studio-beta-api" -> "storefront-studio-beta"
93                    let application = function_name
94                        .rsplit_once('-')
95                        .map(|(prefix, _)| prefix.to_string());
96
97                    let layers = func
98                        .layers
99                        .unwrap_or_default()
100                        .into_iter()
101                        .map(|layer| LambdaLayer {
102                            arn: layer.arn.unwrap_or_default(),
103                            code_size: layer.code_size,
104                        })
105                        .collect();
106
107                    functions.push(LambdaFunction {
108                        name: function_name,
109                        arn: func.function_arn.unwrap_or_default(),
110                        application,
111                        description: func.description.unwrap_or_default(),
112                        package_type: func
113                            .package_type
114                            .map(|p| format!("{:?}", p))
115                            .unwrap_or_default(),
116                        runtime: func.runtime.map(|r| format!("{:?}", r)).unwrap_or_default(),
117                        architecture: func
118                            .architectures
119                            .and_then(|a| a.first().map(|arch| format!("{:?}", arch)))
120                            .unwrap_or_default(),
121                        code_size: func.code_size,
122                        code_sha256: func.code_sha256.unwrap_or_default(),
123                        memory_mb: func.memory_size.unwrap_or(0),
124                        timeout_seconds: func.timeout.unwrap_or(0),
125                        last_modified,
126                        layers,
127                    });
128                }
129            }
130
131            next_marker = response.next_marker;
132            if next_marker.is_none() {
133                break;
134            }
135        }
136
137        Ok(functions)
138    }
139
140    pub async fn list_applications(&self) -> Result<Vec<LambdaApplication>> {
141        let client = self.config.cloudformation_client();
142
143        let mut applications = Vec::new();
144        let mut next_token: Option<String> = None;
145
146        loop {
147            let mut request = client.list_stacks();
148            if let Some(token) = next_token {
149                request = request.next_token(token);
150            }
151
152            let response = request.send().await?;
153
154            if let Some(stacks) = response.stack_summaries {
155                for stack in stacks {
156                    // Only include active stacks
157                    let status = stack
158                        .stack_status
159                        .map(|s| format!("{:?}", s))
160                        .unwrap_or_default();
161                    if status.contains("DELETE") {
162                        continue;
163                    }
164
165                    applications.push(LambdaApplication {
166                        name: stack.stack_name.unwrap_or_default(),
167                        arn: stack.stack_id.unwrap_or_default(),
168                        description: stack.template_description.unwrap_or_default(),
169                        status,
170                        last_modified: stack
171                            .last_updated_time
172                            .or(stack.creation_time)
173                            .map(|dt| {
174                                let timestamp = dt.secs();
175                                let datetime = chrono::DateTime::from_timestamp(timestamp, 0)
176                                    .unwrap_or_default();
177                                datetime.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
178                            })
179                            .unwrap_or_default(),
180                    });
181                }
182            }
183
184            next_token = response.next_token;
185            if next_token.is_none() {
186                break;
187            }
188        }
189
190        Ok(applications)
191    }
192
193    pub async fn list_versions(&self, function_name: &str) -> Result<Vec<LambdaVersion>> {
194        let client = self.config.lambda_client();
195
196        let mut versions = Vec::new();
197        let mut next_marker: Option<String> = None;
198
199        loop {
200            let mut request = client
201                .list_versions_by_function()
202                .function_name(function_name)
203                .max_items(100);
204            if let Some(marker) = next_marker {
205                request = request.marker(marker);
206            }
207
208            let response = request.send().await?;
209
210            if let Some(vers) = response.versions {
211                for ver in vers {
212                    let last_modified = ver
213                        .last_modified
214                        .as_deref()
215                        .map(|s| {
216                            if let Ok(dt) =
217                                chrono::DateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.3f%z")
218                            {
219                                dt.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
220                            } else if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(s) {
221                                dt.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
222                            } else {
223                                s.to_string()
224                            }
225                        })
226                        .unwrap_or_default();
227
228                    versions.push(LambdaVersion {
229                        version: ver.version.unwrap_or_default(),
230                        aliases: String::new(), // Will be populated below
231                        description: ver.description.unwrap_or_default(),
232                        last_modified,
233                        architecture: ver
234                            .architectures
235                            .and_then(|a| a.first().map(|arch| format!("{:?}", arch)))
236                            .unwrap_or_default(),
237                    });
238                }
239            }
240
241            next_marker = response.next_marker;
242            if next_marker.is_none() {
243                break;
244            }
245        }
246
247        // Fetch aliases for all versions
248        let aliases_response = client
249            .list_aliases()
250            .function_name(function_name)
251            .send()
252            .await?;
253
254        if let Some(aliases) = aliases_response.aliases {
255            for alias in aliases {
256                let alias_name = alias.name.unwrap_or_default();
257
258                // Add alias to primary version
259                if let Some(version) = alias.function_version {
260                    if let Some(ver) = versions.iter_mut().find(|v| v.version == version) {
261                        if !ver.aliases.is_empty() {
262                            ver.aliases.push_str(", ");
263                        }
264                        ver.aliases.push_str(&alias_name);
265                    }
266                }
267
268                // Add alias to additional versions in routing config
269                if let Some(routing_config) = alias.routing_config {
270                    if let Some(additional_version_weights) =
271                        routing_config.additional_version_weights
272                    {
273                        for (version, _weight) in additional_version_weights {
274                            if let Some(ver) = versions.iter_mut().find(|v| v.version == version) {
275                                if !ver.aliases.is_empty() {
276                                    ver.aliases.push_str(", ");
277                                }
278                                ver.aliases.push_str(&alias_name);
279                            }
280                        }
281                    }
282                }
283            }
284        }
285
286        Ok(versions)
287    }
288}
289
290#[derive(Clone, Debug)]
291pub struct LambdaApplication {
292    pub name: String,
293    pub arn: String,
294    pub description: String,
295    pub status: String,
296    pub last_modified: String,
297}
298
299impl LambdaClient {
300    pub async fn list_aliases(&self, function_name: &str) -> Result<Vec<LambdaAlias>> {
301        let client = self.config.lambda_client();
302        let response = client
303            .list_aliases()
304            .function_name(function_name)
305            .send()
306            .await?;
307
308        let mut aliases = Vec::new();
309        if let Some(alias_list) = response.aliases {
310            for alias in alias_list {
311                let primary_version = alias.function_version.unwrap_or_default();
312
313                // Check for additional versions in routing config
314                let mut versions_str = primary_version.clone();
315                if let Some(routing_config) = alias.routing_config {
316                    if let Some(additional_version_weights) =
317                        routing_config.additional_version_weights
318                    {
319                        for (version, weight) in additional_version_weights {
320                            versions_str.push_str(&format!(
321                                ", {} ({}%)",
322                                version,
323                                (weight * 100.0) as i32
324                            ));
325                        }
326                    }
327                }
328
329                aliases.push(LambdaAlias {
330                    name: alias.name.unwrap_or_default(),
331                    versions: versions_str,
332                    description: alias.description.unwrap_or_default(),
333                });
334            }
335        }
336
337        Ok(aliases)
338    }
339
340    pub async fn get_invocations_metric(
341        &self,
342        function_name: &str,
343        resource: Option<&str>,
344    ) -> Result<Vec<(i64, f64)>> {
345        let query = MetricQuery::new("AWS/Lambda", "Invocations", Statistic::Sum)
346            .dimension("FunctionName", function_name);
347        let query = if let Some(resource) = resource {
348            query.dimension("Resource", resource)
349        } else {
350            query
351        };
352        MetricReader::new(self.config.cloudwatch_client())
353            .recent(query, 60)
354            .await
355    }
356
357    pub async fn get_duration_metric(
358        &self,
359        function_name: &str,
360        stat: aws_sdk_cloudwatch::types::Statistic,
361    ) -> Result<Vec<(i64, f64)>> {
362        let query = MetricQuery::new("AWS/Lambda", "Duration", stat)
363            .dimension("FunctionName", function_name);
364        MetricReader::new(self.config.cloudwatch_client())
365            .recent(query, 60)
366            .await
367    }
368
369    pub async fn get_errors_metric(&self, function_name: &str) -> Result<Vec<(i64, f64)>> {
370        let query = MetricQuery::new("AWS/Lambda", "Errors", Statistic::Sum)
371            .dimension("FunctionName", function_name);
372        MetricReader::new(self.config.cloudwatch_client())
373            .recent(query, 60)
374            .await
375    }
376
377    pub async fn get_throttles_metric(&self, function_name: &str) -> Result<Vec<(i64, f64)>> {
378        let query = MetricQuery::new("AWS/Lambda", "Throttles", Statistic::Sum)
379            .dimension("FunctionName", function_name);
380        MetricReader::new(self.config.cloudwatch_client())
381            .recent(query, 60)
382            .await
383    }
384
385    pub async fn get_concurrent_executions_metric(
386        &self,
387        function_name: &str,
388    ) -> Result<Vec<(i64, f64)>> {
389        let query = MetricQuery::new("AWS/Lambda", "ConcurrentExecutions", Statistic::Maximum)
390            .dimension("FunctionName", function_name);
391        MetricReader::new(self.config.cloudwatch_client())
392            .recent(query, 60)
393            .await
394    }
395
396    pub async fn get_recursive_invocations_dropped_metric(
397        &self,
398        function_name: &str,
399    ) -> Result<Vec<(i64, f64)>> {
400        let query = MetricQuery::new("AWS/Lambda", "RecursiveInvocationsDropped", Statistic::Sum)
401            .dimension("FunctionName", function_name);
402        MetricReader::new(self.config.cloudwatch_client())
403            .recent(query, 60)
404            .await
405    }
406
407    pub async fn get_async_event_age_metric(
408        &self,
409        function_name: &str,
410        stat: aws_sdk_cloudwatch::types::Statistic,
411    ) -> Result<Vec<(i64, f64)>> {
412        let query = MetricQuery::new("AWS/Lambda", "AsyncEventAge", stat)
413            .dimension("FunctionName", function_name);
414        MetricReader::new(self.config.cloudwatch_client())
415            .recent(query, 60)
416            .await
417    }
418
419    pub async fn get_async_events_received_metric(
420        &self,
421        function_name: &str,
422    ) -> Result<Vec<(i64, f64)>> {
423        let query = MetricQuery::new("AWS/Lambda", "AsyncEventsReceived", Statistic::Sum)
424            .dimension("FunctionName", function_name);
425        MetricReader::new(self.config.cloudwatch_client())
426            .recent(query, 60)
427            .await
428    }
429
430    pub async fn get_async_events_dropped_metric(
431        &self,
432        function_name: &str,
433    ) -> Result<Vec<(i64, f64)>> {
434        let query = MetricQuery::new("AWS/Lambda", "AsyncEventsDropped", Statistic::Sum)
435            .dimension("FunctionName", function_name);
436        MetricReader::new(self.config.cloudwatch_client())
437            .recent(query, 60)
438            .await
439    }
440
441    pub async fn get_destination_delivery_failures_metric(
442        &self,
443        function_name: &str,
444    ) -> Result<Vec<(i64, f64)>> {
445        let query = MetricQuery::new("AWS/Lambda", "DestinationDeliveryFailures", Statistic::Sum)
446            .dimension("FunctionName", function_name);
447        MetricReader::new(self.config.cloudwatch_client())
448            .recent(query, 60)
449            .await
450    }
451
452    pub async fn get_dead_letter_errors_metric(
453        &self,
454        function_name: &str,
455    ) -> Result<Vec<(i64, f64)>> {
456        let query = MetricQuery::new("AWS/Lambda", "DeadLetterErrors", Statistic::Sum)
457            .dimension("FunctionName", function_name);
458        MetricReader::new(self.config.cloudwatch_client())
459            .recent(query, 60)
460            .await
461    }
462
463    pub async fn get_iterator_age_metric(&self, function_name: &str) -> Result<Vec<(i64, f64)>> {
464        let query = MetricQuery::new("AWS/Lambda", "IteratorAge", Statistic::Maximum)
465            .dimension("FunctionName", function_name);
466        MetricReader::new(self.config.cloudwatch_client())
467            .recent(query, 60)
468            .await
469    }
470}
471
472#[cfg(test)]
473mod tests {
474    #[test]
475    fn test_application_extraction_from_function_name() {
476        // Test typical pattern: app-name-suffix
477        let name = "storefront-studio-beta-api";
478        let application = name.rsplit_once('-').map(|(prefix, _)| prefix).unwrap();
479        assert_eq!(application, "storefront-studio-beta");
480
481        // Test single dash
482        let name = "myapp-api";
483        let application = name.rsplit_once('-').map(|(prefix, _)| prefix).unwrap();
484        assert_eq!(application, "myapp");
485
486        // Test no dash
487        let name = "simplefunction";
488        let application = name.rsplit_once('-').map(|(prefix, _)| prefix);
489        assert_eq!(application, None);
490
491        // Test multiple dashes
492        let name = "my-complex-app-name-worker";
493        let application = name.rsplit_once('-').map(|(prefix, _)| prefix).unwrap();
494        assert_eq!(application, "my-complex-app-name");
495    }
496
497    #[test]
498    fn test_invocations_metric_data_structure() {
499        // Test that metric data is a vector of (timestamp, value) tuples
500        let data: Vec<(i64, f64)> =
501            vec![(1700000000, 10.0), (1700000060, 15.0), (1700000120, 20.0)];
502        assert_eq!(data.len(), 3);
503        assert_eq!(data[0].0, 1700000000);
504        assert_eq!(data[0].1, 10.0);
505    }
506
507    #[test]
508    fn test_invocations_metric_sorting() {
509        // Test that data should be sorted by timestamp
510        let mut data: Vec<(i64, f64)> =
511            vec![(1700000120, 20.0), (1700000000, 10.0), (1700000060, 15.0)];
512        data.sort_by_key(|&(timestamp, _)| timestamp);
513        assert_eq!(data[0].0, 1700000000);
514        assert_eq!(data[1].0, 1700000060);
515        assert_eq!(data[2].0, 1700000120);
516    }
517}