File Coverage

blib/lib/App/loggrep.pm
Criterion Covered Total %
statement 190 231 82.2
branch 85 128 66.4
condition 49 69 71.0
subroutine 16 16 100.0
pod 3 3 100.0
total 343 447 76.7


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