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