File Coverage

blib/lib/App/loggrep.pm
Criterion Covered Total %
statement 189 230 82.1
branch 85 128 66.4
condition 47 66 71.2
subroutine 16 16 100.0
pod 3 3 100.0
total 340 443 76.7


line stmt bran cond sub pod time code
1             package App::loggrep;
2             $App::loggrep::VERSION = '0.001';
3             # ABSTRACT: quickly find relevant lines in a log searching by date
4              
5             $App::loggrep::VERSION //= 'dev';
6 1     1   639 use strict;
  1         1  
  1         42  
7 1     1   6 use warnings;
  1         1  
  1         34  
8              
9 1     1   954 use Tie::File;
  1         26045  
  1         47  
10 1     1   820 use Date::Parse qw(str2time);
  1         6306  
  1         1663  
11              
12              
13             sub new {
14 12     12 1 13293 my ( $class, $log, $opt ) = @_;
15 12         63 bless { filename => $log, options => $opt }, $class;
16             }
17              
18              
19             sub init {
20 12     12 1 46 my $self = shift;
21 12         18 my @errors;
22 12         30 my $filename = $self->{filename};
23 12         15 my @lines;
24 12         25 my $opt = delete $self->{options};
25             {
26 12 50       17 unless ( defined $filename ) {
  12         41  
27 0         0 push @errors, 'no log file provided';
28 0         0 last;
29             }
30 12 50       207 unless ( -e $filename ) {
31 0         0 push @errors, "file $filename does not exist";
32 0         0 last;
33             }
34 12 50       108 if ( -d $filename ) {
35 0         0 push @errors, "$filename is a directory";
36 0         0 last;
37             }
38 12 50       98 unless ( -r $filename ) {
39 0         0 push @errors, "cannot read $filename";
40 0         0 last;
41             }
42 12 50       72 tie @lines, 'Tie::File', $filename or push @errors, $!;
43 12         1590 $self->{lines} = \@lines;
44             }
45 12         50 $self->{date} = _make_rx( $opt->date, \@errors, 'date' );
46 12   100     14 my @inclusions = @{ $opt->include // [] };
  12         33  
47 12         99 my $insensitive = $opt->case_insensitive;
48 12         50 $_ = _make_rx( $_, \@errors, 'inclusion', 0, $insensitive ) for @inclusions;
49 12   50     27 push @inclusions, _make_rx( $_, \@errors, 'inclusion', 1, $insensitive )
50 12         18 for @{ $opt->include_quoted // [] };
51 12         94 $self->{include} = [ sort { length($a) <=> length($b) } @inclusions ];
  0         0  
52 12   100     13 my @exclusions = @{ $opt->exclude // [] };
  12         26  
53 12         72 $_ = _make_rx( $_, \@errors, 'exclude', 0, $insensitive ) for @exclusions;
54 12   50     30 push @exclusions, _make_rx( $_, \@errors, 'exclude', 1, $insensitive )
55 12         10 for @{ $opt->exclude_quoted // [] };
56 12         78 $self->{exclude} = [ sort { length($a) <=> length($b) } @exclusions ];
  0         0  
57 12   66     29 my $start = $opt->start // $opt->moment;
58              
59 12 100       91 if ($start) {
60 8         21 my $s = str2time $start;
61 8 50       1646 push @errors, "cannot parse start time: $start" unless $s;
62 8         17 $self->{start} = $s;
63             }
64 12   66     38 my $end = $opt->end // $opt->moment;
65 12 100       86 if ($end) {
66 8         19 my $s = str2time $end;
67 8 50       1011 push @errors, "cannot parse end time: $end" unless $s;
68 8         18 $self->{end} = $s;
69             }
70 12 50 100     63 push @errors, 'you are not filtering at all'
      66        
      33        
71             unless @inclusions || @exclusions || $start || $end;
72              
73 12   66     30 $self->{blank} = $opt->blank || defined $opt->separator;
74 12         136 $self->{separator} = $opt->separator;
75 12         38 $self->{warn} = $opt->warn;
76 12         161 $self->{die} = $opt->die;
77 12         45 my ( $before, $after ) = ( 0, 0 );
78 12 100 100     26 if ( $opt->context || $opt->before || $opt->after ) {
      100        
79 5   100     39 $before = $opt->context // 0;
80 5 100 66     24 $before = $opt->before if $opt->before && $opt->before > $before;
81 5   100     34 $after = $opt->context // 0;
82 5 100 66     25 $after = $opt->after if $opt->after && $opt->after > $after;
83             }
84 12         133 @$self{qw(before after)} = ( $before, $after );
85              
86             { # code options live in their own namespace
87              
88 12         16 package __evaled;
89 12         18 $__evaled::VERSION = '0.001';
90 12   50     11 for my $m ( @{ $opt->module // [] } ) {
  12         24  
91 0   0     0 eval "use $_" for @{ $opt->module // [] };
  0         0  
92 0 0       0 if ($@) {
93 0         0 push @errors, "could not load $m";
94             }
95             }
96             my $evaler = sub {
97 24     24   71 my ( $orig, $option ) = shift;
98 24 50       149 return unless $orig;
99 0         0 my $majv = int $];
100 0         0 my $minv = int( 1000 * ( $] - $majv ) );
101 0         0 my $code =
102             eval "sub { use v$majv.$minv; no strict; no warnings; $orig }";
103 0 0       0 if ( my $e = $@ ) {
104 0         0 $e =~ s/(.*?) at \(eval \d+\).*/$1/s;
105 0         0 push @errors,
106             sprintf 'bad option: --%s; could not evaluate "%s" as perl: %s',
107             $option,
108             $orig,
109             $e;
110             }
111 0         0 return $code;
112 12         117 };
113 12   50 39   23 $self->{code} = $evaler->( $opt->exec, 'exec' ) // sub { shift };
  39         778  
114 12         27 $self->{time} = $evaler->( $opt->time, 'time' );
115             }
116              
117 12         27 return @errors;
118             }
119              
120             # parse a regular expression parameter, registering any errors
121             sub _make_rx {
122 16     16   87 my ( $rx, $errors, $type, $quote, $insensitive ) = @_;
123 16 50       29 unless ($rx) {
124 0         0 push @$errors, "inadequate $type pattern";
125 0         0 return;
126             }
127 16 50       27 $rx = quotemeta $rx if $quote;
128 16 100       19 eval { $rx = $insensitive ? qr/$rx/i : qr/$rx/ };
  16         178  
129 16 50       29 if ($@) {
130 0         0 push @$errors, "bad $type regex: $rx; error: $@";
131 0         0 return;
132             }
133 16         33 return $rx;
134             }
135              
136              
137             sub grep {
138 12     12 1 6937 my $self = shift;
139 12         44 my ( $start, $end, $lines, $include, $exclude, $date, $time ) =
140             @$self{qw(start end lines include exclude date time)};
141 12 100 50 149   82 $time //= sub { str2time $1 if shift =~ $date };
  149         915  
142 12         37 my ( $blank, $warn, $die, $separator, $before, $after, $code ) =
143             @$self{qw(blank warn die separator before after code)};
144 12 50       52 return unless @$lines;
145 12   33     1578 my $quiet = !( $warn || $die );
146 12 100 50     24 $separator //= "" if $blank;
147             my $gd = sub {
148 149     149   215 my $l = shift;
149 149         3964 my $t = $time->($l);
150 149 100       7864 return $t if $t;
151 94 50       256 return if $quiet;
152 0         0 my $msg = qq(could not find date in "$l");
153 0 0       0 if ($warn) {
154 0         0 print STDERR $msg, "\n";
155 0         0 return;
156             }
157 0         0 print STDERR $msg, "\n";
158 0         0 exit;
159 12         45 };
160 12         22 my @include = @$include;
161 12         15 my @exclude = @$exclude;
162 12         13 my ( $previous, @bbuf, $abuf );
163             my $buffer = sub {
164 55     55   67 my ( $line, $lineno ) = @_;
165 55 100       128 if ($abuf) {
    100          
166 4         6 print $code->( $line, $lineno ), "\n";
167 4         6 $previous = $lineno;
168 4         7 $abuf--;
169             }
170             elsif ($before) {
171 24         34 my $pair = [ $line, $lineno ];
172 24         28 push @bbuf, $pair;
173 24 100       51 shift @bbuf if @bbuf > $before;
174             }
175 12         30 };
176             my $printline = sub {
177 34     34   46 my ( $line, $lineno, $match ) = @_;
178 34 100 100     95 print $separator, "\n"
      100        
179             if $blank && $previous && $previous + 1 < $lineno;
180 34         25 $previous = $lineno;
181 34         48 print $code->( $line, $lineno, $match ), "\n";
182 12         33 };
183 12         9 my $i = 0;
184 12   66     32 my $time_filter = $start || $end;
185 12 100       23 if ($time_filter) {
186 8         10 my ( $t1, $t2, $j );
187 8         20 for ( 0 .. $#$lines ) {
188 40         129 $t1 = $gd->( $lines->[$_] );
189 40         51 $j = $_;
190 40 100       67 last if $t1;
191             }
192 8 50       16 return unless $t1;
193 8         29 for ( reverse $j .. $#$lines ) {
194 24         124 $t2 = $gd->( $lines->[$_] );
195 24 100       53 last if $t2;
196             }
197 8 50       25 $start = $t1 unless $start;
198 8 50       15 $end = $t2 unless $end;
199 8 50       17 return unless $end >= $t1;
200 8 50       17 return unless $start <= $t2;
201 8         19 $i = _get_start( $lines, $start, $t1, $t2, $gd );
202             }
203 12 100       28 if ($before) {
204 4         4 $i -= $before;
205 4 50       9 $i = 0 if $i < 0;
206             }
207 12         46 while ( my $line = $lines->[$i] ) {
208 89         5528 my $lineno = $i++;
209 89 100       131 if ($time_filter) {
210 81   100     101 my $t = $gd->($line) // 0;
211 81 100       120 unless ($t) {
212 46         58 $buffer->( $line, $lineno );
213 46         224 next;
214             }
215 35 100       59 if ( $t > $end ) {
216 7 100       15 if ( $abuf-- ) {
217 1         3 print $code->( $line, $lineno ), "\n";
218 1         6 next;
219             }
220             else {
221 6         88 last;
222             }
223             }
224 28 100       48 if ( $t < $start ) {
225 5         11 $buffer->( $line, $lineno );
226 5         26 next;
227             }
228             }
229 31         37 my $good = !@include;
230 31         49 for (@include) {
231 4 100       24 if ( $line =~ $_ ) {
232 3         37 $good = 1;
233 3         5 last;
234             }
235             }
236 31 100       48 if ($good) {
237 30         41 for (@exclude) {
238 4 100       21 if ( $line =~ $_ ) {
239 3         5 undef $good;
240 3         3 last;
241             }
242             }
243             }
244 31 100       47 if ($good) {
245 27         39 $printline->(@$_) for @bbuf;
246 27         42 $printline->( $line, $lineno, 1 );
247 27 100       62 splice @bbuf, 0, scalar @bbuf if $before;
248 27         133 $abuf = $after;
249             }
250             else {
251 4         8 $buffer->( $line, $lineno );
252             }
253             }
254             }
255              
256             # find the log line to begin grepping at
257             sub _get_start {
258 8     8   13 my ( $lines, $start, $t1, $t2, $gd ) = @_;
259 8 100       23 return 0 if $start <= $t1;
260 5         14 my $lim = $#$lines;
261 5         54 my ( $s, $e ) = ( [ 0, $t1 ], [ $lim, $t2 ] );
262 5         10 my ( $last, $revcount ) = ( -1, 0 );
263             {
264 5         7 my $i = _guess( $s, $e, $start );
  9         20  
265 9 100       28 return $i if $i == $s->[0];
266 4         4 my $rev = $last == $i;
267 4         3 $last = $i;
268 4 50       6 if ($rev) { # if we find ourselves looping; bail out
269 0         0 $revcount++;
270 0 0       0 if ( $revcount > 1 ) {
271 0 0       0 --$i if $i;
272 0         0 return $i;
273             }
274             }
275             else {
276 4         4 $revcount = 0;
277             }
278 4         5 my $t;
279             {
280 4         4 $t = $gd->( $lines->[$i] );
  4         7  
281 4 50       9 unless ($t) {
282 0 0       0 $i += $rev ? -1 : 1;
283 0 0       0 return 0 unless $i;
284 0 0       0 return $lim if $i > $lim;
285 0         0 redo;
286             }
287             }
288 4 50       8 return $i if $t == $start;
289 4 100       7 if ( $t < $start ) {
290 3         6 $s = [ $i, $t ];
291             }
292             else {
293 1         2 $e = [ $i, $t ];
294             }
295 4 50       11 if ( $s->[0] == $e->[0] ) {
296 0 0       0 --$i if $i;
297 0         0 return $i;
298             }
299 4         5 redo;
300             }
301             }
302              
303             # estimate the next log line to try
304             sub _guess {
305 9     9   10 my ( $s, $e, $start ) = @_;
306 9         14 my $delta = $start - $s->[1];
307 9 50       12 return $s->[0] unless $delta;
308 9         14 my $diff = $e->[1] - $s->[1];
309 9         21 my $offset = int( ( $e->[0] - $s->[0] ) * $delta / $diff );
310 9         11 return $s->[0] + $offset;
311             }
312              
313             1;
314              
315             __END__