• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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