use std::{sync::OnceLock, time::Instant};
use serde::{Deserialize, Serialize};
use crate::apr::{AprModel, MAGIC as APR_MAGIC};
#[allow(dead_code)]
pub static MODEL_BYTES: OnceLock<&'static [u8]> = OnceLock::new();
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LambdaRequest {
pub features: Vec<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LambdaResponse {
pub prediction: f32,
#[serde(skip_serializing_if = "Option::is_none")]
pub probabilities: Option<Vec<f32>>,
pub latency_ms: f64,
pub cold_start: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BatchLambdaRequest {
pub instances: Vec<LambdaRequest>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_parallelism: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BatchLambdaResponse {
pub predictions: Vec<LambdaResponse>,
pub total_latency_ms: f64,
pub success_count: usize,
pub error_count: usize,
}
#[derive(Debug, Clone, Default)]
pub struct LambdaMetrics {
pub requests_total: u64,
pub requests_success: u64,
pub requests_failed: u64,
pub latency_total_ms: f64,
pub cold_starts: u64,
pub batch_requests: u64,
}
impl LambdaMetrics {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn record_success(&mut self, latency_ms: f64, is_cold_start: bool) {
self.requests_total += 1;
self.requests_success += 1;
self.latency_total_ms += latency_ms;
if is_cold_start {
self.cold_starts += 1;
}
}
pub fn record_failure(&mut self) {
self.requests_total += 1;
self.requests_failed += 1;
}
pub fn record_batch(&mut self, success_count: usize, error_count: usize, latency_ms: f64) {
self.batch_requests += 1;
self.requests_total += (success_count + error_count) as u64;
self.requests_success += success_count as u64;
self.requests_failed += error_count as u64;
self.latency_total_ms += latency_ms;
}
#[must_use]
#[allow(clippy::cast_precision_loss)] pub fn avg_latency_ms(&self) -> f64 {
if self.requests_success == 0 {
0.0
} else {
self.latency_total_ms / self.requests_success as f64
}
}
#[must_use]
pub fn to_prometheus(&self) -> String {
format!(
"# HELP lambda_requests_total Total number of requests\n\
# TYPE lambda_requests_total counter\n\
lambda_requests_total {}\n\
# HELP lambda_requests_success Successful requests\n\
# TYPE lambda_requests_success counter\n\
lambda_requests_success {}\n\
# HELP lambda_requests_failed Failed requests\n\
# TYPE lambda_requests_failed counter\n\
lambda_requests_failed {}\n\
# HELP lambda_latency_avg_ms Average inference latency\n\
# TYPE lambda_latency_avg_ms gauge\n\
lambda_latency_avg_ms {:.3}\n\
# HELP lambda_cold_starts Cold start count\n\
# TYPE lambda_cold_starts counter\n\
lambda_cold_starts {}\n\
# HELP lambda_batch_requests Batch requests processed\n\
# TYPE lambda_batch_requests counter\n\
lambda_batch_requests {}\n",
self.requests_total,
self.requests_success,
self.requests_failed,
self.avg_latency_ms(),
self.cold_starts,
self.batch_requests
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColdStartMetrics {
pub runtime_init_ms: f64,
pub model_load_ms: f64,
pub first_inference_ms: f64,
pub total_ms: f64,
}
pub struct LambdaHandler {
model_bytes: &'static [u8],
model: OnceLock<AprModel>,
init_time: OnceLock<Instant>,
cold_start_metrics: OnceLock<ColdStartMetrics>,
}
impl std::fmt::Debug for LambdaHandler {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LambdaHandler")
.field("model_bytes_len", &self.model_bytes.len())
.field("model_initialized", &self.model.get().is_some())
.field("init_time", &self.init_time)
.field("cold_start_metrics", &self.cold_start_metrics)
.finish()
}
}
impl LambdaHandler {
pub fn from_bytes(model_bytes: &'static [u8]) -> Result<Self, LambdaError> {
if model_bytes.is_empty() {
return Err(LambdaError::EmptyModel);
}
if model_bytes.len() >= 4 {
let magic = &model_bytes[0..4];
if magic != APR_MAGIC {
return Err(LambdaError::InvalidMagic {
expected: "APRN".to_string(),
found: format!("{:?}", &model_bytes[0..4.min(model_bytes.len())]),
});
}
}
Ok(Self {
model_bytes,
model: OnceLock::new(),
init_time: OnceLock::new(),
cold_start_metrics: OnceLock::new(),
})
}
#[must_use]
pub fn is_cold_start(&self) -> bool {
self.init_time.get().is_none()
}
#[must_use]
pub fn model_size_bytes(&self) -> usize {
self.model_bytes.len()
}
#[must_use]
pub fn cold_start_metrics(&self) -> Option<&ColdStartMetrics> {
self.cold_start_metrics.get()
}
pub fn handle(&self, request: &LambdaRequest) -> Result<LambdaResponse, LambdaError> {
let start = Instant::now();
let is_cold = self.is_cold_start();
let _ = self.init_time.get_or_init(|| start);
if request.features.is_empty() {
return Err(LambdaError::EmptyFeatures);
}
let model_load_start = Instant::now();
let model = self.model.get_or_init(|| {
AprModel::from_bytes(self.model_bytes)
.expect("Model bytes already validated in from_bytes()")
});
let model_load_ms = model_load_start.elapsed().as_secs_f64() * 1000.0;
let inference_start = Instant::now();
let output = model
.predict(&request.features)
.map_err(|e| LambdaError::InferenceFailed(format!("Model inference failed: {e}")))?;
let inference_ms = inference_start.elapsed().as_secs_f64() * 1000.0;
let prediction = if output.len() == 1 {
output[0]
} else {
output.iter().sum()
};
let latency = start.elapsed();
if is_cold {
let _ = self.cold_start_metrics.get_or_init(|| ColdStartMetrics {
runtime_init_ms: 0.0,
model_load_ms,
first_inference_ms: inference_ms,
total_ms: latency.as_secs_f64() * 1000.0,
});
}
Ok(LambdaResponse {
prediction,
probabilities: if output.len() > 1 { Some(output) } else { None },
latency_ms: latency.as_secs_f64() * 1000.0,
cold_start: is_cold,
})
}
pub fn handle_batch(
&self,
request: &BatchLambdaRequest,
) -> Result<BatchLambdaResponse, LambdaError> {
let start = Instant::now();
if request.instances.is_empty() {
return Err(LambdaError::EmptyBatch);
}
let mut predictions = Vec::with_capacity(request.instances.len());
let mut success_count = 0;
let mut error_count = 0;
for instance in &request.instances {
if let Ok(response) = self.handle(instance) {
predictions.push(response);
success_count += 1;
} else {
predictions.push(LambdaResponse {
prediction: f32::NAN,
probabilities: None,
latency_ms: 0.0,
cold_start: false,
});
error_count += 1;
}
}
let total_latency = start.elapsed();
Ok(BatchLambdaResponse {
predictions,
total_latency_ms: total_latency.as_secs_f64() * 1000.0,
success_count,
error_count,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum LambdaError {
EmptyModel,
InvalidMagic {
expected: String,
found: String,
},
EmptyFeatures,
EmptyBatch,
ModelDeserialize(String),
InferenceFailed(String),
}
impl std::fmt::Display for LambdaError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LambdaError::EmptyModel => write!(f, "Model bytes are empty"),
LambdaError::InvalidMagic { expected, found } => {
write!(f, "Invalid magic bytes: expected {expected}, found {found}")
},
LambdaError::EmptyFeatures => write!(f, "Request features are empty"),
LambdaError::EmptyBatch => write!(f, "Batch request has no instances"),
LambdaError::ModelDeserialize(msg) => write!(f, "Model deserialization failed: {msg}"),
LambdaError::InferenceFailed(msg) => write!(f, "Inference failed: {msg}"),
}
}
}
impl std::error::Error for LambdaError {}
pub mod arm64 {
#[must_use]
pub const fn is_arm64() -> bool {
cfg!(target_arch = "aarch64")
}
#[must_use]
pub const fn target_arch() -> &'static str {
#[cfg(target_arch = "aarch64")]
{
"aarch64"
}
#[cfg(target_arch = "x86_64")]
{
"x86_64"
}
#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
{
"unknown"
}
}
#[must_use]
pub const fn optimal_simd() -> &'static str {
#[cfg(target_arch = "aarch64")]
{
"NEON"
}
#[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
{
"AVX2"
}
#[cfg(all(target_arch = "x86_64", not(target_feature = "avx2")))]
{
"SSE2"
}
#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
{
"Scalar"
}
}
}
pub mod benchmark {
use serde::{Deserialize, Serialize};
use super::{arm64, Instant, LambdaError, LambdaHandler, LambdaRequest};
pub const TARGET_COLD_START_MS: f64 = 50.0;
pub const TARGET_WARM_INFERENCE_MS: f64 = 10.0;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BenchmarkResult {
pub cold_start_ms: f64,
pub warm_inference_ms: f64,
pub warm_iterations: usize,
pub model_size_bytes: usize,
pub target_arch: String,
pub simd_backend: String,
pub meets_cold_start_target: bool,
pub meets_warm_inference_target: bool,
}
impl BenchmarkResult {
#[must_use]
pub fn meets_all_targets(&self) -> bool {
self.meets_cold_start_target && self.meets_warm_inference_target
}
}
pub fn benchmark_cold_start(
handler: &LambdaHandler,
request: &LambdaRequest,
warm_iterations: usize,
) -> Result<BenchmarkResult, LambdaError> {
let cold_start = Instant::now();
let _cold_response = handler.handle(request)?;
let cold_start_ms = cold_start.elapsed().as_secs_f64() * 1000.0;
let mut warm_latencies = Vec::with_capacity(warm_iterations);
for _ in 0..warm_iterations {
let start = Instant::now();
let _response = handler.handle(request)?;
warm_latencies.push(start.elapsed().as_secs_f64() * 1000.0);
}
warm_latencies.sort_by(|a, b| a.partial_cmp(b).expect("latencies should not contain NaN"));
let warm_inference_ms = if warm_latencies.is_empty() {
0.0
} else {
warm_latencies[warm_latencies.len() / 2]
};
Ok(BenchmarkResult {
cold_start_ms,
warm_inference_ms,
warm_iterations,
model_size_bytes: handler.model_size_bytes(),
target_arch: arm64::target_arch().to_string(),
simd_backend: arm64::optimal_simd().to_string(),
meets_cold_start_target: cold_start_ms <= TARGET_COLD_START_MS,
meets_warm_inference_target: warm_inference_ms <= TARGET_WARM_INFERENCE_MS,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::apr::{AprModelType, ModelWeights, HEADER_SIZE, MAGIC};
fn create_sum_model(input_dim: usize) -> &'static [u8] {
let weights = ModelWeights {
weights: vec![vec![1.0; input_dim]], biases: vec![vec![0.0]], dimensions: vec![input_dim, 1],
};
let payload = serde_json::to_vec(&weights).expect("serialize weights");
let mut data = Vec::with_capacity(HEADER_SIZE + payload.len());
data.extend_from_slice(&MAGIC);
data.push(1);
data.push(0);
data.push(0);
data.push(0);
data.extend_from_slice(&AprModelType::LinearRegression.as_u16().to_le_bytes());
data.extend_from_slice(&0u32.to_le_bytes());
#[allow(clippy::cast_possible_truncation)]
data.extend_from_slice(&(payload.len() as u32).to_le_bytes());
#[allow(clippy::cast_possible_truncation)]
data.extend_from_slice(&(payload.len() as u32).to_le_bytes());
data.extend_from_slice(&[0u8; 10]);
data.extend_from_slice(&payload);
Box::leak(data.into_boxed_slice())
}
#[test]
fn test_lambda_handler_creation_with_valid_apr_model() {
let model_bytes = create_sum_model(3);
let handler = LambdaHandler::from_bytes(model_bytes);
assert!(handler.is_ok(), "Should accept valid .apr model");
let handler = handler.unwrap();
assert!(handler.model_size_bytes() > HEADER_SIZE);
}
#[test]
fn test_lambda_handler_rejects_empty_model() {
let model_bytes: &'static [u8] = b"";
let result = LambdaHandler::from_bytes(model_bytes);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), LambdaError::EmptyModel);
}
#[test]
fn test_lambda_handler_rejects_invalid_magic() {
let model_bytes: &'static [u8] = b"GGUF\x01\x00\x00\x00testmodel";
let result = LambdaHandler::from_bytes(model_bytes);
assert!(result.is_err());
match result.unwrap_err() {
LambdaError::InvalidMagic { expected, found } => {
assert_eq!(expected, "APRN");
assert!(!found.is_empty());
assert!(found.contains("71")); },
_ => panic!("Expected InvalidMagic error"),
}
}
#[test]
fn test_lambda_request_serialization() {
let request = LambdaRequest {
features: vec![0.5, 1.2, -0.3, 0.8],
model_id: Some("sentiment-v1".to_string()),
};
let json = serde_json::to_string(&request).expect("serialization failed");
assert!(json.contains("0.5"));
assert!(json.contains("sentiment-v1"));
}
#[test]
fn test_lambda_request_deserialization() {
let json = r#"{"features": [1.0, 2.0, 3.0], "model_id": "test-model"}"#;
let request: LambdaRequest = serde_json::from_str(json).expect("deserialization failed");
assert_eq!(request.features, vec![1.0, 2.0, 3.0]);
assert_eq!(request.model_id, Some("test-model".to_string()));
}
#[test]
fn test_lambda_response_serialization() {
let response = LambdaResponse {
prediction: 0.85,
probabilities: Some(vec![0.15, 0.85]),
latency_ms: 2.3,
cold_start: true,
};
let json = serde_json::to_string(&response).expect("serialization failed");
assert!(json.contains("0.85"));
assert!(json.contains("cold_start"));
assert!(json.contains("true"));
}
#[test]
fn test_lambda_handler_cold_start_detection() {
let model_bytes = create_sum_model(3);
let handler = LambdaHandler::from_bytes(model_bytes).unwrap();
assert!(handler.is_cold_start());
let request = LambdaRequest {
features: vec![1.0, 2.0, 3.0],
model_id: None,
};
let response = handler.handle(&request).unwrap();
assert!(response.cold_start, "First invocation should be cold start");
assert!(!handler.is_cold_start());
let response2 = handler.handle(&request).unwrap();
assert!(
!response2.cold_start,
"Second invocation should not be cold start"
);
}
#[test]
fn test_lambda_handler_rejects_empty_features() {
let model_bytes = create_sum_model(3);
let handler = LambdaHandler::from_bytes(model_bytes).unwrap();
let request = LambdaRequest {
features: vec![],
model_id: None,
};
let result = handler.handle(&request);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), LambdaError::EmptyFeatures);
}
#[test]
fn test_lambda_handler_real_inference() {
let model_bytes = create_sum_model(3);
let handler = LambdaHandler::from_bytes(model_bytes).unwrap();
let request = LambdaRequest {
features: vec![1.0, 2.0, 3.0],
model_id: None,
};
let response = handler.handle(&request).unwrap();
assert!((response.prediction - 6.0).abs() < 0.001);
assert!(response.latency_ms >= 0.0);
}
#[test]
fn test_cold_start_metrics_recorded() {
let model_bytes = create_sum_model(1);
let handler = LambdaHandler::from_bytes(model_bytes).unwrap();
assert!(handler.cold_start_metrics().is_none());
let request = LambdaRequest {
features: vec![1.0],
model_id: None,
};
let _ = handler.handle(&request).unwrap();
let metrics = handler.cold_start_metrics();
assert!(metrics.is_some());
let metrics = metrics.unwrap();
assert!(metrics.total_ms >= 0.0);
assert!(metrics.first_inference_ms >= 0.0);
assert!(metrics.model_load_ms >= 0.0);
}
#[test]
fn test_arm64_architecture_detection() {
let arch = arm64::target_arch();
assert!(
arch == "aarch64" || arch == "x86_64" || arch == "unknown",
"Should detect valid architecture"
);
}
#[test]
fn test_arm64_simd_detection() {
let simd = arm64::optimal_simd();
let valid_simd = ["NEON", "AVX2", "SSE2", "Scalar"];
assert!(
valid_simd.contains(&simd),
"Should detect valid SIMD backend"
);
}
#[test]
fn test_benchmark_cold_start() {
let model_bytes = create_sum_model(2);
let handler = LambdaHandler::from_bytes(model_bytes).unwrap();
let request = LambdaRequest {
features: vec![1.0, 2.0],
model_id: None,
};
let result = benchmark::benchmark_cold_start(&handler, &request, 10).unwrap();
assert!(result.cold_start_ms >= 0.0);
assert!(result.warm_inference_ms >= 0.0);
assert_eq!(result.warm_iterations, 10);
assert!(result.model_size_bytes > 0);
}
#[test]
fn test_benchmark_targets() {
assert!((benchmark::TARGET_COLD_START_MS - 50.0).abs() < f64::EPSILON);
assert!((benchmark::TARGET_WARM_INFERENCE_MS - 10.0).abs() < f64::EPSILON);
}
#[test]
fn test_benchmark_result_meets_targets() {
let result = benchmark::BenchmarkResult {
cold_start_ms: 45.0,
warm_inference_ms: 8.0,
warm_iterations: 100,
model_size_bytes: 1000,
target_arch: "aarch64".to_string(),
simd_backend: "NEON".to_string(),
meets_cold_start_target: true,
meets_warm_inference_target: true,
};
assert!(result.meets_all_targets());
}
#[test]
fn test_benchmark_result_fails_targets() {
let result = benchmark::BenchmarkResult {
cold_start_ms: 75.0, warm_inference_ms: 8.0,
warm_iterations: 100,
model_size_bytes: 1000,
target_arch: "aarch64".to_string(),
simd_backend: "NEON".to_string(),
meets_cold_start_target: false,
meets_warm_inference_target: true,
};
assert!(!result.meets_all_targets());
}
#[test]
fn test_lambda_error_display() {
assert_eq!(LambdaError::EmptyModel.to_string(), "Model bytes are empty");
assert_eq!(
LambdaError::EmptyFeatures.to_string(),
"Request features are empty"
);
assert_eq!(
LambdaError::EmptyBatch.to_string(),
"Batch request has no instances"
);
assert!(LambdaError::InvalidMagic {
expected: "APR\\0".to_string(),
found: "GGUF".to_string()
}
.to_string()
.contains("Invalid magic"));
}
#[test]
fn test_batch_request_serialization() {
let request = BatchLambdaRequest {
instances: vec![
LambdaRequest {
features: vec![1.0, 2.0],
model_id: None,
},
LambdaRequest {
features: vec![3.0, 4.0],
model_id: None,
},
],
max_parallelism: Some(4),
};
let json = serde_json::to_string(&request).expect("serialization failed");
assert!(json.contains("instances"));
assert!(json.contains("max_parallelism"));
assert!(json.contains("1.0"));
assert!(json.contains("3.0"));
}
#[test]
fn test_batch_response_serialization() {
let response = BatchLambdaResponse {
predictions: vec![LambdaResponse {
prediction: 3.0,
probabilities: None,
latency_ms: 1.5,
cold_start: false,
}],
total_latency_ms: 5.0,
success_count: 1,
error_count: 0,
};
let json = serde_json::to_string(&response).expect("serialization failed");
assert!(json.contains("predictions"));
assert!(json.contains("success_count"));
assert!(json.contains("total_latency_ms"));
}
#[test]
fn test_batch_handler_success() {
let model_bytes = create_sum_model(2);
let handler = LambdaHandler::from_bytes(model_bytes).unwrap();
let request = BatchLambdaRequest {
instances: vec![
LambdaRequest {
features: vec![1.0, 2.0],
model_id: None,
},
LambdaRequest {
features: vec![3.0, 4.0],
model_id: None,
},
],
max_parallelism: None,
};
let response = handler.handle_batch(&request).unwrap();
assert_eq!(response.predictions.len(), 2);
assert_eq!(response.success_count, 2);
assert_eq!(response.error_count, 0);
assert!(response.total_latency_ms >= 0.0);
assert!((response.predictions[0].prediction - 3.0).abs() < 0.001);
assert!((response.predictions[1].prediction - 7.0).abs() < 0.001);
}
#[test]
fn test_batch_handler_with_errors() {
let model_bytes = create_sum_model(2);
let handler = LambdaHandler::from_bytes(model_bytes).unwrap();
let request = BatchLambdaRequest {
instances: vec![
LambdaRequest {
features: vec![1.0, 2.0],
model_id: None,
},
LambdaRequest {
features: vec![], model_id: None,
},
LambdaRequest {
features: vec![5.0, 0.0],
model_id: None,
},
],
max_parallelism: None,
};
let response = handler.handle_batch(&request).unwrap();
assert_eq!(response.predictions.len(), 3);
assert_eq!(response.success_count, 2);
assert_eq!(response.error_count, 1);
assert!((response.predictions[0].prediction - 3.0).abs() < 0.001);
assert!(response.predictions[1].prediction.is_nan()); assert!((response.predictions[2].prediction - 5.0).abs() < 0.001);
}
#[test]
fn test_batch_handler_rejects_empty_batch() {
let model_bytes = create_sum_model(2);
let handler = LambdaHandler::from_bytes(model_bytes).unwrap();
let request = BatchLambdaRequest {
instances: vec![],
max_parallelism: None,
};
let result = handler.handle_batch(&request);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), LambdaError::EmptyBatch);
}
#[test]
fn test_metrics_new() {
let metrics = LambdaMetrics::new();
assert_eq!(metrics.requests_total, 0);
assert_eq!(metrics.requests_success, 0);
assert_eq!(metrics.requests_failed, 0);
assert_eq!(metrics.cold_starts, 0);
assert_eq!(metrics.batch_requests, 0);
}
#[test]
fn test_metrics_record_success() {
let mut metrics = LambdaMetrics::new();
metrics.record_success(5.0, true);
assert_eq!(metrics.requests_total, 1);
assert_eq!(metrics.requests_success, 1);
assert_eq!(metrics.cold_starts, 1);
assert!((metrics.latency_total_ms - 5.0).abs() < 0.001);
metrics.record_success(3.0, false);
assert_eq!(metrics.requests_total, 2);
assert_eq!(metrics.requests_success, 2);
assert_eq!(metrics.cold_starts, 1); assert!((metrics.latency_total_ms - 8.0).abs() < 0.001);
}
#[test]
fn test_metrics_record_failure() {
let mut metrics = LambdaMetrics::new();
metrics.record_failure();
assert_eq!(metrics.requests_total, 1);
assert_eq!(metrics.requests_failed, 1);
assert_eq!(metrics.requests_success, 0);
}
#[test]
fn test_metrics_record_batch() {
let mut metrics = LambdaMetrics::new();
metrics.record_batch(5, 2, 10.0);
assert_eq!(metrics.batch_requests, 1);
assert_eq!(metrics.requests_total, 7); assert_eq!(metrics.requests_success, 5);
assert_eq!(metrics.requests_failed, 2);
assert!((metrics.latency_total_ms - 10.0).abs() < 0.001);
}
#[test]
fn test_metrics_avg_latency() {
let mut metrics = LambdaMetrics::new();
assert!((metrics.avg_latency_ms() - 0.0).abs() < 0.001);
metrics.record_success(4.0, false);
metrics.record_success(6.0, false);
assert!((metrics.avg_latency_ms() - 5.0).abs() < 0.001);
}
#[test]
fn test_metrics_prometheus_format() {
let mut metrics = LambdaMetrics::new();
metrics.record_success(5.0, true);
metrics.record_success(3.0, false);
metrics.record_failure();
metrics.record_batch(10, 2, 20.0);
let prom = metrics.to_prometheus();
assert!(prom.contains("# HELP lambda_requests_total"));
assert!(prom.contains("# TYPE lambda_requests_total counter"));
assert!(prom.contains("lambda_requests_total 15")); assert!(prom.contains("lambda_requests_success 12")); assert!(prom.contains("lambda_requests_failed 3")); assert!(prom.contains("lambda_cold_starts 1"));
assert!(prom.contains("lambda_batch_requests 1"));
assert!(prom.contains("lambda_latency_avg_ms"));
}
#[test]
fn test_metrics_default() {
let metrics = LambdaMetrics::default();
assert_eq!(metrics.requests_total, 0);
assert_eq!(metrics.batch_requests, 0);
}
}