use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::io::{BufRead, Write};
use std::time::Duration;
use crate::models::{Metrics, Result as AttackResult};
use crate::utils::{format_duration, format_size, get_reader, get_writer};
pub async fn run(
buckets: Option<String>,
every: Option<humantime::Duration>,
output: String,
report_type: String,
) -> Result<()> {
let reader = get_reader("stdin")?;
let mut writer = get_writer(&output)?;
let buckets = match buckets {
Some(b) => parse_buckets(&b)?,
None => vec![],
};
if report_type.starts_with("hist[") && report_type.ends_with("]") {
let buckets_str = &report_type[5..report_type.len() - 1];
let buckets = parse_buckets(buckets_str)?;
generate_histogram_report(reader, &mut writer, &buckets)?;
} else {
match report_type.as_str() {
"text" => generate_text_report(reader, &mut writer, every)?,
"json" => generate_json_report(reader, &mut writer, every)?,
"hdrplot" => generate_hdrplot_report(reader, &mut writer)?,
_ => anyhow::bail!("Unsupported report type: {}", report_type),
}
}
Ok(())
}
fn parse_buckets(buckets_str: &str) -> Result<Vec<Duration>> {
let inner = buckets_str.trim_start_matches('[').trim_end_matches(']');
let parts: Vec<&str> = inner.split(',').collect();
let mut buckets = Vec::new();
for part in parts {
let part = part.trim();
if part == "0" {
buckets.push(Duration::from_secs(0));
} else {
let duration = humantime::parse_duration(part)
.map_err(|_| anyhow::anyhow!("Invalid duration: {}", part))?;
buckets.push(duration.into());
}
}
Ok(buckets)
}
fn generate_text_report<R: BufRead, W: Write>(
reader: R,
writer: &mut W,
interval: Option<humantime::Duration>,
) -> Result<()> {
let results: Vec<AttackResult> = reader
.lines()
.filter_map(|line| {
let line = line.ok()?;
serde_json::from_str(&line).ok()
})
.collect();
if results.is_empty() {
writeln!(writer, "No results to report")?;
return Ok(());
}
let metrics = calculate_metrics(&results);
writeln!(writer, "Requests:\t{}", metrics.requests)?;
writeln!(writer, "Duration:\t{}", format_duration(metrics.duration))?;
writeln!(writer, "Rate:\t\t{:.2} req/s", metrics.rate)?;
writeln!(writer, "Success:\t{} ({:.2}%)", metrics.success, metrics.success_rate * 100.0)?;
writeln!(writer, "Min:\t\t{}", format_duration(metrics.min))?;
writeln!(writer, "Mean:\t\t{}", format_duration(metrics.mean))?;
writeln!(writer, "50th percentile:\t{}", format_duration(metrics.p50))?;
writeln!(writer, "90th percentile:\t{}", format_duration(metrics.p90))?;
writeln!(writer, "95th percentile:\t{}", format_duration(metrics.p95))?;
writeln!(writer, "99th percentile:\t{}", format_duration(metrics.p99))?;
writeln!(writer, "Max:\t\t{}", format_duration(metrics.max))?;
writeln!(writer, "Bytes in:\t{}", format_size(metrics.bytes_in))?;
writeln!(writer, "Bytes out:\t{}", format_size(metrics.bytes_out))?;
Ok(())
}
fn generate_json_report<R: BufRead, W: Write>(
reader: R,
writer: &mut W,
interval: Option<humantime::Duration>,
) -> Result<()> {
let results: Vec<AttackResult> = reader
.lines()
.filter_map(|line| {
let line = line.ok()?;
serde_json::from_str(&line).ok()
})
.collect();
if results.is_empty() {
writeln!(writer, "{{}}")?;
return Ok(());
}
let metrics = calculate_metrics(&results);
serde_json::to_writer_pretty(writer, &metrics)?;
Ok(())
}
fn generate_histogram_report<R: BufRead, W: Write>(
reader: R,
writer: &mut W,
buckets: &[Duration],
) -> Result<()> {
let results: Vec<AttackResult> = reader
.lines()
.filter_map(|line| {
let line = line.ok()?;
serde_json::from_str(&line).ok()
})
.collect();
if results.is_empty() {
writeln!(writer, "No results to report")?;
return Ok(());
}
let latencies: Vec<u64> = results
.iter()
.map(|r| r.latency.as_micros() as u64)
.collect();
writeln!(writer, "Bucket\t\tCount\t\tPercentage")?;
let mut prev_bucket = 0;
for bucket in buckets {
let micros = bucket.as_micros() as u64;
let count = latencies.iter()
.filter(|&&lat| lat >= prev_bucket && lat < micros)
.count();
let percentage = (count as f64 / results.len() as f64) * 100.0;
writeln!(
writer,
"[{} - {}]\t{}\t\t{:.2}%",
format_duration(Duration::from_micros(prev_bucket)),
format_duration(*bucket),
count,
percentage
)?;
prev_bucket = micros;
}
let count = latencies.iter()
.filter(|&&lat| lat >= prev_bucket)
.count();
let percentage = (count as f64 / results.len() as f64) * 100.0;
writeln!(
writer,
"[{} - inf]\t{}\t\t{:.2}%",
format_duration(Duration::from_micros(prev_bucket)),
count,
percentage
)?;
Ok(())
}
fn generate_hdrplot_report<R: BufRead, W: Write>(
reader: R,
writer: &mut W,
) -> Result<()> {
let results: Vec<AttackResult> = reader
.lines()
.filter_map(|line| {
let line = line.ok()?;
serde_json::from_str(&line).ok()
})
.collect();
if results.is_empty() {
writeln!(writer, "No results to report")?;
return Ok(());
}
let mut latencies: Vec<Duration> = results.iter().map(|r| r.latency).collect();
latencies.sort();
let percentiles = [
0.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 95.0, 99.0, 99.9, 99.99, 100.0,
];
writeln!(writer, "Percentile\tLatency")?;
for p in percentiles {
let value = percentile(&latencies, p / 100.0);
writeln!(
writer,
"{:.2}%\t\t{}",
p,
format_duration(value)
)?;
}
Ok(())
}
fn calculate_metrics(results: &[AttackResult]) -> Metrics {
if results.is_empty() {
return Metrics {
requests: 0,
success: 0,
duration: Duration::from_secs(0),
min: Duration::from_secs(0),
max: Duration::from_secs(0),
mean: Duration::from_secs(0),
p50: Duration::from_secs(0),
p90: Duration::from_secs(0),
p95: Duration::from_secs(0),
p99: Duration::from_secs(0),
rate: 0.0,
bytes_in: 0,
bytes_out: 0,
success_rate: 0.0,
};
}
let mut sorted_latencies: Vec<Duration> = results.iter().map(|r| r.latency).collect();
sorted_latencies.sort();
let requests = results.len();
let success = results.iter().filter(|r| r.status_code >= 200 && r.status_code < 300).count();
let first_timestamp = results.iter().map(|r| r.timestamp).min().unwrap();
let last_timestamp = results.iter().map(|r| r.timestamp).max().unwrap();
let duration = Duration::from_secs((last_timestamp - first_timestamp).num_seconds() as u64);
let min = *sorted_latencies.first().unwrap();
let max = *sorted_latencies.last().unwrap();
let mean = if requests > 0 {
let sum: Duration = sorted_latencies.iter().sum();
sum / requests as u32
} else {
Duration::from_secs(0)
};
let p50 = percentile(&sorted_latencies, 0.5);
let p90 = percentile(&sorted_latencies, 0.9);
let p95 = percentile(&sorted_latencies, 0.95);
let p99 = percentile(&sorted_latencies, 0.99);
let rate = if duration.as_secs_f64() > 0.0 {
requests as f64 / duration.as_secs_f64()
} else {
0.0
};
let bytes_in: usize = results.iter().map(|r| r.bytes_in).sum();
let bytes_out: usize = results.iter().map(|r| r.bytes_out).sum();
let success_rate = if requests > 0 {
success as f64 / requests as f64
} else {
0.0
};
Metrics {
requests,
success,
duration,
min,
max,
mean,
p50,
p90,
p95,
p99,
rate,
bytes_in,
bytes_out,
success_rate,
}
}
fn percentile(sorted: &[Duration], p: f64) -> Duration {
if sorted.is_empty() {
return Duration::from_secs(0);
}
let index = (sorted.len() as f64 * p).ceil() as usize - 1;
sorted[index.min(sorted.len() - 1)]
}