pvl/syslog/tail.py
changeset 49 30c615bf7751
parent 46 0bdbbda4cdea
child 53 685fd90bc610
equal deleted inserted replaced
48:40ccb8d3c96e 49:30c615bf7751
     1 """
     1 """
     2     `tail -f` style continuous file reads.
     2     Iterate over input lines in files. 
     3 
     3     
     4     Checks if the file was renamed on EOF, and reopens if so.
     4     Can be read up to eof, on blocking inputs, or polled with a timeout (see: pvl.syslog.syslog.SyslogSource)
     5 """
     5 """
     6 
     6 
     7 import os
     7 import os
     8 
     8 
     9 import logging; log = logging.getLogger('pvl.collectd.tail')
     9 import logging; log = logging.getLogger('pvl.syslog.tail')
    10 
    10 
    11 class TailFile (object) :
    11 class Tail (object) :
    12     """
    12     """
    13         A file on the filesystem, that is appended to.
    13         Follow a file-like object, reading lines until EOF.
       
    14 
       
    15         Works with python file objects that buffer readlines() when using e.g. `tail -f ... | python -u ...`.
    14     """
    16     """
    15 
    17 
    16     def __init__ (self, path, skip=None) :
    18     def __init__ (self, file) :
    17         self.path = path
    19         self.file = file
    18         self._file = None
       
    19         self._id = None
       
    20 
    20 
    21         self._open()
    21     def readline (self) :
       
    22         """
       
    23             Reads a line from the file, without trailing \n
    22 
    24 
    23         if skip :
    25             Raises EOF on end-of-file.
    24             for line in self :
       
    25                 pass
       
    26     
       
    27     @property
       
    28     def file (self) :
       
    29         """
       
    30             The underlying file objct, if opened.
       
    31         """
    26         """
    32 
    27 
    33         if self._file is None :
    28         line = self.file.readline()
    34             raise ValueError("I/O operation on closed file: {0}".format(self.path))
       
    35 
    29 
    36         return self._file
    30         if not line :
       
    31             raise EOFError()
       
    32         else :
       
    33             return line.rstrip('\n')
    37 
    34 
    38     @property
    35     def readlines (self) :
    39     def fd (self) :
       
    40         return self.file.fileno()
       
    41 
       
    42     def fileno (self) :
       
    43         """
    36         """
    44             The underlying OS fd.
    37             Reads any available lines from the file.
    45         """
    38         """
    46 
    39 
    47         return self.fd
    40         while True :
       
    41             try :
       
    42                 line = self.readline()
    48 
    43 
    49     def _stat (self) :
    44             except EOFError :
       
    45                 log.debug("EOF")
       
    46                 break
       
    47 
       
    48             else :
       
    49                 yield line
       
    50 
       
    51     def skip (self) :
       
    52         """
       
    53             Skip any available lines.
       
    54         """
       
    55 
       
    56         for line in self.readlines() :
       
    57             pass
       
    58 
       
    59     __iter__ = readlines
       
    60 
       
    61 class TailFile (Tail) :
       
    62     """
       
    63         Follow a file on the filesystem, reading lines until EOF, and re-opening if replaced.
       
    64     """
       
    65 
       
    66     def __init__ (self, path) :
       
    67         self.path = path
       
    68         self.reopen()
       
    69     
       
    70     def stat (cls) :
    50         """
    71         """
    51             Return a key identifying the file at our path.
    72             Return a key identifying the file at our path.
    52         """
    73         """
    53 
    74 
    54         st = os.stat(self.path)
    75         st = os.stat(self.path)
    55 
    76 
    56         return st.st_dev, st.st_ino
    77         return st.st_dev, st.st_ino
       
    78     
       
    79     def open (cls) :
       
    80         return open(self.path, 'r')
    57 
    81 
    58     def _open (self) :
    82     def reopen (cls) :
    59         assert self._file is None
    83         self.file = self.open()
       
    84         self._stat = self.stat()
    60 
    85 
    61         self._file = open(self.path, 'r')
    86     def changed (self) :
    62         self._id = self._stat()
       
    63 
       
    64     def _close (self) :
       
    65         assert self._file is not None
       
    66 
       
    67         self._file.close()
       
    68         self._file = None
       
    69 
       
    70     def _reopen (self) :
       
    71         """
       
    72             Re-open, in case the file changed..
       
    73         """
       
    74 
       
    75         self._close()
       
    76         self._open()
       
    77 
       
    78     def _changed (self) :
       
    79         """
    87         """
    80             Has the underlying file changed?
    88             Has the underlying file changed?
    81         """
    89         """
    82 
    90 
    83         return self._stat() != self._id
    91         return self.stat() != self._stat
    84 
       
    85     def poll (self, timeout) :
       
    86         """
       
    87             XXX: not really implemented...
       
    88         """
       
    89 
       
    90         import time
       
    91 
       
    92         time.sleep(timeout)
       
    93     
       
    94     def readline (self) :
       
    95         """
       
    96             Reads a line from the file.
       
    97 
       
    98             Raises EOF on end-of-file.
       
    99         """
       
   100 
       
   101         line = self.file.readline()
       
   102 
       
   103         # eof?
       
   104         if not line :
       
   105             raise EOFError()
       
   106         
       
   107         return line
       
   108 
    92 
   109     def readlines (self, eof_mark=False) :
    93     def readlines (self, eof_mark=False) :
   110         """
    94         """
   111             Reads any available lines from the file.
    95             Reads any available lines from the file.
   112 
    96 
   118         while True :
   102         while True :
   119             try :
   103             try :
   120                 line = self.readline()
   104                 line = self.readline()
   121 
   105 
   122             except EOFError :
   106             except EOFError :
   123                 if self._changed() :
   107                 if self.changed() :
   124                     log.debug("EOF: file changed: reopen")
   108                     log.debug("EOF: reopen")
   125                     self._reopen()
   109                     self.reopen()
   126                     
   110                     
   127                     if eof_mark :
   111                     if eof_mark :
   128                         # special token
   112                         yield None # special token
   129                         yield None
   113                     
       
   114                     # keep going
       
   115                     continue
   130 
   116 
   131                 else :
   117                 else :
   132                     log.debug("EOF: wait")
   118                     log.debug("EOF")
   133                     # done reading
   119                     break # wait
   134                     return
       
   135 
   120 
   136             else :
   121             else :
   137                 yield line.strip()
   122                 yield line
   138 
   123 
   139     __iter__ = readlines
   124     __iter__ = readlines
   140 
   125 
   141     def close (self) :
       
   142         """
       
   143             Close the fifo.
       
   144         """
       
   145 
       
   146         if self._file is None :
       
   147             raise ValueError("File already closed: {0}".format(self.path))
       
   148 
       
   149         self._close()
       
   150 
       
   151     def __del__ (self) :
       
   152         """
       
   153             Cleanup
       
   154         """
       
   155 
       
   156         if self._file is not None :
       
   157             self._close()
       
   158