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 let last_modified = func
73 .last_modified
74 .as_deref()
75 .map(|s| {
76 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 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 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(), 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 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 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 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 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 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 let name = "myapp-api";
483 let application = name.rsplit_once('-').map(|(prefix, _)| prefix).unwrap();
484 assert_eq!(application, "myapp");
485
486 let name = "simplefunction";
488 let application = name.rsplit_once('-').map(|(prefix, _)| prefix);
489 assert_eq!(application, None);
490
491 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 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 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}