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