File Coverage

lib/DPKG/Log.pm
Criterion Covered Total %
statement 119 138 86.2
branch 31 50 62.0
condition 11 27 40.7
subroutine 18 18 100.0
pod 7 7 100.0
total 186 240 77.5


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             DPKG::Log - Parse the dpkg log
4              
5             =head1 VERSION
6              
7             version 1.20
8              
9             =head1 SYNOPSIS
10              
11             use DPKG::Log;
12              
13             my $dpkg_log = DPKG::Log->new('filename' => 'dpkg.log', 'parse' => 1);
14              
15             =head1 DESCRIPTION
16              
17             This module is used to parse a logfile and store each line
18             as a DPKG::Log::Entry object.
19              
20             =head1 METHODS
21              
22             =over 4
23              
24             =cut
25              
26             package DPKG::Log;
27             BEGIN {
28 6     6   122335 $DPKG::Log::VERSION = '1.20';
29             }
30              
31 6     6   49 use strict;
  6         12  
  6         167  
32 6     6   29 use warnings;
  6         8  
  6         161  
33 6     6   141 use 5.010;
  6         29  
  6         272  
34              
35 6     6   34 use Carp;
  6         12  
  6         496  
36 6     6   2536 use DPKG::Log::Entry;
  6         18  
  6         648  
37 6     6   6656 use DateTime::Format::Strptime;
  6         1313644  
  6         453  
38 6     6   75 use DateTime::TimeZone;
  6         13  
  6         135  
39 6     6   31 use Params::Validate qw(:all);
  6         11  
  6         1370  
40 6     6   7043 use Data::Dumper;
  6         57568  
  6         13261  
41              
42             =item $dpkg_log = DPKG::Log->new()
43              
44             =item $dpkg_log = DPKG::Log->new('filename' => 'dpkg.log')
45              
46             =item $dpkg_log = DPKG::Log->new('filename' => 'dpkg.log', 'parse' => 1 )
47              
48             Returns a new DPKG::Log object. If parse is set to a true value the logfile
49             specified by filename is parsed at the end of the object initialisation.
50             Otherwise the parse routine has to be called.
51             Filename parameter can be ommitted, it defaults to /var/log/dpkg.log.
52              
53             Optionally its possible to specify B or B arguments as timestamps
54             in the standard dpkg.log format or as DateTime objects.
55             This will limit the entries which will be stored in the object to entries in the
56             given timerange.
57             Note that, if this is not what you want, you may ommit these attributes and
58             can use B instead.
59              
60             By default the module will assume that those timestamps are in the local timezone
61             as determined by DateTime::TimeZone. This can be overriden by giving the
62             argument B which takes a timezone string (e.g. 'Europe/Berlin')
63             or a DateTime::TimeZone object.
64             Additionally its possible to override the timestamp_pattern by specifying
65             B. This has to be a valid pattern for DateTime::Format::Strptime.
66              
67             =cut
68             sub new {
69 7     7 1 2445 my $package = shift;
70 7 100       32 $package = ref($package) if ref($package);
71              
72 7         304 my %params = validate(@_,
73             {
74             'filename' => { 'type' => SCALAR, 'default' => '/var/log/dpkg.log' },
75             'parse' => 0,
76             'time_zone' => { 'type' => SCALAR, 'default' => 'local' },
77             'timestamp_pattern' => { 'type' => SCALAR, 'default' => '%F %T' },
78             'from' => 0,
79             'to' => 0
80             }
81             );
82 7         118 my $self = {
83             entries => [],
84             invalid_lines => [],
85             time_zone => undef,
86             from => undef,
87             to => undef,
88             offset => 0,
89             %params
90            
91             };
92              
93 7         25 bless($self, $package);
94            
95 7 100       32 $self->parse if $params{'parse'};
96              
97 7         56 return $self;
98             }
99              
100             =item $dpkg_log->filename
101              
102             =item $dpkg_log->filename('newfilename.log')
103              
104             Get or set the filename of the dpkg logfile.
105              
106             =cut
107             sub filename {
108 4     4 1 7 my ($self, $filename) = @_;
109 4 100       10 if ($filename) {
110 2         4 $self->{filename} = $filename;
111             } else {
112 2         30 $filename = $self->{filename};
113             }
114 4         16 return $filename;
115             }
116              
117             =item $dpkg_log->parse
118              
119             =item $dpkg_log->parse('time_zone' => 'Europe/Berlin')
120              
121             =item $dpkg_log->parse('time_zone' => $dt_tz )
122              
123             Call the parser.
124              
125             The B parameter is optional and specifies in which time zone
126             the dpkg log timestamps are. If its omitted it will use the default
127             local time zone.
128             Its possible to specify either a DateTime::TimeZone object or a string.
129             =cut
130             sub parse {
131 6     6 1 26 my $self = shift;
132 6 50       503 open(my $log_fh, "<", $self->{filename})
133             or croak("unable to open logfile for reading: $!");
134            
135 6         237 my %params = validate(
136             @_, {
137             'from' => { default => $self->{from} },
138             'to' => { default => $self->{to} },
139             'time_zone' => { default => $self->{time_zone} },
140             'timestamp_pattern' => { default => $self->{timestamp_pattern} },
141             }
142             );
143              
144             # Determine system timezone
145 6         43 my $tz;
146 6 50 33     47 if (ref($params{time_zone}) and (ref($params{time_zone}) eq "DateTime::TimeZone")) {
    50          
147 0         0 $tz = $params{time_zone};
148             } elsif (ref($params{time_zone})) {
149 0         0 croak "time_zone argument has to be a string or a DateTime::TimeZone object";
150             } else {
151 6         76 $tz = DateTime::TimeZone->new( 'name' => $params{time_zone} );
152             }
153 6         54816 my $ts_parser = DateTime::Format::Strptime->new(
154             pattern => $params{timestamp_pattern},
155             time_zone => $params{time_zone}
156             );
157              
158 6         34923 my $lineno = 0;
159 6         168 while (my $line = <$log_fh>) {
160 2832         3455 $lineno++;
161 2832         3144 chomp $line;
162 2832 100       7457 next if $line =~ /^$/;
163              
164 2831         2652 my $timestamp;
165            
166 2831         15703 my @entry = split(/\s/, $line);
167 2831 0 33     6123 if (not $entry[0] and not $entry[1]) {
168 0         0 push(@{$self->{invalid_lines}}, $line);
  0         0  
169 0         0 next;
170             }
171              
172 2831         6767 my ($year, $month, $day) = split('-', $entry[0]);
173 2831         6553 my ($hour, $minute, $second) = split(':', $entry[1]);
174              
175 2831 50 33     28528 if ($year and $month and $day and $hour and $minute and $second) {
      33        
      33        
      33        
      33        
176 2831         9788 $timestamp = DateTime->new(
177             year => $year,
178             month => $month,
179             day => $day,
180             hour => $hour,
181             minute => $minute,
182             second => $second,
183             time_zone => $tz
184             );
185             } else {
186 0         0 push(@{$self->{invalid_lines}}, $line);
  0         0  
187 0         0 next;
188             }
189              
190 2831         532741 my $entry_obj;
191 2831 50       9490 if ($entry[2] eq "update-alternatives:") {
    100          
    100          
    50          
    0          
192 0         0 next;
193             } elsif ($entry[2] eq "startup") {
194 176         807 $entry_obj = { line => $line,
195             lineno => $lineno,
196             timestamp => $timestamp,
197             type => 'startup',
198             subject => $entry[3],
199             action => $entry[4]
200             };
201             } elsif ($entry[2] eq "status") {
202 2073         12879 $entry_obj = { line => $line,
203             lineno => $lineno,
204             timestamp => $timestamp,
205             type => 'status',
206             subject => 'package',
207             status => $entry[3],
208             associated_package => $entry[4],
209             installed_version => $entry[5]
210             };
211             } elsif (defined($valid_actions->{$entry[2]}) ) {
212 582         3644 $entry_obj = { line => $line,
213             lineno => $lineno,
214             timestamp => $timestamp,
215             subject => 'package',
216             type => 'action',
217             action => $entry[2],
218             associated_package => $entry[3],
219             installed_version => $entry[4],
220             available_version => $entry[5]
221             };
222             } elsif ($entry[2] eq "conffile") {
223 0         0 $entry_obj = { line => $line,
224             lineno => $lineno,
225             timestamp => $timestamp,
226             subject => 'conffile',
227             type => 'conffile_action',
228             conffile => $entry[3],
229             decision => $entry[4]
230             };
231             } else {
232 0         0 print $line . " invalid\n";
233 0         0 push(@{$self->{invalid_lines}}, $line);
  0         0  
234 0         0 next;
235             }
236              
237 2831         3712 push(@{$self->{entries}}, $entry_obj);
  2831         17508  
238             }
239 6         229 close($log_fh);
240              
241 6 100 66     52 if ($self->{from} or $self->{to}) {
242 2         26 @{$self->{entries}} = $self->filter_by_time( entry_ref => $self->{entries}, %params);
  2         4320  
243             }
244              
245 6         25 return scalar(@{$self->{entries}});
  6         144  
246             }
247              
248             =item @entries = $dpkg_log->entries;
249              
250             =item @entries = $dpkg_log->entries('from' => '2010-01-01.10:00:00', to => '2010-01-02 24:00:00')
251              
252             Return all entries or all entries in a given timerange.
253              
254             B and B are optional arguments, specifying a date before (from) and after (to) which
255             entries aren't returned.
256             If only B is specified all entries from the beginning of the log are read.
257             If only B is specified all entries till the end of the log are read.
258              
259             =cut
260             sub entries {
261 4     4 1 14 my $self = shift;
262              
263 4         114 my %params = validate(
264             @_, {
265             from => 0,
266             to => 0,
267             time_zone => { type => SCALAR, default => $self->{time_zone} }
268             }
269             );
270 4 50       20 croak "Object does not store entries. Eventually parse function were not run or log is empty. " if (not @{$self->{entries}});
  4         23  
271              
272 4 100 66     37 if (not ($params{from} or $params{to})) {
273 3         7 return map { DPKG::Log::Entry->new($_) } @{$self->{entries}};
  721         1802  
  3         14  
274             } else {
275 1         7 return $self->filter_by_time(%params);
276             }
277             }
278              
279             =item $entry = $dpkg_log->next_entry;
280              
281             Return the next entry.
282              
283             =cut
284             sub next_entry {
285 7     7 1 19 my $self = shift;
286 7         23 my $offset = $self->{offset}++;
287 7         14 return DPKG::Log::Entry->new(@{$self->{entries}}[$offset]);
  7         65  
288             }
289              
290             =item @entries = $dpkg_log->filter_by_time(from => ts, to => ts)
291              
292             =item @entries = $dpkg_log->filter_by_time(from => ts)
293              
294             =item @entries = $dpkg_log->filter_by_time(to => ts)
295              
296             =item @entries = $dpkg_log->filter_by_time(from => ts, to => ts, entry_ref => $entry_ref)
297              
298             Filter entries by given B - B range. See the explanations for
299             the new sub for the arguments.
300              
301             If entry_ref is given and an array reference its used instead of $self->{entries}
302             as input source for the entries which are to be filtered.
303             =cut
304             sub filter_by_time {
305 3     3 1 5 my $self = shift;
306 3         101 my %params = validate(
307             @_, {
308             from => 0,
309             to => 0,
310             time_zone => { default => $self->{time_zone} },
311             timestamp_pattern => { default => $self->{timestamp_pattern} },
312             entry_ref => { default => $self->{entries} },
313             }
314             );
315            
316 3         21 my @entries = @{$params{entry_ref}};
  3         481  
317 3 50       11 if (not @entries) {
318 0         0 croak "Object does not store entries. Eventually parse function were not run or log is empty.";
319             }
320              
321 3         20 $self->__eval_datetime_info(%params);
322              
323 3 50       17 @entries = grep { (DPKG::Log::Entry->new($_)->timestamp >= $self->{from}) and (DPKG::Log::Entry->new($_)->timestamp <= $self->{to}) } @entries;
  1695         116306  
324 3         483 return map { DPKG::Log::Entry->new($_) } @entries;
  234         554  
325             }
326              
327             =item ($from, $to) = $dpkg_log->get_datetime_info()
328              
329             Returns the from and to timestamps of the logfile or (if from/to values are set) the
330             values set during object initialisation.
331              
332             =cut
333             sub get_datetime_info {
334 1     1 1 3 my $self = shift;
335              
336 1         2 my $from;
337             my $to;
338 1 50       4 if ($self->{from}) {
339 0         0 $from = $self->{from};
340             } else {
341 1         2 $from = DPKG::Log::Entry->new(%{$self->{entries}->[0]})->timestamp;
  1         7  
342             }
343              
344 1 50       21 if ($self->{to}) {
345 0         0 $to = $self->{to};
346             } else {
347 1         3 $to = DPKG::Log::Entry->new(%{$self->{entries}->[-1]})->timestamp;
  1         8  
348             }
349 1         7 return ($from, $to);
350             }
351              
352             ## Internal methods
353             sub __eval_datetime_info {
354 3     3   5 my $self = shift;
355            
356 3         120 my %params = validate(
357             @_, {
358             from => { default => $self->{from} },
359             to => { default => $self->{to} },
360             time_zone => { default => $self->{time_zone} },
361             timestamp_pattern => { default => $self->{timestamp_pattern} },
362             entry_ref => { default => $self->{entries} },
363             }
364             );
365              
366 3         28 my $entry_ref = $params{entry_ref};
367 3         5 my $from = $params{from};
368 3         6 my $to = $params{to};
369              
370 3         119 my $ts_parser = DateTime::Format::Strptime->new(
371             pattern => $params{timestamp_pattern},
372             time_zone => $params{time_zone}
373             );
374              
375 3 50       24471 if (not $from) {
376 0         0 $from = DPKG::Log::Entry->new($entry_ref->[0])->timestamp;
377             }
378 3 50       20 if (not $to) {
379 0         0 $to = DPKG::Log::Entry->new($entry_ref->[-1])->timestamp;
380             }
381 3 50       13 if (ref($from) ne "DateTime") {
382 3         17 $from = $ts_parser->parse_datetime($from);
383             }
384 3 50       2649 if (ref($to) ne "DateTime") {
385 3         13 $to = $ts_parser->parse_datetime($to);
386             }
387              
388 3         2129 $self->{from} = $from;
389 3         11 $self->{to} = $to;
390 3         25 return;
391             }
392              
393              
394             =back
395              
396             =head1 SEE ALSO
397              
398             L, L, L
399              
400             =head1 AUTHOR
401              
402             Patrick Schoenfeld .
403              
404             =head1 COPYRIGHT AND LICENSE
405              
406             Copyright (C) 2011 Patrick Schoenfeld
407              
408             This library is free software.
409             You can redistribute it and/or modify it under the same terms as perl itself.
410              
411             =cut
412              
413             1;
414             # vim: expandtab:ts=4:sw=4