215 lines
8.0 KiB
Rust
215 lines
8.0 KiB
Rust
use std::{
|
|
collections::BTreeMap,
|
|
error::Error,
|
|
time::{SystemTime,Duration, UNIX_EPOCH,Instant},
|
|
io::{prelude::*, BufReader},
|
|
fs::File,
|
|
thread::spawn,
|
|
sync::{ mpsc, mpsc::{Sender, Receiver}},
|
|
};
|
|
use chrono::{DateTime,Utc};
|
|
use regex::Regex;
|
|
|
|
type MetricMap = BTreeMap<String, BTreeMap<SystemTime, f64>>;
|
|
type MetricMapVec = BTreeMap<String, BTreeMap<SystemTime, Vec<f64>>>;
|
|
|
|
#[derive(Debug)]
|
|
struct MetricLine {
|
|
timestamp: i64,
|
|
name: String,
|
|
value: f64
|
|
}
|
|
|
|
fn show_invalid_line(index: usize, line: &str) {
|
|
println!("invalid line: {} {}", index, line);
|
|
}
|
|
fn parse_line(line: &str, index: usize, re: &Regex) -> Option<MetricLine> {
|
|
if let Some(caps) = re.captures(&line) {
|
|
let timestamp = match caps[1].parse::<i64>() {
|
|
Ok(value) => value,
|
|
Err(e) => {
|
|
println!("Parse timestamp {} error {}",&caps[1],e);
|
|
show_invalid_line(index, line);
|
|
return None;
|
|
}
|
|
};
|
|
let metric_value = match caps[3].parse::<f64>() {
|
|
Ok(value) => value,
|
|
Err(e) => {
|
|
println!("Parse metric_value {} error {}",&caps[3],e);
|
|
show_invalid_line(index, line);
|
|
return None;
|
|
}
|
|
};
|
|
Some( MetricLine {
|
|
timestamp,
|
|
name: caps[2].to_string(),
|
|
value: metric_value
|
|
})
|
|
} else {
|
|
show_invalid_line(index, &line);
|
|
None
|
|
}
|
|
}
|
|
fn parse(
|
|
file: File,
|
|
) -> Result<MetricMap, Box<dyn Error>> {
|
|
let re = Regex::new(r"(\d+) (\w+) (\d+)")?;
|
|
let mut metrics: MetricMapVec = BTreeMap::new();
|
|
let buf = BufReader::new(file);
|
|
buf.lines().enumerate().into_iter().for_each(|(index, read_line)|
|
|
match read_line {
|
|
Ok(line) => {
|
|
if let Some(metric_line) = parse_line(&line, index, &re) {
|
|
let minute =
|
|
UNIX_EPOCH + Duration::from_secs((metric_line.timestamp - (metric_line.timestamp % 60)) as u64);
|
|
if let Some(metric) = metrics.get_mut(&metric_line.name) {
|
|
if let Some(metric_data) = metric.get_mut(&minute) {
|
|
metric_data.push(metric_line.value);
|
|
} else {
|
|
metric.insert(minute, vec![metric_line.value]);
|
|
}
|
|
} else {
|
|
let metric_time: BTreeMap <SystemTime, Vec<f64>> = [(minute, vec![metric_line.value])].into_iter().collect();
|
|
metrics.entry(metric_line.name.to_string()).or_insert( metric_time);
|
|
}
|
|
}
|
|
},
|
|
Err(e) => {
|
|
eprintln!("Error reading line {}: {}", index, e);
|
|
},
|
|
}
|
|
);
|
|
let mut aggregated_metrics: MetricMap = 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);
|
|
}
|
|
})
|
|
});
|
|
Ok(aggregated_metrics)
|
|
}
|
|
fn load_input(file_path: &str) -> Result<MetricMap, 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 generate_metrics(inputs_list: Vec<String>) {
|
|
let n_items = inputs_list.len();
|
|
let mut input_threads = Vec::with_capacity(n_items);
|
|
let (tx, rx): (Sender<(String,MetricMap)>, Receiver<(String,MetricMap)>) = mpsc::channel();
|
|
for input in inputs_list.clone() {
|
|
let thread_tx = tx.clone();
|
|
input_threads.push(spawn(move || {
|
|
let start = Instant::now();
|
|
match load_input(&input) {
|
|
Ok(metrics) => {
|
|
let _ = thread_tx.send((input.clone(), metrics)).unwrap_or_default();
|
|
},
|
|
Err(err) => {
|
|
eprint!("Error: {}", err);
|
|
let _ = thread_tx.send((input.clone(), BTreeMap::new())).unwrap_or_default();
|
|
}
|
|
}
|
|
println!("\nProcessing {} took: {:?} ms", &input, start.elapsed().as_millis())
|
|
}));
|
|
}
|
|
let mut inputs_metrics= Vec::with_capacity(n_items);
|
|
for _ in 0..input_threads.len() {
|
|
match rx.recv() {
|
|
Ok(result) => inputs_metrics.push(result),
|
|
Err(e) => eprint!("Error: {}", e),
|
|
}
|
|
}
|
|
for thread in input_threads {
|
|
let _ = thread.join();
|
|
}
|
|
inputs_metrics.into_iter().for_each(|data_metrics|{
|
|
let (name, metrics) = data_metrics;
|
|
println!("\n{}: ---------------\n", name);
|
|
show_metrics(metrics, "");
|
|
});
|
|
}
|
|
fn main() {
|
|
let main_start = Instant::now();
|
|
let inputs_list = vec![String::from("input.txt"), String::from("input_2.txt")];
|
|
|
|
generate_metrics(inputs_list);
|
|
println!("\nALL Processing took: {:?} ms", main_start.elapsed().as_millis())
|
|
}
|
|
|
|
#[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()),
|
|
}
|
|
}
|
|
#[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))?;
|
|
match parse_line(&contents, 1, &re) {
|
|
Some(_) => Err(format!("Error invalid line value: {}", contents).into()),
|
|
None => Ok(()),
|
|
}
|
|
}
|
|
#[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))?;
|
|
match parse_line(&contents, 1, &re) {
|
|
Some(_) => Err(format!("Error invalid line value: {}", contents).into()),
|
|
None => Ok(()),
|
|
}
|
|
}
|
|
#[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"));
|
|
}
|
|
} |