#!/usr/bin/env python # # logwatcher.py # # Copyright 2017 Todd Shadburn # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # import sys import os import time class LogWatcher(object): def __init__(self, filepath=None, *args, **kwargs): self.filepath = filepath try: st = os.stat(self.filepath) self.fd = open(self.filepath, 'r') self.inode = st.st_ino self.oset = st.st_size self.fd.seek(self.oset, 0) except OSError: self.fd = None self.inode = 0 self.oset = 0 self.prefix = None self.record_callback = None self.rotate_callback = None self.record = '' self.ltime = 0 self.record_timeout = 5 self.poll_timeout = 2 return def set_record_prefix(self, prefix=None): self.prefix = prefix return def set_record_timeout(self, timeout=2): self.record_timeout = timeout return def set_poll_timeout(self, timeout=2): self.poll_timeout = timeout return def set_record_callback(self, callback=None): # callback prototype: watch_callback(record) self.record_callback = callback return def set_rotate_callback(self, callback=None): # callback prototype: rotate_callback(filepath) self.rotate_callback = callback return def set_appended_line_callback(self, callback=None): # callback prototype: appended_line_callback(string) returns:string self.appended_line_callback = callback return def set_prefix_line_callback(self, callback=None): # callback prototype: prefix_line_callback(string) returns:string self.prefix_line_callback = callback return def _get_new_record(self): data = None ts = time.time() # Check for size change and/or rotation st = None try: st = os.stat(self.filepath) except OSError: # File doesn't exist yet or mid-rotate operation if self.fd: self.fd.close() self.fd = None self.inode = 0 return None size = st.st_size if size < self.oset: # Log file was 'rotated' via truncation (yuck!) # Close the file and reset, so we can reopen it # and start from the beginning if self.fd: self.fd.close() self.fd = None self.inode = 0 if st.st_ino != self.inode: # File was rotated, reopen it self.fd = open(self.filepath, 'r') self.oset = 0 self.inode = st.st_ino size = st.st_size if self.rotate_callback: self.rotate_callback(self.filepath) # Process new file data while size > self.oset: self.fd.seek(self.oset, 0) ln = self.fd.readline() if self.prefix: if ln[0:len(self.prefix)] == self.prefix: data = self.record if self.prefix_line_callback: self.record = self.prefix_line_callback(ln) else: self.record = ln self.oset = self.fd.tell() self.ltime = ts break # exit the while loop else: if self.appended_line_callback: self.record += self.appended_line_callback(ln) else: self.record += ln else: data = ln self.oset = self.fd.tell() self.ltime = ts break # exit the while loop self.oset = self.fd.tell() if not data and (ts-self.ltime) > self.record_timeout: data = self.record self.record = '' self.ltime = ts return data def check_once(self): return self._get_new_record() def watch(self, timeout=None): if not self.record_callback: raise ValueError('Record callback must be set prior to calling watch()') if timeout: self.poll_timeout = timeout while 1: data = self._get_new_record() if data: self.record_callback(data) else: time.sleep(self.poll_timeout) return