File Coverage

blib/lib/Log/Log4perl/Layout/JSON/Readable.pm
Criterion Covered Total %
statement 37 37 100.0
branch 5 6 83.3
condition 1 3 33.3
subroutine 8 8 100.0
pod 0 3 0.0
total 51 57 89.4


line stmt bran cond sub pod time code
1             package Log::Log4perl::Layout::JSON::Readable;
2              
3 2     2   6843 use strict;
  2         5  
  2         70  
4 2     2   11 use warnings;
  2         5  
  2         76  
5 2     2   11 use parent 'Log::Log4perl::Layout::JSON';
  2         4  
  2         15  
6             our $VERSION = '1.0.3'; # VERSION
7             # ABSTRACT: JSON layout, but some fields always come first
8              
9             use Class::Tiny +{
10 2         17 first_fields => sub { [qw(time pid level)] },
11 2     2   16227 };
  2         6  
  2         17  
12              
13              
14             sub BUILDARGS {
15 4     4 0 11931 my ($class, @etc) = @_;
16             # the parent class does not have a BUILDARGS, but it may get one
17             # in the future, let's handle both cases
18 4   33     27 my $args = $class->maybe::next::method(@etc) || $etc[0];
19              
20 4 100       116 if (my $first_fields = delete $args->{first_fields}) {
21             $args->{first_fields} = [
22 3         33 grep { length }
23 3         7 map { my $v = $_; $v =~ s/\s+//g; $v }
  3         7  
  3         9  
24             split /\s*,\s*/,
25             $first_fields->{value},
26 2         12 ];
27             }
28              
29 4         15 return $args;
30             }
31              
32             # HACK!! the parent class C<warn>s when it sees an argument it doesn't
33             # expect. To prevent that, we consume it first
34             sub BUILDALL {
35 4     4 0 91 my ($self, $args, @etc) = @_;
36              
37 4 100       13 if (my $first_fields = delete $args->{first_fields}) {
38 2         54 $self->first_fields($first_fields);
39             }
40              
41 4         34 return $self->next::method($args,@etc);
42             }
43              
44             sub render {
45 9     9 0 9550 my $self = shift;
46              
47 9         42 my $json = $self->SUPER::render(@_);
48              
49 9 50       5844 if (my $first_fields = $self->first_fields) {
50 9         52 for my $key (reverse @{$first_fields}) {
  9         46  
51 19         46 _move_field_first(\$json, $key);
52             }
53             }
54              
55 9         57 return $json;
56             }
57              
58             sub _move_field_first {
59 19     19   42 my ($json_ref, $key) = @_;
60             # a JSON value starts with double quotes, and ends with a
61             # non-backslash-escaped double quotes
62 19         27 ${$json_ref} =~ s/^{(.+?),("$key":".*?(?<!\\)")/\{$2,$1/;
  19         486  
63 19         72 return;
64             }
65              
66             1;
67              
68             __END__
69              
70             =pod
71              
72             =encoding UTF-8
73              
74             =head1 NAME
75              
76             Log::Log4perl::Layout::JSON::Readable - JSON layout, but some fields always come first
77              
78             =head1 VERSION
79              
80             version 1.0.3
81              
82             =head1 SYNOPSIS
83              
84             Example configuration:
85              
86             log4perl.appender.Example.layout = Log::Log4perl::Layout::JSON::Readable
87             log4perl.appender.Example.layout.field.message = %m{chomp}
88             log4perl.appender.Example.layout.field.category = %c
89             log4perl.appender.Example.layout.field.time = %d
90             log4perl.appender.Example.layout.field.pid = %P
91             log4perl.appender.Example.layout.field.level = %p
92             log4perl.appender.Example.layout.canonical = 1
93             log4perl.appender.Example.layout.first_fields = time, pid, level
94              
95             =head1 DESCRIPTION
96              
97             This layout works just like L<< C<Log::Log4perl::Layout::JSON> >>, but
98             it always prints some fields first, even with C<< canonical => 1 >>.
99              
100             =for Pod::Coverage first_fields
101              
102             The fields to print first are set via the C<first_fields> attribute,
103             which is a comma-separated list of field names (defaults to C<time,
104             pid, level>, like in the synopsis).
105              
106             So, instead of:
107              
108             {"category":"App.Minion.stats","level":"TRACE","message":"Getting metrics","pid":"6689","time":"2018-04-04 13:57:23,990"}
109              
110             you get:
111              
112             {"time":"2018-04-04 13:57:23,990","pid":"6689","level":"TRACE","category":"App.Minion.stats","message":"Getting metrics"}
113              
114             which is more readable (e.g. for the timestamp) and usable (e.g. for
115             the pid).
116              
117             =head1 AUTHORS
118              
119             =over 4
120              
121             =item *
122              
123             Johan Lindstrom <Johan.Lindstrom@broadbean.com>
124              
125             =item *
126              
127             Gianni Ceccarelli <gianni.ceccarelli@broadbean.com>
128              
129             =back
130              
131             =head1 COPYRIGHT AND LICENSE
132              
133             This software is copyright (c) 2021 by BroadBean UK, a CareerBuilder Company.
134              
135             This is free software; you can redistribute it and/or modify it under
136             the same terms as the Perl 5 programming language system itself.
137              
138             =cut