#!/usr/bin/env python # Copyright 2012 - 2017, New York University and the TUF contributors # SPDX-License-Identifier: MIT OR Apache-2.0 """ log.py Vladimir Diaz April 4, 2012. Based on a previous version of this module by Geremy Condra. See LICENSE-MIT OR LICENSE for licensing information. A central location for all logging-related configuration. This module should be imported once by the main program. If other modules wish to incorporate 'tuf' logging, they should do the following: import logging logger = logging.getLogger('tuf') 'logging' refers to the module name. logging.getLogger() is a function of the module 'logging'. logging.getLogger(name) returns a Logger instance associated with 'name'. Calling getLogger(name) will always return the same instance. In this 'log.py' module, we perform the initial setup for the name 'tuf'. The 'log.py' module should only be imported once by the main program. When any other module does a logging.getLogger('tuf'), it is referring to the same 'tuf' instance, and its associated settings, set here in 'log.py'. See http://docs.python.org/library/logging.html#logger-objects for more information. We use multiple handlers to process log messages in various ways and to configure each one independently. Instead of using one single manner of processing log messages, we can use two built-in handlers that have already been configured for us. For example, the built-in FileHandler will catch log messages and dump them to a file. If we wanted, we could set this file handler to only catch CRITICAL (and greater) messages and save them to a file. Other handlers (e.g., StreamHandler) could handle INFO-level (and greater) messages. Logging Levels: --Level-- --Value-- logging.CRITICAL 50 logging.ERROR 40 logging.WARNING 30 logging.INFO 20 logging.DEBUG 10 logging.NOTSET 0 The logging module is thread-safe. Logging to a single file from multiple threads in a single process is also thread-safe. The logging module is NOT thread-safe when logging to a single file across multiple processes: http://docs.python.org/2/library/logging.html#thread-safety http://docs.python.org/2/howto/logging-cookbook.html """ # Help with Python 3 compatibility, where the print statement is a function, an # implicit relative import is invalid, and the '/' operator performs true # division. Example: print 'hello world' raises a 'SyntaxError' exception. from __future__ import print_function from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals import logging import time import tuf import tuf.settings import tuf.exceptions import securesystemslib.formats # Setting a handler's log level filters only logging messages of that level # (and above). For example, setting the built-in StreamHandler's log level to # 'logging.WARNING' will cause the stream handler to only process messages # of levels: WARNING, ERROR, and CRITICAL. _DEFAULT_LOG_LEVEL = logging.DEBUG _DEFAULT_CONSOLE_LOG_LEVEL = logging.INFO _DEFAULT_FILE_LOG_LEVEL = logging.DEBUG # Set the format for logging messages. # Example format for '_FORMAT_STRING': # [2013-08-13 15:21:18,068 localtime] [tuf] # [INFO][_update_metadata:851@updater.py] _FORMAT_STRING = '[%(asctime)s UTC] [%(name)s] [%(levelname)s] '+\ '[%(funcName)s:%(lineno)s@%(filename)s]\n%(message)s\n' # Ask all Formatter instances to talk GMT. Set the 'converter' attribute of # 'logging.Formatter' so that all formatters use Greenwich Mean Time. # http://docs.python.org/2/library/logging.html#logging.Formatter.formatTime # The 2nd paragraph in the link above contains the relevant information. # GMT = UTC (Coordinated Universal Time). TUF metadata stores timestamps in UTC. # We previously displayed the local time but this lead to confusion when # visually comparing logger events and metadata information. Unix time stamps # are fine but they may be less human-readable than UTC. logging.Formatter.converter = time.gmtime formatter = logging.Formatter(_FORMAT_STRING) # Set the handlers for the logger. The console handler is unset by default. A # module importing 'log.py' should explicitly set the console handler if # outputting log messages to the screen is needed. Adding a console handler can # be done with tuf.log.add_console_handler(). Logging messages to a file is not # set by default. console_handler = None file_handler = None # Set the logger and its settings. logger = logging.getLogger('tuf') logger.setLevel(_DEFAULT_LOG_LEVEL) # Set the built-in file handler. Messages will be logged to # 'settings.LOG_FILENAME', and only those messages with a log level of # '_DEFAULT_LOG_LEVEL'. The log level of messages handled by 'file_handler' # may be modified with 'set_filehandler_log_level()'. 'settings.LOG_FILENAME' # will be opened in append mode. if tuf.settings.ENABLE_FILE_LOGGING: file_handler = logging.FileHandler(tuf.settings.LOG_FILENAME) file_handler.setLevel(_DEFAULT_FILE_LOG_LEVEL) file_handler.setFormatter(formatter) logger.addHandler(file_handler) else: pass # Silently ignore logger exceptions. logging.raiseExceptions = False class ConsoleFilter(logging.Filter): def filter(self, record): """ Use Vinay Sajip's recommendation from Python issue #6435 to modify a LogRecord object. This is meant to be used with our console handler. http://stackoverflow.com/q/6177520 http://stackoverflow.com/q/5875225 http://bugs.python.org/issue6435 http://docs.python.org/2/howto/logging-cookbook.html#filters-contextual http://docs.python.org/2/library/logging.html#logrecord-attributes record: A logging.LogRecord object. None. Replaces the LogRecord exception text attribute. True. """ # If this LogRecord object has an exception, then we will replace its text. if record.exc_info: # We place the record's cached exception text (which usually contains the # exception traceback) with much simpler exception information. This is # most useful for the console handler, which we do not wish to deluge # with too much data. Assuming that this filter is not applied to the # file logging handler, the user may always consult the file log for the # original exception traceback. The exc_info is explained here: # http://docs.python.org/2/library/sys.html#sys.exc_info exc_type, junk, junk = record.exc_info # Simply set the class name as the exception text. record.exc_text = exc_type.__name__ # Always return True to signal that any given record must be formatted. return True def set_log_level(log_level=_DEFAULT_LOG_LEVEL): """ Allow the default log level to be overridden. If 'log_level' is not provided, log level defaults to 'logging.DEBUG'. log_level: The log level to set for the 'log.py' file handler. 'log_level' examples: logging.INFO; logging.CRITICAL. None. Overrides the logging level for the 'log.py' file handler. None. """ # Does 'log_level' have the correct format? # Raise 'securesystems.exceptions.FormatError' if there is a mismatch. securesystemslib.formats.LOGLEVEL_SCHEMA.check_match(log_level) logger.setLevel(log_level) def set_filehandler_log_level(log_level=_DEFAULT_FILE_LOG_LEVEL): """ Allow the default file handler log level to be overridden. If 'log_level' is not provided, log level defaults to 'logging.DEBUG'. log_level: The log level to set for the 'log.py' file handler. 'log_level' examples: logging.INFO; logging.CRITICAL. None. Overrides the logging level for the 'log.py' file handler. None. """ # Does 'log_level' have the correct format? # Raise 'securesystems.exceptions.FormatError' if there is a mismatch. securesystemslib.formats.LOGLEVEL_SCHEMA.check_match(log_level) if file_handler: file_handler.setLevel(log_level) else: raise tuf.exceptions.Error( 'File handler has not been set. Enable file logging' ' before attempting to set its log level') def set_console_log_level(log_level=_DEFAULT_CONSOLE_LOG_LEVEL): """ Allow the default log level for console messages to be overridden. If 'log_level' is not provided, log level defaults to 'logging.INFO'. log_level: The log level to set for the console handler. 'log_level' examples: logging.INFO; logging.CRITICAL. securesystems.exceptions.Error, if the 'log.py' console handler has not been set yet with add_console_handler(). Overrides the logging level for the console handler. None. """ # Does 'log_level' have the correct format? # Raise 'securesystems.exceptions.FormatError' if there is a mismatch. securesystemslib.formats.LOGLEVEL_SCHEMA.check_match(log_level) # Assign to the global console_handler object. global console_handler if console_handler is not None: console_handler.setLevel(log_level) else: message = 'The console handler has not been set with add_console_handler().' raise securesystemslib.exceptions.Error(message) def add_console_handler(log_level=_DEFAULT_CONSOLE_LOG_LEVEL): """ Add a console handler and set its log level to 'log_level'. log_level: The log level to set for the console handler. 'log_level' examples: logging.INFO; logging.CRITICAL. None. Adds a console handler to the 'log.py' logger and sets its logging level to 'log_level'. None. """ # Does 'log_level' have the correct format? # Raise 'securesystems.exceptions.FormatError' if there is a mismatch. securesystemslib.formats.LOGLEVEL_SCHEMA.check_match(log_level) # Assign to the global console_handler object. global console_handler if not console_handler: # Set the console handler for the logger. The built-in console handler will # log messages to 'sys.stderr' and capture 'log_level' messages. console_handler = logging.StreamHandler() # Get our filter for the console handler. console_filter = ConsoleFilter() console_format_string = '%(message)s' console_formatter = logging.Formatter(console_format_string) console_handler.setLevel(log_level) console_handler.setFormatter(console_formatter) console_handler.addFilter(console_filter) logger.addHandler(console_handler) logger.debug('Added a console handler.') else: logger.warning('We already have a console handler.') def remove_console_handler(): """ Remove the console handler from the logger in 'log.py', if previously added. None. None. A handler belonging to the console is removed from the 'log.py' logger and the console handler is marked as unset. None. """ # Assign to the global 'console_handler' object. global console_handler if console_handler: logger.removeHandler(console_handler) console_handler = None logger.debug('Removed a console handler.') else: logger.warning('We do not have a console handler.') def enable_file_logging(log_filename=tuf.settings.LOG_FILENAME): """ Log messages to a file (i.e., 'log_filename'). The log level for the file handler can be set with set_filehandler_log_level(). log_filename: Logging messages are saved to this file. If not provided, the log filename specified in tuf.settings.LOG_FILENAME is used. securesystemslib.exceptions.FormatError, if any of the arguments are not the expected format. tuf.exceptions.Error, if the file handler has already been set. The global file handler is set. None. """ # Are the arguments properly formatted? securesystemslib.formats.PATH_SCHEMA.check_match(log_filename) global file_handler # Add a file handler to the logger if not already set. if not file_handler: file_handler = logging.FileHandler(log_filename) file_handler.setLevel(_DEFAULT_FILE_LOG_LEVEL) file_handler.setFormatter(formatter) logger.addHandler(file_handler) else: raise tuf.exceptions.Error( 'The file handler has already been been set. A new file handler' ' can be set by first calling disable_file_logging()') def disable_file_logging(): """ Disable file logging by removing any previously set file handler. A warning is logged if the file handler cannot be removed. The file that was written to will not be deleted. None. None. The global file handler is unset. None. """ # Assign to the global 'file_handler' object. global file_handler if file_handler: logger.removeHandler(file_handler) file_handler = None logger.debug('Removed the file handler.') else: logger.warning('A file handler has not been set.')