Home | History | Annotate | Download | only in cros
      1 # Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 import abc, logging, os, re, time
      6 import subprocess
      7 
      8 import common
      9 from autotest_lib.client.bin import utils
     10 from autotest_lib.client.common_lib import error
     11 from constants import CLEANUP_LOGS_PAUSED_FILE
     12 
     13 
     14 def strip_timestamp(msg):
     15     """
     16     Strips timestamps and PIDs from a syslog message to facilitate
     17     failure reason aggregation when this message is used as an autotest
     18     exception text.
     19     """
     20     kernel = re.search(r' kernel: \[ *\d+.\d+\] (.*)$', msg)
     21     if kernel:
     22         return 'kernel: ' + kernel.group(1)
     23 
     24     user = re.match(r'[^a-z]+ [^ ]* ([^\[ ]+)(?:\[.*\])?(: .*)$', msg)
     25     if user:
     26         return user.group(1) + user.group(2)
     27 
     28     logging.warning('Could not parse syslog message: ' + msg)
     29     return msg
     30 
     31 
     32 def extract_kernel_timestamp(msg):
     33     """Extract a timestmap that appears in kernel log messages and looks
     34     like this:
     35     ... kernel: [78791.721832] ...
     36 
     37     Returns:
     38         The timestamp as float in seconds since last boot.
     39     """
     40 
     41     match = re.search('\[\s*([0-9]+\.[0-9]+)\] ', msg)
     42     if match:
     43         return float(match.group(1))
     44     raise error.TestError('Could not extract timestamp from message: ' + msg)
     45 
     46 
     47 class AbstractLogReader(object):
     48 
     49     def __init__(self):
     50         self._start_line = 1
     51 
     52     @abc.abstractmethod
     53     def read_all_logs(self):
     54         """Read all content from log files.
     55 
     56         Generator function.
     57         Return an iterator on the content of files.
     58 
     59         This generator can peek a line once (and only once!) by using
     60         .send(offset). Must iterate over the peeked line before you can
     61         peek again.
     62         """
     63         pass
     64 
     65     def set_start_by_regexp(self, index, regexp):
     66         """Set the start of logs based on a regular expression.
     67 
     68         @param index: line matching regexp to start at, earliest log at 0.
     69                 Negative numbers indicate matches since end of log.
     70         """
     71         regexp_compiled = re.compile(regexp)
     72         starts = []
     73         line_number = 1
     74         self._start_line = 1
     75         for line in self.read_all_logs():
     76             if regexp_compiled.match(line):
     77                 starts.append(line_number)
     78             line_number += 1
     79         if index < -len(starts):
     80             self._start_line = 1
     81         elif index >= len(starts):
     82             self._start_line = line_number
     83         else:
     84             self._start_line = starts[index]
     85 
     86 
     87     def set_start_by_reboot(self, index):
     88         """ Set the start of logs (must be system log) based on reboot.
     89 
     90         @param index: reboot to start at, earliest log at 0.  Negative
     91                 numbers indicate reboots since end of log.
     92         """
     93         # Include a 'kernel' tag to avoid matching boot messages logged by
     94         # crosvm: https://crbug.com/817946
     95         return self.set_start_by_regexp(index,
     96                                         r'kernel:.*000\] Linux version \d')
     97 
     98 
     99     def set_start_by_current(self, relative=0):
    100         """ Set start of logs based on current last line.
    101 
    102         @param relative: line relative to current to start at.  1 means
    103                 to start the log after this line.
    104         """
    105         count = self._start_line + relative
    106         for line in self.read_all_logs():
    107             count += 1
    108         self._start_line = count
    109 
    110 
    111     def get_logs(self):
    112         """ Get logs since the start line.
    113 
    114         Start line is set by set_start_* functions or
    115         since the start of the file if none were called.
    116 
    117         @return string of contents of file since start line.
    118         """
    119         logs = []
    120         for line in self.read_all_logs():
    121             logs.append(line)
    122         return ''.join(logs)
    123 
    124 
    125     def can_find(self, string):
    126         """ Try to find string in the logs.
    127 
    128         @return boolean indicating if we found the string.
    129         """
    130         return string in self.get_logs()
    131 
    132 
    133     def get_last_msg(self, patterns, retries=0, sleep_seconds=0.2):
    134         """Search the logs and return the latest occurrence of a message
    135         matching one of the patterns.
    136 
    137         Args:
    138             patterns: A regexp or a list of regexps to search the logs with.
    139                 The function returns as soon as it finds any match to one of
    140                 the patters, it will not search for the other patterns.
    141             retries: Number of times to retry if none of the patterns were
    142                 found. Default is one attempt.
    143             sleep_seconds: Time to sleep between retries.
    144 
    145         Returns:
    146             The last occurrence of the first matching pattern. "None" if none
    147             of the patterns matched.
    148         """
    149 
    150         if not type(patterns) in (list, tuple):
    151             patterns = [patterns]
    152 
    153         for retry in xrange(retries + 1):
    154             for pattern in patterns:
    155                 regexp_compiled = re.compile(pattern)
    156                 last_match = None
    157                 for line in self.read_all_logs():
    158                     if regexp_compiled.search(line):
    159                         last_match = line
    160                 if last_match:
    161                     return last_match
    162             time.sleep(sleep_seconds)
    163 
    164         return None
    165 
    166 
    167 class LogRotationPauser(object):
    168     """
    169     Class to control when logs are rotated from either server or client.
    170 
    171     Assumes all setting of CLEANUP_LOGS_PAUSED_FILE is done by this class
    172     and that all calls to begin and end are properly
    173     nested.  For instance, [ a.begin(), b.begin(), b.end(), a.end() ] is
    174     supported, but [ a.begin(), b.begin(), a.end(), b.end() ]  is not.
    175     We do support redundant calls to the same class, such as
    176     [ a.begin(), a.begin(), a.end() ].
    177     """
    178     def __init__(self, host=None):
    179         self._host = host
    180         self._begun = False
    181         self._is_nested = True
    182 
    183 
    184     def _run(self, command, *args, **dargs):
    185         if self._host:
    186             return self._host.run(command, *args, **dargs).exit_status
    187         else:
    188             return utils.system(command, *args, **dargs)
    189 
    190 
    191     def begin(self):
    192         """Make sure that log rotation is disabled."""
    193         if self._begun:
    194             return
    195         self._is_nested = (self._run(('[ -r %s ]' %
    196                                       CLEANUP_LOGS_PAUSED_FILE),
    197                                      ignore_status=True) == 0)
    198         if self._is_nested:
    199             logging.info('File %s was already present' %
    200                          CLEANUP_LOGS_PAUSED_FILE)
    201         else:
    202             self._run('touch ' + CLEANUP_LOGS_PAUSED_FILE)
    203         self._begun = True
    204 
    205 
    206     def end(self):
    207         assert self._begun
    208         if not self._is_nested:
    209             self._run('rm -f ' + CLEANUP_LOGS_PAUSED_FILE)
    210         else:
    211             logging.info('Leaving existing %s file' % CLEANUP_LOGS_PAUSED_FILE)
    212         self._begun = False
    213 
    214 
    215 class LogReader(AbstractLogReader):
    216     """Class to read traditional text log files.
    217 
    218     Be default reads all logs from /var/log/messages.
    219     """
    220 
    221     def __init__(self, filename='/var/log/messages', include_rotated_logs=True):
    222         AbstractLogReader.__init__(self)
    223         self._filename = filename
    224         self._include_rotated_logs = include_rotated_logs
    225         if not os.path.exists(CLEANUP_LOGS_PAUSED_FILE):
    226             raise error.TestError('LogReader created without ' +
    227                                   CLEANUP_LOGS_PAUSED_FILE)
    228 
    229     def read_all_logs(self):
    230         log_files = []
    231         line_number = 0
    232         if self._include_rotated_logs:
    233             log_files.extend(utils.system_output(
    234                 'ls -tr1 %s.*' % self._filename,
    235                 ignore_status=True).splitlines())
    236         log_files.append(self._filename)
    237         for log_file in log_files:
    238             f = open(log_file)
    239             for line in f:
    240                 line_number += 1
    241                 if line_number < self._start_line:
    242                     continue
    243                 peek = yield line
    244                 if peek:
    245                   buf = [f.next() for _ in xrange(peek)]
    246                   yield buf[-1]
    247                   while buf:
    248                     yield buf.pop(0)
    249             f.close()
    250 
    251 
    252 class JournalLogReader(AbstractLogReader):
    253     """A class to read logs stored by systemd-journald.
    254     """
    255 
    256     def read_all_logs(self):
    257       proc = subprocess.Popen(['journalctl'], stdout=subprocess.PIPE)
    258       line_number = 0
    259       for line in proc.stdout:
    260           line_number += 1
    261           if line_number < self._start_line:
    262               continue
    263           yield line
    264       proc.terminate()
    265 
    266 
    267 class ContinuousLogReader(AbstractLogReader):
    268   """Reads log file from where it left over last time.
    269 
    270   Useful when reading a growing log file.
    271   """
    272   def __init__(self, filename):
    273     self._filename = filename
    274     # Keeps file offset of last read.
    275     self._last_pos = 0
    276 
    277   def read_all_logs(self):
    278     try:
    279         with open(self._filename, 'r') as f:
    280             f.seek(self._last_pos)
    281             for line in f:
    282                 yield line
    283             # Notice that _last_pos is recorded only if all lines in the file is
    284             # read up. Maybe put this into a context manager so it's always
    285             # recorded if needed.
    286             self._last_pos = f.tell()
    287     except IOError:
    288         logging.error('Could not read log file %s', self._filename)
    289 
    290 
    291 def make_system_log_reader():
    292     """Create a system log reader.
    293 
    294     This will create JournalLogReader() or LogReader() depending on
    295     whether the system is configured with systemd.
    296     """
    297     if os.path.exists("/var/log/journal"):
    298         return JournalLogReader()
    299     else:
    300         return LogReader()
    301