4 Does tail -f on log files in a given directory.
5 The display is in chronological order of the logged lines,
6 given that the first column of log files is timestamp.
7 It can be altered to fit other formats too
11 from optparse import OptionParser
15 subversion_id = "$Id$"
17 default_time_format = "%H:%M:%S"
19 def __init__ (self, args ):
21 # internal structure for tracking changes
23 # parse command-line args : will set options and args
28 def parse_args (self, args):
29 usage = """usage: %prog [options] file-or-dir ...
31 # %prog -e '*access*' /var/log"""
32 parser=OptionParser(usage=usage,version=self.subversion_id)
34 parser.add_option("-p","--period", type="int", dest="tail_period", default=1,
35 help="Files check period in seconds")
37 parser.add_option("-d","--dir-period", type="int", dest="rescan_period", default=20,
38 help="Directories rescan period in seconds")
40 parser.add_option("-f","--format", dest="time_format", default=mtail.default_time_format,
41 help="Time format, defaults to " + mtail.default_time_format)
43 parser.add_option("-r","--raw", action="store_true", dest="show_time", default=True,
44 help="Suppresses time display")
46 # note for exclusion patterns
47 parser.add_option("-e","--exclude", action="append", dest="excludes", default=[],
48 help="Exclusion pattern -- can be specified multiple times applies on files not explicitly mentioned on the command-line")
50 parser.add_option("-u","--usual",action="store_true",dest="plc_mode",default=False,
51 help="Shortcut for watching /var/log with default settings")
54 parser.add_option("-v","--verbose", action="store_true", dest="verbose", default=False,
55 help="Run in verbose mode")
57 (self.options, self.args) = parser.parse_args(args)
58 self.optparse = parser
61 if self.options.plc_mode:
62 # monitor all files in /var/log with some exceptions
63 self.options.excludes.append('*access_log')
64 self.options.excludes.append('*request_log')
65 self.options.excludes.append('*.swp')
66 self.args.append('/var/log')
67 # watch the postgresql logs as well
68 self.args.append('/var/lib/pgsql/data/pg_log')
70 if self.options.verbose:
71 print 'Version:',self.subversion_id
72 print 'Options:',self.options
73 print 'Arguments:',self.args
75 def file_size (self,filename):
76 return os.stat(filename)[6]
78 def number_files (self):
79 return len(self.files)
81 # scans given arguments, and updates files accordingly
82 # can be run several times
83 def scan_files (self) :
85 if self.options.verbose:
86 print 'entering scan_files, files=',self.files
88 # mark entries in files as pre-existing
89 for key in self.files:
90 self.files[key]['old-file']=True
92 # refreshes the proper set of filenames
95 if self.options.verbose:
96 print 'scan_files -- Considering arg',arg
97 if os.path.isfile (arg):
99 elif os.path.isdir (arg) :
100 filenames += self.walk (arg)
102 print "mtail : no such file or directory %s -- ignored"%arg
105 for filename in filenames :
107 if self.files.has_key(filename):
108 size = self.file_size(filename)
109 offset = self.files[filename]['size']
111 self.show_file_end(filename,offset,size)
112 self.files[filename]['size']=size
114 self.show_file_when_size_decreased(filename,offset,size)
116 del self.files[filename]['old-file']
120 # enter file with current size
121 # if we didn't set format yet, it's because we are initializing
125 print self.format%filename,"new file"
126 self.show_file_end(filename,0,self.file_size(filename))
129 self.files[filename]={'size':self.file_size(filename)}
132 # avoid side-effects on the current loop basis
133 read_filenames = self.files.keys()
134 for filename in read_filenames:
135 if self.files[filename].has_key('old-file'):
137 print self.format%filename,"file has gone"
138 del self.files[filename]
140 # compute margin and format
142 print sys.argv[0],": WARNING : no file in scope"
145 if len(filenames)==1:
146 self.margin=len(filenames[0])
148 # this stupidly fails when there's only 1 file
149 self.margin=max(*[len(f) for f in filenames])
150 self.format="%%%ds"%self.margin
151 if self.options.verbose:
152 print 'Current set of files:',filenames
154 def tail_files (self):
156 if self.options.verbose:
158 for filename in self.files:
159 size = self.file_size(filename)
160 offset = self.files[filename]['size']
162 self.show_file_end(filename,offset,size)
163 self.files[filename]['size']=size
166 if self.options.show_time:
167 label=time.strftime(self.options.time_format,time.localtime())
170 def show_file_end (self, filename, offset, size):
171 file = open(filename,"r")
173 line=file.read(size-offset)
175 print self.format%filename,'----------------------------------------'
179 def show_file_when_size_decreased (self, filename, offset, size):
180 print self.format%filename,'---------- file size decreased ---------',
181 if self.options.verbose:
182 print 'size during last check',offset,'current size',size
186 # get all files under a directory
187 def walk ( self, root ):
188 import fnmatch, os, string
193 # must have at least root folder
195 names = os.listdir(root)
201 fullname = os.path.normpath(os.path.join(root, name))
203 # a file : check for excluded, otherwise append
204 if os.path.isfile(fullname):
206 for exclude in self.options.excludes:
207 if fnmatch.fnmatch(name, exclude):
208 raise Exception('excluded')
209 result.append(fullname)
212 # a dir : let's recurse - avoid symlinks for anti-loop
213 elif os.path.isdir(fullname) and not os.path.islink(fullname):
214 result = result + self.walk( fullname )
220 if self.number_files() == 0:
221 self.optparse.print_help()
227 # dont do this twice at startup
228 if (counter !=0 and counter % self.options.rescan_period == 0):
231 if (counter % self.options.tail_period == 0):
238 if __name__ == '__main__':
239 mtail (sys.argv[1:]).run()