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::histogram::Histogram; |
||||||
pub use self::matchbar::{MatchBar, MatchBarRow}; |
pub use self::matchbar::{MatchBar, MatchBarRow}; |
||||||
|
pub use self::splittimehist::SplitTimeHistogram; |
||||||
pub use self::timehist::TimeHistogram; |
pub use self::timehist::TimeHistogram; |
||||||
pub use self::xy::XyPlot; |
pub use self::xy::XyPlot; |
||||||
|
|
||||||
mod histogram; |
mod histogram; |
||||||
mod matchbar; |
mod matchbar; |
||||||
|
mod splittimehist; |
||||||
mod timehist; |
mod timehist; |
||||||
mod xy; |
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