cronlog.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. #
  2. # Copyright 2013, Martin Owens <doctormo@gmail.com>
  3. #
  4. # This library is free software; you can redistribute it and/or
  5. # modify it under the terms of the GNU Lesser General Public
  6. # License as published by the Free Software Foundation; either
  7. # version 3.0 of the License, or (at your option) any later version.
  8. #
  9. # This library is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  12. # Lesser General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU Lesser General Public
  15. # License along with this library.
  16. #
  17. """
  18. Access logs in known locations to find information about them.
  19. """
  20. import os
  21. import re
  22. import codecs
  23. import platform
  24. PY3 = platform.python_version()[0] == '3'
  25. if PY3:
  26. # pylint: disable=W0622
  27. unicode = str
  28. basestring = str
  29. from dateutil import parser as dateparse
  30. MATCHER = r'(?P<date>\w+ +\d+ +\d\d:\d\d:\d\d) (?P<host>\w+) ' + \
  31. r'CRON\[(?P<pid>\d+)\]: \((?P<user>\w+)\) CMD \((?P<cmd>.*)\)'
  32. class LogReader(object):
  33. """Opens a Log file, reading backwards and watching for changes"""
  34. def __init__(self, filename, mass=4096):
  35. self.filename = filename
  36. self.mass = mass
  37. self.size = -1
  38. self.read = -1
  39. self.pipe = None
  40. def __enter__(self):
  41. self.size = os.stat(self.filename)[6]
  42. self.pipe = codecs.open(self.filename, 'r', encoding='utf-8')
  43. return self
  44. def __exit__(self, error_type, value, traceback):
  45. self.pipe.close()
  46. def __iter__(self):
  47. if self.pipe is None:
  48. with self as reader:
  49. for (offset, line) in reader.readlines():
  50. yield line
  51. else:
  52. for (offset, line) in self.readlines():
  53. yield line
  54. def readlines(self, until=0):
  55. """Iterator for reading lines from a file backwards"""
  56. if not self.pipe or self.pipe.closed:
  57. raise IOError("Can't readline, no opened file.")
  58. # Always seek to the end of the file, this accounts for file updates
  59. # that happen during our running process.
  60. location = self.size
  61. halfline = ''
  62. while location > until:
  63. location -= self.mass
  64. mass = self.mass
  65. if location < 0:
  66. mass = self.mass + location
  67. location = 0
  68. self.pipe.seek(location)
  69. line = self.pipe.read(mass) + halfline
  70. data = line.split('\n')
  71. if location != 0:
  72. halfline = data.pop(0)
  73. loc = location + mass
  74. data.reverse()
  75. for line in data:
  76. if line.strip() == '':
  77. continue
  78. yield (loc, line)
  79. loc -= len(line)
  80. class CronLog(LogReader):
  81. """Use the LogReader to make a Cron specific log reader"""
  82. def __init__(self, filename='/var/log/syslog', user=None):
  83. LogReader.__init__(self, filename)
  84. self.user = user
  85. def for_program(self, command):
  86. """Return log entries for this specific command name"""
  87. return ProgramLog(self, command)
  88. def __iter__(self):
  89. for line in super(CronLog, self).__iter__():
  90. match = re.match(MATCHER, unicode(line))
  91. datum = match and match.groupdict()
  92. if datum and (not self.user or datum['user'] == self.user):
  93. datum['date'] = dateparse.parse(datum['date'])
  94. yield datum
  95. class ProgramLog(object):
  96. """Specific log control for a single command/program"""
  97. def __init__(self, log, command):
  98. self.log = log
  99. self.command = command
  100. def __iter__(self):
  101. for entry in self.log:
  102. if entry['cmd'] == unicode(self.command):
  103. yield entry