File Coverage

blib/lib/File/LogReader.pm
Criterion Covered Total %
statement 68 71 95.7
branch 19 28 67.8
condition n/a
subroutine 13 13 100.0
pod 3 3 100.0
total 103 115 89.5


line stmt bran cond sub pod time code
1             package File::LogReader;
2 1     1   20578 use strict;
  1         2  
  1         30  
3 1     1   4 use warnings;
  1         2  
  1         26  
4 1     1   737 use Digest::SHA1 qw/sha1_hex/;
  1         883  
  1         66  
5 1     1   722 use YAML qw/DumpFile LoadFile/;
  1         11218  
  1         65  
6 1     1   12 use Fcntl ':flock';
  1         2  
  1         988  
7              
8             =head1 NAME
9              
10             File::LogReader - tail log files with state between runs
11              
12             =cut
13              
14             =head1 SYNOPSIS
15              
16             Tail log files across multiple runs over time.
17              
18             use File::LogReader;
19              
20             my $lr = File::LogReader->new( filename => $filename );
21             while( my $line = $lr->read_line ) {
22             # do stuff with $line
23             }
24             $lr->commit;
25              
26             =head1 DESCRIPTION
27              
28             This module makes it easy to periodically check a file for new content
29             and act on it. For instance, you may want to parse a log file whenever
30             it is updated.
31              
32             =cut
33              
34             our $VERSION = '0.04';
35              
36             =head2 METHODS
37              
38             =head3 new
39              
40             Create a new object. Options:
41              
42             =over 4
43              
44             =item filename
45              
46             The name of the file to read from
47              
48             =item state_dir
49              
50             A directory to store state files. Defaults to ~/.logreader
51              
52             =back
53              
54             =cut
55              
56             sub new {
57 10     10 1 2320 my $class = shift;
58 10         69 my $self = {
59             state_dir => "$ENV{HOME}/.logreader",
60             @_,
61             };
62              
63 10 50       40 die 'filename is mandatory!' unless $self->{filename};
64 10 50       154 die 'file must exist!' unless -e $self->{filename};
65              
66 10 100       125 unless( -d $self->{state_dir} ) {
67 1 50       66 mkdir $self->{state_dir}
68             or die "Can't make the state directory: $self->{state_dir}: $!";
69             }
70              
71 10         71 (my $pathless = $self->{filename}) =~ s#.+/##;
72 10         35 $self->{state_file} = "$self->{state_dir}/$pathless.state";
73              
74 10         24 bless $self, $class;
75 10         24 $self->_set_file_position;
76              
77 10 100       29 return undef unless $self->_obtain_lock;
78 9         25 return $self;
79             }
80              
81             =head3 read_line
82              
83             Return a single line of input from the file, or undef;
84              
85             =cut
86              
87             sub read_line {
88 17     17 1 448 my $self = shift;
89              
90 17         40 my $fh = $self->_fh;
91 17         165 return <$fh>;
92             }
93              
94             =head2 commit
95              
96             Saves the read position of the current file.
97              
98             =cut
99              
100             sub commit {
101 6     6 1 15 my $self = shift;
102 6         11 my $fh = $self->_fh;
103 6 50       17 die "Nothing to commit!" unless $fh;
104              
105 6         13 my $pos = tell($fh);
106 6         18 DumpFile( $self->{state_file},
107             {
108             pos => $pos,
109             hash => $self->_calc_hash($pos),
110             },
111             );
112 6         23699 $self->_release_lock;
113             }
114              
115             sub _set_file_position {
116 10     10   12 my $self = shift;
117              
118 10 100       161 return unless -f $self->{state_file};
119 9         36 my $state = LoadFile($self->{state_file});
120              
121 9         28565 my $fh = $self->_fh;
122 9         65 seek $fh, $state->{pos}, 1;
123 9         19 my $pos = tell($fh);
124              
125 9 50       27 if ($pos < $state->{pos}) {
126             # warn "File is smaller! - seeking to beginning of file";
127 0         0 seek $fh, 0, 0;
128 0         0 return;
129             }
130              
131 9         30 my $current_hash = $self->_calc_hash($state->{pos});
132 9 100       66 if ($current_hash ne $state->{hash}) {
133             # warn "hash doesn't match! seeking to beginning of file";
134 2         15 seek $fh, 0, 0;
135 2         6 return;
136             }
137              
138             # warn "hash matches - staying put";
139             }
140              
141             sub _calc_hash {
142 15     15   22 my $self = shift;
143 15         19 my $from_pos = shift;
144              
145 15         23 my $MAX_BYTES = 1024;
146              
147 15         31 my $fh = $self->_fh;
148              
149             # Compute a hash from the specified byte range
150 15 50       38 my $num_bytes = $from_pos < $MAX_BYTES ? $from_pos : $MAX_BYTES;
151 15         112 seek $fh, $from_pos - $num_bytes, 0;
152            
153 15         18 my $content;
154 15         162 my $rc = read $fh, $content, $num_bytes;
155 15 50       34 unless (defined $rc) {
156 0         0 die "Couldn't read $num_bytes bytes from $self->{filename}: $!";
157             }
158 15         185 return sha1_hex($content),
159             }
160              
161             sub _fh {
162 47     47   55 my $self = shift;
163 47 100       122 if (!exists $self->{fh}) {
164 10 50       406 open($self->{fh}, $self->{filename})
165             or die "Can't open $self->{filename}: $!";
166             }
167 47         106 return $self->{fh};
168             }
169              
170             sub _release_lock {
171 6     6   12 my $self = shift;
172 6         192 undef $self->{_lock_fh};
173             }
174              
175             sub _obtain_lock {
176 10     10   18 my $self = shift;
177 10         24 my $lock_file = "$self->{state_file}.lock";
178              
179 10 50       615 open(my $lock_fh, ">$lock_file") or die "Can't open $lock_file: $!";
180 10         23 $self->{_lock_fh} = $lock_fh;
181 10         112 return flock($lock_fh, LOCK_EX | LOCK_NB);
182             }
183              
184             =head1 AUTHOR
185              
186             Luke Closs, C<< >>
187              
188             =head1 BUGS
189              
190             Please report any bugs or feature requests through the web interface at
191             L.
192             I will be notified, and then you'll automatically be notified of progress on
193             your bug as I make changes.
194              
195             =head1 SUPPORT
196              
197             You can find documentation for this module with the perldoc command.
198              
199             perldoc File::LogReader
200              
201             You can also look for information at:
202              
203             =over 4
204              
205             =item * AnnoCPAN: Annotated CPAN documentation
206              
207             L
208              
209             =item * CPAN Ratings
210              
211             L
212              
213             =item * RT: CPAN's request tracker
214              
215             L
216              
217             =item * Search CPAN
218              
219             L
220              
221             =back
222              
223             =head1 OTHER CONTRIBUTORS
224              
225             Thanks to Matthew O'Connor for pairing on the locking.
226              
227             =head1 COPYRIGHT & LICENSE
228              
229             Copyright 2007,2008 Luke Closs, all rights reserved.
230              
231             This program is free software; you can redistribute it and/or modify it
232             under the same terms as Perl itself.
233              
234             =cut
235              
236             1;