• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
5import abc, logging, os, re, time
6import subprocess
7
8import common
9from autotest_lib.client.bin import utils
10from autotest_lib.client.common_lib import error
11from constants import CLEANUP_LOGS_PAUSED_FILE
12
13
14def 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
32def 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
47class 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
167class 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
215class 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
252class 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
267class 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
291def 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