File Coverage

lib/Mail/SpamAssassin/Logger.pm
Criterion Covered Total %
statement 47 150 31.3
branch 7 84 8.3
condition 2 15 13.3
subroutine 16 21 76.1
pod 8 8 100.0
total 80 278 28.7


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 44     44   308 use strict;
  44         115  
  44         1474  
39 44     44   272 use warnings;
  44         118  
  44         1564  
40             # use bytes;
41 44     44   297 use re 'taint';
  44         84  
  44         1460  
42              
43 44     44   261 use Exporter ();
  44         84  
  44         903  
44 44     44   24009 use Time::HiRes ();
  44         60704  
  44         3248  
45              
46             our @ISA = qw(Exporter);
47             our @EXPORT = qw(dbg info would_log);
48             our @EXPORT_OK = qw(log_message);
49              
50 44     44   330 use constant ERROR => 0;
  44         84  
  44         4051  
51 44     44   270 use constant WARNING => 1;
  44         88  
  44         2293  
52 44     44   629 use constant INFO => 2;
  44         73  
  44         2153  
53 44     44   281 use constant DBG => 3;
  44         113  
  44         6756  
54              
55             my %log_level = (
56             0 => 'ERROR',
57             1 => 'WARNING',
58             2 => 'INFO',
59             3 => 'DBG',
60             );
61              
62             # global shared object
63             our %LOG_SA;
64             our $LOG_ENTERED; # to avoid recursion on die or warn from within logging
65             # duplicate message line suppressor
66             our $LOG_DUPMIN = 10; # only start suppressing after x duplicate lines
67             our $LOG_DUPLINE = ''; # remembers last log line
68             our $LOG_DUPLEVEL = ''; # remembers last log level
69             our $LOG_DUPTIME; # remembers last log line timestamp
70             our $LOG_DUPCNT = 0; # counts duplicates
71              
72             # defaults
73             $LOG_SA{level} = WARNING; # log info, warnings and errors
74             $LOG_SA{facility} = {}; # no dbg facilities turned on
75              
76             # always log to stderr initially
77 44     44   15440 use Mail::SpamAssassin::Logger::Stderr;
  44         96  
  44         86665  
78             $LOG_SA{method}->{stderr} = Mail::SpamAssassin::Logger::Stderr->new();
79              
80             =head1 METHODS
81              
82             =over 4
83              
84             =item add_facilities(facilities)
85              
86             Enable debug logging for specific facilities. Each facility is the area
87             of code to debug. Facilities can be specified as a hash reference (the
88             key names are used), an array reference, an array, or a comma-separated
89             scalar string. Facility names are case-sensitive.
90              
91             If "all" is listed, then all debug facilities are implicitly enabled,
92             except for those explicitly disabled. A facility name may be preceded
93             by a "no" (case-insensitive), which explicitly disables it, overriding
94             the "all". For example: all,norules,noconfig,nodcc. When facility names
95             are given as an ordered list (array or scalar, not a hash), the last entry
96             applies, e.g. 'nodcc,dcc,dcc,noddc' is equivalent to 'nodcc'. Note that
97             currently no facility name starts with a "no", it is advised to keep this
98             practice with newly added facility names to make life easier.
99              
100             Higher priority informational messages that are suitable for logging in
101             normal circumstances are available with an area of "info". Some very
102             verbose messages require the facility to be specifically enabled (see
103             C<would_log> below).
104              
105             =cut
106              
107             sub add_facilities {
108 91     91 1 506 my ($facilities) = @_;
109              
110 91         282 my @facilities;
111 91 50       646 if (ref ($facilities) eq '') {
    0          
    0          
112 91 50 66     743 if (defined $facilities && $facilities ne '0') {
113 0         0 @facilities = split(/,/, $facilities);
114             }
115             }
116             elsif (ref ($facilities) eq 'ARRAY') {
117 0         0 @facilities = @{ $facilities };
  0         0  
118             }
119             elsif (ref ($facilities) eq 'HASH') {
120 0         0 @facilities = keys %{ $facilities };
  0         0  
121             }
122 91         349 @facilities = grep(/^\S+$/, @facilities);
123 91 50       549 if (@facilities) {
124 0         0 for my $fac (@facilities) {
125 0         0 local ($1,$2);
126 0 0       0 $LOG_SA{facility}->{$2} = !defined($1) if $fac =~ /^(no)?(.+)\z/si;
127             }
128             # turn on debugging if facilities other than "info" are enabled
129 0 0 0     0 if (grep { !/^info\z/ && !/^no./si } keys %{ $LOG_SA{facility} }) {
  0         0  
  0         0  
130 0 0       0 $LOG_SA{level} = DBG if $LOG_SA{level} < DBG;
131             }
132             else {
133 0 0       0 $LOG_SA{level} = INFO if $LOG_SA{level} < INFO;
134             }
135             # debug statement last so we might see it
136 0         0 dbg("logger: adding facilities: " . join(", ", @facilities));
137 0         0 dbg("logger: logging level is " . $log_level{$LOG_SA{level}});
138             }
139             }
140              
141             =item log_message($level, @message)
142              
143             Log a message at a specific level. Levels are specified as strings:
144             "warn", "error", "info", and "dbg". The first element of the message
145             must be prefixed with a facility name followed directly by a colon.
146              
147             =cut
148              
149             sub log_message {
150 0     0 1 0 my ($level, @message) = @_;
151              
152             # too many die and warn messages out there, don't log the ones that we don't
153             # own. jm: off: this makes no sense -- if a dependency module dies or warns,
154             # we want to know about it, unless we're *SURE* it's not something worth
155             # worrying about.
156             # if ($level eq "error" or $level eq "warn") {
157             # return unless $message[0] =~ /^\S+:/;
158             # }
159              
160 0 0       0 if ($level eq "error") {
161             # don't log alarm timeouts or broken pipes of various plugins' network checks
162 0 0       0 return if (index($message[0], '__ignore__') != -1);
163              
164             # dos: we can safely ignore any die's that we eval'd in our own modules so
165             # don't log them -- this is caller 0, the use'ing package is 1, the eval is 2
166 0         0 my @caller = caller 2;
167 0 0 0     0 return if (defined $caller[3] && defined $caller[0] &&
      0        
      0        
168             $caller[3] =~ /^\(eval\)$/ &&
169             $caller[0] =~ m#^Mail::SpamAssassin(?:$|::)#);
170             }
171              
172 0 0       0 return if $LOG_ENTERED; # avoid recursion on die or warn from within logging
173 0         0 $LOG_ENTERED = 1; # no 'returns' from this point on, must clear the flag
174              
175 0         0 my $message = join(" ", @message);
176 0         0 $message =~ s/[\r\n]+$//; # remove any trailing newlines
177              
178 0         0 my $now = Time::HiRes::time;
179              
180             # suppress duplicate loglines
181 0 0       0 if ($message eq $LOG_DUPLINE) {
182 0         0 $LOG_DUPCNT++;
183 0         0 $LOG_DUPTIME = $now;
184             # only start suppressing after x identical lines
185 0 0       0 if ($LOG_DUPCNT >= $LOG_DUPMIN) {
186 0         0 $LOG_ENTERED = 0;
187 0         0 return;
188             }
189             } else {
190 0 0       0 if ($LOG_DUPCNT >= $LOG_DUPMIN) {
191 0         0 $LOG_DUPCNT -= $LOG_DUPMIN - 1;
192 0 0       0 if ($LOG_DUPCNT > 1) {
193 0         0 _log_message($LOG_DUPLEVEL,
194             "$LOG_DUPLINE [... logline repeated $LOG_DUPCNT times]",
195             $LOG_DUPTIME);
196             } else {
197 0         0 _log_message($LOG_DUPLEVEL, $LOG_DUPLINE, $LOG_DUPTIME);
198             }
199             }
200 0         0 $LOG_DUPCNT = 0;
201 0         0 $LOG_DUPLINE = $message;
202 0         0 $LOG_DUPLEVEL = $level;
203             }
204              
205 0         0 _log_message($level, $message, $now);
206              
207 0         0 $LOG_ENTERED = 0;
208             }
209              
210             # Private helper
211             sub _log_message {
212             # split on newlines and call log_message multiple times; saves
213             # the subclasses having to understand multi-line logs
214 0     0   0 my $first = 1;
215 0         0 foreach my $line (split(/\n/, $_[1])) {
216             # replace control characters with "_", tabs and spaces get
217             # replaced with a single space.
218 0         0 $line =~ tr/\x09\x20\x00-\x1f/ _/s;
219 0 0       0 if ($first) {
220 0         0 $first = 0;
221             } else {
222 0         0 local $1;
223 0         0 $line =~ s/^([^:]+?):/$1: [...]/;
224             }
225 0         0 while (my ($name, $object) = each %{ $LOG_SA{method} }) {
  0         0  
226 0         0 $object->log_message($_[0], $line, $_[2]);
227             }
228             }
229             }
230              
231             =item dbg("facility: message")
232              
233             This is used for all low priority debugging messages.
234              
235             =cut
236              
237             sub dbg {
238 22074 50   22074 1 58475 _log(DBG, @_) if $LOG_SA{level} >= DBG;
239 22074         76669 1; # always return the same simple value, regardless of log level
240             }
241              
242             =item info("facility: message")
243              
244             This is used for informational messages indicating a normal, but
245             significant, condition. This should be infrequently called. These
246             messages are typically logged when SpamAssassin is run as a daemon.
247              
248             =cut
249              
250             sub info {
251 17 50   17 1 110 _log(INFO, @_) if $LOG_SA{level} >= INFO;
252 17         37 1; # always return the same simple value, regardless of log level
253             }
254              
255             # remember to avoid deep recursion, my friend
256             sub _log {
257 0     0   0 my $facility;
258 0         0 local ($1);
259              
260             # it's faster to access this as the $_[1] alias, and not to perform
261             # string mods until we're sure we actually want to log anything
262 0 0       0 if ($_[1] =~ /^([a-z0-9_-]*):/i) {
263 0         0 $facility = $1;
264             } else {
265 0         0 $facility = "generic";
266             }
267              
268             # log all info, warn, and error messages;
269             # only debug if asked to
270 0 0       0 if ($_[0] == DBG) {
271             return unless
272             exists $LOG_SA{facility}->{$facility} ? $LOG_SA{facility}->{$facility}
273 0 0       0 : $LOG_SA{facility}->{all};
    0          
274             }
275              
276 0         0 my ($level, $message, @args) = @_;
277 0         0 $message =~ s/^(?:[a-z0-9_-]*):\s*//i;
278              
279 0 0       0 $message = sprintf($message,@args) if @args;
280 0         0 $message =~ s/\n+$//s;
281 0         0 $message =~ s/^/${facility}: /mg;
282              
283             # no reason to go through warn()
284 0 0       0 log_message(($level == INFO ? "info" : "dbg"), $message);
285             }
286              
287             =item add(method => 'syslog', socket => $socket, facility => $facility)
288              
289             C<socket> is the type the syslog ("unix" or "inet"). C<facility> is the
290             syslog facility (typically "mail").
291              
292             =item add(method => 'file', filename => $file)
293              
294             C<filename> is the name of the log file.
295              
296             =item add(method => 'stderr')
297              
298             No options are needed for stderr logging, just don't close stderr first.
299              
300             =cut
301              
302             sub add {
303 0     0 1 0 my %params = @_;
304              
305 0         0 my $name = lc($params{method});
306 0         0 my $class = ucfirst($name);
307              
308 0 0       0 return 0 if $class !~ /^\w+$/; # be paranoid
309              
310             eval 'use Mail::SpamAssassin::Logger::'.$class.'; 1'
311 0 0       0 or do {
312 0 0       0 my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  0         0  
313 0         0 die "logger: add $class failed: $eval_stat\n";
314             };
315              
316 0 0       0 if (!exists $LOG_SA{method}->{$name}) {
317 0         0 my $object;
318             my $eval_stat;
319             eval '$object = Mail::SpamAssassin::Logger::'.$class.'->new(%params); 1'
320 0 0       0 or do {
321 0 0       0 $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  0         0  
322 0         0 undef $object; # just in case
323             };
324 0 0       0 if (!$object) {
325 0 0       0 if (!defined $eval_stat) {
326 0         0 $eval_stat = "Mail::SpamAssassin::Logger::$class->new ".
327             "failed to return an object";
328             }
329 0         0 warn "logger: failed to add $name method: $eval_stat\n";
330             }
331             else {
332 0         0 $LOG_SA{method}->{$name} = $object;
333 0         0 dbg("logger: successfully added $name method\n");
334 0         0 return 1;
335             }
336 0         0 return 0;
337             }
338              
339 0         0 warn "logger: $name method already added\n";
340 0         0 return 1;
341             }
342              
343             =item remove(method)
344              
345             Remove a logging method. Only the method name needs to be passed as a
346             scalar.
347              
348             =cut
349              
350             sub remove {
351 0     0 1 0 my ($method) = @_;
352              
353 0         0 my $name = lc($method);
354 0 0       0 if (exists $LOG_SA{method}->{$name}) {
355 0         0 delete $LOG_SA{method}->{$name};
356 0         0 info("logger: removing $name method");
357 0         0 return 1;
358             }
359 0         0 warn "logger: unable to remove $name method, not present to be removed\n";
360 0         0 return 1;
361             }
362              
363             =item would_log($level, $facility)
364              
365             Returns false if a message at the given level and with the given facility
366             would not be logged. Returns 1 if a message at a given level and facility
367             would be logged normally. Returns 2 if the facility was specifically
368             enabled.
369              
370             The facility argument is optional.
371              
372             =cut
373              
374             sub would_log {
375 1794     1794 1 4493 my ($level, $facility) = @_;
376              
377 1794 50       4167 if ($level eq 'dbg') {
    0          
378 1794 50       8467 return 0 if $LOG_SA{level} < DBG;
379 0 0       0 return 1 if !$facility;
380             return ($LOG_SA{facility}->{$facility} ? 2 : 0)
381 0 0       0 if exists $LOG_SA{facility}->{$facility};
    0          
382 0 0       0 return 1 if $LOG_SA{facility}->{all};
383 0         0 return 0;
384             } elsif ($level eq 'info') {
385 0         0 return $LOG_SA{level} >= INFO;
386             }
387              
388 0         0 warn "logger: would_log called with unknown level: $level\n";
389 0         0 return 0;
390             }
391              
392             =item close_log()
393              
394             Close all logs.
395              
396             =cut
397              
398             sub close_log {
399 44     44 1 235 while (my ($name, $object) = each %{ $LOG_SA{method} }) {
  88         2109  
400 44         597 $object->close_log();
401             }
402             }
403              
404             END {
405 43     43   2472231 close_log();
406             }
407              
408             1;
409              
410             =back
411              
412             =cut