Setting tag myplc-5.3-5
[myplc.git] / bin / mtail.py
1 #!/usr/bin/env python
2
3 '''
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
8 '''
9
10 import os, sys, time
11 import select
12 import sys, tty, termios
13
14 from optparse import OptionParser
15
16 class mtail:
17
18     default_time_format = "%H:%M:%S"
19     
20     def __init__ (self, args ):
21         
22         # internal structure for tracking changes
23         self.files = {}
24         # parse command-line args : will set options and args
25         self.parse_args(args)
26         # initialize 
27         self.scan_files()
28
29     def parse_args (self, args):
30         usage = """usage: %prog [options] file-or-dir ...
31 example: 
32 # %prog -e '*access*' /var/log"""
33         parser=OptionParser(usage=usage)
34         # tail_period
35         parser.add_option("-p","--period", type="int", dest="tail_period", default=1,
36                           help="Files check period in seconds")
37         # rescan_period
38         parser.add_option("-d","--dir-period", type="int", dest="rescan_period", default=5,
39                           help="Directories rescan period in seconds")
40         # time format
41         parser.add_option("-f","--format", dest="time_format", default=mtail.default_time_format,
42                           help="Time format, defaults to " + mtail.default_time_format)
43         # show time
44         parser.add_option("-r","--raw", action="store_true", dest="show_time", default=True,
45                           help="Suppresses time display")
46
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")
50
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")
55
56         # verbosity
57         parser.add_option("-v","--verbose", action="store_true", dest="verbose", default=False, 
58                           help="Run in verbose mode")
59
60         (self.options, self.args) = parser.parse_args(args)
61         self.option_parser = parser
62
63         ### plc shortcuts
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')
72
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")
77
78         if self.options.verbose:
79             print 'Options:',self.options
80             print 'Arguments:',self.args
81
82     def file_size (self,filename):
83         try:
84             return os.stat(filename)[6]
85         except:
86             print "WARNING: file %s has vanished"%filename
87             return 0
88                 
89     def number_files (self):
90         return len(self.files)
91
92     # scans given arguments, and updates files accordingly
93     # can be run several times
94     def scan_files (self) :
95
96         if self.options.verbose:
97             print 'entering scan_files, files=',self.files
98
99         # mark entries in files as pre-existing
100         for key in self.files:
101             self.files[key]['old-file']=True
102
103         # refreshes the proper set of filenames
104         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):
109                 filenames += [ arg ]
110             elif os.path.isdir (arg) :
111                 filenames += self.walk (arg)
112             else:
113                 print "mtail : no such file or directory %s -- ignored"%arg
114
115         # updates files
116         for filename in filenames :
117             # known file
118             if self.files.has_key(filename):
119                 size = self.file_size(filename)
120                 offset = self.files[filename]['size']
121                 if size > offset:
122                     self.show_file_end(filename,offset,size)
123                     self.files[filename]['size']=size
124                 elif size < offset:
125                     self.show_file_when_size_decreased(filename,offset,size)
126                 try:
127                     del self.files[filename]['old-file']
128                 except:
129                     pass
130             else:
131                 # enter file with current size
132                 # if we didn't set format yet, it's because we are initializing
133                 try:
134                     self.format
135                     self.show_now()
136                     print self.format%filename,"new file"
137                     self.show_file_end(filename,0,self.file_size(filename))
138                 except:
139                     pass
140                 self.files[filename]={'size':self.file_size(filename)}
141         
142         # cleanup 
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'):
147                 self.show_now()
148                 print self.format%filename,"file has gone"
149                 del self.files[filename]
150
151         # compute margin and format
152         if not filenames:
153             print sys.argv[0],": WARNING : no file in scope"
154             self.format="%s"
155         else:
156             if len(filenames)==1:
157                 self.margin=len(filenames[0])
158             else:
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
164
165     def tail_files (self):
166
167         if self.options.verbose:
168             print 'tail_files'
169         for filename in self.files:
170             size = self.file_size(filename)
171             offset = self.files[filename]['size']
172             if size != offset:
173                 self.show_file_end(filename,offset,size)
174                 self.files[filename]['size']=size
175
176     def show_now (self):
177         if self.options.show_time:
178             label=time.strftime(self.options.time_format,time.localtime())
179             print label,
180
181     def show_file_end (self, filename, offset, size):
182         try:
183             file = open(filename,"r")
184         # file has vanished
185         except:
186             return
187         file.seek(offset)
188         line=file.read(size-offset)
189         self.show_now()
190         print self.format%filename,'----------------------------------------'
191         print line
192         file.close()
193
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
198         else:
199             print ''
200
201     # get all files under a directory
202     def walk ( self, root ):
203         import fnmatch, os, string
204         
205         # initialize
206         result = []
207
208         # must have at least root folder
209         try:
210             names = os.listdir(root)
211         except os.error:
212             return result
213
214         # check each file
215         for name in names:
216             fullname = os.path.normpath(os.path.join(root, name))
217
218             # a file : check for excluded, otherwise append
219             if os.path.isfile(fullname):
220                 try:
221                     for exclude in self.options.excludes:
222                         if fnmatch.fnmatch(name, exclude):
223                             raise Exception('excluded')
224                     result.append(fullname)
225                 except:
226                     pass
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 )
230
231         return result
232
233     def run (self):
234
235         if len(self.args) == 0:
236             self.option_parser.print_help()
237             sys.exit(1)
238         counter = 0
239     
240         fdin = sys.stdin.fileno()
241         while True:
242             ## hit the period ?
243             # dont do this twice at startup
244             if (counter !=0 and counter % self.options.rescan_period == 0):
245                 self.scan_files()
246
247             if (counter % self.options.tail_period == 0):
248                 self.tail_files()
249
250             time.sleep(1)
251             counter += 1
252             # react on some keys - rough but convenient
253             if os.isatty(fdin):
254                 # read stdin
255                 typed=[]
256                 while select.select([sys.stdin,],[],[],0.0)[0]:
257                     typed.append(sys.stdin.read(1))
258 #                print 'found chars',typed
259                 for char in 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
266 m: mark
267 q: quit
268 h: help"""
269                     
270 ###
271 if __name__ == '__main__':
272     mtail (sys.argv[1:]).run()