File Coverage

blib/lib/App/jl.pm
Criterion Covered Total %
statement 186 189 98.4
branch 85 94 90.4
condition 39 47 82.9
subroutine 31 33 93.9
pod 3 3 100.0
total 344 366 93.9


line stmt bran cond sub pod time code
1             package App::jl;
2 5     5   414633 use strict;
  5         50  
  5         152  
3 5     5   26 use warnings;
  5         7  
  5         127  
4 5     5   2140 use JSON qw//;
  5         38502  
  5         114  
5 5     5   2312 use Sub::Data::Recursive;
  5         3388  
  5         210  
6 5     5   38 use B;
  5         9  
  5         257  
7 5     5   3774 use Getopt::Long qw/GetOptionsFromArray/;
  5         53075  
  5         22  
8              
9             our $VERSION = '0.19';
10              
11             my $MAX_RECURSIVE_CALL = 255;
12              
13             my $MAYBE_UNIXTIME = join '|', (
14             'create',
15             'update',
16             'expire',
17             '.._(?:at|on)',
18             '.ed$',
19             'date',
20             'time',
21             'since',
22             'when',
23             );
24              
25             my $LOG_LEVEL_STRINGS = join '|', (
26             'debug',
27             'trace',
28             'info',
29             'notice',
30             'warn',
31             'error',
32             'crit(?:ical)?',
33             'fatal',
34             'emerg(?:ency)?',
35             );
36              
37             my $L_BRACKET = '[ \\( \\{ \\< ]';
38             my $R_BRACKET = '[ \\) \\} \\> ]';
39              
40             my $UNIXTIMESTAMP_KEY = '';
41              
42             my $GMTIME;
43              
44             my $INVOKER = 'Sub::Data::Recursive';
45              
46             sub new {
47 32     32 1 23239 my $class = shift;
48 32         74 my @argv = @_;
49              
50 32         79 my $opt = $class->_parse_opt(@argv);
51              
52             my $self = bless {
53             _opt => $opt,
54 32         412 _json => JSON->new->utf8->pretty(!$opt->{no_pretty})->canonical(1),
55             __current_orig_line => undef,
56             }, $class;
57              
58 32         103 $self->_lazyload_modules;
59              
60 32         100 return $self;
61             }
62              
63             sub opt {
64 321     321 1 511 my ($self, $key) = @_;
65              
66 321         953 return $self->{_opt}{$key};
67             }
68              
69             sub run {
70 4     4 1 7 my ($self) = @_;
71              
72 4         10 local $| = !!$self->opt('unbuffered');
73              
74 4 100       9 my $out = !!$self->opt('stderr') ? *STDERR : *STDOUT;
75              
76 4         29 while ($self->{__current_orig_line} = ) {
77 4 100       11 if (my $line = $self->_run_line) {
78 3         61 print $out $line;
79             }
80 4         120 $self->{__current_orig_line} = undef;
81             }
82             }
83              
84             sub _run_line {
85 32     32   158 my ($self) = @_;
86              
87 32 100       128 if ($self->{__current_orig_line} =~ m!^[\s\t\r\n]+$!) {
88 1         4 return;
89             }
90              
91 31 100       129 if ($self->{__current_orig_line} !~ m!^\s*[\[\{]!) {
92 2 100       5 return $self->opt('sweep') ? undef : $self->{__current_orig_line};
93             }
94              
95 29 100       55 if (my $rs = $self->opt('grep')) {
96 4 100       22 if (!$self->_match_grep($rs)) {
97 1         4 return; # no match
98             }
99             }
100              
101 28 100       61 if (my $rs = $self->opt('ignore')) {
102 3 50       8 if ($self->_match_grep($rs)) {
103 3         8 return; # ignore if even one match
104             }
105             }
106              
107 25         56 return $self->_process;
108             }
109              
110             sub _match_grep {
111 7     7   12 my ($self, $rs) = @_;
112              
113 7         7 for my $r (@{$rs}) {
  7         15  
114 7 100       64 return 1 if $self->{__current_orig_line} =~ m!$r!;
115             }
116             }
117              
118             sub _lazyload_modules {
119 32     32   57 my ($self) = @_;
120              
121 32 100 66     69 if ($self->opt('xxxx') || $self->opt('timestamp_key')) {
122 10         1207 require 'POSIX.pm'; ## no critic
123 10         13554 POSIX->import;
124             }
125              
126 32 100       22514 if ($self->opt('yaml')) {
127 1         556 require 'YAML/Syck.pm'; ## no critic
128 1         2344 YAML::Syck->import;
129 1         3 $YAML::Syck::SortKeys = 1;
130             }
131             }
132              
133             sub _process {
134 25     25   36 my ($self) = @_;
135              
136 25         42 my $decoded = eval {
137 25         261 $self->{_json}->decode($self->{__current_orig_line});
138             };
139 25 100       66 if ($@) {
140 1         4 return $self->{__current_orig_line};
141             }
142             else {
143 24         61 $self->_recursive_process($decoded);
144 24         457 return $self->_encode($decoded);
145             }
146             }
147              
148             sub _encode {
149 24     24   45 my ($self, $decoded) = @_;
150              
151 24 100       44 if ($self->opt('yaml')) {
152 1         5 return YAML::Syck::Dump($decoded);
153             }
154             else {
155 23         308 return $self->{_json}->encode($decoded);
156             }
157             }
158              
159             sub _recursive_process {
160 24     24   42 my ($self, $decoded) = @_;
161              
162 24         60 $self->_recursive_pre_process($decoded);
163              
164 24         197 $self->{_recursive_call} = $MAX_RECURSIVE_CALL;
165 24         63 $self->_recursive_decode_json($decoded);
166              
167 24         738 $self->_recursive_post_process($decoded);
168             }
169              
170             sub _recursive_pre_process {
171 24     24   54 my ($self, $decoded) = @_;
172              
173 24         122 $INVOKER->invoke(\&_trim => $decoded);
174              
175 24 100       346 $self->_invoker(\&_split_lf => $decoded) if $self->opt('x');
176             }
177              
178             sub _recursive_post_process {
179 24     24   48 my ($self, $decoded) = @_;
180              
181 24 100       46 if ($self->opt('x')) {
182 14         35 $self->_invoker(\&_split_lf => $decoded);
183             }
184              
185 24 100       283 if ($self->opt('xx')) {
186 14         43 $self->_invoker(\&_split_comma => $decoded);
187             }
188              
189 24 100       264 if ($self->opt('xxx')) {
190 12         29 $self->_invoker(\&_split_label => $decoded);
191             }
192              
193 24 100 66     236 if ($self->opt('xxxx') || $self->opt('timestamp_key')) {
194 10 100       19 if ($self->opt('xxxxx')) {
195 7         22 $INVOKER->invoke(\&_forcely_convert_timestamp => $decoded);
196             }
197             else {
198 3         10 $self->_invoker(\&_convert_timestamp => $decoded);
199             }
200             }
201              
202 24         252 $INVOKER->invoke(\&_trim => $decoded);
203             }
204              
205             my $LAST_VALUE;
206              
207             sub _invoker {
208 57     57   100 my ($self, $code_ref, $hash) = @_;
209              
210 57         79 $LAST_VALUE = '';
211 57         136 $INVOKER->massive_invoke($code_ref => $hash);
212             }
213              
214             sub _skippable_value {
215 309     309   480 my ($context, $last_value) = @_;
216              
217 309   100     1575 return $context && $context eq 'HASH'
218             && $last_value && $last_value =~ m!user[\-\_\s]*agent!i;
219             }
220              
221             sub _split_lf {
222 135     135   2045 my $line = $_[0];
223 135         190 my $context = $_[1];
224              
225 135 100       204 if (_skippable_value($context, $LAST_VALUE)) {
226 14         23 $LAST_VALUE = $line;
227 14         35 return $line;
228             }
229              
230 121         193 $LAST_VALUE = $line;
231              
232 121 100       382 if ($line =~ m![\t\r\n]!) {
233 8         14 chomp $line;
234 8         37 my @elements = split /[\t\r\n]+/, $line;
235 8 50       51 $_[0] = \@elements if scalar @elements > 1;
236             }
237             }
238              
239             sub _split_comma {
240 88     88   1318 my $line = $_[0];
241 88         105 my $context = $_[1];
242              
243 88 100       132 if (_skippable_value($context, $LAST_VALUE)) {
244 7         12 $LAST_VALUE = $line;
245 7         17 return $line;
246             }
247              
248 81         130 $LAST_VALUE = $line;
249              
250 81 100 66     311 return $line if $line !~ m!, ! || $line =~ m!\\!;
251              
252 2         5 chomp $line;
253              
254 2         28 my @elements = split /,\s+/, $line;
255              
256 2 50       17 $_[0] = \@elements if scalar @elements > 1;
257             }
258              
259             sub _split_label {
260 86     86   1261 my $line = $_[0];
261 86         105 my $context = $_[1];
262              
263 86 100       121 if (_skippable_value($context, $LAST_VALUE)) {
264 6         8 $LAST_VALUE = $line;
265 6         16 return $line;
266             }
267              
268 80         126 $LAST_VALUE = $line;
269              
270 80 50       164 return $line if $line =~ m!\\!;
271              
272 80         112 chomp $line;
273              
274             # remove spaces between braces
275 80         331 $line =~ s!( $R_BRACKET ) [\s\t]+ ( $R_BRACKET )!$1$2!xg;
276              
277             # replace square brackets label
278 80         167 $line =~ s!( \[ [\s\t]* .+ [\s\t]* \] )!$1\n!ixg;
279              
280             # replace log level labels
281 80         469 $line =~ s!( $L_BRACKET ) [\s\t]* ( $LOG_LEVEL_STRINGS ) [\s\t]* ( $R_BRACKET )!$1$2$3\n!ixg;
282              
283 80         173 my @elements = split /\n/, $line;
284              
285 80 100       287 $_[0] = \@elements if scalar @elements > 1;
286             }
287              
288             sub _convert_timestamp {
289 37     37   617 my $line = $_[0];
290 37         47 my $context = $_[1];
291              
292 37 100 66     122 return $line if !$context || $context ne 'HASH';
293              
294 26 100 100     341 if (
      66        
      66        
      100        
295             ($UNIXTIMESTAMP_KEY && $LAST_VALUE eq $UNIXTIMESTAMP_KEY && $line =~ m!(\d+(\.\d+)?)!)
296             || ($LAST_VALUE =~ m!(?:$MAYBE_UNIXTIME)!i && $line =~ m!(\d+(\.\d+)?)!)
297             ) {
298 9 50       21 if (my $date = _ts2date($1, $2)) {
299 9         24 $_[0] = $date;
300             }
301             }
302              
303 26         93 $LAST_VALUE = $line;
304             }
305              
306             sub _forcely_convert_timestamp {
307 46     46   524 my $line = $_[0];
308              
309 46 100       148 if ($line =~ m!(\d+(\.\d+)?)!) {
310 7 100       19 if (my $date = _ts2date($1, $2)) {
311 3         22 $_[0] = $date;
312             }
313             }
314             }
315              
316             sub _ts2date {
317 16     16   39 my $unix_timestamp = shift;
318 16   100     52 my $msec = shift || '';
319              
320             # 946684800 = 2000-01-01T00:00:00Z
321 16 100 100     103 if ($unix_timestamp >= 946684800 && $unix_timestamp <= ((2**32 - 1) * 1000)) {
322 12 100       31 if ($unix_timestamp > 2**32 -1) {
323 3         40 ($msec) = ($unix_timestamp =~ m!(\d\d\d)$!);
324 3         10 $msec = ".$msec";
325 3         10 $unix_timestamp = int($unix_timestamp / 1000);
326             }
327 12 100       356 my @t = $GMTIME ? gmtime($unix_timestamp) : localtime($unix_timestamp);
328 12         463 return POSIX::strftime('%Y-%m-%d %H:%M:%S', @t) . $msec;
329             }
330             }
331              
332             sub _trim {
333 219     219   3098 my $line = $_[0];
334              
335 219         249 my $trim = 0;
336              
337 219 100       520 if ($line =~ m!^[\s\t\r\n]+!) {
338 3         9 $line =~ s!^[\s\t\r\n]+!!;
339 3         28 $trim = 1;
340             }
341              
342 219 100       480 if ($line =~ m![\s\t\r\n]+$!) {
343 9         42 $line =~ s![\s\t\r\n]+$!!;
344 9         14 $trim = 1;
345             }
346              
347 219 100       537 if ($trim) {
348 11         52 $_[0] = $line;
349             }
350             }
351              
352             sub _recursive_decode_json {
353 63     63   116 my ($self, $hash) = @_;
354              
355             Sub::Data::Recursive->invoke(sub {
356 174 50   174   2780 if ($self->{_recursive_call} > 0) {
357 174         238 my $orig = $_[0];
358 174 50       399 return if $orig =~ m!^\[\d+\]$!;
359 174 100       290 if (!_is_number($_[0])) {
360 149         191 my $decoded = eval {
361 149         1126 $self->{_json}->decode($orig);
362             };
363 149 100       728 if (!$@) {
364 39         61 $_[0] = $decoded;
365 39         49 $self->{_recursive_call}--;
366 39         88 $self->_recursive_decode_json($_[0]); # recursive calling
367             }
368             }
369             }
370 63         280 } => $hash);
371             }
372              
373             # copied from Data::Recursive::Encode
374             sub _is_number {
375 174     174   223 my $value = shift;
376 174 50       285 return 0 unless defined $value;
377              
378 174         406 my $b_obj = B::svref_2object(\$value);
379 174         340 my $flags = $b_obj->FLAGS;
380 174 100 66     668 return $flags & ( B::SVp_IOK | B::SVp_NOK ) && !( $flags & B::SVp_POK ) ? 1 : 0;
381             }
382              
383             sub _parse_opt {
384 35     35   1397 my ($class, @argv) = @_;
385              
386 35         56 my $opt = {};
387              
388             GetOptionsFromArray(
389             \@argv,
390             'no-pretty' => \$opt->{no_pretty},
391             'x' => \$opt->{x},
392             'xx' => \$opt->{xx},
393             'xxx' => \$opt->{xxx},
394             'xxxx' => \$opt->{xxxx},
395             'X|xxxxx' => \$opt->{xxxxx},
396             'timestamp-key=s' => \$opt->{timestamp_key},
397             'gmtime' => \$opt->{gmtime},
398             'g|grep=s@' => \$opt->{grep},
399             'ignore=s@' => \$opt->{ignore},
400             'yaml|yml' => \$opt->{yaml},
401             'unbuffered' => \$opt->{unbuffered},
402             'stderr' => \$opt->{stderr},
403             'sweep' => \$opt->{sweep},
404             'h|help' => sub {
405 0     0   0 $class->_show_usage(1);
406             },
407             'v|version' => sub {
408 0     0   0 print "$0 $VERSION\n";
409 0         0 exit 1;
410             },
411 35 50       357 ) or $class->_show_usage(2);
412              
413 35   100     34785 $opt->{xxxx} ||= $opt->{xxxxx};
414 35   100     155 $opt->{xxx} ||= $opt->{xxxx};
415 35   100     137 $opt->{xx} ||= $opt->{xxx};
416 35   66     113 $opt->{x} ||= $opt->{xx};
417              
418 35         63 $UNIXTIMESTAMP_KEY = $opt->{timestamp_key};
419              
420 35         61 $GMTIME = $opt->{gmtime};
421              
422 35         68 return $opt;
423             }
424              
425             sub _show_usage {
426 1     1   87 my ($class, $exitval) = @_;
427              
428 1         570 require Pod::Usage;
429 1         52102 Pod::Usage::pod2usage(-exitval => $exitval);
430             }
431              
432             1;
433              
434             __END__