logging模塊的簡單使用以及tornado中的log簡單介紹

剛知道tornado原來是facebook開源的。

logging的簡單介紹和使用

python內置模塊logging,用於記錄日誌,格式化輸出。通過getLogger()獲得的是單例且線程安全的(進程不安全),下文會簡單介紹logging的常用方法和簡單logging源碼分析,以及tornado中的日誌模塊。

使用basicConfig()(給root handler添加基本配置),除了levelfilename參數外還有filemode(默認a),format(輸出格式),handlers(給root logger添加handlers)

import logging

logging.basicConfig(level=logging.DEBUG, filename='main.log')
logging.debug('bug log')

logging的日誌分爲這幾種級別:

CRITICAL = 50
FATAL = CRITICAL
ERROR = 40
WARNING = 30
WARN = WARNING
INFO = 20
DEBUG = 10
NOTSET = 0

logging.info()等實際上是根looger(root logger)的info()方法。

利用logger的相對應的infodebugwaring等可以輸出日誌,只有當方法級別>日誌級別時才能夠輸出(如level=logging.INFO時,用logging.error('error')能夠輸出,而logging.debug('debug')無輸出)。

值得一提的是還有一個logging.exception()方法,能夠簡單的記錄Traceback信息(實際上是相當於logging.error(e, exc_info=True)),所有的輸出方法最終都是調用了logger的_log()方法。

getLogger()
import logging

root_logger = logging.getLogger()
mine_logger = logging.getLogger('mine')

參數缺省時默認獲取的是root_logger,是所有logger的最終父logger。
在未作任何配置時,root_logger的日誌級別是warning

閱讀getLogger()源碼可以發現

root = RootLogger(WARNING)
Logger.root = root
Logger.manager = Manager(Logger.root)

def getLogger(name=None):
    """
    Return a logger with the specified name, creating it if necessary.

    If no name is specified, return the root logger.
    """
    if name:
        return Logger.manager.getLogger(name)
    else:
        return root

執行logging.getLogger('mine')方法,即Manager(Logger.root).getLogger('mine'),再來看一下ManagergetLogger()

    def getLogger(self, name):
        """
        Get a logger with the specified name (channel name), creating it
        if it doesn't yet exist. This name is a dot-separated hierarchical
        name, such as "a", "a.b", "a.b.c" or similar.

        If a PlaceHolder existed for the specified name [i.e. the logger
        didn't exist but a child of it did], replace it with the created
        logger and fix up the parent/child references which pointed to the
        placeholder to now point to the logger.
        """
        rv = None
        if not isinstance(name, str):
            raise TypeError('A logger name must be a string')
        _acquireLock()
        try:
            if name in self.loggerDict:
                rv = self.loggerDict[name]
                if isinstance(rv, PlaceHolder):
                    ph = rv
                    rv = (self.loggerClass or _loggerClass)(name)
                    rv.manager = self
                    self.loggerDict[name] = rv
                    self._fixupChildren(ph, rv)
                    self._fixupParents(rv)
            else:
                rv = (self.loggerClass or _loggerClass)(name)
                rv.manager = self
                self.loggerDict[name] = rv
                self._fixupParents(rv)
        finally:
            _releaseLock()
        return rv

getLogger的時候會先從Manager的loggerDict中查找,找不到時才生成新logger ,通過loggerDictPlaceHodler,將a,a.b,a.b.c類似的,子logger繼承父logger的levelhandler等,最終繼承root_handler。

Handlers, Formatter

每個logger都能有多個handler,默認的是StreamHandler,能夠通過addHandler()removeHandler()來添加刪除handler。

import logging

mine_logger = logging.getLogger('mine')
mine_logger.setLevel(logging.INFO)

console_handler = logging.StreamHandler()
file_handler = logging.FileHandler('./logs/test.log')

formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
console_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)

mine_logger.addHandler(console_handler)
mine_logger.addHandler(file_handler)

mine_logger.info('info message')

新建了console_handlerfile_handler,通過addHandler()添加到了mine_logger上,這樣mine_logger.info('info message')既能輸出到文件又能打印在控制檯。

ps:可以利用logging.Formatter來格式化輸出,但每個handler只能有一種類formatter。

高級handlers

logging.handers下面有很多封裝好的功能強大的handler。
RotatingFileHandler :根據大小切割日誌。
TimedRotatingFileHandler :根據時間切割日誌。
還有MemoryHandlerSocketHandler等等。

config

利用配置文件能夠快速的對logging進行配置。

[loggers]
keys=root, common

[logger_root]
level=INFO
handlers=root_handler

[logger_common]
level=DEBUG
handlers=common_handler
qualname=common

[handlers]
keys=root_handler,common_handler

[handler_root_handler]
class=StreamHandler
level=INFO
formatter=form01
args=(sys.stdout,)

[handler_common_handler]
class=FileHandler
level=DEBUG
formatter=form01
args=('main.log',"a")

[formatters]
keys=form01

[formatter_form01]
format=[%(asctime)s %(levelname)s] file: %(filename)s, line:%(lineno)d %(process)d %(message)s 
datefmt=%Y-%m-%d %H:%M:%S

通過配置文件來定義logger,handler,formatter等。上述配置中handler都是指向logging模塊自帶的handler,也可以指向自定義的handler。

利用logging.config加載配置文件。

import logging
import logging.config

logging_config_path = 'logging.conf'
logging.config.fileConfig(logging_config_path)

root_logger = logging.getLogger('root')
root_logger.info('test message')


common_logger = logging.getLogger('common')
common_logger.info('test common message')

tornado中的log簡單介紹

在tornado的log.py模塊中,初始化了三種logger,都繼承於logging模塊的root logger。

'''Tornado uses three logger streams:

* ``tornado.access``: Per-request logging for Tornado's HTTP servers (and
  potentially other servers in the future)
* ``tornado.application``: Logging of errors from application code (i.e.
  uncaught exceptions from callbacks)
* ``tornado.general``: General-purpose logging, including any errors
  or warnings from Tornado itself.
'''

# Logger objects for internal tornado use
access_log = logging.getLogger("tornado.access")
app_log = logging.getLogger("tornado.application")
gen_log = logging.getLogger("tornado.general")

在啓動tornado前利用tornado.options.parse_command_line()來初始化一些配置。
tornado.options.parse_command_line() 會默認調用enable_pretty_logging()方法,對日誌輸出做一些格式化和rotate並輸出到日誌文件,可以通過tornado.options.define()來傳遞這些參數。

def enable_pretty_logging(options=None, logger=None):
    """Turns on formatted logging output as configured.

    This is called automatically by `tornado.options.parse_command_line`
    and `tornado.options.parse_config_file`.
    """
    if options is None:
        import tornado.options
        options = tornado.options.options
    if options.logging is None or options.logging.lower() == 'none':
        return
    if logger is None:
        logger = logging.getLogger()
    logger.setLevel(getattr(logging, options.logging.upper()))
    if options.log_file_prefix:
        rotate_mode = options.log_rotate_mode
        if rotate_mode == 'size':
            channel = logging.handlers.RotatingFileHandler(
                filename=options.log_file_prefix,
                maxBytes=options.log_file_max_size,
                backupCount=options.log_file_num_backups)
        elif rotate_mode == 'time':
            channel = logging.handlers.TimedRotatingFileHandler(
                filename=options.log_file_prefix,
                when=options.log_rotate_when,
                interval=options.log_rotate_interval,
                backupCount=options.log_file_num_backups)
        else:
            error_message = 'The value of log_rotate_mode option should be ' +\
                            '"size" or "time", not "%s".' % rotate_mode
            raise ValueError(error_message)
        channel.setFormatter(LogFormatter(color=False))
        logger.addHandler(channel)

    if (options.log_to_stderr or
            (options.log_to_stderr is None and not logger.handlers)):
        # Set up color if we are in a tty and curses is installed
        channel = logging.StreamHandler()
        channel.setFormatter(LogFormatter())
        logger.addHandler(channel)

如現在我們對tornado的日誌進行每間隔一天進行分割,並輸出到文件中。

    tornado.options.define('log_file_prefix', default='./logs/test.log')
    tornado.options.define('log_rotate_mode', default='time')
    tornado.options.define('log_rotate_when', default='M')
    tornado.options.define('log_rotate_interval', default=1)
    tornado.options.parse_command_line() 

更多參數可以參考源碼:

options.define("logging", default="info",
                   help=("Set the Python log level. If 'none', tornado won't touch the "
                         "logging configuration."),
                   metavar="debug|info|warning|error|none")
    options.define("log_to_stderr", type=bool, default=None,
                   help=("Send log output to stderr (colorized if possible). "
                         "By default use stderr if --log_file_prefix is not set and "
                         "no other logging is configured."))
    options.define("log_file_prefix", type=str, default=None, metavar="PATH",
                   help=("Path prefix for log files. "
                         "Note that if you are running multiple tornado processes, "
                         "log_file_prefix must be different for each of them (e.g. "
                         "include the port number)"))
    options.define("log_file_max_size", type=int, default=100 * 1000 * 1000,
                   help="max size of log files before rollover")
    options.define("log_file_num_backups", type=int, default=10,
                   help="number of log files to keep")

    options.define("log_rotate_when", type=str, default='midnight',
                   help=("specify the type of TimedRotatingFileHandler interval "
                         "other options:('S', 'M', 'H', 'D', 'W0'-'W6')"))
    options.define("log_rotate_interval", type=int, default=1,
                   help="The interval value of timed rotating")

    options.define("log_rotate_mode", type=str, default='size',
                   help="The mode of rotating files(time or size)")

對於access_logger,初始化後tornado會記錄每次請求的信息,該記錄log的操作是在handler中完成的。

在RequestHandler的finish()中會執行_log()方法,該方法會找到該handler所屬Applicationlog_request()方法:

    def log_request(self, handler):
        """Writes a completed HTTP request to the logs.

        By default writes to the python root logger.  To change
        this behavior either subclass Application and override this method,
        or pass a function in the application settings dictionary as
        ``log_function``.
        """
        if "log_function" in self.settings:
            self.settings["log_function"](handler)
            return
        if handler.get_status() < 400:
            log_method = access_log.info
        elif handler.get_status() < 500:
            log_method = access_log.warning
        else:
            log_method = access_log.error
        request_time = 1000.0 * handler.request.request_time()
        log_method("%d %s %.2fms", handler.get_status(),
                   handler._request_summary(), request_time)

很直觀的看出來在請求狀態碼不同時,輸出的日誌級別也不同,除了狀態碼,還記錄了請求時間,以及ip,uri和方法,若要簡單更改輸出的日誌格式,可以繼承RequestHandler並重寫_request_summary()方法。

    def _request_summary(self):
        return "%s %s (%s)" % (self.request.method, self.request.uri,
                               self.request.remote_ip)

ps: 也能夠通過配置文件對tornado日誌進行配置。


logging模塊是線程安全的,但多進程時會出現問題,多進程解決方法參考:
- Python多進程log日誌切分錯誤的解決方案
- python logging日誌模塊以及多進程日誌

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章