168 lines
6.3 KiB
Rust
Raw Normal View History

2024-10-17 15:27:08 +01:00
use std::{collections::BTreeMap, process::exit};
2024-10-17 15:05:05 +01:00
use std::error::Error;
use std::fs::File;
use std::io::Read;
2024-10-17 15:27:08 +01:00
use std::time::{SystemTime,Duration, UNIX_EPOCH};
use chrono::{DateTime,Utc};
use regex::Regex;
2024-10-17 15:05:05 +01:00
fn parse(
2024-10-17 15:27:08 +01:00
mut file: File,
) -> Result<BTreeMap<String, BTreeMap<SystemTime, f64>>, Box<dyn Error>> {
2024-10-17 15:05:05 +01:00
let mut contents = String::new();
file.read_to_string(&mut contents)?;
2024-10-17 15:27:08 +01:00
let re = regex::Regex::new(r"(\d+) (\w+) (\d+)")?;
Ok(parse_content(contents, re))
}
fn parse_content(
contents: String,
re: Regex,
) -> BTreeMap<String, BTreeMap<SystemTime, f64>> {
dbg!(&contents);
let mut metrics: BTreeMap<String, BTreeMap<SystemTime, Vec<f64>>> = BTreeMap::new();
let show_invalid_line = | index: usize, line: &str | println!("invalid line: {} {}", index, line);
2024-10-17 15:05:05 +01:00
2024-10-17 15:27:08 +01:00
for (index,line ) in contents.lines().enumerate() {
2024-10-17 15:05:05 +01:00
if let Some(caps) = re.captures(line) {
let timestamp_raw = &caps[1];
let metric_name = &caps[2];
let metric_value_raw = &caps[3];
2024-10-17 15:27:08 +01:00
let timestamp = timestamp_raw.parse::<i64>().unwrap_or_else(|e|{
println!("Parse timestamp {} error {}",timestamp_raw,e);
0
});
if timestamp == 0 {
show_invalid_line(index, line);
continue;
}
let metric_value = metric_value_raw.parse::<f64>().unwrap_or_else(|e|{
println!("Parse metric_value {} error {}",metric_value_raw,e);
0 as f64
});
if metric_value == 0 as f64 {
show_invalid_line(index, line);
continue;
}
2024-10-17 15:05:05 +01:00
let minute = UNIX_EPOCH + Duration::from_secs((timestamp - (timestamp % 60)) as u64);
2024-10-17 15:27:08 +01:00
if let Some(metric) = metrics.get_mut(metric_name) {
if let Some(metric_data) = metric.get_mut(&minute) {
metric_data.push(metric_value);
} else {
metric.insert(minute, vec![metric_value]);
}
} else {
let metric_time: BTreeMap <SystemTime, Vec<f64>> = [(minute, vec![metric_value])].into_iter().collect();
metrics.entry(metric_name.to_string()).or_insert( metric_time);
}
2024-10-17 15:05:05 +01:00
} else {
2024-10-17 15:27:08 +01:00
show_invalid_line(index, line);
2024-10-17 15:05:05 +01:00
}
}
2024-10-17 15:27:08 +01:00
let mut aggregated_metrics: BTreeMap<String, BTreeMap<SystemTime, f64>> = BTreeMap::new();
metrics.into_iter().for_each(|(metric_name, time_val_list)| {
time_val_list.into_iter().for_each(|(time, values)| {
let average = values.iter().sum::<f64>() / values.len() as f64;
if let Some(metric) = aggregated_metrics.get_mut(&metric_name) {
if let Some(metric_data) = metric.get_mut(&time) {
*metric_data = average;
} else {
metric.insert(time, average);
}
} else {
let metric_time: BTreeMap <SystemTime,f64> = [(time, average)].into_iter().collect();
aggregated_metrics.entry(metric_name.to_string()).or_insert( metric_time);
2024-10-17 15:05:05 +01:00
}
2024-10-17 15:27:08 +01:00
})
});
aggregated_metrics
}
2024-10-17 15:05:05 +01:00
2024-10-17 15:27:08 +01:00
fn load_input(file_path: &str) -> Result<BTreeMap<String, BTreeMap<SystemTime, f64>>, Box<dyn Error>> {
let file = File::open(&file_path)
.map_err(|err| format!("Error reading file: {} {}", &file_path, err))?;
let metrics = parse(file)
.map_err(|err| format!("Unable to parse: {} {}", &file_path, err))?;
Ok(metrics)
}
fn show_metrics(metrics: BTreeMap<String, BTreeMap<SystemTime, f64>>, output_path: &str) -> Vec<String> {
let mut output = Vec::new();
metrics.into_iter().for_each(|(metric_name, time_val)|
for (time, value) in time_val {
let output_line = format!(
"{} {} {:?}",
DateTime::<Utc>::from(time).format("%Y-%m-%dT%H:%M:%SZ"),
metric_name,
value
);
match output_path {
"vec" => output.push(output_line),
"print" | _ => println!("{}", output_line),
}
}
);
output
}
fn main() {
let default_input = String::from("input.txt");
match load_input(&default_input) {
Ok(metrics) => {
let _ = show_metrics(metrics, "");
},
Err(err) => {
eprint!("Error: {}", err);
exit(1);
2024-10-17 15:05:05 +01:00
}
}
}
2024-10-17 15:27:08 +01:00
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_load_input() -> Result<(), String>{
let default_input = String::from("input.txt");
match load_input(&default_input) {
Ok(_) => Ok(()),
Err(e) => Err(format!("Error: {}",e).into()),
2024-10-17 15:05:05 +01:00
}
}
2024-10-17 15:27:08 +01:00
#[test]
fn test_invalid_line_value() -> Result<(), String>{
let contents = String::from("1650973075 cpu A47\n");
let re = regex::Regex::new(r"(\d+) (\w+) (\d+)")
.map_err(|err| format!("Error regex: {}", err))?;
let result = parse_content(contents.clone(), re);
if result.len() == 0 {
Ok(())
} else {
Err(format!("Error invalid line value: {}", contents).into())
}
}
#[test]
fn test_invalid_line_time() -> Result<(), String>{
let contents = String::from("1650973075A cpu 47\n");
let re = regex::Regex::new(r"(\d+) (\w+) (\d+)")
.map_err(|err| format!("Error regex: {}", err))?;
let result = parse_content(contents.clone(), re);
if result.len() == 0 {
Ok(())
} else {
Err(format!("Error invalid line time: {}", contents).into())
}
}
#[test]
fn test_expected_metrics() {
use std::io::{prelude::*, BufReader};
let default_input = String::from("input.txt");
let metrics = load_input(&default_input).unwrap_or_default();
let data_metrics = show_metrics(metrics, "vec");
let expected_output = String::from("output_expected.txt");
let file = File::open(expected_output.clone()).expect(format!("no such file: {}", expected_output).as_str());
let buf = BufReader::new(file);
let lines: Vec<String> = buf.lines().map(|l| l.expect("Could not parse line")).collect();
assert_eq!(lines.join("\n"),data_metrics.join("\n"));
}
}