File Coverage

blib/lib/Log/Message/JSON.pm
Criterion Covered Total %
statement 48 62 77.4
branch 13 20 65.0
condition 3 6 50.0
subroutine 13 14 92.8
pod 8 8 100.0
total 85 110 77.2


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3             =head1 NAME
4              
5             Log::Message::JSON - structured messages that stringify to JSON
6              
7             =head1 SYNOPSIS
8              
9             package My::Application::Module;
10              
11             use Log::Log4perl;
12             use Log::Message::JSON qw{logmsg};
13              
14             sub do_something {
15             my ($self, $foo, $bar, @rest) = @_;
16              
17             my $logger = Log::Log4perl->get_logger();
18              
19             $logger->info(logmsg message => "do_something entered",
20             foo => $foo, bar => $bar, rest => \@rest);
21              
22             # ...
23             }
24              
25             # in flat-file logs entry would look like:
26             # Dec 28 00:24:52 example.net My-Application[1587]: {"message":"do_something entered","foo":"value of foo","bar":"value of bar","rest":["value","of","rest"]}
27              
28             =head1 DESCRIPTION
29              
30             Good logging requires today a lot more than in Good Ol' Times[tm]. Each log
31             entry should have a structure and be machine-parseable. On the other hand,
32             there are lot of logging libraries that don't quite support structured logs
33             and only process flat strings.
34              
35             L architecture allows both, flat strings and structured
36             entries. It's up to appender module whether it accepts one or another form.
37             Unfortunately, this makes application developer to decide in advance, which
38             appenders could be in use and defeats much of Log::Log4perl's flexibility.
39              
40             Log::Message::JSON is an attempt to solve this problem. Developer can create
41             a message that has an internal structure (i.e. is a hash(ref)), and at the
42             same time it can be used as a simple string, instantly serializing to
43             single-line JSON. This way the developer don't need to decide on appenders in
44             advance. Moreover, flat string logfiles are easier to parse, especially if
45             entries have this form.
46              
47             Of course, you don't need Log::Log4perl to use this module. It could be used
48             wherever a hashref needs to be sensibly stringified while preserving its all
49             hash-like features.
50              
51             =cut
52              
53             #-----------------------------------------------------------------------------
54              
55             package Log::Message::JSON;
56              
57 7     7   308932 use warnings;
  7         17  
  7         268  
58 7     7   357 use strict;
  7         16  
  7         384  
59              
60 7     7   42 use base qw{Exporter};
  7         17  
  7         1302  
61             our @EXPORT_OK = qw{&logmsg &logmess &msg &json};
62              
63 7     7   18799 use overload ('""' => \&to_json);
  7         9889  
  7         67  
64              
65 7     7   7021 use Log::Message::JSON::Hash;
  7         27  
  7         306  
66 7     7   52 use Carp;
  7         16  
  7         8240  
67              
68             #-----------------------------------------------------------------------------
69              
70             our $VERSION = '0.30.01';
71              
72             #-----------------------------------------------------------------------------
73              
74             =head1 API
75              
76             The preferred way is the short way. Object-oriented API is described here
77             mainly for reference.
78              
79             =head2 Short Way
80              
81             =over
82              
83             =cut
84              
85             #-----------------------------------------------------------------------------
86              
87             =item C
88              
89             =item C
90              
91             =item C
92              
93             =item C
94              
95             These are plain functions. They all are exported (but none by default, you
96             need to specifically ask for them), they all do the same and they all accept
97             the same arguments. They are provided for your convenience. Choose the one
98             that don't clash with your methods (but please, make your life easier in
99             future and choose one for whole application).
100              
101             These functions accept either a reference to a hash or a list of
102             C<< key => value >> pairs. The latter form preserves keys order, so I believe
103             it's more useful. Also, in the latter form you may skip the first key name;
104             the value will be stored under C key in such case.
105              
106             Returned value is an object created with C method (see
107             L), so it's a reference to a hash (blessed, but still
108             hashref, with all its consequences).
109              
110             Usage example:
111              
112             use Log::Message::JSON qw{logmsg};
113             use Log::Log4perl;
114              
115             my $msg1 = logmsg { key1 => 1, key2 => 2 };
116             my $msg2 = logmsg foo => 1, bar => 2, text => "some text";
117             my $msg3 = logmsg "my log message", host => hostname();
118              
119             my $logger = Log::Log4perl->get_logger();
120             $logger->info($msg1);
121             $logger->debug($msg2);
122             $logger->warn($msg3);
123              
124             print $msg1;
125             printf "%s => %s\n", $_, $msg2->{$_} for keys %$msg2;
126              
127             =cut
128              
129             sub json {
130 1     1 1 1017 return __PACKAGE__->new(@_);
131             }
132              
133             sub logmsg {
134 1     1 1 643 return __PACKAGE__->new(@_);
135             }
136              
137             sub logmess {
138 1     1 1 660 return __PACKAGE__->new(@_);
139             }
140              
141             sub msg {
142 3     3 1 1480 return __PACKAGE__->new(@_);
143             }
144              
145             =back
146              
147             =cut
148              
149             #-----------------------------------------------------------------------------
150              
151             =head2 Object-Oriented API
152              
153             =over
154              
155             =cut
156              
157             #-----------------------------------------------------------------------------
158              
159             =item C<< new(key => value, ...) >>
160              
161             =item C<< new({ key => value, ... }) >>
162              
163             Constructor.
164              
165             This method creates a new hash reference. The underlying hash is tied to
166             L (actually, to a proxy class that uses L as
167             a backend) and filled with arguments. Because of overloaded stringification
168             operator, reference is blessed with Log::Message::JSON package.
169              
170             If the first call form (list of pairs) was used, the order of key/value pairs
171             is preserved. If the number of elements is odd, the first element is believed
172             to be value of C key.
173              
174             If the second call form (hashref) was used, key/value pairs are sorted using
175             C operator, unless the referred hash was tied to L.
176              
177             =cut
178              
179             sub new {
180 6     6 1 23 my ($class, @args) = @_;
181              
182 6         46 tie my %self, 'Log::Message::JSON::Hash';
183              
184 6 50 33     40 if (@args == 1 && (ref $args[0] eq 'HASH' || eval {$args[0]->isa('HASH')})) {
      66        
185 0 0       0 if (eval { tied(%{ $args[0] })->isa("Tie::IxHash") }) {
  0         0  
  0         0  
186             # no sort, hash probably tied to Tie::IxHash or something
187 0         0 %self = %{ $args[0] };
  0         0  
188             } else {
189             # sort keys from the hash
190 0         0 %self = map { $_ => $args[0]{$_} } sort keys %{ $args[0] };
  0         0  
  0         0  
191             }
192             } else {
193             # keep the order
194 6 100       20 if (@args % 2 == 1) {
195 1         5 %self = ("message", @args);
196             } else {
197 5         29 %self = @args;
198             }
199             }
200              
201 6         123 return bless \%self, $class;
202             }
203              
204             #-----------------------------------------------------------------------------
205             #
206             # auxiliary functions
207             #
208             #-----------------------------------------------------------------------------
209              
210             =begin Test::Pod::Coverage
211              
212             =item C
213              
214             Helper function for quoting strings in JSON.
215              
216             =end Test::Pod::Coverage
217              
218             =cut
219              
220             sub quote($) {
221 9     9 1 12 my ($str) = @_;
222              
223 9         42 my %q = (
224             "\\" => "\\\\",
225             '"' => '\"',
226             "\n" => "\\n",
227             "\r" => "\\r",
228             "\t" => "\\t",
229             );
230 9         15 $str =~ s/([\\"\n\r\t])/$q{$1}/g;
231              
232 9         64 return $str;
233             }
234              
235             =item C
236              
237             JSON encoding method. This method returns a JSON string that contains no tabs
238             nor newlines. Just a single line of text.
239              
240             =cut
241              
242             # This is my own JSON encoder. This serves as two purposes: first, it relaxes
243             # dependencies on external modules; second, it detects object of class
244             # Log::Message::JSON (at root level, possibly) to preserve keys order when
245             # JSON-infying.
246             sub to_json($) {
247 12     12 1 158 my ($value) = @_;
248              
249 12 100       99 if (ref $value eq __PACKAGE__) { # plain hash, tied
    100          
    100          
    50          
    50          
    50          
250 1         2 my $tied = tied %$value;
251              
252             # store cache for this object if there was no cache for it
253 1 50       5 if (not defined $tied->cache) {
254 4         93 my @pairs = map {
255 1         6 sprintf '%s:%s', to_json($_), to_json($value->{$_})
256             } keys %$value;
257 1         8 $tied->cache(sprintf "{%s}", join ",", @pairs);
258             }
259              
260 1         3 return $tied->cache;
261             } elsif (ref $value eq "HASH") { # plain hash
262 1         3 my @pairs = map {
263 1         5 sprintf '%s:%s', to_json($_), to_json($value->{$_})
264             } sort keys %$value;
265 1         7 return sprintf "{%s}", join ",", @pairs;
266             } elsif (ref $value eq "ARRAY") { # plain array
267 1         3 my @elems = map { to_json($_) } @$value;
  1         5  
268 1         7 return sprintf "[%s]", join ",", @elems;
269             } elsif (ref $value eq "SCALAR") { # plain scalar (reference)
270 0         0 return sprintf '"%s"', quote($$value);
271             } elsif (not defined $value) { # undef (null)
272 0         0 return 'null';
273             } elsif (not ref $value) { # plain scalar
274 9         18 return sprintf '"%s"', quote($value);
275             } else { # compound object
276             # TODO
277 0           croak "Type @{[ref $value]} unsupported yet\n";
  0            
278             }
279             }
280              
281             =back
282              
283             =cut
284              
285             #-----------------------------------------------------------------------------
286              
287             =head1 C NOTES
288              
289             To use Log::Message::JSON as a reason for C, you need to assign it to
290             C<$@> variable and call C without arguments (works for Perl 5.8+).
291              
292             unless (open my $f, "<", $file) {
293             $@ = msg "error opening file",
294             filename => $file,
295             error => "$!", errno => $! + 0;
296             die;
297             }
298              
299             Of course just calling C will work as well, but it will result
300             in a message without end-of-line character.
301              
302             =begin Test::Pod::Coverage
303              
304             =item C
305              
306             B: C
307              
308             =end Test::Pod::Coverage
309              
310             =cut
311              
312              
313             sub PROPAGATE {
314 0     0 1   my ($self, $file, $line) = @_;
315              
316 0           return sprintf "died at %s line %d, %s\n", $file, $line, $self;
317             }
318              
319             #-----------------------------------------------------------------------------
320              
321             =head1 C NOTES
322              
323             You might be tempted to use custom I to stringify the
324             message. It would look like this:
325              
326             my $logger = Log::Log4perl->get_logger();
327             $logger->info({ filter => \&dumper, value => $mydata });
328              
329             This won't work too well: the filter gets called before appender module, so
330             the appender gets a string instead of a structured message. The better way
331             would be:
332              
333             my $logger = Log::Log4perl->get_logger();
334             $logger->info(logmsg $mydata);
335              
336             Log::Log4perl only processes C and C when the object is
337             a plain, unblessed hash, so you may safely use these two key names.
338              
339             =cut
340              
341             #-----------------------------------------------------------------------------
342              
343             =head1 AUTHOR
344              
345             Stanislaw Klekot, C<< >>
346              
347             =head1 LICENSE AND COPYRIGHT
348              
349             Copyright 2012 Stanislaw Klekot.
350              
351             This program is free software; you can redistribute it and/or modify it
352             under the terms of either: the GNU General Public License as published
353             by the Free Software Foundation; or the Artistic License.
354              
355             See http://dev.perl.org/licenses/ for more information.
356              
357             =head1 SEE ALSO
358              
359             L, L,
360             L, L,
361             L.
362              
363             =cut
364              
365             #-----------------------------------------------------------------------------
366             1;
367             # vim:ft=perl