13 changed files with 555 additions and 31 deletions
|
After Width: | Height: | Size: 46 KiB |
@ -1,9 +1,36 @@
|
||||
pub use self::histogram::Histogram; |
||||
pub use self::matchbar::{MatchBar, MatchBarRow}; |
||||
pub use self::splittimehist::SplitTimeHistogram; |
||||
pub use self::timehist::TimeHistogram; |
||||
pub use self::xy::XyPlot; |
||||
|
||||
mod histogram; |
||||
mod matchbar; |
||||
mod splittimehist; |
||||
mod timehist; |
||||
mod xy; |
||||
|
||||
/// Returns a datetime formating string with a resolution that makes sense for a
|
||||
/// given number of seconds
|
||||
fn date_fmt_string(seconds: i64) -> &'static str { |
||||
match seconds { |
||||
x if x > 86400 => "%Y-%m-%d %H:%M:%S", |
||||
x if x > 300 => "%H:%M:%S", |
||||
x if x > 1 => "%H:%M:%S%.3f", |
||||
_ => "%H:%M:%S%.6f", |
||||
} |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
mod tests { |
||||
|
||||
use super::*; |
||||
|
||||
#[test] |
||||
fn test_fmt_strings() { |
||||
assert_eq!(date_fmt_string(100000), "%Y-%m-%d %H:%M:%S"); |
||||
assert_eq!(date_fmt_string(1000), "%H:%M:%S"); |
||||
assert_eq!(date_fmt_string(10), "%H:%M:%S%.3f"); |
||||
assert_eq!(date_fmt_string(0), "%H:%M:%S%.6f"); |
||||
} |
||||
} |
||||
|
||||
@ -0,0 +1,213 @@
|
||||
use std::fmt; |
||||
|
||||
use chrono::{DateTime, Duration, FixedOffset}; |
||||
use yansi::Color::{Blue, Cyan, Green, Magenta, Red}; |
||||
|
||||
use crate::plot::date_fmt_string; |
||||
|
||||
const COLORS: &[yansi::Color] = &[Red, Blue, Magenta, Green, Cyan]; |
||||
|
||||
#[derive(Debug)] |
||||
struct TimeBucket { |
||||
start: DateTime<FixedOffset>, |
||||
count: Vec<usize>, |
||||
} |
||||
|
||||
impl TimeBucket { |
||||
fn new(start: DateTime<FixedOffset>, counts: usize) -> TimeBucket { |
||||
TimeBucket { |
||||
start, |
||||
count: vec![0; counts], |
||||
} |
||||
} |
||||
|
||||
fn inc(&mut self, index: usize) { |
||||
self.count[index] += 1; |
||||
} |
||||
|
||||
fn total(&self) -> usize { |
||||
self.count.iter().sum::<usize>() |
||||
} |
||||
} |
||||
|
||||
#[derive(Debug)] |
||||
pub struct SplitTimeHistogram { |
||||
vec: Vec<TimeBucket>, |
||||
strings: Vec<String>, |
||||
min: DateTime<FixedOffset>, |
||||
max: DateTime<FixedOffset>, |
||||
step: Duration, |
||||
last: usize, |
||||
nanos: u64, |
||||
} |
||||
|
||||
impl SplitTimeHistogram { |
||||
pub fn new( |
||||
size: usize, |
||||
strings: Vec<String>, |
||||
ts: &[(DateTime<FixedOffset>, usize)], |
||||
) -> SplitTimeHistogram { |
||||
let mut vec = Vec::<TimeBucket>::with_capacity(size); |
||||
let min = ts.iter().min().unwrap().0; |
||||
let max = ts.iter().max().unwrap().0; |
||||
let step = max - min; |
||||
let inc = step / size as i32; |
||||
for i in 0..size { |
||||
vec.push(TimeBucket::new(min + (inc * i as i32), strings.len())); |
||||
} |
||||
let mut sth = SplitTimeHistogram { |
||||
vec, |
||||
strings, |
||||
min, |
||||
max, |
||||
step, |
||||
last: size - 1, |
||||
nanos: (max - min).num_microseconds().unwrap() as u64, |
||||
}; |
||||
sth.load(ts); |
||||
sth |
||||
} |
||||
|
||||
fn load(&mut self, vec: &[(DateTime<FixedOffset>, usize)]) { |
||||
for x in vec { |
||||
self.add(x.0, x.1); |
||||
} |
||||
} |
||||
|
||||
fn add(&mut self, ts: DateTime<FixedOffset>, index: usize) { |
||||
if let Some(slot) = self.find_slot(ts) { |
||||
self.vec[slot].inc(index); |
||||
} |
||||
} |
||||
|
||||
fn find_slot(&self, ts: DateTime<FixedOffset>) -> Option<usize> { |
||||
if ts < self.min || ts > self.max { |
||||
None |
||||
} else { |
||||
let x = (ts - self.min).num_microseconds().unwrap() as u64; |
||||
Some(((x * self.vec.len() as u64 / self.nanos) as usize).min(self.last)) |
||||
} |
||||
} |
||||
|
||||
// Clippy gets badly confused necause self.strings and COLORS may have
|
||||
// different lengths
|
||||
#[allow(clippy::needless_range_loop)] |
||||
fn fmt_row( |
||||
&self, |
||||
f: &mut fmt::Formatter, |
||||
row: &TimeBucket, |
||||
divisor: usize, |
||||
widths: &[usize], |
||||
ts_fmt: &str, |
||||
) -> fmt::Result { |
||||
write!( |
||||
f, |
||||
"[{}] [", |
||||
Blue.paint(format!("{}", row.start.format(ts_fmt))) |
||||
)?; |
||||
for i in 0..self.strings.len() { |
||||
write!( |
||||
f, |
||||
"{}", |
||||
COLORS[i].paint(format!("{:width$}", row.count[i], width = widths[i])) |
||||
)?; |
||||
if i < self.strings.len() - 1 { |
||||
write!(f, "/")?; |
||||
} |
||||
} |
||||
write!(f, "] ")?; |
||||
for i in 0..self.strings.len() { |
||||
write!( |
||||
f, |
||||
"{}", |
||||
COLORS[i].paint("∎".repeat(row.count[i] / divisor).to_string()) |
||||
)?; |
||||
} |
||||
writeln!(f) |
||||
} |
||||
} |
||||
|
||||
impl fmt::Display for SplitTimeHistogram { |
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
||||
let width = f.width().unwrap_or(100); |
||||
let total = self.vec.iter().map(|r| r.total()).sum::<usize>(); |
||||
let top = self.vec.iter().map(|r| r.total()).max().unwrap_or(1); |
||||
let divisor = 1.max(top / width); |
||||
// These are the widths of every count column
|
||||
let widths: Vec<usize> = (0..self.strings.len()) |
||||
.map(|i| { |
||||
self.vec |
||||
.iter() |
||||
.map(|r| r.count[i].to_string().len()) |
||||
.max() |
||||
.unwrap() |
||||
}) |
||||
.collect(); |
||||
|
||||
writeln!(f, "Matches: {}.", total)?; |
||||
for (i, s) in self.strings.iter().enumerate() { |
||||
let total = self.vec.iter().map(|r| r.count[i]).sum::<usize>(); |
||||
writeln!(f, "{}: {}.", COLORS[i].paint(s), total)?; |
||||
} |
||||
writeln!( |
||||
f, |
||||
"Each {} represents a count of {}", |
||||
Red.paint("∎"), |
||||
divisor |
||||
)?; |
||||
let ts_fmt = date_fmt_string(self.step.num_seconds()); |
||||
for row in self.vec.iter() { |
||||
self.fmt_row(f, row, divisor, &widths, ts_fmt)?; |
||||
} |
||||
Ok(()) |
||||
} |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
mod tests { |
||||
use super::*; |
||||
use yansi::Paint; |
||||
|
||||
#[test] |
||||
fn test_big_time_interval() { |
||||
Paint::disable(); |
||||
let mut vec = Vec::<(DateTime<FixedOffset>, usize)>::new(); |
||||
vec.push(( |
||||
DateTime::parse_from_rfc3339("2021-04-15T04:25:00+00:00").unwrap(), |
||||
1, |
||||
)); |
||||
vec.push(( |
||||
DateTime::parse_from_rfc3339("2022-04-15T04:25:00+00:00").unwrap(), |
||||
1, |
||||
)); |
||||
vec.push(( |
||||
DateTime::parse_from_rfc3339("2022-04-15T04:25:00+00:00").unwrap(), |
||||
0, |
||||
)); |
||||
vec.push(( |
||||
DateTime::parse_from_rfc3339("2022-04-15T04:25:00+00:00").unwrap(), |
||||
2, |
||||
)); |
||||
for _ in 0..11 { |
||||
vec.push(( |
||||
DateTime::parse_from_rfc3339("2023-04-15T04:25:00+00:00").unwrap(), |
||||
2, |
||||
)); |
||||
} |
||||
let th = SplitTimeHistogram::new( |
||||
3, |
||||
vec!["one".to_string(), "two".to_string(), "three".to_string()], |
||||
&vec, |
||||
); |
||||
println!("{}", th); |
||||
let display = format!("{}", th); |
||||
assert!(display.contains("Matches: 15")); |
||||
assert!(display.contains("one: 1.")); |
||||
assert!(display.contains("two: 2.")); |
||||
assert!(display.contains("three: 12.")); |
||||
assert!(display.contains("represents a count of 1")); |
||||
assert!(display.contains("[2021-04-15 04:25:00] [0/1/ 0] ∎\n")); |
||||
assert!(display.contains("[2021-12-14 12:25:00] [1/1/ 1] ∎∎∎\n")); |
||||
assert!(display.contains("[2022-08-14 20:25:00] [0/0/11] ∎∎∎∎∎∎∎∎∎∎∎\n")); |
||||
} |
||||
} |
||||
@ -0,0 +1,169 @@
|
||||
use std::io::BufRead; |
||||
|
||||
use chrono::{DateTime, FixedOffset}; |
||||
|
||||
use crate::read::dateparser::LogDateParser; |
||||
use crate::read::open_file; |
||||
|
||||
#[derive(Default, Builder)] |
||||
pub struct SplitTimeReader { |
||||
#[builder(setter(strip_option), default)] |
||||
matches: Vec<String>, |
||||
#[builder(setter(strip_option), default)] |
||||
ts_format: Option<String>, |
||||
} |
||||
|
||||
impl SplitTimeReader { |
||||
pub fn read(&self, path: &str) -> Vec<(DateTime<FixedOffset>, usize)> { |
||||
let mut vec: Vec<(DateTime<FixedOffset>, usize)> = Vec::new(); |
||||
let mut iterator = open_file(path).lines(); |
||||
let first_line = match iterator.next() { |
||||
Some(Ok(as_string)) => as_string, |
||||
Some(Err(error)) => { |
||||
error!("{}", error); |
||||
return vec; |
||||
} |
||||
_ => return vec, |
||||
}; |
||||
let parser = match LogDateParser::new(&first_line, &self.ts_format) { |
||||
Ok(p) => p, |
||||
Err(error) => { |
||||
error!("Could not figure out parsing strategy: {}", error); |
||||
return vec; |
||||
} |
||||
}; |
||||
if let Ok(x) = parser.parse(&first_line) { |
||||
self.push_conditionally(x, &mut vec, &first_line); |
||||
} |
||||
for line in iterator { |
||||
match line { |
||||
Ok(string) => { |
||||
if let Ok(x) = parser.parse(&string) { |
||||
self.push_conditionally(x, &mut vec, &string); |
||||
} |
||||
} |
||||
Err(error) => error!("{}", error), |
||||
} |
||||
} |
||||
vec |
||||
} |
||||
|
||||
fn push_conditionally( |
||||
&self, |
||||
d: DateTime<FixedOffset>, |
||||
vec: &mut Vec<(DateTime<FixedOffset>, usize)>, |
||||
line: &str, |
||||
) { |
||||
for (i, s) in self.matches.iter().enumerate() { |
||||
if line.contains(s) { |
||||
vec.push((d, i)); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
mod tests { |
||||
|
||||
use super::*; |
||||
use std::io::Write; |
||||
use tempfile::NamedTempFile; |
||||
|
||||
#[test] |
||||
fn split_time_reader_basic() { |
||||
let mut builder = SplitTimeReaderBuilder::default(); |
||||
builder.matches(vec![ |
||||
"foo".to_string(), |
||||
"bar".to_string(), |
||||
"gnat".to_string(), |
||||
]); |
||||
let reader = builder.build().unwrap(); |
||||
let mut file = NamedTempFile::new().unwrap(); |
||||
writeln!(file, "[2021-04-15T06:25:31+00:00] foo").unwrap(); |
||||
writeln!(file, "[2021-04-15T06:26:31+00:00] bar").unwrap(); |
||||
writeln!(file, "[2021-04-15T06:27:31+00:00] foobar").unwrap(); |
||||
writeln!(file, "[2021-04-15T06:28:31+00:00] none").unwrap(); |
||||
writeln!(file, "[2021-04-15T06:29:31+00:00] foo").unwrap(); |
||||
writeln!(file, "[2021-04-15T06:30:31+00:00] none again").unwrap(); |
||||
writeln!(file, "not even a timestamp").unwrap(); |
||||
let ts = reader.read(file.path().to_str().unwrap()); |
||||
assert_eq!(ts.len(), 5); |
||||
assert_eq!( |
||||
ts[0].0, |
||||
DateTime::parse_from_rfc3339("2021-04-15T06:25:31+00:00").unwrap() |
||||
); |
||||
assert_eq!( |
||||
ts[1].0, |
||||
DateTime::parse_from_rfc3339("2021-04-15T06:26:31+00:00").unwrap() |
||||
); |
||||
assert_eq!( |
||||
ts[2].0, |
||||
DateTime::parse_from_rfc3339("2021-04-15T06:27:31+00:00").unwrap() |
||||
); |
||||
assert_eq!( |
||||
ts[3].0, |
||||
DateTime::parse_from_rfc3339("2021-04-15T06:27:31+00:00").unwrap() |
||||
); |
||||
assert_eq!( |
||||
ts[4].0, |
||||
DateTime::parse_from_rfc3339("2021-04-15T06:29:31+00:00").unwrap() |
||||
); |
||||
assert_eq!(ts[0].1, 0); |
||||
assert_eq!(ts[1].1, 1); |
||||
assert_eq!(ts[2].1, 0); |
||||
assert_eq!(ts[3].1, 1); |
||||
assert_eq!(ts[4].1, 0); |
||||
} |
||||
|
||||
#[test] |
||||
fn split_time_no_matches() { |
||||
let reader = SplitTimeReader::default(); |
||||
let mut file = NamedTempFile::new().unwrap(); |
||||
writeln!(file, "[2021-04-15T06:25:31+00:00] foo").unwrap(); |
||||
writeln!(file, "[2021-04-15T06:26:31+00:00] bar").unwrap(); |
||||
let ts = reader.read(file.path().to_str().unwrap()); |
||||
assert_eq!(ts.len(), 0); |
||||
} |
||||
|
||||
#[test] |
||||
fn split_time_zero_matches() { |
||||
let mut builder = SplitTimeReaderBuilder::default(); |
||||
builder.matches(vec![ |
||||
"foo".to_string(), |
||||
"bar".to_string(), |
||||
"gnat".to_string(), |
||||
]); |
||||
builder.ts_format(String::from("%Y_%m_%d %H:%M")); |
||||
let reader = builder.build().unwrap(); |
||||
let mut file = NamedTempFile::new().unwrap(); |
||||
writeln!(file, "_2021_04_15 06:25] none").unwrap(); |
||||
writeln!(file, "_2021_04_15 06:26] none").unwrap(); |
||||
writeln!(file, "[2021-04-15T06:25:31+00:00] foo").unwrap(); |
||||
let ts = reader.read(file.path().to_str().unwrap()); |
||||
assert_eq!(ts.len(), 0); |
||||
} |
||||
|
||||
#[test] |
||||
fn split_time_bad_guess() { |
||||
let mut builder = SplitTimeReaderBuilder::default(); |
||||
builder.matches(vec![ |
||||
"foo".to_string(), |
||||
"bar".to_string(), |
||||
"gnat".to_string(), |
||||
]); |
||||
let reader = builder.build().unwrap(); |
||||
let mut file = NamedTempFile::new().unwrap(); |
||||
writeln!(file, "XXX none").unwrap(); |
||||
writeln!(file, "[2021-04-15T06:25:31+00:00] foo").unwrap(); |
||||
let ts = reader.read(file.path().to_str().unwrap()); |
||||
assert_eq!(ts.len(), 0); |
||||
} |
||||
|
||||
#[test] |
||||
fn split_time_bad_file() { |
||||
let reader = SplitTimeReader::default(); |
||||
let file = NamedTempFile::new().unwrap(); |
||||
let ts = reader.read(file.path().to_str().unwrap()); |
||||
assert_eq!(ts.len(), 0); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue