1#!/usr/bin/env python3 2# 3# Copyright 2016 - The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17from __future__ import print_function 18 19import datetime 20import logging 21import os 22import re 23 24from copy import copy 25 26from acts import tracelogger 27from acts.libs.logging import log_stream 28from acts.libs.logging.log_stream import LogStyles 29from acts.utils import create_dir 30 31 32log_line_format = "%(asctime)s.%(msecs).03d %(levelname)s %(message)s" 33# The micro seconds are added by the format string above, 34# so the time format does not include ms. 35log_line_time_format = "%Y-%m-%d %H:%M:%S" 36log_line_timestamp_len = 23 37 38logline_timestamp_re = re.compile("\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d.\d\d\d") 39 40 41# yapf: disable 42class Style: 43 RESET = '\033[0m' 44 BRIGHT = '\033[1m' 45 DIM = '\033[2m' 46 NORMAL = '\033[22m' 47 48 49class Fore: 50 BLACK = '\033[30m' 51 RED = '\033[31m' 52 GREEN = '\033[32m' 53 YELLOW = '\033[33m' 54 BLUE = '\033[34m' 55 MAGENTA = '\033[35m' 56 CYAN = '\033[36m' 57 WHITE = '\033[37m' 58 RESET = '\033[39m' 59 60 61class Back: 62 BLACK = '\033[40m' 63 RED = '\033[41m' 64 GREEN = '\033[42m' 65 YELLOW = '\033[43m' 66 BLUE = '\033[44m' 67 MAGENTA = '\033[45m' 68 CYAN = '\033[46m' 69 WHITE = '\033[47m' 70 RESET = '\033[49m' 71 72 73LOG_LEVELS = { 74 'DEBUG': {'level': 10, 'style': Fore.GREEN + Style.BRIGHT}, 75 'CASE': {'level': 11, 'style': Back.BLUE + Fore.WHITE + Style.BRIGHT}, 76 'SUITE': {'level': 12, 'style': Back.MAGENTA + Fore.WHITE + Style.BRIGHT}, 77 'INFO': {'level': 20, 'style': Style.NORMAL}, 78 'STEP': {'level': 15, 'style': Fore.WHITE + Style.BRIGHT}, 79 'WARNING': {'level': 30, 'style': Fore.YELLOW + Style.BRIGHT}, 80 'ERROR': {'level': 40, 'style': Fore.RED + Style.BRIGHT}, 81 'EXCEPTION': {'level': 45, 'style': Back.RED + Fore.WHITE + Style.BRIGHT}, 82 'DEVICE': {'level': 51, 'style': Fore.CYAN + Style.BRIGHT}, 83} 84# yapf: enable 85 86 87class ColoredLogFormatter(logging.Formatter): 88 def format(self, record): 89 colored_record = copy(record) 90 level_name = colored_record.levelname 91 style = LOG_LEVELS[level_name]['style'] 92 formatted_level_name = '%s%s%s' % (style, level_name, Style.RESET) 93 colored_record.levelname = formatted_level_name 94 return super().format(colored_record) 95 96 97def _parse_logline_timestamp(t): 98 """Parses a logline timestamp into a tuple. 99 100 Args: 101 t: Timestamp in logline format. 102 103 Returns: 104 An iterable of date and time elements in the order of month, day, hour, 105 minute, second, microsecond. 106 """ 107 date, time = t.split(' ') 108 year, month, day = date.split('-') 109 h, m, s = time.split(':') 110 s, ms = s.split('.') 111 return year, month, day, h, m, s, ms 112 113 114def is_valid_logline_timestamp(timestamp): 115 if len(timestamp) == log_line_timestamp_len: 116 if logline_timestamp_re.match(timestamp): 117 return True 118 return False 119 120 121def logline_timestamp_comparator(t1, t2): 122 """Comparator for timestamps in logline format. 123 124 Args: 125 t1: Timestamp in logline format. 126 t2: Timestamp in logline format. 127 128 Returns: 129 -1 if t1 < t2; 1 if t1 > t2; 0 if t1 == t2. 130 """ 131 dt1 = _parse_logline_timestamp(t1) 132 dt2 = _parse_logline_timestamp(t2) 133 for u1, u2 in zip(dt1, dt2): 134 if u1 < u2: 135 return -1 136 elif u1 > u2: 137 return 1 138 return 0 139 140 141def _get_timestamp(time_format, delta=None): 142 t = datetime.datetime.now() 143 if delta: 144 t = t + datetime.timedelta(seconds=delta) 145 return t.strftime(time_format)[:-3] 146 147 148def epoch_to_log_line_timestamp(epoch_time): 149 """Converts an epoch timestamp in ms to log line timestamp format, which 150 is readable for humans. 151 152 Args: 153 epoch_time: integer, an epoch timestamp in ms. 154 155 Returns: 156 A string that is the corresponding timestamp in log line timestamp 157 format. 158 """ 159 s, ms = divmod(epoch_time, 1000) 160 d = datetime.datetime.fromtimestamp(s) 161 return d.strftime("%Y-%m-%d %H:%M:%S.") + str(ms) 162 163 164def get_log_line_timestamp(delta=None): 165 """Returns a timestamp in the format used by log lines. 166 167 Default is current time. If a delta is set, the return value will be 168 the current time offset by delta seconds. 169 170 Args: 171 delta: Number of seconds to offset from current time; can be negative. 172 173 Returns: 174 A timestamp in log line format with an offset. 175 """ 176 return _get_timestamp("%Y-%m-%d %H:%M:%S.%f", delta) 177 178 179def get_log_file_timestamp(delta=None): 180 """Returns a timestamp in the format used for log file names. 181 182 Default is current time. If a delta is set, the return value will be 183 the current time offset by delta seconds. 184 185 Args: 186 delta: Number of seconds to offset from current time; can be negative. 187 188 Returns: 189 A timestamp in log file name format with an offset. 190 """ 191 return _get_timestamp("%Y-%m-%d_%H-%M-%S-%f", delta) 192 193 194def _setup_test_logger(log_path, prefix=None): 195 """Customizes the root logger for a test run. 196 197 The logger object has a stream handler and a file handler. The stream 198 handler logs INFO level to the terminal, the file handler logs DEBUG 199 level to files. 200 201 Args: 202 log_path: Location of the log file. 203 prefix: A prefix for each log line in terminal. 204 """ 205 logging.log_path = log_path 206 log_styles = [LogStyles.LOG_INFO + LogStyles.TO_STDOUT, 207 LogStyles.DEFAULT_LEVELS + LogStyles.TESTCASE_LOG] 208 terminal_format = log_line_format 209 if prefix: 210 terminal_format = "[{}] {}".format(prefix, log_line_format) 211 stream_formatter = ColoredLogFormatter(terminal_format, 212 log_line_time_format) 213 file_formatter = logging.Formatter(log_line_format, log_line_time_format) 214 log = log_stream.create_logger('test_run', '', log_styles=log_styles, 215 stream_format=stream_formatter, 216 file_format=file_formatter) 217 log.setLevel(logging.DEBUG) 218 _enable_additional_log_levels() 219 220 221def _enable_additional_log_levels(): 222 """Enables logging levels used for tracing tests and debugging devices.""" 223 for log_type, log_data in LOG_LEVELS.items(): 224 logging.addLevelName(log_data['level'], log_type) 225 226 227def kill_test_logger(logger): 228 """Cleans up a test logger object by removing all of its handlers. 229 230 Args: 231 logger: The logging object to clean up. 232 """ 233 for h in list(logger.handlers): 234 logger.removeHandler(h) 235 if isinstance(h, logging.FileHandler): 236 h.close() 237 238 239def create_latest_log_alias(actual_path): 240 """Creates a symlink to the latest test run logs. 241 242 Args: 243 actual_path: The source directory where the latest test run's logs are. 244 """ 245 link_path = os.path.join(os.path.dirname(actual_path), "latest") 246 if os.path.islink(link_path): 247 os.remove(link_path) 248 os.symlink(actual_path, link_path) 249 250 251def setup_test_logger(log_path, prefix=None): 252 """Customizes the root logger for a test run. 253 254 Args: 255 log_path: Location of the report file. 256 prefix: A prefix for each log line in terminal. 257 filename: Name of the files. The default is the time the objects 258 are requested. 259 """ 260 create_dir(log_path) 261 _setup_test_logger(log_path, prefix) 262 create_latest_log_alias(log_path) 263 264 265def normalize_log_line_timestamp(log_line_timestamp): 266 """Replace special characters in log line timestamp with normal characters. 267 268 Args: 269 log_line_timestamp: A string in the log line timestamp format. Obtained 270 with get_log_line_timestamp. 271 272 Returns: 273 A string representing the same time as input timestamp, but without 274 special characters. 275 """ 276 norm_tp = log_line_timestamp.replace(' ', '_') 277 norm_tp = norm_tp.replace(':', '-') 278 return norm_tp 279 280 281class LoggerAdapter(logging.LoggerAdapter): 282 """A LoggerAdapter class that takes in a lambda for transforming logs.""" 283 284 def __init__(self, logging_lambda): 285 self.logging_lambda = logging_lambda 286 super(LoggerAdapter, self).__init__(logging.getLogger(), {}) 287 288 def process(self, msg, kwargs): 289 return self.logging_lambda(msg), kwargs 290 291 292def create_logger(logging_lambda=lambda message: message): 293 """Returns a logger with logging defined by a given lambda. 294 295 Args: 296 logging_lambda: A lambda of the form: 297 >>> lambda log_message: return 'string' 298 """ 299 return tracelogger.TraceLogger(LoggerAdapter(logging_lambda)) 300 301 302def create_tagged_trace_logger(tag=''): 303 """Returns a logger that logs each line with the given prefix. 304 305 Args: 306 tag: The tag of the log line, E.g. if tag == tag123, the output 307 line would be: 308 309 <TESTBED> <TIME> <LOG_LEVEL> [tag123] logged message 310 """ 311 312 def logging_lambda(msg): 313 return '[%s] %s' % (tag, msg) 314 315 return create_logger(logging_lambda) 316