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 self.options.excludes.append('*access_log')
63 self.options.excludes.append('*request_log')
64 self.options.excludes.append('*.swp')
65 self.args.append('/var/log')
67 if self.options.verbose:
68 print 'Version:',self.subversion_id
69 print 'Options:',self.options
70 print 'Arguments:',self.args
72 def file_size (self,filename):
73 return os.stat(filename)[6]
75 def number_files (self):
76 return len(self.files)
78 # scans given arguments, and updates files accordingly
79 # can be run several times
80 def scan_files (self) :
82 if self.options.verbose:
83 print 'entering scan_files, files=',self.files
85 # mark entries in files as pre-existing
86 for key in self.files:
87 self.files[key]['old-file']=True
89 # refreshes the proper set of filenames
92 if self.options.verbose:
93 print 'scan_files -- Considering arg',arg
94 if os.path.isfile (arg):
96 elif os.path.isdir (arg) :
97 filenames += self.walk (arg)
99 print "mtail : no such file or directory %s -- ignored"%arg
102 for filename in filenames :
104 if self.files.has_key(filename):
105 size = self.file_size(filename)
106 offset = self.files[filename]['size']
108 self.show_file_end(filename,offset,size)
109 self.files[filename]['size']=size
111 self.show_file_when_size_decreased(filename,offset,size)
112 del self.files[filename]['old-file']
114 # enter file with current size
115 # if we didn't set format yet, it's because we are initializing
119 print self.format%filename,"new file"
120 self.show_file_end(filename,0,self.file_size(filename))
123 self.files[filename]={'size':self.file_size(filename)}
126 # avoid side-effects on the current loop basis
127 read_filenames = self.files.keys()
128 for filename in read_filenames:
129 if self.files[filename].has_key('old-file'):
131 print self.format%filename,"file has gone"
132 del self.files[filename]
134 # compute margin and format
136 print sys.argv[0],": WARNING : no file in scope"
139 self.margin=max(*[len(f) for f in filenames])
140 self.format="%%%ds"%self.margin
141 if self.options.verbose:
142 print 'Current set of files:',filenames
144 def tail_files (self):
146 if self.options.verbose:
148 for filename in self.files:
149 size = self.file_size(filename)
150 offset = self.files[filename]['size']
152 self.show_file_end(filename,offset,size)
153 self.files[filename]['size']=size
156 if self.options.show_time:
157 label=time.strftime(self.options.time_format,time.localtime())
160 def show_file_end (self, filename, offset, size):
161 file = open(filename,"r")
163 line=file.read(size-offset)
165 print self.format%filename,'----------------------------------------'
169 def show_file_when_size_decreased (self, filename, offset, size):
170 print self.format%filename,'---------- file size decreased ---------',
171 if self.options.verbose:
172 print 'size during last check',offset,'current size',size
176 # get all files under a directory
177 def walk ( self, root ):
178 import fnmatch, os, string
183 # must have at least root folder
185 names = os.listdir(root)
191 fullname = os.path.normpath(os.path.join(root, name))
193 # a file : check for excluded, otherwise append
194 if os.path.isfile(fullname):
196 for exclude in self.options.excludes:
197 if fnmatch.fnmatch(name, exclude):
198 raise Exception('excluded')
199 result.append(fullname)
202 # a dir : let's recurse - avoid symlinks for anti-loop
203 elif os.path.isdir(fullname) and not os.path.islink(fullname):
204 result = result + self.walk( fullname )
210 if self.number_files() == 0:
211 self.optparse.print_help()
217 # dont do this twice at startup
218 if (counter !=0 and counter % self.options.rescan_period == 0):
221 if (counter % self.options.tail_period == 0):
228 if __name__ == '__main__':
229 mtail (sys.argv[1:]).run()