File Coverage

lib/Parse/Syslog.pm
Criterion Covered Total %
statement 107 165 64.8
branch 48 88 54.5
condition 30 46 65.2
subroutine 14 16 87.5
pod 0 4 0.0
total 199 319 62.3


line stmt bran cond sub pod time code
1             package Parse::Syslog;
2              
3 8     8   105163 use Carp;
  8         23  
  8         782  
4 8     8   10912 use Symbol;
  8         9333  
  8         1022  
5 8     8   10240 use Time::Local;
  8         24643  
  8         566  
6 8     8   9076 use IO::File;
  8         91816  
  8         1165  
7 8     8   75 use strict;
  8         17  
  8         292  
8 8     8   41 use vars qw($VERSION);
  8         12  
  8         5664  
9              
10             $VERSION = '1.10';
11              
12             my %months_map = (
13             'Jan' => 0, 'Feb' => 1, 'Mar' => 2,
14             'Apr' => 3, 'May' => 4, 'Jun' => 5,
15             'Jul' => 6, 'Aug' => 7, 'Sep' => 8,
16             'Oct' => 9, 'Nov' =>10, 'Dec' =>11,
17             'jan' => 0, 'feb' => 1, 'mar' => 2,
18             'apr' => 3, 'may' => 4, 'jun' => 5,
19             'jul' => 6, 'aug' => 7, 'sep' => 8,
20             'oct' => 9, 'nov' =>10, 'dec' =>11,
21             );
22              
23             sub is_dst_switch($$$)
24             {
25 0     0 0 0 my ($self, $t, $time) = @_;
26              
27             # calculate the time in one hour and see if the difference is 3600 seconds.
28             # if not, we are in a dst-switch hour
29             # note that right now we only support 1-hour dst offsets
30              
31             # cache the result
32 0 0 0     0 if(defined $self->{is_dst_switch_last_hour} and
33             $self->{is_dst_switch_last_hour} == $t->[3]<<5+$t->[2]) {
34 0         0 return @{$self->{is_dst_switch_result}};
  0         0  
35             }
36              
37             # calculate a number out of the day and hour to identify the hour
38 0         0 $self->{is_dst_switch_last_hour} = $t->[3]<<5+$t->[2];
39              
40             # calculating hour+1 (below) is a problem if the hour is 23. as far as I
41             # know, nobody does the DST switch at this time, so just assume it isn't
42             # DST switch if the hour is 23.
43 0 0       0 if($t->[2]==23) {
44 0         0 @{$self->{is_dst_switch_result}} = (0, undef);
  0         0  
45 0         0 return @{$self->{is_dst_switch_result}};
  0         0  
46             }
47              
48             # let's see the timestamp in one hour
49             # 0: sec, 1: min, 2: h, 3: day, 4: month, 5: year
50 0         0 my $time_plus_1h = timelocal($t->[0], $t->[1], $t->[2]+1, $t->[3], $t->[4], $t->[5]);
51              
52 0 0       0 if($time_plus_1h - $time > 4000) {
53 0         0 @{$self->{is_dst_switch_result}} = (3600, $time-$time%3600+3600);
  0         0  
54             }
55             else {
56 0         0 @{$self->{is_dst_switch_result}} = (0, undef);
  0         0  
57             }
58              
59 0         0 return @{$self->{is_dst_switch_result}};
  0         0  
60             }
61              
62             # fast timelocal, cache minute's timestamp
63             # don't cache more than minute because of daylight saving time switch
64             # 0: sec, 1: min, 2: h, 3: day, 4: month, 5: year
65             sub str2time($$$$$$$$)
66             {
67 62     62 0 88 my $self = shift @_;
68 62         115 my $GMT = pop @_;
69              
70 62         227 my $lastmin = $self->{str2time_lastmin};
71 62 100 100     720 if(defined $lastmin and
      66        
      100        
      100        
      66        
72             $lastmin->[0] == $_[1] and
73             $lastmin->[1] == $_[2] and
74             $lastmin->[2] == $_[3] and
75             $lastmin->[3] == $_[4] and
76             $lastmin->[4] == $_[5])
77             {
78 22         58 $self->{last_time} = $self->{str2time_lastmin_time} + $_[0];
79 22   50     124 return $self->{last_time} + ($self->{dst_comp}||0);
80             }
81              
82 40         48 my $time;
83 40 100       64 if($GMT) {
84 6         27 $time = timegm(@_);
85             }
86             else {
87 34         118 $time = timelocal(@_);
88             }
89              
90             # compensate for DST-switch
91             # - if a timewarp is detected (1:00 -> 1:30 -> 1:00):
92             # - test if we are in a DST-switch-hour
93             # - compensate if yes
94             # note that we assume that the DST-switch goes like this:
95             # time 1:00 1:30 2:00 2:30 2:00 2:30 3:00 3:30
96             # stamp 1 2 3 4 3 3 7 8
97             # comp. 0 0 0 0 2 2 0 0
98             # result 1 2 3 4 5 6 7 8
99             # old Time::Local versions behave differently (1 2 5 6 5 6 7 8)
100              
101 40 50 66     2749 if(!$GMT and !defined $self->{dst_comp} and
      100        
      100        
      66        
102             defined $self->{last_time} and
103             $self->{last_time}-$time > 1200 and
104             $self->{last_time}-$time < 3600)
105             {
106 0         0 my ($off, $until) = $self->is_dst_switch(\@_, $time);
107 0 0       0 if($off) {
108 0         0 $self->{dst_comp} = $off;
109 0         0 $self->{dst_comp_until} = $until;
110             }
111             }
112 40 50 33     143 if(defined $self->{dst_comp_until} and $time > $self->{dst_comp_until}) {
113 0         0 delete $self->{dst_comp};
114 0         0 delete $self->{dst_comp_until};
115             }
116              
117 40         196 $self->{str2time_lastmin} = [ @_[1..5] ];
118 40         100 $self->{str2time_lastmin_time} = $time-$_[0];
119 40         56 $self->{last_time} = $time;
120 40   50     219 return $time+($self->{dst_comp}||0);
121             }
122              
123             sub _use_locale($)
124             {
125 8     8   8239 use POSIX qw(locale_h strftime);
  8         75457  
  8         58  
126 0     0   0 my $old_locale = setlocale(LC_TIME);
127 0         0 for my $locale (@_) {
128 0 0       0 croak "new(): wrong 'locale' value: '$locale'" unless setlocale(LC_TIME, $locale);
129 0         0 for my $month (0..11) {
130 0         0 $months_map{strftime("%b", 0, 0, 0, 1, $month, 96)} = $month;
131             }
132             }
133 0         0 setlocale(LC_TIME, $old_locale);
134             }
135              
136              
137             sub new($$;%)
138             {
139 7     7 0 3970 my ($class, $file, %data) = @_;
140 7 50       112 croak "new() requires one argument: file" unless defined $file;
141 7 50       28 %data = () unless %data;
142 7 50       49 if(not defined $data{year}) {
143 0         0 $data{year} = (localtime(time))[5]+1900;
144             }
145 7 100       43 $data{type} = 'syslog' unless defined $data{type};
146 7         20 $data{_repeat}=0;
147              
148 7 50       119 if(UNIVERSAL::isa($file, 'IO::Handle')) {
    50          
    50          
149 0         0 $data{file} = $file;
150             }
151             elsif(UNIVERSAL::isa($file, 'File::Tail')) {
152 0         0 $data{file} = $file;
153 0         0 $data{filetail}=1;
154             }
155             elsif(! ref $file) {
156 7 50       26 if($file eq '-') {
157 0         0 my $io = new IO::Handle;
158 0         0 $data{file} = $io->fdopen(fileno(STDIN),"r");
159             }
160             else {
161 7         69 $data{file} = new IO::File($file, "<");
162 7 50       903 defined $data{file} or croak "can't open $file: $!";
163             }
164             }
165             else {
166 0         0 croak "argument must be either a file-name or an IO::Handle object.";
167             }
168              
169 7 50       31 if(defined $data{locale}) {
170 0 0       0 if(ref $data{locale} eq 'ARRAY') {
    0          
171 0         0 _use_locale @{$data{locale}};
  0         0  
172             }
173             elsif(ref $data{locale} eq '') {
174 0         0 _use_locale $data{locale};
175             }
176             else {
177 0         0 croak "'locale' parameter must be scalar or array of scalars";
178             }
179             }
180              
181 7         31 return bless \%data, $class;
182             }
183              
184             sub _year_increment($$)
185             {
186 62     62   88 my ($self, $mon) = @_;
187              
188             # year change
189 62 100       160 if($mon==0) {
    100          
190 7 100 66     62 $self->{year}++ if defined $self->{_last_mon} and $self->{_last_mon} == 11;
191 7         12 $self->{enable_year_decrement} = 1;
192             }
193             elsif($mon == 11) {
194 3 100       10 if($self->{enable_year_decrement}) {
195 1 50 33     9 $self->{year}-- if defined $self->{_last_mon} and $self->{_last_mon} != 11;
196             }
197             }
198             else {
199 52         119 $self->{enable_year_decrement} = 0;
200             }
201              
202 62         124 $self->{_last_mon} = $mon;
203             }
204              
205             sub _next_line($)
206             {
207 69     69   82 my $self = shift;
208 69         104 my $f = $self->{file};
209 69 50       142 if(defined $self->{filetail}) {
210 0         0 return $f->read;
211             }
212             else {
213 69         2000 return $f->getline;
214             }
215             }
216              
217             sub _next_syslog($)
218             {
219 146     146   174 my ($self) = @_;
220              
221 146         359 while($self->{_repeat}>0) {
222 84         104 $self->{_repeat}--;
223 84         229 return $self->{_repeat_data};
224             }
225              
226 62         97 my $file = $self->{file};
227 62         138 line: while(defined (my $str = $self->_next_line)) {
228             # date, time and host
229             $str =~ /^
230             (\S{3})\s+(\d+) # date -- 1, 2
231             \s
232             (\d+):(\d+):(\d+) # time -- 3, 4, 5
233             (?:\s<\w+\.\w+>)? # FreeBSD's verbose-mode
234             \s
235             ([-\w\.\@:]+) # host -- 6
236             \s+
237             (?:\[LOG_[A-Z]+\]\s+)? # FreeBSD
238             (.*) # text -- 7
239             $/x or do
240 59 50       2181 {
241 0         0 warn "WARNING: line not in syslog format: $str";
242 0         0 next line;
243             };
244            
245 59         127 my $mon = $months_map{$1};
246 59 50       120 defined $mon or croak "unknown month $1\n";
247              
248 59         205 $self->_year_increment($mon);
249              
250             # convert to unix time
251 59         280 my $time = $self->str2time($5,$4,$3,$2,$mon,$self->{year}-1900,$self->{GMT});
252 59 50       173 if(not $self->{allow_future}) {
253             # accept maximum one day in the present future
254 59 50       186 if($time - time > 86400) {
255 0         0 warn "WARNING: ignoring future date in syslog line: $str";
256 0         0 next line;
257             }
258             }
259              
260 59         160 my ($host, $text) = ($6, $7);
261              
262             # last message repeated ... times
263 59 100       224 if($text =~ /^(?:last message repeated|above message repeats) (\d+) time/) {
264 14 50 33     58 next line if defined $self->{repeat} and not $self->{repeat};
265 14 100       64 next line if not defined $self->{_last_data}{$host};
266 12 50       40 $1 > 0 or do {
267 0         0 warn "WARNING: last message repeated 0 or less times??\n";
268 0         0 next line;
269             };
270 12         27 $self->{_repeat}=$1-1;
271 12         32 $self->{_repeat_data}=$self->{_last_data}{$host};
272 12         54 return $self->{_last_data}{$host};
273             }
274              
275             # marks
276 45 100       99 next if $text eq '-- MARK --';
277              
278             # some systems send over the network their
279             # hostname prefixed to the text. strip that.
280 44         479 $text =~ s/^$host\s+//;
281              
282             # discard ':' in HP-UX 'su' entries like this:
283             # Apr 24 19:09:40 remedy : su : + tty?? root-oracle
284 44         77 $text =~ s/^:\s+//;
285              
286             $text =~ /^
287             ([^:]+?) # program -- 1
288             (?:\[(\d+)\])? # PID -- 2
289             :\s+
290             (?:\[ID\ (\d+)\ ([a-z0-9]+)\.([a-z]+)\]\ )? # Solaris 8 "message id" -- 3, 4, 5
291             (.*) # text -- 6
292             $/x or do
293 44 50       340 {
294 0         0 warn "WARNING: line not in syslog format: $str";
295 0         0 next line;
296             };
297              
298 44 50       129 if($self->{arrayref}) {
299 0         0 $self->{_last_data}{$host} = [
300             $time, # 0: timestamp
301             $host, # 1: host
302             $1, # 2: program
303             $2, # 3: pid
304             $6, # 4: text
305             ];
306             }
307             else {
308 44         495 $self->{_last_data}{$host} = {
309             timestamp => $time,
310             host => $host,
311             program => $1,
312             pid => $2,
313             msgid => $3,
314             facility => $4,
315             level => $5,
316             text => $6,
317             };
318             }
319              
320 44         246 return $self->{_last_data}{$host};
321             }
322 6         236 return undef;
323             }
324              
325             sub _next_metalog($)
326             {
327 4     4   5 my ($self) = @_;
328 4         6 my $file = $self->{file};
329 4         10 line: while(my $str = $self->_next_line) {
330             # date, time and host
331            
332             $str =~ /^
333             (\S{3})\s+(\d+) # date -- 1, 2
334             \s
335             (\d+):(\d+):(\d+) # time -- 3, 4, 5
336             # host is not logged
337             \s+
338             (.*) # text -- 6
339             $/x or do
340 3 50       119 {
341 0         0 warn "WARNING: line not in metalog format: $str";
342 0         0 next line;
343             };
344            
345 3         7 my $mon = $months_map{$1};
346 3 50       9 defined $mon or croak "unknown month $1\n";
347              
348 3         7 $self->_year_increment($mon);
349              
350             # convert to unix time
351 3         17 my $time = $self->str2time($5,$4,$3,$2,$mon,$self->{year}-1900,$self->{GMT});
352            
353 3         8 my $text = $6;
354              
355             $text =~ /^
356             \[(.*?)\] # program -- 1
357             # no PID
358             \s+
359             (.*) # text -- 2
360             $/x or do
361 3 50       15 {
362 0         0 warn "WARNING: text line not in metalog format: $text ($str)";
363 0         0 next line;
364             };
365              
366 3 50       5 if($self->{arrayref}) {
367             return [
368 0         0 $time, # 0: timestamp
369             'localhost', # 1: host
370             $1, # 2: program
371             undef, # 3: (no) pid
372             $2, # 4: text
373             ];
374             }
375             else {
376             return {
377 3         23 timestamp => $time,
378             host => 'localhost',
379             program => $1,
380             text => $2,
381             };
382             }
383             }
384 1         35 return undef;
385             }
386              
387             sub next($)
388             {
389 150     150 0 47254 my ($self) = @_;
390 150 100       396 if($self->{type} eq 'syslog') {
    50          
391 146         290 return $self->_next_syslog();
392             }
393             elsif($self->{type} eq 'metalog') {
394 4         9 return $self->_next_metalog();
395             }
396 0           croak "Internal error: unknown type: $self->{type}";
397             }
398              
399             1;
400              
401             __END__