Skip to main content

krapslog/
lib.rs

1mod time_marker;
2mod timestamp_finder;
3
4use anyhow::Result;
5use std::io::{prelude::*, BufReader};
6
7use crate::timestamp_finder::TimestampFinder;
8
9const SPARKS: &[&str] = &["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
10
11pub fn build_sparkline(timestamps: &[i64], width: usize, height: usize) -> String {
12    let timestamp_frequencies = timestamp_frequency_distribution(timestamps, width);
13    let (min, max) = (
14        *timestamp_frequencies.iter().min().unwrap() as f64,
15        *timestamp_frequencies.iter().max().unwrap() as f64,
16    );
17    let mut canvas = vec![vec![" "; width]; height];
18    let slots_per_line = SPARKS.len();
19
20    timestamp_frequencies
21        .iter()
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()
48}
49
50pub fn scan_for_timestamps<R>(reader: R, format: &str) -> Result<Vec<i64>>
51where
52    R: Read,
53{
54    let date_finder = TimestampFinder::new(format)?;
55    let timestamps = BufReader::new(reader)
56        .lines()
57        .map_while(Result::ok)
58        .filter_map(|line| date_finder.find_timestamp(&line))
59        .collect();
60    Ok(timestamps)
61}
62
63pub fn build_time_markers(
64    timestamps: &[i64],
65    marker_count: usize,
66    terminal_width: usize,
67) -> (String, String) {
68    if marker_count < 2 || timestamps.len() < 2 {
69        return (String::from(""), String::from(""));
70    }
71
72    let mut footer_marker_count = marker_count / 2;
73    if footer_marker_count % 2 != 0 {
74        footer_marker_count += 1;
75    }
76
77    let marker_timestamp_offsets: Vec<usize> = (0..marker_count)
78        .map(|i| (i as f64 * timestamps.len() as f64 / (marker_count - 1) as f64).ceil() as usize)
79        .collect();
80    let header_timestamp_offsets = marker_timestamp_offsets[footer_marker_count..].to_vec();
81    let footer_timestamp_offsets = marker_timestamp_offsets[..footer_marker_count].to_vec();
82
83    let marker_terminal_offsets = marker_offsets(marker_count, terminal_width);
84    let header_terminal_offsets = marker_terminal_offsets[footer_marker_count..].to_vec();
85    let footer_terminal_offsets = marker_terminal_offsets[..footer_marker_count].to_vec();
86
87    let mut header_canvas =
88        time_marker::Canvas::new(terminal_width, header_timestamp_offsets.len() + 1);
89    let mut footer_canvas =
90        time_marker::Canvas::new(terminal_width, footer_timestamp_offsets.len() + 1);
91
92    header_timestamp_offsets
93        .iter()
94        .enumerate()
95        .map(|(index, timestamp_index)| time_marker::TimeMarker {
96            horizontal_offset: header_terminal_offsets[index],
97            timestamp: timestamps[*timestamp_index - 1],
98            timestamp_location: time_marker::TimestampLocation::Top,
99            vertical_offset: index + 1,
100        })
101        .for_each(|time_marker| {
102            if let Err(e) = time_marker.render(&mut header_canvas) {
103                eprintln!("couldn't render time marker: {}", e);
104            }
105        });
106
107    footer_timestamp_offsets
108        .iter()
109        .enumerate()
110        .map(|(index, timestamp_index)| time_marker::TimeMarker {
111            horizontal_offset: footer_terminal_offsets[index],
112            timestamp: timestamps[*timestamp_index],
113            timestamp_location: time_marker::TimestampLocation::Bottom,
114            vertical_offset: footer_timestamp_offsets.len() - index,
115        })
116        .for_each(|time_marker| {
117            if let Err(e) = time_marker.render(&mut footer_canvas) {
118                eprintln!("couldn't render time marker: {}", e);
119            }
120        });
121
122    (format!("{}", header_canvas), format!("{}", footer_canvas))
123}
124
125fn marker_offsets(count: usize, terminal_width: usize) -> Vec<usize> {
126    // Always show a marker at the left edge
127    let mut offsets = vec![0];
128
129    // Divide the non-edge offsets into equally-sized segments, placing a marker between them
130    let skip = (terminal_width - 2) as f64 / (count - 1) as f64;
131    let mut current_offset = skip;
132    (0..(count - 2)).for_each(|_| {
133        offsets.push(current_offset.ceil() as usize % terminal_width);
134        current_offset += skip;
135    });
136
137    // Always show a marker at the right edge
138    offsets.push(terminal_width - 1);
139
140    offsets
141}
142
143fn timestamp_frequency_distribution(timestamps: &[i64], bucket_count: usize) -> Vec<usize> {
144    let first_timestamp = timestamps.iter().min().unwrap();
145    let last_timestamp = timestamps.iter().max().unwrap();
146    let duration_seconds = last_timestamp - first_timestamp;
147    let seconds_per_bucket = duration_seconds as f64 / bucket_count as f64;
148
149    let mut timestamps_per_bucket = vec![0; bucket_count];
150    for timestamp in timestamps {
151        let bucket_index = usize::min(
152            ((timestamp - first_timestamp) as f64 / seconds_per_bucket) as usize,
153            bucket_count - 1,
154        );
155        timestamps_per_bucket[bucket_index] += 1;
156    }
157
158    timestamps_per_bucket
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn build_sparkline_with_one_line() {
167        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 \
168        /2518cb13a48bdf53b2f936f44e7042a3cc7baa06 HTTP/1.1\"
169Nov 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\
170ce8bd70bc6e7 HTTP/1.1\"
171Nov 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\
1727ce0e21ef1a01 HTTP/1.1\"
173Nov 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\
17422554f6d HTTP/1.1\"
175Nov 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\
176e0ceb71e62b5 HTTP/1.1\"
177Nov 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\
178e3a19fb8cafdb6 HTTP/1.1\"
179Nov 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\
180d7cb85478496e4 HTTP/1.1\"
181Nov 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\
182c0db08bb3 HTTP/1.1\"
183Nov 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\
1840db67ab23ee7 HTTP/1.1\"
185Nov 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\
186e4ff7f1d9d80f HTTP/1.1\"
187";
188        let format = "%d/%b/%Y:%H:%M:%S%.f";
189        let timestamps = scan_for_timestamps(log.as_bytes(), format).unwrap();
190        let sparkline = build_sparkline(&timestamps, 80, 1);
191        assert_eq!(
192            sparkline,
193            "█▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁█"
194        );
195    }
196
197    #[test]
198    fn build_sparkline_with_multiple_lines() {
199        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 \
200        /2518cb13a48bdf53b2f936f44e7042a3cc7baa06 HTTP/1.1\"
201Nov 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\
202ce8bd70bc6e7 HTTP/1.1\"
203Nov 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\
2047ce0e21ef1a01 HTTP/1.1\"
205Nov 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\
20622554f6d HTTP/1.1\"
207Nov 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\
208e0ceb71e62b5 HTTP/1.1\"
209Nov 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\
210e3a19fb8cafdb6 HTTP/1.1\"
211Nov 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\
212d7cb85478496e4 HTTP/1.1\"
213Nov 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\
214c0db08bb3 HTTP/1.1\"
215Nov 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\
2160db67ab23ee7 HTTP/1.1\"
217Nov 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\
218e4ff7f1d9d80f HTTP/1.1\"
219";
220        let format = "%d/%b/%Y:%H:%M:%S%.f";
221        let timestamps = scan_for_timestamps(log.as_bytes(), format).unwrap();
222        let sparkline = build_sparkline(&timestamps, 80, 5);
223        assert_eq!(
224            sparkline,
225            "█       █        █        █        █        █        █        █        █       █
226█       █        █        █        █        █        █        █        █       █
227█       █        █        █        █        █        █        █        █       █
228█       █        █        █        █        █        █        █        █       █
229█▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁█"
230        );
231    }
232
233    #[test]
234    fn build_time_markers_even() {
235        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 \
236        /2518cb13a48bdf53b2f936f44e7042a3cc7baa06 HTTP/1.1\"
237Nov 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\
238ce8bd70bc6e7 HTTP/1.1\"
239Nov 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\
2407ce0e21ef1a01 HTTP/1.1\"
241Nov 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\
24222554f6d HTTP/1.1\"
243Nov 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\
244e0ceb71e62b5 HTTP/1.1\"
245Nov 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\
246e3a19fb8cafdb6 HTTP/1.1\"
247Nov 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\
248d7cb85478496e4 HTTP/1.1\"
249Nov 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\
250c0db08bb3 HTTP/1.1\"
251Nov 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\
2520db67ab23ee7 HTTP/1.1\"
253Nov 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\
254e4ff7f1d9d80f HTTP/1.1\"
255";
256        let format = "%d/%b/%Y:%H:%M:%S%.f";
257        let timestamps = scan_for_timestamps(log.as_bytes(), format).unwrap();
258        let (header, footer) = build_time_markers(&timestamps, 8, 80);
259        assert_eq!(
260            header,
261            "                                                             2019-11-23 06:35:49
262                                                 2019-11-23 06:34:48           |
263                                      2019-11-23 06:33:47          |           |
264                           2019-11-23 06:31:45          |          |           |
265                                             |          |          |           |
266"
267        );
268        assert_eq!(
269            footer,
270            "|           |          |          |                                             
271|           |          |          2019-11-23 06:31:45                           
272|           |          2019-11-23 06:29:43                                      
273|           2019-11-23 06:28:42                                                 
2742019-11-23 06:26:40                                                             
275"
276        );
277    }
278
279    #[test]
280    fn build_time_markers_odd() {
281        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 \
282        /2518cb13a48bdf53b2f936f44e7042a3cc7baa06 HTTP/1.1\"
283Nov 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\
284ce8bd70bc6e7 HTTP/1.1\"
285Nov 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\
2867ce0e21ef1a01 HTTP/1.1\"
287Nov 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\
28822554f6d HTTP/1.1\"
289Nov 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\
290e0ceb71e62b5 HTTP/1.1\"
291Nov 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\
292e3a19fb8cafdb6 HTTP/1.1\"
293Nov 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\
294d7cb85478496e4 HTTP/1.1\"
295Nov 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\
296c0db08bb3 HTTP/1.1\"
297Nov 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\
2980db67ab23ee7 HTTP/1.1\"
299Nov 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\
300e4ff7f1d9d80f HTTP/1.1\"
301";
302        let format = "%d/%b/%Y:%H:%M:%S%.f";
303        let timestamps = scan_for_timestamps(log.as_bytes(), format).unwrap();
304        let (header, footer) = build_time_markers(&timestamps, 15, 80);
305        assert_eq!(
306            header,
307            "                                                             2019-11-23 06:35:49
308                                                       2019-11-23 06:35:49     |
309                                                 2019-11-23 06:34:48     |     |
310                                            2019-11-23 06:33:47    |     |     |
311                                      2019-11-23 06:33:47     |    |     |     |
312                                 2019-11-23 06:32:46    |     |    |     |     |
313                           2019-11-23 06:31:45     |    |     |    |     |     |
314                                             |     |    |     |    |     |     |
315"
316        );
317        assert_eq!(
318            footer,
319            "|     |     |    |     |    |     |    |                                        
320|     |     |    |     |    |     |    2019-11-23 06:31:45                      
321|     |     |    |     |    |     2019-11-23 06:31:45                           
322|     |     |    |     |    2019-11-23 06:30:44                                 
323|     |     |    |     2019-11-23 06:29:43                                      
324|     |     |    2019-11-23 06:29:43                                            
325|     |     2019-11-23 06:28:42                                                 
326|     2019-11-23 06:27:41                                                       
3272019-11-23 06:26:40                                                             
328"
329        );
330    }
331
332    #[test]
333    fn build_time_markers_too_few_timestamps() {
334        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 \
335        /2518cb13a48bdf53b2f936f44e7042a3cc7baa06 HTTP/1.1\"\n";
336        let format = "%d/%b/%Y:%H:%M:%S%.f";
337        let timestamps = scan_for_timestamps(log.as_bytes(), format).unwrap();
338        let (header, footer) = build_time_markers(&timestamps, 8, 80);
339        assert_eq!(header, "");
340        assert_eq!(footer, "");
341    }
342
343    #[test]
344    fn scan_for_timestamps_() {
345        let log = "Nov 23 06:26:40 ip-10-1-26-81 haproxy[20128]: 54.242.135.245:57305 [23/Nov/2019:06:26:40.781] public repackager/i-05fa49c0e7db8c328 0/0/0/78/78 206 913/458 - - ---- 9/9/6/0/0 0/0 {1.1 v1-akamaitech.net(ghost) (AkamaiGHost), 1.1 v1-akamaitech.net(ghost) (AkamaiGHost), 1.1 akamai.n|bytes=0-0} {||1|bytes 0-0/499704} \"GET /deliveries/2518cb13a48bdf53b2f936f44e7042a3cc7baa06.m3u8/seg-88-v1-a1.ts HTTP/1.1\"
346Nov 23 14:21:53 ip-10-1-26-81 haproxy[20128]: 54.209.125.72:58030 [23/Nov/2019:14:21:53.241] public repackager/i-0728dc03214405429 0/0/0/246/246 200 810/8324 - - ---- 17/17/12/0/0 0/0 {1.1 v1-akamaitech.net(ghost) (AkamaiGHost), 1.1 v1-akamaitech.net(ghost) (AkamaiGHost), 1.1 akamai.n|} {||7870|} \"GET /deliveries/4fb7b6ff75f8a13da4ac482e25e29790105ba075.m3u8?origin_v2=1 HTTP/1.1\"
347";
348        let format = "%d/%b/%Y:%H:%M:%S%.f";
349        let timestamps = scan_for_timestamps(log.as_bytes(), format).unwrap();
350        assert_eq!(timestamps, [1574490400, 1574518913]);
351    }
352
353    #[test]
354    fn timestamp_frequency_distribution_() {
355        let timestamps = vec![1, 2, 3, 4, 5];
356        let bins = timestamp_frequency_distribution(&timestamps, 5);
357        assert_eq!(bins, [1, 1, 1, 1, 1]);
358
359        let timestamps = vec![1, 2, 3, 4, 5, 6];
360        let bins = timestamp_frequency_distribution(&timestamps, 3);
361        assert_eq!(bins, [2, 2, 2]);
362    }
363
364    #[test]
365    fn marker_offsets_() {
366        assert_eq![marker_offsets(2, 2), vec![0, 1]];
367        assert_eq![marker_offsets(2, 5), vec![0, 4]];
368        assert_eq!(marker_offsets(5, 10), vec![0, 2, 4, 6, 9]);
369        assert_eq!(marker_offsets(3, 5), vec![0, 2, 4]);
370    }
371}