File Coverage

blib/lib/Log/ger/Output/Composite.pm
Criterion Covered Total %
statement 155 174 89.0
branch 36 56 64.2
condition 12 18 66.6
subroutine 10 11 90.9
pod 1 3 33.3
total 214 262 81.6


line stmt bran cond sub pod time code
1             package Log::ger::Output::Composite;
2              
3             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
4             our $DATE = '2020-11-17'; # DATE
5             our $DIST = 'Log-ger-Output-Composite'; # DIST
6             our $VERSION = '0.017'; # VERSION
7              
8 1     1   1987 use strict;
  1         3  
  1         37  
9 1     1   5 use warnings;
  1         2  
  1         28  
10 1     1   5 use Log::ger::Util;
  1         2  
  1         713  
11              
12             # this can be used to override all level settings as it has the highest
13             # precedence.
14             our $Current_Level;
15              
16             sub meta { +{
17 7     7 0 19303 v => 2,
18             } }
19              
20             sub _debug {
21 14 50   14   43 return unless $ENV{LOG_GER_OUTPUT_COMPOSITE_DEBUG};
22 0         0 warn "[Log::ger::Output::Composite] debug: $_[0]\n";
23             }
24              
25             sub _get_min_max_level {
26 2160     2160   3790 my $level = shift;
27 2160         3201 my ($min, $max);
28 2160 100       4030 if (defined $level) {
29 1620 50       3604 if (defined $Current_Level) {
    100          
30 0         0 $min = 0;
31 0         0 $max = $Current_Level;
32             } elsif (ref $level eq 'ARRAY') {
33 144         345 $min = Log::ger::Util::numeric_level($level->[0]);
34 144         1088 $max = Log::ger::Util::numeric_level($level->[1]);
35 144 100       980 ($min, $max) = ($max, $min) if $min > $max;
36             } else {
37 1476         2077 $min = 0;
38 1476         3032 $max = Log::ger::Util::numeric_level($level);
39             }
40             }
41 2160         12266 ($min, $max);
42             }
43              
44             sub get_hooks {
45 7     7 0 111 my %plugin_conf = @_;
46              
47             #_debug "In get_hooks()";
48              
49 7         15 my $empty_hashref = {};
50 7         16 my %outputter_get_hooks_cache; # key = "$output $conf", value = result from outputter's get_hooks()
51             my %layouter_get_hooks_cache ; # key = "$output $conf", value = result from layouter's get_hooks()
52              
53             # check arguments
54 7         27 for my $k (keys %plugin_conf) {
55 9         19 my $conf = $plugin_conf{$k};
56 9 100       32 if ($k eq 'outputs') {
    50          
57 7         26 for my $o (keys %$conf) {
58 7 50       31 for my $oconf (ref $conf->{$o} eq 'ARRAY' ?
59 7         19 @{ $conf->{$o} } : $conf->{$o}) {
60 13         47 for my $k2 (keys %$oconf) {
61 20 50       108 unless ($k2 =~
62             /\A(conf|level|category_level|layout)\z/) {
63 0         0 die "Unknown configuration for output '$o': '$k2'";
64             }
65             }
66             }
67             }
68             } elsif ($k =~ /\A(category_level)\z/) {
69             } else {
70 0         0 die "Unknown configuration: '$k'";
71             }
72             }
73              
74 7         14 my @ospecs;
75             {
76 7         13 my $outputs = $plugin_conf{outputs};
  7         13  
77 7         23 for my $oname (sort keys %$outputs) {
78 7         23 my $ospec0 = $outputs->{$oname};
79 7         12 my @ospecs0;
80 7 50       21 if (ref $ospec0 eq 'ARRAY') {
81 7         16 @ospecs0 = map { +{ %{$_} } } @$ospec0;
  13         21  
  13         44  
82             } else {
83 0         0 @ospecs0 = (+{ %{ $ospec0 } });
  0         0  
84             }
85              
86 7 50       53 die "Invalid output name '$oname'"
87             unless $oname =~ /\A\w+(::\w+)*\z/;
88 7         23 my $mod = "Log::ger::Output::$oname";
89 7         47 (my $mod_pm = "$mod.pm") =~ s!::!/!g;
90 7         644 require $mod_pm;
91 7         499 for my $ospec (@ospecs0) {
92 13         28 $ospec->{_name} = $oname;
93 13         22 $ospec->{_mod} = $mod;
94 13         35 push @ospecs, $ospec;
95             }
96             }
97             }
98              
99             return {
100             create_outputter => [
101             __PACKAGE__, # key
102             9, # priority.
103             # we use a high priority to override Log::ger's default hook (at
104             # priority 10) which create null outputter for levels lower than the
105             # general level, since we want to do our own custom level checking.
106             sub { # hook
107 1     1   8 no strict 'refs';
  1         2  
  1         660  
108 468     468   108427 require Data::Dmp;
109              
110 468         3887 my %hook_args = @_; # see Log::ger::Manual::Internals/"Arguments passed to hook"
111              
112 468         919 my $outputters = [];
113 468         833 my $layouters = [];
114 468         920 for my $ospec (@ospecs) {
115 828         1478 my $oname = $ospec->{_name};
116 828         1674 my $mod = "Log::ger::Output::$oname";
117 828   33     1876 my $ospec_conf = $ospec->{conf} || $empty_hashref;
118 828         1862 my $cache_key = "$oname $ospec_conf";
119 828   66     1855 my $hooks = $outputter_get_hooks_cache{$cache_key} || do {
120             _debug("calling $oname\'s get_hooks() ...");
121             my $res = &{"$mod\::get_hooks"}(%$ospec_conf)
122             or die "Output module $mod does not return any hooks";
123             $outputter_get_hooks_cache{$cache_key} = $res;
124             $res;
125             };
126             my @hook_args = (
127             routine_name => $hook_args{routine_name},
128             target_type => $hook_args{target_type},
129             target_name => $hook_args{target_name},
130             per_target_conf => $hook_args{per_target_conf},
131 828         2269 );
132 828         1279 my $res;
133             {
134 828         1142 push @hook_args, (level => 60, str_level => 'trace');
  828         1693  
135 828 50       1675 if ($hooks->{create_log_routine}) { # old name, will be removed in the future
136 0         0 $res = $hooks->{create_log_routine}->[2]->(
137             @hook_args);
138 0 0       0 if ($res->[0]) {
139 0         0 push @$outputters, $res->[0];
140 0         0 last;
141             }
142             }
143 828 50       1638 if ($hooks->{create_outputter}) {
144 828         2109 $res = $hooks->{create_outputter}->[2]->(
145             @hook_args);
146 828 50       10643 if ($res->[0]) {
147 828         1504 push @$outputters, $res->[0];
148 828         1390 last;
149             }
150             }
151 0         0 die "Output module $mod does not produce outputter in ".
152             "its create_outputter (or create_log_routine) hook"; # old name create_log_routine will be removed in the future
153             }
154 828 100       1611 if ($ospec->{layout}) {
155 36         72 my $lname = $ospec->{layout}[0];
156 36   33     78 my $lconf = $ospec->{layout}[1] || $empty_hashref;
157 36         69 my $lmod = "Log::ger::Layout::$lname";
158 36         183 (my $lmod_pm = "$lmod.pm") =~ s!::!/!g;
159 36         844 require $lmod_pm;
160 36         4501 my $cache_key = "$lname $lconf";
161 36   66     89 my $lhooks = $layouter_get_hooks_cache{$cache_key} || do {
162             _debug("calling layouter $lname\'s get_hooks() ...");
163             my $res = &{"$lmod\::get_hooks"}(%$lconf)
164             or die "Layout module $lmod does not return any hooks";
165             $layouter_get_hooks_cache{$cache_key} = $res;
166             $res;
167             };
168             $lhooks->{create_layouter}
169 36 50       81 or die "Layout module $mod does not declare ".
170             "layouter";
171             my @lhook_args = (
172             target_type => $hook_args{target_type},
173             target_name => $hook_args{target_name},
174             per_target_conf => $hook_args{per_target_conf},
175 36         104 );
176 36 50       105 my $lres = $lhooks->{create_layouter}->[2]->(
177             @lhook_args) or die "Hook from layout module ".
178             "$lmod does not produce layout routine";
179 36 50       412 ref $lres->[0] eq 'CODE'
180             or die "Layouter from layout module $lmod ".
181             "is not a coderef";
182 36         160 push @$layouters, $lres->[0];
183             } else {
184 792         2475 push @$layouters, undef;
185             }
186             }
187 468 50       1060 unless (@$outputters) {
188 0         0 $Log::ger::_outputter_is_null = 1;
189 0         0 return [sub {0}];
  0         0  
190             }
191              
192             # put the data that are mentioned in string-eval'ed code in a
193             # package so they are addressable
194 468         661 my $varname = do {
195 468         693 my $suffix;
196 468 50       1086 if ($hook_args{target_type} eq 'package') {
197 468         774 $suffix = $hook_args{target_name};
198             } else {
199 0         0 ($suffix) = "$hook_args{target_name}" =~ /\(0x(\w+)/;
200             }
201 468         997 "Log::ger::Stash::OComposite_$suffix";
202             };
203             {
204 1     1   17 no strict 'refs';
  1         2  
  1         972  
  468         669  
205 468         752 ${$varname} = [];
  468         3709  
206 468         907 ${$varname}->[0] = $outputters;
  468         1324  
207 468         716 ${$varname}->[1] = $layouters;
  468         1026  
208 468         774 ${$varname}->[2] = $hook_args{per_target_conf};
  468         1069  
209             }
210              
211             # generate our outputter routine
212 468         754 my $composite_outputter;
213             {
214 468         656 my @src;
  468         775  
215 468         743 push @src, "sub {\n";
216 468         669 push @src, " my (\$per_target_conf, \$fmsg, \$per_msg_conf) = \@_;\n";
217 468 50       1588 push @src, " my \$lvl; if (\$per_msg_conf) { \$lvl = \$per_msg_conf->{level} }", (defined $hook_args{level} ? " if (!defined \$lvl) { \$lvl = $hook_args{level} }" : ""), "\n";
218 468         815 push @src, " if (!\$per_msg_conf) { \$per_msg_conf = {level=>\$lvl} }\n"; # since we want to pass level etc to other outputs
219              
220 468         1250 for my $i (0..$#ospecs) {
221 828         1328 my $ospec = $ospecs[$i];
222 828         2017 push @src, " # output #$i: $ospec->{_name}\n";
223 828         1257 push @src, " {\n";
224              
225             # filter by output's category_level and category-level
226 828 100 100     2770 if ($ospec->{category_level} || $plugin_conf{category_level}) {
227 252         460 push @src, " my \$cat = (\$per_msg_conf ? \$per_msg_conf->{category} : undef) || \$per_target_conf->{category} || '';\n";
228 252         390 push @src, " local \$per_msg_conf->{category} = \$cat;\n";
229              
230 252         370 my @cats;
231 252 100       591 if ($ospec->{category_level}) {
232 180         285 for my $cat (keys %{$ospec->{category_level}}) {
  180         520  
233 612         1020 my $clevel = $ospec->{category_level}{$cat};
234 612         1355 push @cats, [$cat, 1, $clevel];
235             }
236             }
237 252 100       607 if ($plugin_conf{category_level}) {
238 144         206 for my $cat (keys %{$plugin_conf{category_level}}) {
  144         402  
239 720         1113 my $clevel = $plugin_conf{category_level}{$cat};
240 720         1488 push @cats, [$cat, 2, $clevel];
241             }
242             }
243              
244 252         828 for my $cat (sort {
245 2124 50 100     6040 length($b->[0]) <=> length($a->[0]) ||
246             $a->[0] cmp $b->[0] ||
247             $a->[1] <=> $b->[1]} @cats) {
248 1332         3004 push @src, " if (\$cat eq ".Data::Dmp::dmp($cat->[0])." || index(\$cat, ".Data::Dmp::dmp("$cat->[0]\::").") == 0) { ";
249 1332         72598 my ($min_level, $max_level) =
250             _get_min_max_level($cat->[2]);
251 1332         3880 push @src, "if (\$lvl >= $min_level && ".
252             "\$lvl <= $max_level) { goto LOG } else { last }";
253 1332         2991 push @src, " }\n";
254             }
255 252         820 push @src, "\n";
256             }
257              
258             # filter by output level
259             my ($min_level, $max_level) = _get_min_max_level(
260 828         2149 $ospec->{level});
261 828 100       1982 if (defined $min_level) {
262 288         820 push @src, " if (\$lvl >= $min_level && ".
263             "\$lvl <= $max_level) { goto LOG } else { last }\n";
264             }
265              
266             # filter by general level
267 828         1382 push @src, " if (\$Log::ger::Current_Level >= \$lvl) { goto LOG } else { last }\n";
268              
269             # run output's log routine
270 828         1228 push @src, " LOG:\n";
271 828         1800 push @src, " if (\$$varname\->[1][$i]) {\n";
272 828         2403 push @src, " \$$varname\->[0][$i]->(\$per_target_conf, \$$varname\->[1][$i]->(\$fmsg, \$$varname\->[2], \$lvl, Log::ger::Util::string_level(\$lvl)), \$per_msg_conf);\n";
273 828         1307 push @src, " } else {\n";
274 828         1688 push @src, " \$$varname\->[0][$i]->(\$per_target_conf, \$fmsg, \$per_msg_conf);\n";
275 828         1176 push @src, " }\n";
276 828         1236 push @src, " }\n";
277 828         1982 push @src, " # end output #$i\n\n";
278             } # for ospec
279              
280 468         786 push @src, "};\n";
281 468         2557 my $src = join("", @src);
282 468 50       1176 if ($ENV{LOG_LOG_GER_OUTPUT_COMPOSITE_CODE}) {
283 0         0 warn "Log::ger::Output::Composite logger source code (target type=$hook_args{target_type} target name=$hook_args{target_name}, routine name=$hook_args{routine_name}): <<$src>>\n";
284             }
285              
286 468         153520 $composite_outputter = eval $src;
287             }
288 468         2817 [$composite_outputter];
289 7         164 }] # hook record
290             };
291             }
292              
293             sub set_level {
294 0     0 1   $Current_Level = Log::ger::Util::numeric_level(shift);
295 0           Log::ger::Util::reinit_all_targets();
296             }
297              
298             1;
299             # ABSTRACT: Composite output
300              
301             __END__