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
12 import sys, tty, termios
14 from optparse import OptionParser
18 subversion_id = "$Id$"
20 default_time_format = "%H:%M:%S"
22 def __init__ (self, args ):
24 # internal structure for tracking changes
26 # parse command-line args : will set options and args
31 def parse_args (self, args):
32 usage = """usage: %prog [options] file-or-dir ...
34 # %prog -e '*access*' /var/log"""
35 parser=OptionParser(usage=usage,version=self.subversion_id)
37 parser.add_option("-p","--period", type="int", dest="tail_period", default=1,
38 help="Files check period in seconds")
40 parser.add_option("-d","--dir-period", type="int", dest="rescan_period", default=5,
41 help="Directories rescan period in seconds")
43 parser.add_option("-f","--format", dest="time_format", default=mtail.default_time_format,
44 help="Time format, defaults to " + mtail.default_time_format)
46 parser.add_option("-r","--raw", action="store_true", dest="show_time", default=True,
47 help="Suppresses time display")
49 # note for exclusion patterns
50 parser.add_option("-e","--exclude", action="append", dest="excludes", default=[],
51 help="Exclusion pattern -- can be specified multiple times applies on files not explicitly mentioned on the command-line")
53 parser.add_option("-u","--usual",action="store_true",dest="plc_mode",default=False,
54 help="Shortcut for watching /var/log with default settings")
55 parser.add_option("-s","--sfa",action="store_true",dest="sfa_mode",default=False,
56 help="Shortcut for monitoring server-side SFA log files")
59 parser.add_option("-v","--verbose", action="store_true", dest="verbose", default=False,
60 help="Run in verbose mode")
62 (self.options, self.args) = parser.parse_args(args)
63 self.option_parser = parser
66 if self.options.plc_mode:
67 # monitor all files in /var/log with some exceptions
68 self.options.excludes.append('*access_log')
69 self.options.excludes.append('*request_log')
70 self.options.excludes.append('*.swp')
71 self.args.append('/var/log')
72 # watch the postgresql logs as well
73 self.args.append('/var/lib/pgsql/data/pg_log')
75 if self.options.sfa_mode:
76 self.args.append("/var/log/sfa.log")
77 self.args.append("/var/log/sfa_import.log")
78 self.args.append("/var/log/httpd/sfa_access_log")
80 if self.options.verbose:
81 print 'Version:',self.subversion_id
82 print 'Options:',self.options
83 print 'Arguments:',self.args
85 def file_size (self,filename):
87 return os.stat(filename)[6]
89 print "WARNING: file %s has vanished"%filename
92 def number_files (self):
93 return len(self.files)
95 # scans given arguments, and updates files accordingly
96 # can be run several times
97 def scan_files (self) :
99 if self.options.verbose:
100 print 'entering scan_files, files=',self.files
102 # mark entries in files as pre-existing
103 for key in self.files:
104 self.files[key]['old-file']=True
106 # refreshes the proper set of filenames
108 for arg in self.args:
109 if self.options.verbose:
110 print 'scan_files -- Considering arg',arg
111 if os.path.isfile (arg):
113 elif os.path.isdir (arg) :
114 filenames += self.walk (arg)
116 print "mtail : no such file or directory %s -- ignored"%arg
119 for filename in filenames :
121 if self.files.has_key(filename):
122 size = self.file_size(filename)
123 offset = self.files[filename]['size']
125 self.show_file_end(filename,offset,size)
126 self.files[filename]['size']=size
128 self.show_file_when_size_decreased(filename,offset,size)
130 del self.files[filename]['old-file']
134 # enter file with current size
135 # if we didn't set format yet, it's because we are initializing
139 print self.format%filename,"new file"
140 self.show_file_end(filename,0,self.file_size(filename))
143 self.files[filename]={'size':self.file_size(filename)}
146 # avoid side-effects on the current loop basis
147 read_filenames = self.files.keys()
148 for filename in read_filenames:
149 if self.files[filename].has_key('old-file'):
151 print self.format%filename,"file has gone"
152 del self.files[filename]
154 # compute margin and format
156 print sys.argv[0],": WARNING : no file in scope"
159 if len(filenames)==1:
160 self.margin=len(filenames[0])
162 # this stupidly fails when there's only 1 file
163 self.margin=max(*[len(f) for f in filenames])
164 self.format="%%%ds"%self.margin
165 if self.options.verbose:
166 print 'Current set of files:',filenames
168 def tail_files (self):
170 if self.options.verbose:
172 for filename in self.files:
173 size = self.file_size(filename)
174 offset = self.files[filename]['size']
176 self.show_file_end(filename,offset,size)
177 self.files[filename]['size']=size
180 if self.options.show_time:
181 label=time.strftime(self.options.time_format,time.localtime())
184 def show_file_end (self, filename, offset, size):
186 file = open(filename,"r")
191 line=file.read(size-offset)
193 print self.format%filename,'----------------------------------------'
197 def show_file_when_size_decreased (self, filename, offset, size):
198 print self.format%filename,'---------- file size decreased ---------',
199 if self.options.verbose:
200 print 'size during last check',offset,'current size',size
204 # get all files under a directory
205 def walk ( self, root ):
206 import fnmatch, os, string
211 # must have at least root folder
213 names = os.listdir(root)
219 fullname = os.path.normpath(os.path.join(root, name))
221 # a file : check for excluded, otherwise append
222 if os.path.isfile(fullname):
224 for exclude in self.options.excludes:
225 if fnmatch.fnmatch(name, exclude):
226 raise Exception('excluded')
227 result.append(fullname)
230 # a dir : let's recurse - avoid symlinks for anti-loop
231 elif os.path.isdir(fullname) and not os.path.islink(fullname):
232 result = result + self.walk( fullname )
238 if len(self.args) == 0:
239 self.option_parser.print_help()
243 fdin = sys.stdin.fileno()
246 # dont do this twice at startup
247 if (counter !=0 and counter % self.options.rescan_period == 0):
250 if (counter % self.options.tail_period == 0):
255 # react on some keys - rough but convenient
259 while select.select([sys.stdin,],[],[],0.0)[0]:
260 typed.append(sys.stdin.read(1))
261 # print 'found chars',typed
263 if char.lower() in ['l']: os.system("clear")
264 elif char.lower() in ['m']:
265 for i in range(3) : print 60 * '='
266 elif char.lower() in ['q']: sys.exit(0)
267 elif char.lower() in ['h']:
268 print """l: refresh page
274 if __name__ == '__main__':
275 mtail (sys.argv[1:]).run()