File Coverage

lib/Mail/SpamAssassin/Logger.pm
Criterion Covered Total %
statement 45 131 34.3
branch 8 74 10.8
condition 2 15 13.3
subroutine 15 19 78.9
pod 8 8 100.0
total 78 247 31.5


line stmt bran cond sub pod time code
1             # <@LICENSE>
2             # Licensed to the Apache Software Foundation (ASF) under one or more
3             # contributor license agreements. See the NOTICE file distributed with
4             # this work for additional information regarding copyright ownership.
5             # The ASF licenses this file to you under the Apache License, Version 2.0
6             # (the "License"); you may not use this file except in compliance with
7             # the License. You may obtain a copy of the License at:
8             #
9             # http://www.apache.org/licenses/LICENSE-2.0
10             #
11             # Unless required by applicable law or agreed to in writing, software
12             # distributed under the License is distributed on an "AS IS" BASIS,
13             # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14             # See the License for the specific language governing permissions and
15             # limitations under the License.
16             # </@LICENSE>
17              
18             =head1 NAME
19              
20             Mail::SpamAssassin::Logger - SpamAssassin logging module
21              
22             =head1 SYNOPSIS
23              
24             use Mail::SpamAssassin::Logger;
25              
26             $SIG{__WARN__} = sub {
27             log_message("warn", $_[0]);
28             };
29              
30             $SIG{__DIE__} = sub {
31             log_message("error", $_[0]) if !$^S;
32             };
33              
34             =cut
35              
36             package Mail::SpamAssassin::Logger;
37              
38 43     43   305 use strict;
  43         142  
  43         1473  
39 43     43   275 use warnings;
  43         108  
  43         1341  
40             # use bytes;
41 43     43   253 use re 'taint';
  43         116  
  43         1478  
42              
43 43     43   293 use Exporter ();
  43         118  
  43         3211  
44              
45             our @ISA = qw(Exporter);
46             our @EXPORT = qw(dbg info would_log);
47             our @EXPORT_OK = qw(log_message);
48              
49 43     43   291 use constant ERROR => 0;
  43         108  
  43         4082  
50 43     43   281 use constant WARNING => 1;
  43         97  
  43         2408  
51 43     43   292 use constant INFO => 2;
  43         88  
  43         2146  
52 43     43   267 use constant DBG => 3;
  43         108  
  43         4865  
53              
54             my %log_level = (
55             0 => 'ERROR',
56             1 => 'WARNING',
57             2 => 'INFO',
58             3 => 'DBG',
59             );
60              
61             # global shared object
62             our %LOG_SA;
63             our $LOG_ENTERED; # to avoid recursion on die or warn from within logging
64              
65             # defaults
66             $LOG_SA{level} = WARNING; # log info, warnings and errors
67             $LOG_SA{facility} = {}; # no dbg facilities turned on
68              
69             # always log to stderr initially
70 43     43   13997 use Mail::SpamAssassin::Logger::Stderr;
  43         105  
  43         70774  
71             $LOG_SA{method}->{stderr} = Mail::SpamAssassin::Logger::Stderr->new();
72              
73             =head1 METHODS
74              
75             =over 4
76              
77             =item add_facilities(facilities)
78              
79             Enable debug logging for specific facilities. Each facility is the area
80             of code to debug. Facilities can be specified as a hash reference (the
81             key names are used), an array reference, an array, or a comma-separated
82             scalar string. Facility names are case-sensitive.
83              
84             If "all" is listed, then all debug facilities are implicitly enabled,
85             except for those explicitly disabled. A facility name may be preceded
86             by a "no" (case-insensitive), which explicitly disables it, overriding
87             the "all". For example: all,norules,noconfig,nodcc. When facility names
88             are given as an ordered list (array or scalar, not a hash), the last entry
89             applies, e.g. 'nodcc,dcc,dcc,noddc' is equivalent to 'nodcc'. Note that
90             currently no facility name starts with a "no", it is advised to keep this
91             practice with newly added facility names to make life easier.
92              
93             Higher priority informational messages that are suitable for logging in
94             normal circumstances are available with an area of "info". Some very
95             verbose messages require the facility to be specifically enabled (see
96             C<would_log> below).
97              
98             =cut
99              
100             sub add_facilities {
101 81     81 1 451 my ($facilities) = @_;
102              
103 81         253 my @facilities;
104 81 50       527 if (ref ($facilities) eq '') {
    0          
    0          
105 81 50 66     715 if (defined $facilities && $facilities ne '0') {
106 0         0 @facilities = split(/,/, $facilities);
107             }
108             }
109             elsif (ref ($facilities) eq 'ARRAY') {
110 0         0 @facilities = @{ $facilities };
  0         0  
111             }
112             elsif (ref ($facilities) eq 'HASH') {
113 0         0 @facilities = keys %{ $facilities };
  0         0  
114             }
115 81         377 @facilities = grep(/^\S+$/, @facilities);
116 81 50       488 if (@facilities) {
117 0         0 for my $fac (@facilities) {
118 0         0 local ($1,$2);
119 0 0       0 $LOG_SA{facility}->{$2} = !defined($1) if $fac =~ /^(no)?(.+)\z/si;
120             }
121             # turn on debugging if facilities other than "info" are enabled
122 0 0 0     0 if (grep { !/^info\z/ && !/^no./si } keys %{ $LOG_SA{facility} }) {
  0         0  
  0         0  
123 0 0       0 $LOG_SA{level} = DBG if $LOG_SA{level} < DBG;
124             }
125             else {
126 0 0       0 $LOG_SA{level} = INFO if $LOG_SA{level} < INFO;
127             }
128             # debug statement last so we might see it
129 0         0 dbg("logger: adding facilities: " . join(", ", @facilities));
130 0         0 dbg("logger: logging level is " . $log_level{$LOG_SA{level}});
131             }
132             }
133              
134             =item log_message($level, @message)
135              
136             Log a message at a specific level. Levels are specified as strings:
137             "warn", "error", "info", and "dbg". The first element of the message
138             must be prefixed with a facility name followed directly by a colon.
139              
140             =cut
141              
142             sub log_message {
143 0     0 1 0 my ($level, @message) = @_;
144              
145             # too many die and warn messages out there, don't log the ones that we don't
146             # own. jm: off: this makes no sense -- if a dependency module dies or warns,
147             # we want to know about it, unless we're *SURE* it's not something worth
148             # worrying about.
149             # if ($level eq "error" or $level eq "warn") {
150             # return unless $message[0] =~ /^\S+:/;
151             # }
152              
153 0 0       0 if ($level eq "error") {
154             # don't log alarm timeouts or broken pipes of various plugins' network checks
155 0 0       0 return if ($message[0] =~ /__ignore__/);
156              
157             # dos: we can safely ignore any die's that we eval'd in our own modules so
158             # don't log them -- this is caller 0, the use'ing package is 1, the eval is 2
159 0         0 my @caller = caller 2;
160 0 0 0     0 return if (defined $caller[3] && defined $caller[0] &&
      0        
      0        
161             $caller[3] =~ /^\(eval\)$/ &&
162             $caller[0] =~ m#^Mail::SpamAssassin(?:$|::)#);
163             }
164              
165 0 0       0 return if $LOG_ENTERED; # avoid recursion on die or warn from within logging
166 0         0 $LOG_ENTERED = 1; # no 'returns' from this point on, must clear the flag
167              
168 0         0 my $message = join(" ", @message);
169 0         0 $message =~ s/[\r\n]+$//; # remove any trailing newlines
170              
171             # split on newlines and call log_message multiple times; saves
172             # the subclasses having to understand multi-line logs
173 0         0 my $first = 1;
174 0         0 foreach my $line (split(/\n/, $message)) {
175             # replace control characters with "_", tabs and spaces get
176             # replaced with a single space.
177 0         0 $line =~ tr/\x09\x20\x00-\x1f/ _/s;
178 0 0       0 if ($first) {
179 0         0 $first = 0;
180             } else {
181 0         0 local $1;
182 0         0 $line =~ s/^([^:]+?):/$1: [...]/;
183             }
184 0         0 while (my ($name, $object) = each %{ $LOG_SA{method} }) {
  0         0  
185 0         0 $object->log_message($level, $line);
186             }
187             }
188 0         0 $LOG_ENTERED = 0;
189             }
190              
191             =item dbg("facility: message")
192              
193             This is used for all low priority debugging messages.
194              
195             =cut
196              
197             sub dbg {
198 29337 50   29337 1 64389 _log(DBG, @_) if $LOG_SA{level} >= DBG;
199 29337         78465 1; # always return the same simple value, regardless of log level
200             }
201              
202             =item info("facility: message")
203              
204             This is used for informational messages indicating a normal, but
205             significant, condition. This should be infrequently called. These
206             messages are typically logged when SpamAssassin is run as a daemon.
207              
208             =cut
209              
210             sub info {
211 10 50   10 1 25 _log(INFO, @_) if $LOG_SA{level} >= INFO;
212 10         21 1; # always return the same simple value, regardless of log level
213             }
214              
215             # remember to avoid deep recursion, my friend
216             sub _log {
217 0     0   0 my $facility;
218 0         0 local ($1);
219              
220             # it's faster to access this as the $_[1] alias, and not to perform
221             # string mods until we're sure we actually want to log anything
222 0 0       0 if ($_[1] =~ /^([a-z0-9_-]*):/i) {
223 0         0 $facility = $1;
224             } else {
225 0         0 $facility = "generic";
226             }
227              
228             # log all info, warn, and error messages;
229             # only debug if asked to
230 0 0       0 if ($_[0] == DBG) {
231             return unless
232             exists $LOG_SA{facility}->{$facility} ? $LOG_SA{facility}->{$facility}
233 0 0       0 : $LOG_SA{facility}->{all};
    0          
234             }
235              
236 0         0 my ($level, $message, @args) = @_;
237 0         0 $message =~ s/^(?:[a-z0-9_-]*):\s*//i;
238              
239 0 0       0 $message = sprintf($message,@args) if @args;
240 0         0 $message =~ s/\n+$//s;
241 0         0 $message =~ s/^/${facility}: /mg;
242              
243             # no reason to go through warn()
244 0 0       0 log_message(($level == INFO ? "info" : "dbg"), $message);
245             }
246              
247             =item add(method => 'syslog', socket => $socket, facility => $facility)
248              
249             C<socket> is the type the syslog ("unix" or "inet"). C<facility> is the
250             syslog facility (typically "mail").
251              
252             =item add(method => 'file', filename => $file)
253              
254             C<filename> is the name of the log file.
255              
256             =item add(method => 'stderr')
257              
258             No options are needed for stderr logging, just don't close stderr first.
259              
260             =cut
261              
262             sub add {
263 0     0 1 0 my %params = @_;
264              
265 0         0 my $name = lc($params{method});
266 0         0 my $class = ucfirst($name);
267              
268             eval 'use Mail::SpamAssassin::Logger::'.$class.'; 1'
269 0 0       0 or do {
270 0 0       0 my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  0         0  
271 0         0 die "logger: add $class failed: $eval_stat\n";
272             };
273              
274 0 0       0 if (!exists $LOG_SA{method}->{$name}) {
275 0         0 my $object;
276             my $eval_stat;
277             eval '$object = Mail::SpamAssassin::Logger::'.$class.'->new(%params); 1'
278 0 0       0 or do {
279 0 0       0 $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  0         0  
280 0         0 undef $object; # just in case
281             };
282 0 0       0 if (!$object) {
283 0 0       0 if (!defined $eval_stat) {
284 0         0 $eval_stat = "Mail::SpamAssassin::Logger::$class->new ".
285             "failed to return an object";
286             }
287 0         0 warn "logger: failed to add $name method: $eval_stat\n";
288             }
289             else {
290 0         0 $LOG_SA{method}->{$name} = $object;
291 0         0 dbg("logger: successfully added $name method\n");
292 0         0 return 1;
293             }
294 0         0 return 0;
295             }
296              
297 0         0 warn "logger: $name method already added\n";
298 0         0 return 1;
299             }
300              
301             =item remove(method)
302              
303             Remove a logging method. Only the method name needs to be passed as a
304             scalar.
305              
306             =cut
307              
308             sub remove {
309 0     0 1 0 my ($method) = @_;
310              
311 0         0 my $name = lc($method);
312 0 0       0 if (exists $LOG_SA{method}->{$name}) {
313 0         0 delete $LOG_SA{method}->{$name};
314 0         0 info("logger: removing $name method");
315 0         0 return 1;
316             }
317 0         0 warn "logger: unable to remove $name method, not present to be removed\n";
318 0         0 return 1;
319             }
320              
321             =item would_log($level, $facility)
322              
323             Returns false if a message at the given level and with the given facility
324             would not be logged. Returns 1 if a message at a given level and facility
325             would be logged normally. Returns 2 if the facility was specifically
326             enabled.
327              
328             The facility argument is optional.
329              
330             =cut
331              
332             sub would_log {
333 2600     2600 1 5179 my ($level, $facility) = @_;
334              
335 2600 50       5097 if ($level eq "info") {
336 0         0 return $LOG_SA{level} >= INFO;
337             }
338 2600 50       4670 if ($level eq "dbg") {
339 2600 50       12438 return 0 if $LOG_SA{level} < DBG;
340 0 0       0 return 1 if !$facility;
341             return ($LOG_SA{facility}->{$facility} ? 2 : 0)
342 0 0       0 if exists $LOG_SA{facility}->{$facility};
    0          
343 0 0       0 return 1 if $LOG_SA{facility}->{all};
344 0         0 return 0;
345             }
346 0         0 warn "logger: would_log called with unknown level: $level\n";
347 0         0 return 0;
348             }
349              
350             =item close_log()
351              
352             Close all logs.
353              
354             =cut
355              
356             sub close_log {
357 43     43 1 206 while (my ($name, $object) = each %{ $LOG_SA{method} }) {
  86         1940  
358 43         553 $object->close_log();
359             }
360             }
361              
362             END {
363 42     42   2178423 close_log();
364             }
365              
366             1;
367              
368             =back
369              
370             =cut