strest 0.1.10

Blazing-fast async HTTP load tester in Rust - lock-free design, real-time stats, distributed runs, and optional chart exports for high-load API testing.
Documentation
use std::path::Path;

use tokio::fs;
use tracing::{error, info};

use crate::args::TesterArgs;
use crate::error::AppResult;
use crate::metrics::{AggregatedMetricSample, StreamingChartData};

use super::super::{
    LatencyPercentilesSeries, plot_aggregated_average_response_time,
    plot_aggregated_cumulative_error_rate, plot_aggregated_cumulative_successful_requests,
    plot_aggregated_cumulative_total_requests, plot_aggregated_latency_percentiles,
    plot_aggregated_requests_per_second, plot_average_response_time_from_buckets,
    plot_cumulative_error_rate_from_buckets, plot_cumulative_successful_requests_from_buckets,
    plot_cumulative_total_requests_from_buckets, plot_error_rate_breakdown_from_counts,
    plot_inflight_requests_from_counts, plot_latency_percentiles_series,
    plot_requests_per_second_from_counts, plot_status_code_distribution_from_counts,
    plot_timeouts_per_second_from_counts,
};
use super::naming::resolve_chart_output_dir;

#[cfg(feature = "legacy-charts")]
use super::super::{
    plot_average_response_time, plot_cumulative_error_rate, plot_cumulative_successful_requests,
    plot_cumulative_total_requests, plot_error_rate_breakdown, plot_inflight_requests,
    plot_latency_percentiles, plot_requests_per_second, plot_status_code_distribution,
    plot_timeouts_per_second,
};
#[cfg(feature = "legacy-charts")]
use crate::metrics::MetricRecord;

#[cfg(feature = "legacy-charts")]
pub async fn plot_metrics(
    metrics: &[MetricRecord],
    args: &TesterArgs,
) -> AppResult<Option<String>> {
    if metrics.is_empty() {
        return Ok(None);
    }
    let output_dir = resolve_chart_output_dir(args);
    let path = output_dir.to_string_lossy().to_string();
    let expected_status_code = args.expected_status_code;

    if let Err(e) = fs::create_dir_all(Path::new(&path)).await {
        error!("Failed to create output directory '{}': {}", path, e);
        return Err(e.into());
    }

    info!("Plotting average response time...");

    plot_average_response_time(metrics, &format!("{}/average_response_time.png", path))?;

    info!("Plotting cumulative successful requests...");

    plot_cumulative_successful_requests(
        metrics,
        expected_status_code,
        &format!("{}/cumulative_successful_requests.png", path),
    )?;

    info!("Plotting cumulative error rate...");

    plot_cumulative_error_rate(
        metrics,
        expected_status_code,
        &format!("{}/cumulative_error_rate.png", path),
    )?;

    info!("Plotting latency percentiles...");

    plot_latency_percentiles(
        metrics,
        expected_status_code,
        &format!("{}/latency_percentiles", path),
    )?;

    info!("Plotting requests per second...");

    plot_requests_per_second(metrics, &format!("{}/requests_per_second.png", path))?;

    info!("Plotting timeouts per second...");

    plot_timeouts_per_second(metrics, &format!("{}/timeouts_per_second.png", path))?;

    info!("Plotting error rate breakdown...");

    plot_error_rate_breakdown(
        metrics,
        expected_status_code,
        &format!("{}/error_rate_breakdown.png", path),
    )?;

    info!("Plotting status code distribution...");

    plot_status_code_distribution(metrics, &format!("{}/status_code_distribution.png", path))?;

    info!("Plotting in-flight requests...");

    plot_inflight_requests(metrics, &format!("{}/inflight_requests.png", path))?;

    info!("Plotting cumulative total requests...");

    plot_cumulative_total_requests(metrics, &format!("{}/cumulative_total_requests.png", path))?;

    Ok(Some(path))
}

pub async fn plot_aggregated_metrics(
    samples: &[AggregatedMetricSample],
    args: &TesterArgs,
) -> AppResult<Option<String>> {
    if samples.is_empty() {
        return Ok(None);
    }
    let output_dir = resolve_chart_output_dir(args);
    let path = output_dir.to_string_lossy().to_string();

    if let Err(e) = fs::create_dir_all(Path::new(&path)).await {
        error!("Failed to create output directory '{}': {}", path, e);
        return Err(e.into());
    }

    info!("Plotting average response time (aggregated)...");
    plot_aggregated_average_response_time(samples, &format!("{}/average_response_time.png", path))?;

    info!("Plotting cumulative successful requests (aggregated)...");
    plot_aggregated_cumulative_successful_requests(
        samples,
        &format!("{}/cumulative_successful_requests.png", path),
    )?;

    info!("Plotting cumulative error rate (aggregated)...");
    plot_aggregated_cumulative_error_rate(samples, &format!("{}/cumulative_error_rate.png", path))?;

    info!("Plotting latency percentiles (aggregated)...");
    plot_aggregated_latency_percentiles(samples, &format!("{}/latency_percentiles", path))?;

    info!("Plotting requests per second (aggregated)...");
    plot_aggregated_requests_per_second(samples, &format!("{}/requests_per_second.png", path))?;

    info!("Plotting cumulative total requests (aggregated)...");
    plot_aggregated_cumulative_total_requests(
        samples,
        &format!("{}/cumulative_total_requests.png", path),
    )?;

    Ok(Some(path))
}

pub async fn plot_streaming_metrics(
    data: &StreamingChartData,
    args: &TesterArgs,
) -> AppResult<Option<String>> {
    if data.avg_buckets.is_empty() && data.rps_counts.is_empty() {
        return Ok(None);
    }
    let output_dir = resolve_chart_output_dir(args);
    let path = output_dir.to_string_lossy().to_string();

    if let Err(e) = fs::create_dir_all(Path::new(&path)).await {
        error!("Failed to create output directory '{}': {}", path, e);
        return Err(e.into());
    }

    info!("Plotting average response time...");
    plot_average_response_time_from_buckets(
        &data.avg_buckets,
        &format!("{}/average_response_time.png", path),
    )?;

    info!("Plotting cumulative successful requests...");
    plot_cumulative_successful_requests_from_buckets(
        &data.success_buckets,
        &format!("{}/cumulative_successful_requests.png", path),
    )?;

    info!("Plotting cumulative error rate...");
    plot_cumulative_error_rate_from_buckets(
        &data.error_buckets,
        &format!("{}/cumulative_error_rate.png", path),
    )?;

    info!("Plotting latency percentiles...");
    let percentiles = LatencyPercentilesSeries {
        buckets_ms: &data.latency_buckets_ms,
        bucket_ms: data.latency_bucket_ms,
        p50: &data.p50,
        p90: &data.p90,
        p99: &data.p99,
        p50_ok: &data.p50_ok,
        p90_ok: &data.p90_ok,
        p99_ok: &data.p99_ok,
    };
    plot_latency_percentiles_series(&percentiles, &format!("{}/latency_percentiles", path))?;

    info!("Plotting requests per second...");
    plot_requests_per_second_from_counts(
        &data.rps_counts,
        &format!("{}/requests_per_second.png", path),
    )?;

    info!("Plotting timeouts per second...");
    plot_timeouts_per_second_from_counts(
        &data.timeouts,
        &format!("{}/timeouts_per_second.png", path),
    )?;

    info!("Plotting error rate breakdown...");
    plot_error_rate_breakdown_from_counts(
        &data.timeouts,
        &data.transports,
        &data.non_expected,
        &format!("{}/error_rate_breakdown.png", path),
    )?;

    info!("Plotting status code distribution...");
    plot_status_code_distribution_from_counts(
        &data.status_2xx,
        &data.status_3xx,
        &data.status_4xx,
        &data.status_5xx,
        &data.status_other,
        &format!("{}/status_code_distribution.png", path),
    )?;

    info!("Plotting in-flight requests...");
    plot_inflight_requests_from_counts(&data.inflight, &format!("{}/inflight_requests.png", path))?;

    info!("Plotting cumulative total requests...");
    plot_cumulative_total_requests_from_buckets(
        &data.total_buckets,
        &format!("{}/cumulative_total_requests.png", path),
    )?;

    Ok(Some(path))
}