cargo-bill 0.1.5

A Rust CLI tool that estimates AWS Lambda costs based on the compiled binary size.
mod analysis_engine;
mod builder;
mod cli;
mod pricing_provider;

use anyhow::Result;
use cli::{parse_args, BillSubcommands};
use prettytable::{format, row, Table};
use serde_json::json;
use tracing::{info, warn};

#[tokio::main]
async fn main() -> Result<()> {
    let args = parse_args();

    let BillSubcommands::Lambda(ref lambda_args) = args.command;
    if !lambda_args.json {
        tracing_subscriber::fmt::init();
    }

    match args.command {
        BillSubcommands::Lambda(lambda_args) => {
            if !lambda_args.json {
                info!("Initializing cargo-bill for AWS Lambda cost estimation...");
                info!(
                    "Region: {}, Memory: {} MB, Executions: {}, Architecture: {}",
                    lambda_args.region,
                    lambda_args.memory,
                    lambda_args.executions,
                    lambda_args.architecture
                );
            }

            let (binary_path, metadata) = builder::execute_build(lambda_args.json)?;

            let analysis = analysis_engine::analyze_binary(&binary_path)?;

            let stripped_str = if analysis.is_stripped { "Yes" } else { "No" };
            let costs = pricing_provider::calculate_costs(
                analysis.size_mb,
                lambda_args.executions,
                lambda_args.memory,
                &lambda_args.region,
                &lambda_args.architecture,
                lambda_args.include_free_tier,
                lambda_args.provisioned_concurrency,
            )
            .await;

            if std::env::consts::OS != "linux" && !lambda_args.json {
                warn!(
                    "You are building on '{}'. The binary size might differ slightly from the actual Amazon Linux (ELF) deployment.",
                    std::env::consts::OS
                );
                warn!("For strict production accuracy, consider cross-compiling with 'cargo-lambda' or 'cross'.");
            }

            if analysis.size_mb > 30.0 && !lambda_args.json {
                let has_heavy_deps = metadata.packages.iter().any(|p| p.name.contains("aws-sdk"));
                if has_heavy_deps {
                    warn!(
                        "Your binary is unusually large ({:.2} MB).",
                        analysis.size_mb
                    );
                    warn!("You are compiling aws-sdk crates (e.g., aws-sdk-s3 or aws-sdk-pricing) potentially with all features enabled.");
                    warn!("Consider using default-features = false to reduce Cold Start time.");
                } else {
                    warn!(
                        "Your binary is unusually large ({:.2} MB).",
                        analysis.size_mb
                    );
                    warn!("Consider optimizing your dependencies to reduce Cold Start time.");
                }
            }

            if lambda_args.json {
                let output = json!({
                    "metadata": {
                        "binary_size_mb": analysis.size_mb,
                        "architecture": lambda_args.architecture,
                        "stripped": analysis.is_stripped,
                        "has_debug_symbols": analysis.has_debug_symbols,
                        "executions": lambda_args.executions,
                        "memory_mb": lambda_args.memory,
                        "region": lambda_args.region,
                        "include_free_tier": lambda_args.include_free_tier,
                        "provisioned_concurrency": lambda_args.provisioned_concurrency
                    },
                    "estimation": costs
                });
                println!("{}", serde_json::to_string_pretty(&output)?);
                return Ok(());
            }

            let mut table = Table::new();
            table.set_format(*format::consts::FORMAT_BORDERS_ONLY);
            table.set_titles(row!["Metric", "Value"]);
            table.add_row(row!["Binary Size (MB)", format!("{:.2}", analysis.size_mb)]);
            table.add_row(row!["Architecture", lambda_args.architecture]);
            table.add_row(row!["Stripped", stripped_str]);
            table.add_row(row![
                "Has Debug Symbols",
                if analysis.has_debug_symbols {
                    "Yes"
                } else {
                    "No"
                }
            ]);
            table.add_row(row![
                "Estimated Monthly Storage Cost",
                format!("${:.4}", costs.storage_cost_monthly)
            ]);
            table.add_row(row![
                format!("Estimated Cost per {} Requests", lambda_args.executions),
                format!("${:.4}", costs.compute_cost_1m)
            ]);
            table.add_row(row![
                "Predicted Cold Start Latency",
                format!("{:.2} ms", costs.predicted_cold_start_ms)
            ]);
            table.add_row(row![
                "Dynamic API Pricing Used",
                if costs.dynamic_pricing_used {
                    "Yes"
                } else {
                    "No (Fallback)"
                }
            ]);

            info!("\nAWS Lambda Cost Estimation Report:");
            table.printstd();
        }
    }

    Ok(())
}