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::PathBuf;

use tokio::io::{AsyncBufReadExt, BufReader};

use crate::error::{AppError, AppResult, MetricsError};
use crate::metrics;

pub(super) async fn load_log_records(
    paths: &[PathBuf],
    metrics_range: &Option<metrics::MetricsRange>,
    metrics_max: usize,
) -> AppResult<(Vec<metrics::MetricRecord>, bool)> {
    let mut records: Vec<metrics::MetricRecord> = Vec::new();
    let mut metrics_truncated = false;

    for path in paths {
        let file = tokio::fs::File::open(path).await.map_err(|err| {
            AppError::metrics(MetricsError::Io {
                context: "open metrics log",
                source: err,
            })
        })?;
        let mut reader = BufReader::new(file);
        let mut line = String::new();

        loop {
            line.clear();
            let bytes = reader.read_line(&mut line).await.map_err(|err| {
                AppError::metrics(MetricsError::Io {
                    context: "read metrics log",
                    source: err,
                })
            })?;
            if bytes == 0 {
                break;
            }

            let trimmed = line.trim_end();
            if trimmed.is_empty() {
                continue;
            }
            let mut parts = trimmed.split(',');
            let elapsed_ms = match parts.next().and_then(|value| value.parse::<u64>().ok()) {
                Some(value) => value,
                None => continue,
            };
            let latency_ms = match parts.next().and_then(|value| value.parse::<u64>().ok()) {
                Some(value) => value,
                None => continue,
            };
            let status_code = match parts.next().and_then(|value| value.parse::<u16>().ok()) {
                Some(value) => value,
                None => continue,
            };
            let timed_out = parts
                .next()
                .and_then(|value| value.parse::<u8>().ok())
                .is_some_and(|value| value != 0);
            let transport_error = parts
                .next()
                .and_then(|value| value.parse::<u8>().ok())
                .is_some_and(|value| value != 0);
            let response_bytes = parts
                .next()
                .and_then(|value| value.parse::<u64>().ok())
                .unwrap_or(0);
            let in_flight_ops = parts
                .next()
                .and_then(|value| value.parse::<u64>().ok())
                .unwrap_or(0);

            let seconds_elapsed = elapsed_ms / 1000;
            let in_range = match metrics_range {
                Some(metrics::MetricsRange(range)) => range.contains(&seconds_elapsed),
                None => true,
            };
            if !in_range {
                continue;
            }

            if metrics_max == 0 || records.len() < metrics_max {
                records.push(metrics::MetricRecord {
                    elapsed_ms,
                    latency_ms,
                    status_code,
                    timed_out,
                    transport_error,
                    response_bytes,
                    in_flight_ops,
                });
            } else {
                metrics_truncated = true;
                break;
            }
        }

        if metrics_truncated && metrics_max > 0 {
            break;
        }
    }

    if metrics_max > 0 && records.len() > metrics_max {
        records.truncate(metrics_max);
        metrics_truncated = true;
    }
    records.sort_by_key(|record| record.elapsed_ms);

    Ok((records, metrics_truncated))
}