Skip to content

Commit

Permalink
Implement timehist subcommand
Browse files Browse the repository at this point in the history
  • Loading branch information
juan-leon committed May 4, 2021
1 parent 67f205f commit 9804427
Show file tree
Hide file tree
Showing 9 changed files with 866 additions and 59 deletions.
43 changes: 43 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ yansi = "0.5.0"
isatty = "0.1"
derive_builder = "0.10.0"
regex = "1.4.5"
chrono = "0.4"

[dev-dependencies]
float_eq = "0.5.0"
Expand Down
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ terminal.
Type `lowcharts --help`, or `lowcharts PLOT-TYPE --help` for a complete list of
options.

Currently three basic types of plots are supported:
Currently four basic types of plots are supported:

#### Bar chart for matches in the input

Expand Down Expand Up @@ -95,6 +95,34 @@ of a metric over time, but not the speed of that evolution.

There is regex support for this type of plots.

#### Time Histogram

This chart is generated using `strace -tt ls -lR * 2>&1 | lowcharts timehist --intervals 10`:

[![Sample plot with lowcharts](resources/timehist-example.png)](resources/timehist-example.png)

Things like `lowcharts timehist --regex ' 404 ' nginx.log` should work in a
similar way, and would give you a glimpse of when and how many 404s are being
triggered in your server.

The idea is to depict the frequency of logs that match a regex (by default any
log that is read by the tool). The sub-command can autodetect the more common
(in my personal and biased experience) datetime/timestamp formats: rfc 3339, rfc
2822, python `%(asctime)s`, golang default log format, nginx, rabbitmq, strace
-t (or -tt, or -ttt),ltrace,... as long as the timestamp is present in the first
line in the log and the format is consistent in all the lines that contain
timestamp. It is ok to have lines with no timestamp. The consistency is
required because of performance reasons: the 1st log line is the only one that
triggers the heuristics needed to create an specialized datetime parser on the
fly.

However, if you have a format that lowcharts cannot autodetected, you can
specify it via command line flag. For instance, `--format
'%d-%b-%Y::%H:%M:%S'`. Note that, as of today, you need to leave out the
timezone part of the format string (the autodetection works fine with
timezones).


### Installing

#### Via release
Expand Down
Binary file added resources/timehist-example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
154 changes: 96 additions & 58 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,32 @@
use clap::{self, App, AppSettings, Arg};

fn add_common_options(app: App) -> App {
fn add_input(app: App) -> App {
app.arg(
Arg::new("input")
.about("Input file")
.default_value("-")
.long_about("If not present or a single dash, standard input will be used"),
)
}

fn add_min_max(app: App) -> App {
app.arg(
Arg::new("max")
.long("max")
.short('M')
.about("Filter out values bigger than this")
.takes_value(true),
)
.arg(
Arg::new("min")
.long("min")
.short('m')
.about("Filter out values smaller than this")
.takes_value(true),
)
}

fn add_regex(app: App) -> App {
const LONG_RE_ABOUT: &str = "\
A regular expression used for capturing the values to be plotted inside input
lines.
Expand All @@ -15,60 +41,44 @@ Examples of regex are ' 200 \\d+ ([0-9.]+)' (where there is one anonymous captur
group) and 'a(a)? (?P<value>[0-9.]+)' (where there are two capture groups, and
the named one will be used).
";

app.arg(
Arg::new("max")
.long("max")
.short('M')
.about("Filter out values bigger than this")
.takes_value(true),
)
.arg(
Arg::new("min")
.long("min")
.short('m')
.about("Filter out values smaller than this")
Arg::new("regex")
.long("regex")
.short('R')
.about("Use a regex to capture input values")
.long_about(LONG_RE_ABOUT)
.takes_value(true),
)
.arg(
}

fn add_width(app: App) -> App {
app.arg(
Arg::new("width")
.long("width")
.short('w')
.about("Use this many characters as terminal width")
.default_value("110")
.takes_value(true),
)
.arg(
Arg::new("regex")
.long("regex")
.short('R')
.about("Use a regex to capture input values")
.long_about(LONG_RE_ABOUT)
}

fn add_intervals(app: App) -> App {
app.arg(
Arg::new("intervals")
.long("intervals")
.short('i')
.about("Use no more than this amount of buckets to classify data")
.default_value("20")
.takes_value(true),
)
.arg(
Arg::new("input")
.about("Input file")
.default_value("-")
.long_about("If not present or a single dash, standard input will be used"),
)
}

pub fn get_app() -> App<'static> {
let mut hist = App::new("hist")
.version(clap::crate_version!())
.setting(AppSettings::ColoredHelp)
.about("Plot an histogram from input values")
.arg(
Arg::new("intervals")
.long("intervals")
.short('i')
.about("Use no more than this amount of buckets to classify data")
.default_value("20")
.takes_value(true),
);

hist = add_common_options(hist);
.about("Plot an histogram from input values");
hist = add_input(add_regex(add_width(add_min_max(add_intervals(hist)))));

let mut plot = App::new("plot")
.version(clap::crate_version!())
Expand All @@ -82,34 +92,33 @@ pub fn get_app() -> App<'static> {
.default_value("40")
.takes_value(true),
);
plot = add_common_options(plot);
plot = add_input(add_regex(add_width(add_min_max(plot))));

let matches = App::new("matches")
let mut matches = App::new("matches")
.version(clap::crate_version!())
.setting(AppSettings::ColoredHelp)
.setting(AppSettings::AllowMissingPositional)
.about("Plot barchar with counts of occurences of matches params")
.about("Plot barchar with counts of occurences of matches params");
matches = add_input(add_width(matches)).arg(
Arg::new("match")
.about("Count maches for those strings")
.required(true)
.takes_value(true)
.multiple(true),
);

let mut timehist = App::new("timehist")
.version(clap::crate_version!())
.setting(AppSettings::ColoredHelp)
.about("Plot histogram with amount of matches over time")
.arg(
Arg::new("width")
.long("width")
.short('w')
.about("Use this many characters as terminal width")
.default_value("110")
Arg::new("format")
.long("format")
.short('f')
.about("Use this string formatting")
.takes_value(true),
)
.arg(
Arg::new("input")
.about("Input file")
.required(true)
.long_about("If not present or a single dash, standard input will be used"),
)
.arg(
Arg::new("match")
.about("Count maches for those strings")
.required(true)
.takes_value(true)
.multiple(true),
);
timehist = add_input(add_width(add_regex(add_intervals(timehist))));

App::new("lowcharts")
.author(clap::crate_authors!())
Expand All @@ -136,6 +145,7 @@ pub fn get_app() -> App<'static> {
.subcommand(hist)
.subcommand(plot)
.subcommand(matches)
.subcommand(timehist)
}

#[cfg(test)]
Expand Down Expand Up @@ -183,4 +193,32 @@ mod tests {
assert!(false, "Subcommand `plot` not detected");
}
}

#[test]
fn matches_subcommand_arg_parsing() {
let arg_vec = vec!["lowcharts", "matches", "-", "A", "B", "C"];
let m = get_app().get_matches_from(arg_vec);
if let Some(sub_m) = m.subcommand_matches("matches") {
assert_eq!("-", sub_m.value_of("input").unwrap());
assert_eq!(
// vec![String::from("A"), String::from("B"), String::from("C")],
vec!["A", "B", "C"],
sub_m.values_of("match").unwrap().collect::<Vec<&str>>()
);
} else {
assert!(false, "Subcommand `matches` not detected");
}
}

#[test]
fn timehist_subcommand_arg_parsing() {
let arg_vec = vec!["lowcharts", "timehist", "--regex", "foo", "some"];
let m = get_app().get_matches_from(arg_vec);
if let Some(sub_m) = m.subcommand_matches("timehist") {
assert_eq!("some", sub_m.value_of("input").unwrap());
assert_eq!("foo", sub_m.value_of("regex").unwrap());
} else {
assert!(false, "Subcommand `timehist` not detected");
}
}
}
Loading

0 comments on commit 9804427

Please sign in to comment.