File Coverage

blib/lib/App/JsonLogUtils.pm
Criterion Covered Total %
statement 68 109 62.3
branch 12 28 42.8
condition 2 6 33.3
subroutine 20 25 80.0
pod 3 8 37.5
total 105 176 59.6


line stmt bran cond sub pod time code
1             package App::JsonLogUtils;
2             # ABSTRACT: Command line utilities for dealing with JSON-formatted log files
3             $App::JsonLogUtils::VERSION = '0.02';
4              
5              
6 3     3   660879 use strict;
  3         26  
  3         90  
7 3     3   15 use warnings;
  3         7  
  3         103  
8              
9 3     3   16 use Fcntl qw(:seek);
  3         5  
  3         441  
10 3     3   1591 use Iterator::Simple qw(iterator iter igrep imap ichain);
  3         8839  
  3         228  
11 3     3   760 use JSON::XS qw(decode_json encode_json);
  3         5086  
  3         157  
12 3     3   1603 use Time::HiRes qw(sleep);
  3         4191  
  3         12  
13 3     3   2047 use Term::SimpleColor;
  3         6746  
  3         248  
14              
15 3     3   1415 use parent 'Exporter';
  3         894  
  3         17  
16              
17             our @EXPORT_OK = qw(
18             lines
19             tail
20             json_log
21             json_cols
22             json_cut
23             json_grep
24             );
25              
26              
27             #-------------------------------------------------------------------------------
28             # Internal utilities
29             #-------------------------------------------------------------------------------
30 0     0 0 0 sub log_warn { warn red, @_, default, "\n" }
31 0     0 0 0 sub log_info { warn yellow, @_, default, "\n" }
32              
33             sub _open {
34 5   50 5   18 my $path = shift || return;
35 5 50       18 return $path if ref $path;
36              
37 0 0       0 open my $fh, '<', $path or do{
38 0         0 log_warn $!;
39 0         0 return;
40             };
41              
42 0         0 return $fh;
43             }
44              
45              
46              
47             sub lines ($) {
48 5     5 0 19087 my $path = shift;
49 5   50     28 my $fh = _open $path || return;
50 5     17   41 imap{ chomp $_; $_ } iter $fh;
  17         211  
  17         64  
51             }
52              
53              
54              
55             sub tail ($) {
56 0     0 0 0 my $path = shift;
57 0   0     0 my $fh = _open $path || return;
58 0         0 my $pos = 0;
59 0         0 my $stop = 0;
60              
61 0         0 seek $fh, 0, SEEK_END;
62 0         0 $pos = tell $fh;
63              
64             $SIG{INT} = sub{
65 0     0   0 log_info 'Stopped';
66 0         0 $stop = 1;
67 0         0 };
68              
69             iterator{
70 0     0   0 LINE:do{
71             # Check for control-c
72 0 0       0 if ($stop) {
73 0         0 undef $SIG{INT};
74 0         0 return;
75             }
76              
77             # Check for file truncation
78 0         0 my $eof = eof $fh;
79 0         0 my $cur = tell $fh;
80              
81 0         0 seek $fh, 0, SEEK_END;
82 0         0 my $end = tell $fh;
83              
84 0 0       0 if ($end < $cur) {
85 0         0 log_info 'File truncated';
86 0         0 $pos = $end;
87             }
88             else {
89 0         0 $pos = $cur;
90             }
91              
92 0         0 seek $fh, $pos, SEEK_SET;
93 0 0       0 <$fh> if $eof;
94              
95             # Return next line
96 0 0       0 if (defined(my $line = <$fh>)) {
97 0         0 chomp $line;
98 0         0 return $line;
99             }
100              
101             # Reset position
102 0         0 seek $fh, $pos, SEEK_SET;
103              
104             # Reset EOF condition on handle and wait for new input
105 0         0 seek $fh, 0, SEEK_CUR;
106 0         0 sleep 0.2;
107              
108             # Try again
109 0         0 goto LINE;
110             };
111 0         0 };
112             }
113              
114              
115              
116             sub json_log ($) {
117 5     5 0 11 my $lines = shift;
118              
119             iterator{
120 22     22   16198 while (defined(my $line = <$lines>)) {
121 17 50       40 if (!$line) {
122 0         0 log_info 'empty line';
123 0         0 next;
124             }
125              
126 17         30 my $obj = eval{ decode_json $line };
  17         92  
127              
128 17 50       46 if ($@) {
129 0         0 log_warn "invalid JSON: $line";
130 0         0 next;
131             }
132              
133 17         53 return [$obj, $line];
134             }
135              
136 5         65 return;
137 5         44 };
138             }
139              
140              
141              
142             sub json_cols ($$$) {
143 1     1 1 130 my ($cols, $sep, $lines) = @_;
144 1 50       11 my @cols = ref $cols ? @$cols : split /\s+/, $cols;
145 1         6 my $head = iter [ join($sep, @cols) ];
146             my $rows = imap{
147 3     3   15 my $obj = $_->[0];
148 3 50       7 return join($sep, map{ $obj->{$_} || '' } @cols);
  6         30  
149 1         45 } json_log $lines;
150 1         28 ichain $head, $rows;
151             }
152              
153              
154              
155             sub json_cut ($$$) {
156 2     2 1 111 my ($fields, $inverse, $lines) = @_;
157 2 50       15 my @fields = ref $fields ? @$fields : split /\s+/, $fields;
158              
159 2 100       5 if ($inverse) {
160             imap{
161 3     3   15 foreach my $field (@fields) {
162 3         8 delete $_->[0]{$field};
163             }
164              
165 3         12 $_->[0];
166 1         5 } json_log $lines;
167             }
168             else {
169             imap{
170 3     3   12 my %filtered;
171 3         7 foreach my $field (@fields) {
172 6         13 $filtered{$field} = $_->[0]{$field};
173             }
174              
175 3         15 \%filtered;
176 1         16 } json_log $lines;
177             }
178             }
179              
180              
181              
182             sub json_grep ($$$) {
183 2     2 1 106 my ($patterns, $inverse, $lines) = @_;
184             return igrep{
185 8     8   38 my $obj = $_->[0];
186              
187 8         20 foreach my $field (keys %$patterns) {
188 8         14 foreach my $pattern (@{$patterns->{$field}}) {
  8         15  
189             return unless $inverse
190             ? $obj->{$field} !~ $pattern
191 8 100       52 : $obj->{$field} =~ $pattern;
    100          
192             }
193             }
194              
195 4         10 return 1;
196             }
197 2         10 json_log $lines;
198             }
199              
200              
201              
202             1;
203              
204             __END__