heater 0.2.3

simple sitemap-based cache-warming with header variations.
use anyhow::Result;
use clap::{crate_authors, crate_name, crate_version, App, Arg};
use console::style;
use log::info;
use url::Url;

mod config;
mod heater;
mod sitemaps;
mod status;

fn validate_header(input: String) -> Result<(), String> {
    config::parse_header(&input)
        .map(|_| ())
        .map_err(|e| format!("{}", e))
}

#[tokio::main]
pub async fn main() -> Result<()> {
    pretty_env_logger::init();

    let matches = App::new(crate_name!())
        .about("heats up website caches")
        .version(crate_version!())
        .author(crate_authors!())
        .arg(
            Arg::with_name("sitemap_url")
                .help("sitemap URL")
                .required(true)
                .index(1),
        )
        .arg(
            Arg::with_name("header_variation")
                .long("header")
                .value_name("HEADER:VALUE")
                .validator(validate_header)
                .multiple(true)
                .help("header variation"),
        )
        .arg(
            Arg::with_name("language")
                .long("language")
                .value_name("IEFT language tag")
                .multiple(true)
                .help(
                    "language tags will be used to generate all \
                    possible permutations of these languages, \
                    including their order",
                ),
        )
        .get_matches();

    let config = config::Config::new_from_arguments(&matches);

    let sitemap_url = matches.value_of("sitemap_url").unwrap();

    info!("fetching sitemap from {}", sitemap_url);

    let urls: Vec<Url> = sitemaps::get(sitemap_url).await?;

    info!("... found {} URLs", urls.len());
    status::initialize_progress(urls.len() as u64 * config.possible_variations());

    info!("running heater...");
    let (statuses, cache_hits, histogram) = heater::heat(&config, urls.iter().cloned()).await;

    if let Some(status) = status::get_progress() {
        status.finish_and_clear();
    }

    println!("{}", style("Summary").bold());

    println!("\t{}", style("Statuscodes:").bold());
    for (status, count) in statuses.iter() {
        println!("\t{:>10} => {:>5}", style(status).bold(), count);
    }

    println!();
    println!("\t{}", style("Response times:").bold());
    for p in &[50.0, 90.0, 99.0] {
        println!(
            "\tp{:.0}: {:>5}ms",
            style(p).bold(),
            histogram.percentile(*p).unwrap()
        );
    }

    if cache_hits.keys().any(|h| h.is_some()) {
        println!();
        println!("\t{}", style("CDN caching:").bold());

        if let Some(h) = cache_hits.get(&Some(true)) {
            println!("\t{:>4}: {:>7}", style("HIT").bold(), h);
        }
        if let Some(h) = cache_hits.get(&Some(false)) {
            println!("\t{:>4}: {:>7}", style("MISS").bold(), h);
        }
        if let Some(h) = cache_hits.get(&None) {
            println!("\t{}: {:>7}", style("UNKNOWN").italic(), h);
        }
    }

    Ok(())
}