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 default_time_format = "%H:%M:%S"
20 def __init__ (self, args ):
22 # internal structure for tracking changes
24 # parse command-line args : will set options and args
29 def parse_args (self, args):
30 usage = """usage: %prog [options] file-or-dir ...
32 # %prog -e '*access*' /var/log"""
33 parser=OptionParser(usage=usage)
35 parser.add_option("-p","--period", type="int", dest="tail_period", default=1,
36 help="Files check period in seconds")
38 parser.add_option("-d","--dir-period", type="int", dest="rescan_period", default=5,
39 help="Directories rescan period in seconds")
41 parser.add_option("-f","--format", dest="time_format", default=mtail.default_time_format,
42 help="Time format, defaults to " + mtail.default_time_format)
44 parser.add_option("-r","--raw", action="store_true", dest="show_time", default=True,
45 help="Suppresses time display")
47 # note for exclusion patterns
48 parser.add_option("-e","--exclude", action="append", dest="excludes", default=[],
49 help="Exclusion pattern -- can be specified multiple times applies on files not explicitly mentioned on the command-line")
51 parser.add_option("-u","--usual",action="store_true",dest="plc_mode",default=False,
52 help="Shortcut for watching /var/log with default settings")
53 parser.add_option("-s","--sfa",action="store_true",dest="sfa_mode",default=False,
54 help="Shortcut for monitoring server-side SFA log files")
57 parser.add_option("-v","--verbose", action="store_true", dest="verbose", default=False,
58 help="Run in verbose mode")
60 (self.options, self.args) = parser.parse_args(args)
61 self.option_parser = parser
64 if self.options.plc_mode:
65 # monitor all files in /var/log with some exceptions
66 self.options.excludes.append('*access_log')
67 self.options.excludes.append('*request_log')
68 self.options.excludes.append('*.swp')
69 self.args.append('/var/log')
70 # watch the postgresql logs as well
71 self.args.append('/var/lib/pgsql/data/pg_log')
73 if self.options.sfa_mode:
74 self.args.append("/var/log/sfa.log")
75 self.args.append("/var/log/sfa_import.log")
76 self.args.append("/var/log/httpd/sfa_access_log")
78 if self.options.verbose:
79 print 'Options:',self.options
80 print 'Arguments:',self.args
82 def file_size (self,filename):
84 return os.stat(filename)[6]
86 print "WARNING: file %s has vanished"%filename
89 def number_files (self):
90 return len(self.files)
92 # scans given arguments, and updates files accordingly
93 # can be run several times
94 def scan_files (self) :
96 if self.options.verbose:
97 print 'entering scan_files, files=',self.files
99 # mark entries in files as pre-existing
100 for key in self.files:
101 self.files[key]['old-file']=True
103 # refreshes the proper set of filenames
105 for arg in self.args:
106 if self.options.verbose:
107 print 'scan_files -- Considering arg',arg
108 if os.path.isfile (arg):
110 elif os.path.isdir (arg) :
111 filenames += self.walk (arg)
113 print "mtail : no such file or directory %s -- ignored"%arg
116 for filename in filenames :
118 if self.files.has_key(filename):
119 size = self.file_size(filename)
120 offset = self.files[filename]['size']
122 self.show_file_end(filename,offset,size)
123 self.files[filename]['size']=size
125 self.show_file_when_size_decreased(filename,offset,size)
127 del self.files[filename]['old-file']
131 # enter file with current size
132 # if we didn't set format yet, it's because we are initializing
136 print self.format%filename,"new file"
137 self.show_file_end(filename,0,self.file_size(filename))
140 self.files[filename]={'size':self.file_size(filename)}
143 # avoid side-effects on the current loop basis
144 read_filenames = self.files.keys()
145 for filename in read_filenames:
146 if self.files[filename].has_key('old-file'):
148 print self.format%filename,"file has gone"
149 del self.files[filename]
151 # compute margin and format
153 print sys.argv[0],": WARNING : no file in scope"
156 if len(filenames)==1:
157 self.margin=len(filenames[0])
159 # this stupidly fails when there's only 1 file
160 self.margin=max(*[len(f) for f in filenames])
161 self.format="%%%ds"%self.margin
162 if self.options.verbose:
163 print 'Current set of files:',filenames
165 def tail_files (self):
167 if self.options.verbose:
169 for filename in self.files:
170 size = self.file_size(filename)
171 offset = self.files[filename]['size']
173 self.show_file_end(filename,offset,size)
174 self.files[filename]['size']=size
177 if self.options.show_time:
178 label=time.strftime(self.options.time_format,time.localtime())
181 def show_file_end (self, filename, offset, size):
183 file = open(filename,"r")
188 line=file.read(size-offset)
190 print self.format%filename,'----------------------------------------'
194 def show_file_when_size_decreased (self, filename, offset, size):
195 print self.format%filename,'---------- file size decreased ---------',
196 if self.options.verbose:
197 print 'size during last check',offset,'current size',size
201 # get all files under a directory
202 def walk ( self, root ):
203 import fnmatch, os, string
208 # must have at least root folder
210 names = os.listdir(root)
216 fullname = os.path.normpath(os.path.join(root, name))
218 # a file : check for excluded, otherwise append
219 if os.path.isfile(fullname):
221 for exclude in self.options.excludes:
222 if fnmatch.fnmatch(name, exclude):
223 raise Exception('excluded')
224 result.append(fullname)
227 # a dir : let's recurse - avoid symlinks for anti-loop
228 elif os.path.isdir(fullname) and not os.path.islink(fullname):
229 result = result + self.walk( fullname )
235 if len(self.args) == 0:
236 self.option_parser.print_help()
240 fdin = sys.stdin.fileno()
243 # dont do this twice at startup
244 if (counter !=0 and counter % self.options.rescan_period == 0):
247 if (counter % self.options.tail_period == 0):
252 # react on some keys - rough but convenient
256 while select.select([sys.stdin,],[],[],0.0)[0]:
257 typed.append(sys.stdin.read(1))
258 # print 'found chars',typed
260 if char.lower() in ['l']: os.system("clear")
261 elif char.lower() in ['m']:
262 for i in range(3) : print 60 * '='
263 elif char.lower() in ['q']: sys.exit(0)
264 elif char.lower() in ['h']:
265 print """l: refresh page
271 if __name__ == '__main__':
272 mtail (sys.argv[1:]).run()