blob: 8701b6815b0e34ccf94aa6f6b1300d5125209c08 [file] [log] [blame]
# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Logging Configuration library.
Allows for easy setup of logging in scripts with color output
and plaintext output.
Example usage:
import logging
from lib import logging_config
logging_config.ConfigureConsoleLogger(color=True)
logging.error('This is red')
"""
import copy
import logging
import logging.handlers
import os
import time
import fixup_path
fixup_path.FixupPath()
from chromite.lib.paygen import urilib
# These are ANSI escape codes for printing in color
_RED = '\x1b[31m'
_YELLOW = '\x1b[1;33m'
_MAGENTA = '\x1b[35m'
_GREEN = '\x1b[32m'
_DEFAULT = '\x1b[0m'
class ConsoleHandler(logging.StreamHandler):
"""StreamHandler with color optionally added.
Use self.setColor(True) or self.setColor(False) to turn color on or off.
"""
def __init__(self, *args, **kwargs):
super(ConsoleHandler, self).__init__(*args, **kwargs)
self.color = False
def setColor(self, color):
"""Turn colorizing on or off."""
self.color = color
def emit(self, record):
"""Override of the logging.StreamHandler emit function."""
if self.color:
# Adjust record.levelname to be colorized, but use a copy of record
# so the record is not affected for other streamhandlers.
record = copy.copy(record)
levelno = record.levelno
if levelno >= 50:
# Critical / Fatal
color = _RED
elif levelno >= 40:
# Error
color = _RED
elif levelno >= 30:
# Warning
color = _YELLOW
elif levelno >= 20:
# Info
color = _GREEN
elif levelno >= 10:
# Debug
color = _MAGENTA
else: # NOTSET and anything else
color = _DEFAULT
record.levelname = '%s%s%s' % (color, record.levelname, _DEFAULT)
super(ConsoleHandler, self).emit(record)
class ListLogger(object):
"""Contextable logger for a list of objects.
When used as a context, all lines are guaranteed to be output consecutively
when the context terminates. Otherwise, will log objects directly when
instructed.
"""
def __init__(self, title):
self._title = title + ':'
self._title_logged = False
self._lines = None
self._index = 0
def __enter__(self):
self._lines = []
return self
def __exit__(self, exc_type, exc_value, exc_traceback):
if exc_type is not None:
return False
if self._lines:
for line in self._lines:
self._LogLine(line)
else:
self._LogNoObjects()
def _LogLine(self, line):
"""Logs a single line, preceded by a title if needed."""
if not self._title_logged:
logging.info(self._title)
self._title_logged = True
logging.info(line)
def _LogNoObjects(self):
"""Outputs a "no-object" message."""
self._LogLine(' (no objects listed)')
def LogItem(self, obj):
"""Logs the next item of the list.
When used as a context, actual logging is deferred to context termination,
which guarantees that other logging calls will not interfere with the
listing of items.
"""
self._index += 1
line = ' %d: %s' % (self._index, obj)
if self._lines is not None:
self._lines.append(line)
else:
self._LogLine(line)
def LogAll(self, obj_list):
"""Logs all items in the provided list.
When not used as a context, this is assumed to be the only call made to
this logger and so it will print a "no object" message in lieu of the
missing items to be logged.
"""
if obj_list:
for obj in obj_list:
self.LogItem(obj)
elif self._lines is None:
self._LogNoObjects()
# Logging Format: 04/04 18:39:14 INFO[log_test-29]: test message
_FORMAT = '%(asctime)s %(levelname)s[%(module)s-%(lineno)d]: %(message)s'
_DATEFMT = '%m/%d %H:%M:%S'
def _GetRootLogger():
"""Get the root logger object, and set DEBUG level on it."""
root_logger = logging.getLogger()
# Configure the root logger to show all messages, and let sub handlers off
# that root logger filter to a different level if they choose.
root_logger.setLevel(logging.DEBUG)
return root_logger
_console_handler = None
def _GetConsoleHandler():
"""Get the console handler object, creating and registering it if needed."""
# pylint: disable=W0603
global _console_handler
# There is one and only one handler for output to the console supported,
# so re-use the same global object that is created on demand.
if not _console_handler:
_console_handler = ConsoleHandler()
_console_handler.setFormatter(logging.Formatter(_FORMAT, datefmt=_DATEFMT))
# Register the console handler now.
_GetRootLogger().addHandler(_console_handler)
return _console_handler
def ConfigureConsoleLogger(level=logging.INFO, color=True):
"""Adjust the level and color of current console handler.
Args:
level: Level of logging to display, i.e. logging.ERROR, logging.DEBUG,
logging.INFO etc. Default: logging.INFO.
color: If true color is turned on, otherwise color is turned off.
"""
console_handler = _GetConsoleHandler()
console_handler.setLevel(level)
console_handler.setColor(color)
# TODO(mtennant): Deprecated. Use ConfigureConsoleLogger.
def ColorLogger(level=logging.INFO):
"""Deprecated. Use ConfigureConsoleLogger instead."""
ConfigureConsoleLogger(level, True)
# TODO(mtennant): Deprecated. Use ConfigureConsoleLogger.
def PlainLogger(level=logging.INFO):
"""Deprecated. Use ConfigureConsoleLogger instead."""
ConfigureConsoleLogger(level, False)
def AddFileLogger(log_path='debug.log', max_bytes=1024*1024*4,
level=logging.DEBUG, backup_count=5, rollover_now=False):
"""Add a rotating file handler for logging to a file.
Args:
log_path: The path to the log file.
max_bytes: Maximum bytes to let the log file grow to before rotation
kicks in. Defaults to 4 megs.
level: Level of logging to display, i.e. logging.ERROR, logging.DEBUG,
logging.INFO etc. Default: logging.DEBUG.
backup_count: Number of previous log files to keep around.
rollover_now: If True, do rollover operation (copy log to log.1, log.1 to
log.2, etc.) at start of run rather than waiting for max_bytes threshold.
This effectively makes each log file specific to one run.
"""
handler = logging.handlers.RotatingFileHandler(log_path, maxBytes=max_bytes,
backupCount=backup_count)
handler.setFormatter(logging.Formatter(_FORMAT, datefmt=_DATEFMT))
handler.setLevel(level)
_GetRootLogger().addHandler(handler)
if rollover_now:
handler.doRollover()
# TODO(mtennant): Deprecated. Use AddFileLogger.
def FileLogger(*args, **kwargs):
"""Deprecated. Use AddFileLogger instead."""
return AddFileLogger(*args, **kwargs)
def ArchiveLog(log_path, archive_dir, strftime_format='%Y%m%d-%H%M%S-UTC',
archive_basename=None):
"""Copy a log file to an archive directory, adding UTC timestamp to filename.
Args:
log_path: Path to log file. Can be local or on supported
remote system (e.g. Google Storage).
archive_dir: Path to directory to archive log in. Can be any path type
accepted by urilib.Copy (e.g. local path or Google Storage path).
strftime_format: String representing strftime format as accepted by
time.strftime(). If archive_name is not specified, then this
will be used to add a timestamp suffix to the archived log file name
(suffix will be inserted before any ending file extension suffix).
archive_basename: String for name of archived log file. The string is pass
through time.strftime() before it is used, so any custom timestamp can
be included anywhere in the archive_name.
Returns:
Full path of archived log file.
"""
# Construct basename from log_path and strftime_format, if none specified.
if not archive_basename:
basename = os.path.basename(log_path)
basename_no_ext, ext = os.path.splitext(basename)
archive_basename = '%s-%s%s' % (basename_no_ext, strftime_format, ext)
# Insert timestamp (with UTC time) that will be used.
archive_basename = time.strftime(archive_basename, time.gmtime())
# Copy log file contents to archive destination.
archive_path = os.path.join(archive_dir, archive_basename)
urilib.Copy(log_path, archive_path)
return archive_path