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