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