1 # 2 # Copyright (C) 2016 The Android Open Source Project 3 # 4 # Licensed under the Apache License, Version 2.0 (the "License"); 5 # you may not use this file except in compliance with the License. 6 # You may obtain a copy of the License at 7 # 8 # http://www.apache.org/licenses/LICENSE-2.0 9 # 10 # Unless required by applicable law or agreed to in writing, software 11 # distributed under the License is distributed on an "AS IS" BASIS, 12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 # See the License for the specific language governing permissions and 14 # limitations under the License. 15 # 16 17 from __future__ import print_function 18 19 import datetime 20 import logging 21 import os 22 import re 23 import sys 24 25 from vts.runners.host import utils 26 27 log_line_format = "%(asctime)s.%(msecs).03d %(levelname)s %(message)s" 28 # The micro seconds are added by the format string above, 29 # so the time format does not include ms. 30 log_line_time_format = "%m-%d %H:%M:%S" 31 log_line_timestamp_len = 18 32 33 logline_timestamp_re = re.compile("\d\d-\d\d \d\d:\d\d:\d\d.\d\d\d") 34 35 log_severity_map = { 36 "ERROR": logging.ERROR, 37 "WARNING": logging.WARNING, 38 "INFO": logging.INFO, 39 "DEBUG": logging.DEBUG, 40 } 41 42 def _parse_logline_timestamp(t): 43 """Parses a logline timestamp into a tuple. 44 45 Args: 46 t: Timestamp in logline format. 47 48 Returns: 49 An iterable of date and time elements in the order of month, day, hour, 50 minute, second, microsecond. 51 """ 52 date, time = t.split(' ') 53 month, day = date.split('-') 54 h, m, s = time.split(':') 55 s, ms = s.split('.') 56 return (month, day, h, m, s, ms) 57 58 59 def isValidLogLineTimestamp(timestamp): 60 if len(timestamp) == log_line_timestamp_len: 61 if logline_timestamp_re.match(timestamp): 62 return True 63 return False 64 65 66 def logLineTimestampComparator(t1, t2): 67 """Comparator for timestamps in logline format. 68 69 Args: 70 t1: Timestamp in logline format. 71 t2: Timestamp in logline format. 72 73 Returns: 74 -1 if t1 < t2; 1 if t1 > t2; 0 if t1 == t2. 75 """ 76 dt1 = _parse_logline_timestamp(t1) 77 dt2 = _parse_logline_timestamp(t2) 78 for u1, u2 in zip(dt1, dt2): 79 if u1 < u2: 80 return -1 81 elif u1 > u2: 82 return 1 83 return 0 84 85 86 def _get_timestamp(time_format, delta=None): 87 t = datetime.datetime.now() 88 if delta: 89 t = t + datetime.timedelta(seconds=delta) 90 return t.strftime(time_format)[:-3] 91 92 93 def epochToLogLineTimestamp(epoch_time): 94 d = datetime.datetime.fromtimestamp(epoch_time / 1000) 95 return d.strftime("%m-%d %H:%M:%S.%f")[:-3] 96 97 98 def getLogLineTimestamp(delta=None): 99 """Returns a timestamp in the format used by log lines. 100 101 Default is current time. If a delta is set, the return value will be 102 the current time offset by delta seconds. 103 104 Args: 105 delta: Number of seconds to offset from current time; can be negative. 106 107 Returns: 108 A timestamp in log line format with an offset. 109 """ 110 return _get_timestamp("%m-%d %H:%M:%S.%f", delta) 111 112 113 def getLogFileTimestamp(delta=None): 114 """Returns a timestamp in the format used for log file names. 115 116 Default is current time. If a delta is set, the return value will be 117 the current time offset by delta seconds. 118 119 Args: 120 delta: Number of seconds to offset from current time; can be negative. 121 122 Returns: 123 A timestamp in log filen name format with an offset. 124 """ 125 return _get_timestamp("%m-%d-%Y_%H-%M-%S-%f", delta) 126 127 128 def _initiateTestLogger(log_path, prefix=None, filename=None, log_severity="INFO"): 129 """Customizes the root logger for a test run. 130 131 The logger object has a stream handler and a file handler. Both handler logs data 132 according to the log severty level. 133 134 Args: 135 log_path: Location of the log file. 136 prefix: A prefix for each log line in terminal. 137 filename: Name of the log file. The default is the time the logger 138 is requested. 139 log_severity: string, set the log severity level, default is INFO. 140 """ 141 log = logging.getLogger() 142 # Clean up any remaining handlers. 143 killTestLogger(log) 144 log.propagate = False 145 146 log.setLevel(log_severity_map.get(log_severity, logging.INFO)) 147 # Log info to stream 148 terminal_format = log_line_format 149 if prefix: 150 terminal_format = "[{}] {}".format(prefix, log_line_format) 151 c_formatter = logging.Formatter(terminal_format, log_line_time_format) 152 ch = logging.StreamHandler(sys.stdout) 153 ch.setFormatter(c_formatter) 154 ch.setLevel(log_severity_map.get(log_severity, logging.INFO)) 155 # Log everything to file 156 f_formatter = logging.Formatter(log_line_format, log_line_time_format) 157 # All the logs of this test class go into one directory 158 if filename is None: 159 filename = getLogFileTimestamp() 160 utils.create_dir(log_path) 161 fh = logging.FileHandler(os.path.join(log_path, 'test_run_details.txt')) 162 fh.setFormatter(f_formatter) 163 fh.setLevel(log_severity_map.get(log_severity, logging.INFO)) 164 log.addHandler(ch) 165 log.addHandler(fh) 166 log.log_path = log_path 167 logging.log_path = log_path 168 169 170 def killTestLogger(logger): 171 """Cleans up the handlers attached to a test logger object. 172 173 Args: 174 logger: The logging object to clean up. 175 """ 176 for h in list(logger.handlers): 177 if isinstance(h, logging.FileHandler): 178 h.close() 179 logger.removeHandler(h) 180 181 182 def isSymlinkSupported(): 183 """Checks whether the OS supports symbolic link. 184 185 Returns: 186 A boolean representing whether the OS supports symbolic link. 187 """ 188 return hasattr(os, "symlink") 189 190 191 def createLatestLogAlias(actual_path): 192 """Creates a symlink to the latest test run logs. 193 194 Args: 195 actual_path: The source directory where the latest test run's logs are. 196 """ 197 link_path = os.path.join(os.path.dirname(actual_path), "latest") 198 if os.path.islink(link_path): 199 os.remove(link_path) 200 os.symlink(actual_path, link_path) 201 202 203 def setupTestLogger(log_path, prefix=None, filename=None, log_severity="INFO"): 204 """Customizes the root logger for a test run. 205 206 Args: 207 log_path: Location of the report file. 208 prefix: A prefix for each log line in terminal. 209 filename: Name of the files. The default is the time the objects 210 are requested. 211 """ 212 if filename is None: 213 filename = getLogFileTimestamp() 214 utils.create_dir(log_path) 215 logger = _initiateTestLogger(log_path, prefix, filename, log_severity) 216 if isSymlinkSupported(): 217 createLatestLogAlias(log_path) 218 219 220 def normalizeLogLineTimestamp(log_line_timestamp): 221 """Replace special characters in log line timestamp with normal characters. 222 223 Args: 224 log_line_timestamp: A string in the log line timestamp format. Obtained 225 with getLogLineTimestamp. 226 227 Returns: 228 A string representing the same time as input timestamp, but without 229 special characters. 230 """ 231 norm_tp = log_line_timestamp.replace(' ', '_') 232 norm_tp = norm_tp.replace(':', '-') 233 return norm_tp 234 235 236 class LoggerProxy(object): 237 """This class is for situations where a logger may or may not exist. 238 239 e.g. In controller classes, sometimes we don't have a logger to pass in, 240 like during a quick try in python console. In these cases, we don't want to 241 crash on the log lines because logger is None, so we should set self.log to 242 an object of this class in the controller classes, instead of the actual 243 logger object. 244 """ 245 246 def __init__(self, logger=None): 247 self.log = logger 248 249 @property 250 def log_path(self): 251 if self.log: 252 return self.log.log_path 253 return "/tmp/logs" 254 255 def __getattr__(self, name): 256 def log_call(*args): 257 if self.log: 258 return getattr(self.log, name)(*args) 259 print(*args) 260 261 return log_call 262