source: trunk/core/lib/Foswiki/Logger/PlainFile.pm @ 13029

Revision 13029, 11.0 KB checked in by PaulHarvey, 19 months ago (diff)

Item11185: perltidy

Line 
1# See bottom of file for license and copyright information
2package Foswiki::Logger::PlainFile;
3
4use strict;
5use warnings;
6use utf8;
7use Assert;
8
9use Foswiki::Logger ();
10use Foswiki::Configure::Load;
11our @ISA = ('Foswiki::Logger');
12
13=begin TML
14
15---+ package Foswiki::Logger::PlainFile
16
17Plain file implementation of the Foswiki Logger interface. Mostly
18compatible with TWiki (and Foswiki 1.0.0) log files, except that dates
19are recorded using ISO format, and include the time, and it dies when
20a log can't be written (rather than printing a warning).
21
22This logger implementation maps groups of levels to a single logfile, viz.
23   * =debug= messages are output to $Foswiki::cfg{Log}{Dir}/debug.log
24   * =info= messages are output to $Foswiki::cfg{Log}{Dir}/events.log
25   * =warning=, =error=, =critical=, =alert=, =emergency= messages are
26     output to $Foswiki::cfg{Log}{Dir}/error.log.
27   * =error=, =critical=, =alert=, and =emergency= messages are also
28     written to standard error (the webserver log file, usually)
29
30=cut
31
32use Foswiki::Time         ();
33use Foswiki::ListIterator ();
34
35# Map from a log level to the root of a log file name
36our %LEVEL2LOG = (
37    debug     => 'debug',
38    info      => 'events',
39    warning   => 'error',
40    error     => 'error',
41    critical  => 'error',
42    alert     => 'error',
43    emergency => 'error'
44);
45
46our $nextCheckDue = 0;
47
48# Symbols used so we can override during unit testing
49our $dontRotate = 0;
50sub _time { time() }
51sub _stat { stat(@_); }
52
53sub new {
54    my $class = shift;
55    return bless( {}, $class );
56}
57
58=begin TML
59
60---++ ObjectMethod log($level, @fields)
61
62See Foswiki::Logger for the interface.
63
64=cut
65
66sub log {
67    my ( $this, $level, @fields ) = @_;
68
69    my $log = _getLogForLevel($level);
70    my $now = _time();
71    _rotate( $log, $now );
72    my $time = Foswiki::Time::formatTime( $now, 'iso', 'gmtime' );
73
74    # Unfortunate compatibility requirement; need the level, but the old
75    # logfile format doesn't allow us to add fields. Since we are changing
76    # the date format anyway, the least pain is to concatenate the level
77    # to the date; Foswiki::Time::ParseTime can handle it, and it looks
78    # OK too.
79    unshift( @fields, "$time $level" );
80    my $message =
81      '| ' . join( ' | ', map { s/\|/&vbar;/g; $_ } @fields ) . ' |';
82
83    my $file;
84    my $mode = '>>';
85
86    # Item10764, SMELL UNICODE: actually, perhaps we should open the stream this
87    # way for any encoding, not just utf8. Babar says: check what Catalyst does.
88    if ( $Foswiki::cfg{Site}{CharSet} =~ /^utf-?8$/ ) {
89        $mode .= ":encoding($Foswiki::cfg{Site}{CharSet})";
90    }
91    elsif ( utf8::is_utf8($message) ) {
92        require Encode;
93        $message = Encode::encode( $Foswiki::cfg{Site}{CharSet}, $message, 0 );
94    }
95    if ( open( $file, $mode, $log ) ) {
96        print $file "$message\n";
97        close($file);
98    }
99    else {
100        if ( !-w $log ) {
101            die
102"ERROR: Could not open logfile $log for write. Your admin should 'configure' now and fix the errors!\n";
103        }
104
105        # die to force the admin to get permissions correct
106        die 'ERROR: Could not write ' . $message . ' to ' . "$log: $!\n";
107    }
108    if ( $level =~ /^(error|critical|alert|emergency)$/ ) {
109        print STDERR "$message\n";
110    }
111}
112
113{
114
115    # Private subclass of LineIterator that splits events into fields
116    package Foswiki::Logger::PlainFile::EventIterator;
117    require Foswiki::LineIterator;
118    @Foswiki::Logger::PlainFile::EventIterator::ISA = ('Foswiki::LineIterator');
119
120    sub new {
121        my ( $class, $fh, $threshold, $level ) = @_;
122        my $this = $class->SUPER::new($fh);
123        $this->{_threshold} = $threshold;
124        $this->{_level}     = $level;
125        return $this;
126    }
127
128    sub hasNext {
129        my $this = shift;
130        return 1 if defined $this->{_nextEvent};
131        while ( $this->SUPER::hasNext() ) {
132            my @line = split( /\s*\|\s*/, $this->SUPER::next() );
133            shift @line;    # skip the leading empty cell
134            if (
135                $line[0] =~ s/\s+$this->{_level}\s*$//    # test the level
136                  # accept a plain 'old' format date with no level only if reading info (statistics)
137                || $line[0] =~ /^\d{1,2} [a-z]{3} \d{4}/i
138                && $this->{_level} eq 'info'
139              )
140            {
141                $line[0] = Foswiki::Time::parseTime( $line[0] );
142                if ( $line[0] >= $this->{_threshold} ) {    # test the time
143                    $this->{_nextEvent} = \@line;
144                    return 1;
145                }
146            }
147        }
148        return 0;
149    }
150
151    sub next {
152        my $this = shift;
153        my $data = $this->{_nextEvent};
154        undef $this->{_nextEvent};
155        return $data;
156    }
157}
158
159=begin TML
160
161---++ StaticMethod eachEventSince($time, $level) -> $iterator
162
163See Foswiki::Logger for the interface.
164
165This logger implementation maps groups of levels to a single logfile, viz.
166   * =info= messages are output together.
167   * =warning=, =error=, =critical=, =alert=, =emergency= messages are
168     output together.
169This method cannot
170
171=cut
172
173sub eachEventSince {
174    my ( $this, $time, $level ) = @_;
175    my $log = _getLogForLevel($level);
176
177    # Find the year-month for the current time
178    my $now         = _time();
179    my $nowLogYear  = Foswiki::Time::formatTime( $now, '$year', 'servertime' );
180    my $nowLogMonth = Foswiki::Time::formatTime( $now, '$mo', 'servertime' );
181
182    # Find the year-month for the first time in the range
183    my $logYear  = Foswiki::Time::formatTime( $time, '$year', 'servertime' );
184    my $logMonth = Foswiki::Time::formatTime( $time, '$mo',   'servertime' );
185
186    # Get the names of all the logfiles in the time range
187    my @logs;
188    while ( !( $logMonth == $nowLogMonth && $logYear == $nowLogYear ) ) {
189        my $logfile = $log;
190        my $logTime = $logYear . sprintf( "%02d", $logMonth );
191        $logfile =~ s/\.log$/.$logTime/g;
192        push( @logs, $logfile );
193        $logMonth++;
194        if ( $logMonth == 13 ) {
195            $logMonth = 1;
196            $logYear++;
197        }
198    }
199
200    # Finally the current log
201    push( @logs, $log );
202
203    my @iterators;
204    foreach my $logfile (@logs) {
205        next unless -r $logfile;
206        my $fh;
207        if ( open( $fh, '<', $logfile ) ) {
208            push(
209                @iterators,
210                new Foswiki::Logger::PlainFile::EventIterator(
211                    $fh, $time, $level
212                )
213            );
214        }
215        else {
216
217            # Would be nice to report this, but it's chicken and egg and
218            # besides, empty logfiles can happen.
219            #print STDERR "Failed to open $logfile: $!";
220        }
221    }
222    return new Foswiki::ListIterator( \@iterators ) if scalar(@iterators) == 0;
223    return $iterators[0] if scalar(@iterators) == 1;
224    return new Foswiki::AggregateIterator( \@iterators );
225}
226
227# Get the name of the log for a given reporting level
228sub _getLogForLevel {
229    my $level = shift;
230    ASSERT( defined $LEVEL2LOG{$level} ) if DEBUG;
231    my $log = $Foswiki::cfg{Log}{Dir} . '/' . $LEVEL2LOG{$level} . '.log';
232
233    # SMELL: Expand should not be needed, except if bin/configure tries
234    # to log to locations relative to $Foswiki::cfg{WorkingDir}, DataDir, etc.
235    # Windows seemed to be the most difficult to fix - this was the only thing
236    # that I could find that worked all the time.
237    Foswiki::Configure::Load::expandValue($log);
238    return $log;
239}
240
241sub _time2month {
242    my $time = shift;
243    my @t    = gmtime($time);
244    $t[5] += 1900;
245    return sprintf( '%0.4d%0.2d', $t[5], $t[4] + 1 );
246}
247
248# See if the log needs to be rotated. If the log was last modified
249# last month, we need to rotate it.
250sub _rotate {
251    my ( $log, $now ) = @_;
252
253    return if $dontRotate;
254
255    # Don't bother checking if we have checked in this process already
256    return if ( $now < $nextCheckDue );
257
258    # Work out the current month
259    my $curMonth = _time2month($now);
260
261    # After this check, don't check again for a month.
262    $curMonth =~ /(\d{4})(\d{2})/;
263    my ( $y, $m ) = ( $1, $2 + 1 );
264    if ( $m > 12 ) {
265        $m = '01';
266        $y++;
267    }
268    else {
269        $m = sprintf( '%0.2d', $m );
270    }
271    $nextCheckDue = Foswiki::Time::parseTime("$y-$m-01");
272
273    # If there's no existing log, there's nothing to rotate
274    return unless -e $log;
275
276    # Check when the log was last modified. If it was in the previous
277    # month, if may need to be rotated.
278    my @stat     = _stat($log);
279    my $modMonth = _time2month( $stat[9] );
280    return if ( $modMonth == $curMonth );
281
282    # The log was last modified in a month that was not the current month.
283    # Rotate older entries out into month-by-month logfiles.
284
285    #print STDERR ">> Checking $log entries\n";
286
287    # Open the current log
288    my $lf;
289    return unless open( $lf, '<', $log );
290
291    # Analyse the log and partition the lines into month groups
292    my %months;
293
294    local $/ = "\n";
295    my $line;
296    while ( $line = <$lf> ) {
297        my @event = split( /\s*\|\s*/, $line );
298        last unless $event[1];
299        my $eventTime = Foswiki::Time::parseTime( $event[1] );
300
301        if ( !$eventTime ) {
302
303            #print STDERR ">> Bad time in log: $line\n";
304            close($lf);
305            return;
306        }
307
308        my $eventMonth = _time2month($eventTime);
309
310        if ( $eventMonth < $curMonth ) {
311            push( @{ $months{$eventMonth} }, $line );
312        }
313        else {
314
315            # Reached the start of log entries for this month
316            last;
317        }
318    }
319
320    if ( !scalar( keys %months ) ) {
321
322        # no old months, we're done. The modify time on the current
323        # log will be touched by the next write, so we won't attempt
324        # to rotate again until next month (or $forceRotate is set).
325        #print STDERR ">> No old months\n";
326        close($lf);
327        return;
328    }
329
330    # Sook up the rest of the current log
331    $line ||= '';
332    $/ = undef;
333    my $curLog = $line . <$lf>;
334    close($lf);
335
336    foreach my $month ( keys %months ) {
337        my $bf;
338        my $backup = $log;
339        $backup =~ s/log$/$month/;
340        if ( -e $backup || !open( $bf, '>', $backup ) ) {
341
342            #print STDERR ">> Could not create $backup\n";
343            return;
344        }
345        print $bf join( '', @{ $months{$month} } );
346        close($bf);
347    }
348
349    # Finally rewrite the shortened current log
350    return unless open( $lf, '>', $log );
351    print $lf $curLog;
352    close($lf);
353}
354
3551;
356__END__
357Module of Foswiki - The Free and Open Source Wiki, http://foswiki.org/
358
359Copyright (C) 2008-2010 Foswiki Contributors. Foswiki Contributors
360are listed in the AUTHORS file in the root of this distribution.
361NOTE: Please extend that file, not this notice.
362
363This program is free software; you can redistribute it and/or
364modify it under the terms of the GNU General Public License
365as published by the Free Software Foundation; either version 2
366of the License, or (at your option) any later version. For
367more details read LICENSE in the root of this distribution.
368
369This program is distributed in the hope that it will be useful,
370but WITHOUT ANY WARRANTY; without even the implied warranty of
371MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
372
373As per the GPL, removal of this notice is prohibited.
Note: See TracBrowser for help on using the repository browser.