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