Skip to content

Commit 9c60ac2

Browse files
committed
Implement multiline sparklines
Resolves #6
1 parent 9ec2b60 commit 9c60ac2

File tree

4 files changed

+106
-46
lines changed

4 files changed

+106
-46
lines changed

README.md

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,21 @@ cargo install krapslog
2424
## Usage
2525

2626
```
27-
$ krapslog -h
28-
krapslog 0.3.2
27+
$ krapslog --help
2928
Visualize log files using sparklines
3029
31-
USAGE:
32-
krapslog [OPTIONS] [FILE]
30+
Usage: krapslog [OPTIONS] [FILE]
3331
34-
ARGS:
35-
<FILE> Log file to analyze
32+
Arguments:
33+
[FILE] Log file to analyze
3634
37-
OPTIONS:
38-
-c, --concurrency <CONCURRENCY> Number of threads to use when processing large files
39-
(defaults to number of CPU cores) [default: 8]
40-
-F, --format <FORMAT> Timestamp format to match [default: %d/%b/%Y:%H:%M:%S%.f]
41-
-h, --help Print help information
42-
-m, --markers <MARKERS> Number of time markers to display [default: 0]
43-
-V, --version Print version information
35+
Options:
36+
-F, --format <FORMAT> Timestamp format to match [default: %d/%b/%Y:%H:%M:%S%.f]
37+
-m, --markers <MARKERS> Number of time markers to display [default: 0]
38+
-h, --height <HEIGHT> Height (in lines) of the displayed sparkline [default: 1]
39+
-c, --concurrency <CONCURRENCY> Number of threads to use when processing large files (defaults to number of CPU cores) [default: 8]
40+
-h, --help Print help
41+
-V, --version Print version
4442
```
4543

4644
## Examples
@@ -71,6 +69,17 @@ $ krapslog --markers 10 /var/log/haproxy.log
7169
Sat Nov 23 06:26:40
7270
```
7371

72+
Increase the display resolution:
73+
74+
```
75+
$ krapslog --height 5 /var/log/haproxy.log
76+
▁ ▁ ▁▃▃██
77+
▁ ▁▅█▃▅▂▂▄▃▃▅▅▇▆█▇██████
78+
▁▆▅▇▅▃▆▇ ▁ ▁▁▄█▇██████████████████████
79+
▁▁ ▁▂ ▅▂ ▂▃▂▁ ▃▁ ▂▂▅▅▂▄▅████████▇▆█▅███████████████████████████
80+
▇▇▇▆▇▇▅▅▆▅▅▄▃▄▄▇▄▆▃▅▄▅▅▆▅▅▃▁▁▃▃▄▄▄▃▄▅▅▆█▅▅▇▅██▇██████▇████▇█████████████████████████████████████████████████
81+
```
82+
7483
Integrate with other tools:
7584

7685
```

src/bin.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ fn main() -> Result<()> {
3737
.value_parser(clap::value_parser!(usize))
3838
.default_value("0"),
3939
)
40+
.arg(
41+
Arg::new("HEIGHT")
42+
.short('h')
43+
.long("height")
44+
.help("Height (in lines) of the displayed sparkline")
45+
.value_parser(clap::value_parser!(usize))
46+
.default_value("1")
47+
)
4048
.arg(
4149
Arg::new("CONCURRENCY")
4250
.short('c')
@@ -85,8 +93,9 @@ fn main() -> Result<()> {
8593
};
8694

8795
let num_markers: usize = *arg_matches.get_one("MARKERS").unwrap();
96+
let num_lines: usize = std::cmp::max(1, *arg_matches.get_one("HEIGHT").unwrap());
8897
let (header, footer) = krapslog::build_time_markers(&timestamps, num_markers, terminal_width);
89-
let sparkline = krapslog::build_sparkline(&timestamps, terminal_width)?;
98+
let sparkline = krapslog::build_sparkline(&timestamps, terminal_width, num_lines);
9099
print!("{}", header);
91100
println!("{}", sparkline);
92101
print!("{}", footer);

src/lib.rs

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,50 @@
1-
mod sparkline;
21
mod time_marker;
32
mod timestamp_finder;
43

54
use anyhow::Result;
65
use std::io::{prelude::*, BufReader};
76

8-
use crate::sparkline::sparkline;
97
use crate::timestamp_finder::TimestampFinder;
108

11-
pub fn build_sparkline(timestamps: &[i64], length: usize) -> Result<String> {
12-
let timestamps_per_bucket = timestamp_frequency_distribution(timestamps, length);
9+
const SPARKS: &[&str] = &["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
10+
11+
pub fn build_sparkline(timestamps: &[i64], width: usize, height: usize) -> String {
12+
let timestamp_frequencies = timestamp_frequency_distribution(timestamps, width);
1313
let (min, max) = (
14-
*timestamps_per_bucket.iter().min().unwrap() as f64,
15-
*timestamps_per_bucket.iter().max().unwrap() as f64,
14+
*timestamp_frequencies.iter().min().unwrap() as f64,
15+
*timestamp_frequencies.iter().max().unwrap() as f64,
1616
);
17-
let sparkline = timestamps_per_bucket
17+
let mut canvas = vec![vec![" "; width]; height];
18+
let slots_per_line = SPARKS.len();
19+
20+
timestamp_frequencies
1821
.iter()
19-
.map(|count| sparkline(min, max, *count as f64).to_owned())
20-
.collect();
21-
Ok(sparkline)
22+
.enumerate()
23+
.for_each(|(column, freq)| {
24+
let proportion = (*freq as f64 - min) / (max - min);
25+
let scaled_proportion = proportion * height as f64;
26+
let mut slots_left = (scaled_proportion * slots_per_line as f64).ceil() as usize;
27+
if slots_left == 0 {
28+
// Always fill at least one slot
29+
slots_left = 1;
30+
}
31+
(0..height).for_each(|row| {
32+
if slots_left > slots_per_line {
33+
canvas[row][column] = *SPARKS.last().unwrap();
34+
slots_left -= slots_per_line;
35+
} else if slots_left > 0 {
36+
canvas[row][column] = SPARKS[slots_left - 1];
37+
slots_left = 0;
38+
}
39+
})
40+
});
41+
42+
canvas
43+
.iter()
44+
.rev()
45+
.map(|chars| chars.join(""))
46+
.reduce(|a, b| format!("{}\n{}", a, b))
47+
.unwrap()
2248
}
2349

2450
pub fn scan_for_timestamps<R>(reader: R, format: &str) -> Result<Vec<i64>>
@@ -139,7 +165,7 @@ mod tests {
139165
use super::*;
140166

141167
#[test]
142-
fn build_sparkline_() {
168+
fn build_sparkline_with_one_line() {
143169
let log = "Nov 23 06:26:40 ip-10-1-1-1 haproxy[20128]: 10.1.1.10:57305 [23/Nov/2019:06:26:40.781] public myapp/i-05fa49c0e7db8c328 0/0/0/78/78 206 913/458 - - ---- 9/9/6/0/0 0/0 {bytes=0-0} {||1|bytes 0-0/499704} \"GET \
144170
/2518cb13a48bdf53b2f936f44e7042a3cc7baa06 HTTP/1.1\"
145171
Nov 23 06:26:41 ip-10-1-1-1 haproxy[20128]: 10.1.1.11:51819 [23/Nov/2019:06:27:41.780] public myapp/i-059c225b48702964a 0/0/0/80/80 200 802/142190 - - ---- 8/8/5/0/0 0/0 {} {||141752|} \"GET /2043f2eb9e2691edcc0c8084d1ff\
@@ -163,13 +189,49 @@ e4ff7f1d9d80f HTTP/1.1\"
163189
";
164190
let format = "%d/%b/%Y:%H:%M:%S%.f";
165191
let timestamps = scan_for_timestamps(log.as_bytes(), format).unwrap();
166-
let sparkline = build_sparkline(&timestamps, 80).unwrap();
192+
let sparkline = build_sparkline(&timestamps, 80, 1);
167193
assert_eq!(
168194
sparkline,
169195
"█▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁█"
170196
);
171197
}
172198

199+
#[test]
200+
fn build_sparkline_with_multiple_lines() {
201+
let log = "Nov 23 06:26:40 ip-10-1-1-1 haproxy[20128]: 10.1.1.10:57305 [23/Nov/2019:06:26:40.781] public myapp/i-05fa49c0e7db8c328 0/0/0/78/78 206 913/458 - - ---- 9/9/6/0/0 0/0 {bytes=0-0} {||1|bytes 0-0/499704} \"GET \
202+
/2518cb13a48bdf53b2f936f44e7042a3cc7baa06 HTTP/1.1\"
203+
Nov 23 06:26:41 ip-10-1-1-1 haproxy[20128]: 10.1.1.11:51819 [23/Nov/2019:06:27:41.780] public myapp/i-059c225b48702964a 0/0/0/80/80 200 802/142190 - - ---- 8/8/5/0/0 0/0 {} {||141752|} \"GET /2043f2eb9e2691edcc0c8084d1ff\
204+
ce8bd70bc6e7 HTTP/1.1\"
205+
Nov 23 06:26:42 ip-10-1-1-1 haproxy[20128]: 10.1.1.12:38870 [23/Nov/2019:06:28:42.773] public myapp/i-048088fd46abe7ed0 0/0/0/77/100 200 823/512174 - - ---- 8/8/5/0/0 0/0 {} {||511736|} \"GET /eb59c0b5dad36f080f3d261c625\
206+
7ce0e21ef1a01 HTTP/1.1\"
207+
Nov 23 06:26:43 ip-10-1-1-1 haproxy[20128]: 10.1.1.13:35528 [23/Nov/2019:06:29:43.775] public myapp/i-05e9315b035d50f62 0/0/0/103/105 200 869/431481 - - ---- 8/8/1/0/0 0/0 {} {|||} \"GET /164672c9d75c76a8fa237c24f9cbfd22\
208+
22554f6d HTTP/1.1\"
209+
Nov 23 06:26:44 ip-10-1-1-1 haproxy[20128]: 10.1.1.14:48553 [23/Nov/2019:06:30:44.808] public myapp/i-0008bfe6b1c98e964 0/0/0/72/73 200 840/265518 - - ---- 7/7/5/0/0 0/0 {} {||265080|} \"GET /e3b526928196d19ab3419d433f3d\
210+
e0ceb71e62b5 HTTP/1.1\"
211+
Nov 23 06:26:45 ip-10-1-1-1 haproxy[20128]: 10.1.1.15:60969 [23/Nov/2019:06:31:45.727] public myapp/i-005a2bfdba4c405a8 0/0/0/146/167 200 852/304622 - - ---- 7/7/5/0/0 0/0 {} {||304184|} \"GET /52f5edb4a46276defe54ead2fa\
212+
e3a19fb8cafdb6 HTTP/1.1\"
213+
Nov 23 06:26:46 ip-10-1-1-1 haproxy[20128]: 10.1.1.14:48539 [23/Nov/2019:06:32:46.730] public myapp/i-03b180605be4fa176 0/0/0/171/171 200 889/124142 - - ---- 6/6/4/0/0 0/0 {} {||123704|} \"GET /ef9e0c85cc1c76d7dc777f5b19\
214+
d7cb85478496e4 HTTP/1.1\"
215+
Nov 23 06:26:47 ip-10-1-1-1 haproxy[20128]: 10.1.1.11:51847 [23/Nov/2019:06:33:47.886] public myapp/i-0aa566420409956d6 0/0/0/28/28 206 867/458 - - ---- 6/6/4/0/0 0/0 {bytes=0-0} {} \"GET /3c7ace8c683adcad375a4d14995734a\
216+
c0db08bb3 HTTP/1.1\"
217+
Nov 23 06:26:48 ip-10-1-1-1 haproxy[20128]: 10.1.1.13:35554 [23/Nov/2019:06:34:48.866] public myapp/i-07f4205f35b4774b6 0/0/0/23/49 200 816/319662 - - ---- 5/5/3/0/0 0/0 {} {||319224|} \"GET /b95db0578977cd32658fa28b386c\
218+
0db67ab23ee7 HTTP/1.1\"
219+
Nov 23 06:26:49 ip-10-1-1-1 haproxy[20128]: 10.1.1.12:38899 [23/Nov/2019:06:35:49.879] public myapp/i-08cb5309afd22e8c0 0/0/0/59/59 200 1000/112110 - - ---- 5/5/3/0/0 0/0 {} {||111672|} \"GET /5314ca870ed0f5e48a71adca185\
220+
e4ff7f1d9d80f HTTP/1.1\"
221+
";
222+
let format = "%d/%b/%Y:%H:%M:%S%.f";
223+
let timestamps = scan_for_timestamps(log.as_bytes(), format).unwrap();
224+
let sparkline = build_sparkline(&timestamps, 80, 5);
225+
assert_eq!(
226+
sparkline,
227+
"█ █ █ █ █ █ █ █ █ █
228+
█ █ █ █ █ █ █ █ █ █
229+
█ █ █ █ █ █ █ █ █ █
230+
█ █ █ █ █ █ █ █ █ █
231+
█▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁█"
232+
);
233+
}
234+
173235
#[test]
174236
fn build_time_markers_even() {
175237
let log = "Nov 23 06:26:40 ip-10-1-1-1 haproxy[20128]: 10.1.1.10:57305 [23/Nov/2019:06:26:40.781] public myapp/i-05fa49c0e7db8c328 0/0/0/78/78 206 913/458 - - ---- 9/9/6/0/0 0/0 {bytes=0-0} {||1|bytes 0-0/499704} \"GET \

src/sparkline.rs

Lines changed: 0 additions & 20 deletions
This file was deleted.

0 commit comments

Comments
 (0)