剛知道tornado原來是facebook開源的。
logging的簡單介紹和使用
python內置模塊logging,用於記錄日誌,格式化輸出。通過getLogger()
獲得的是單例且線程安全的(進程不安全),下文會簡單介紹logging的常用方法和簡單logging源碼分析,以及tornado中的日誌模塊。
使用basicConfig()
(給root handler添加基本配置),除了level
和filename
參數外還有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
的相對應的info
,debug
,waring
等可以輸出日誌,只有當方法級別>日誌級別時才能夠輸出(如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')
,再來看一下Manager
的getLogger()
。
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 ,通過loggerDict
和PlaceHodler
,將a,a.b,a.b.c類似的,子logger繼承父logger的level
,handler
等,最終繼承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_handler
和file_handler
,通過addHandler()
添加到了mine_logger
上,這樣mine_logger.info('info message')
既能輸出到文件又能打印在控制檯。
ps:可以利用logging.Formatter
來格式化輸出,但每個handler只能有一種類formatter。
高級handlers
在logging.handers
下面有很多封裝好的功能強大的handler。
RotatingFileHandler
:根據大小切割日誌。
TimedRotatingFileHandler
:根據時間切割日誌。
還有MemoryHandler
,SocketHandler
等等。
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所屬Application
的log_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日誌模塊以及多進程日誌