#!/usr/bin/perl # logtailer is a stateful "tail -f" ... it remembers where you were in the # log and starts up there the next time. it also handles a few forms of # log rotation (but not compression). # # usage: logtailer logfile statefile # # Copyright (c) 2006 dean gaudet # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), # to deal in the Software without restriction, including without limitation # the rights to use, copy, modify, merge, publish, distribute, sublicense, # and/or sell copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included # in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR # OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. use warnings; use strict; use File::stat; use Date::Format; use IO::File; @ARGV == 2 or die "usage: $0 logfile statefile\n"; my $logfile = shift; my $statefile = shift; sub updatestate($$$$) { my ($dev, $ino, $size, $logfile) = @_; open(STATE, ">$statefile") or die "unable to open $statefile for writing: $!\n"; print STATE "dev $dev\n"; print STATE "ino $ino\n"; print STATE "size $size\n"; print STATE "logfile $logfile\n"; close(STATE); } # initialze state if it doesn't exist. we print nothing # this first time. unless (-f $statefile) { my $fh = new IO::File; $fh->open("<$logfile") or die "unable to open $logfile for reading: $!\n"; my $sb = stat($fh); $fh->close; updatestate($sb->dev, $sb->ino, $sb->size, $logfile); exit(0); } open(STATE, "<$statefile") or die "unable to open $statefile for reading: $!\n"; my %state; while () { chomp; my ($field, $value) = m#^(\S+)\s+(.*)#; $state{$field} = $value; } close(STATE); defined($state{'dev'}) or die "invalid statefile\n"; defined($state{'ino'}) or die "invalid statefile\n"; defined($state{'size'}) or die "invalid statefile\n"; defined($state{'logfile'}) or die "invalid statefile\n"; $logfile eq $state{'logfile'} or die "logfile/statefile mismatch\n"; sub taillog($) { my ($filename) = @_; my $fh = new IO::File; unless ($fh->open("<$filename")) { return undef; } my $sb = stat($fh); unless (defined($sb) and $sb->dev == $state{'dev'} and $sb->ino == $state{'ino'} and $sb->size >= $state{'size'}) { return undef; } $fh->seek($state{'size'}, 0); print STDOUT <$fh>; my $offs = $fh->tell; $fh->close; return [$sb, $offs]; } my $newstate = taillog($logfile); unless (defined $newstate) { # log has probably been rotated -- try the rotated logfiles. twinlark # rotates to logfile.YYYYMMDD, uncompressed... other setups rotate to # .0 or .1. XXX: compressed logs not handled. my $today = time2str("%Y%M%d"); $newstate = taillog("$logfile.$today") || taillog("$logfile.0") || taillog("$logfile.1"); # now follow on with the current log my $fh = new IO::File; unless ($fh->open("<$logfile")) { # maybe we caught things right at the rotation... let's wait a moment sleep(5); unless ($fh->open("<$logfile")) { # we have no idea what to write in the statefile... so # we'll lose some data warn "unable to read from $logfile: $!\n"; unlink($statefile); exit(0); } } print STDOUT <$fh>; my $sb = stat($fh); my $offs = $fh->tell; $fh->close; $newstate = [$sb, $offs]; } updatestate($newstate->[0]->dev, $newstate->[0]->ino, $newstate->[1], $logfile);