File Coverage

lib/Parse/Syslog.pm
Criterion Covered Total %
statement 119 176 67.6
branch 56 94 59.5
condition 33 49 67.3
subroutine 15 17 88.2
pod 0 4 0.0
total 223 340 65.5


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