目录
【问题总结及代码】
参考一:参考点:logging基础内容了解
参考二:参考点:tornado.log 的 配置设置、日志自动分割、自定义格式化输出日志
参考三:参考点:自定义日志输出、日志分割
参考四:参考点:修改tornado.log 的 日志等级。 tornado.options
注意tornado中的日志级别, 一直改不了,才发现是 tornado 把 logging 设置的日志级别 就行了2次更改;
tornado原来是facebook开源的?
【问题】注意tornado中的日志级别, 一直改不了,才发现是 tornado 把 logging 设置的日志级别 就行了2次更改; 详见: 参考三
【问题】注意tornado中的日志, 一直改不了,才发现是 tornado 把 logging 设置的日志级别 就行了2次更改; 详见: 参考三
【采纳后,更新代码】:
import os
import tornado
import tornado.ioloop
import tornado.httpserver
import tornado.web
import tornado.log
import logging
from tornado.options import options, define
class Application(tornado.web.Application):
def __init__(self):
super(Application, self).__init__(handlers, **self.settings)
# 格式化日志输出格式
# 默认是这种的:[I 160807 09:27:17 web:1971] 200 GET / (::1) 7.00ms
# 格式化成这种的:[2016-08-07 09:38:01 执行文件名:执行函数名:执行行数 日志等级] 内容消息
class LogFormatter(tornado.log.LogFormatter):
def __init__(self):
super(LogFormatter, self).__init__(
fmt='%(color)s[%(asctime)s %(filename)s:%(funcName)s:%(lineno)d %(levelname)s]%(end_color)s %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
def main():
tornado.options.define("port", default="8888", help="run port", type=int) # 设置默认端口port
tornado.options.define("table", default=True, help="create table", type=bool) # 设置默认创建数据库表
print(tornado.options.options.logging, '打印出为 info ------------------')
tornado.options.options.logging = "error" # 设置日志登记
tornado.options.define('log_file_prefix', default='./logs/DMS.log') # 存储路径
tornado.options.define('log_rotate_mode', default='time') # 切割方式:按时间
tornado.options.define('log_rotate_when', default='D') # 切割单位:天
tornado.options.define('log_rotate_interval', default=1) # 间隔值:1天
print(tornado.options.options.logging, '打印出为 error -------------------')
tornado.options.parse_command_line() # 解析配置,应用前面的配置项
[i.setFormatter(LogFormatter()) for i in logging.getLogger().handlers] # 自定义格式化日志, 必须放在 配置解析 后
if options.table:
create_table.run() # 引入自定义函数,省略
http_server = tornado.httpserver.HTTPServer(Application())
http_server.listen(tornado.options.options.port) # 在这里应用之前的全局变量port
tornado.ioloop.IOLoop.current().start() # 启动监听
if __name__ == "__main__":
main()
日志输出样式:
这是一个提供日志功能的模块,它可以让你更敏捷的为你程序提供日志功能
一、常用日志记录场景及最佳解决方案:
日志记录方式 最佳记录日志方案
普通情况下,在控制台显示输出 print()
报告正常程序操作过程中发生的事件 logging.info()(或者更详细的logging.debug())
发出有关特定事件的警告 warnings.warn()或者logging.warning()
报告错误 弹出异常
在不引发异常的情况下报告错误 logging.error(), logging.exception()或者logging.critical()
二、日志等级:
logging模块定义了下表所示的日志级别,按事件严重程度由低到高排列(注意是全部大写!因为它们是常量。):
级别 级别数值 使用时机
DEBUG 10 详细信息,常用于调试。
INFO 20 程序正常运行过程中产生的一些信息。
WARNING 30 警告用户,虽然程序还在正常工作,但有可能发生错误。
ERROR 40 由于更严重的问题,程序已不能执行一些功能了。
CRITICAL 50 严重错误,程序已不能继续运行。
默认级别是WARNING,表示只有WARING和比WARNING更严重的事件才会被记录到日志内,低级别的信息会被忽略。因此,默认情况下,DEBUG和INFO会被忽略,WARING、ERROR和CRITICAL会被记录。
三、logging基本使用方法:
产生五种日志级别(WARING、ERROR和CRITICAL会直接输出()内的提示语至屏幕):
logging.info('info')
logging.debug('debug')
logging.warning('warning')
logging.error('error')
logging.critical('critical')
日志系统配置文件(定义日志一些规则):
logging.basicConfig(filename='test.log', level=logging.INFO, filemode='a', format='%(levelname)s:%(asctime)s:%(message)s')
filename:日志要保存至哪个文件中(定义了这个后日志将不再在屏幕上打印)
level:什么级别以上的日志需要保存
filemode:有"w"、"a"两种模式,同open一样,"a"追加,"w"覆盖
format:定义日志格式(后面提供日志元素表,建议通过":"将各种日志元素连接成合理的日志格式)
format定义格式时用的日志元素表:
日志元素 描述
%(asctime)s 日志产生的时间,默认格式为2003-07-08 16:49:45,896
%(created)f time.time()生成的日志创建时间戳
%(filename)s 生成日志的程序名
%(funcName)s 调用日志的函数名
%(levelname)s 日志级别 ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL')
%(levelno)s 日志级别对应的数值
%(lineno)d 日志所针对的代码行号(如果可用的话)
%(module)s 生成日志的模块名
%(msecs)d 日志生成时间的毫秒部分
%(message)s 具体的日志信息
%(name)s 日志调用者
%(pathname)s 生成日志的文件的完整路径
%(process)d 生成日志的进程ID(如果可用)
%(processName)s 进程名(如果可用)
%(thread)d 生成日志的线程ID(如果可用)
%(threadName)s 线程名(如果可用)
四、logging高级用法(让日志即能写入文件又能在屏幕打印):
包含关系(左边包含右边):
记录器<——处理器<——格式化器
import logging
"""
logging模块采用了模块化设计,主要包含四种组件:
Loggers:记录器,提供应用程序代码能直接使用的接口;
Handlers:处理器,将记录器产生的日志发送至目的地;
Filters:过滤器,提供更好的粒度控制,决定哪些日志会被输出;
Formatters:格式化器,设置日志内容的组成结构和消息字段。
"""
# 创建一个记录器loggers,并设置默认等级
logger = logging.getLogger('jack') # ‘jack’位置定义了日志调用者的名字
logger.setLevel(logging.INFO)
# 创建两个处理器handlers(一个发往日志文件、一个发往屏幕),并分别设置他们的日志等级
stream = logging.StreamHandler() # 发往屏幕
stream.setLevel(logging.DEBUG) # 定义什么样级别以上的日志发往屏幕
file = logging.FileHandler('test.log') # 发往日志文件(需指定文件名称)
file.setLevel(logging.ERROR) # 定义什么样级别以上的日志发往日志文件
# 分别为两个处理器handlers创建格式化器formatters(可以让其在屏幕和日志文件中以不同的格式输出)
formatter_stream = logging.Formatter('%(levelname)s:%(asctime)s:%(message)s')
formatter_file = logging.Formatter('%(name)s:%(levelname)s:%(asctime)s:%(message)s')
# 为各个处理器handlers设置相应的格式化器
stream.setFormatter(formatter_stream)
file.setFormatter(formatter_file)
# 将所有的处理器handler加入自定义的记录器logger内
logger.addHandler(stream)
logger.addHandler(file)
# 测试日志功能
logger.debug('debug')
logger.info('info')
logger.warning('warn')
logger.error('error')
logger.critical('critical')
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日志模块以及多进程日志
1.需求
将http访问记录,程序自定义日志输出到文件,按天分割,保留最近30天的日志。
2.使用示例
init_logging("%s/QYK.%s.%s.log" % (log_path, tornado.options.options.mode, tornado.options.options.port))
def init_logging(log_file):
# 使用TimedRotatingFileHandler处理器
file_handler = TimedRotatingFileHandler(log_file, when="d", interval=1, backupCount=30)
# 输出格式
log_formatter = logging.Formatter(
"%(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s] [%(lineno)d] %(message)s"
)
file_handler.setFormatter(log_formatter)
# 将处理器附加到根logger
root_logger = logging.getLogger()
root_logger.addHandler(file_handler)
logging.info('测试日志输出')
运行后日志文件内容:
tornado中会将logging的输出级别设置为info
3.http访问日志
tornado中http访问的日志是由access_log处理器完成的,access_log继承了根logger,因此,
access_log处理器也会将日志输出到示例中的日志文件中,如下所示:
import logging
import datetime
class Test1Handler(ApiHandler):
async def get(self, *args, **kwargs):
self.return_success_response()
return
运行后日志文件内容:
查看tornado源码tornado\web.py,可以看到access_log的输出格式:
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)
如何自定义http访问日志的输出格式呢?通过源码,可知有两种方法可以解决这个问题
1)我们可以在Application中自定义一个log_function即可
def log_func(handler):
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 %s (%s) %s %s %.2fms",
handler.get_status(), handler.request.method,
handler.request.uri, handler.request.remote_ip,
handler.request.headers["User-Agent"],
handler.request.arguments,
request_time)
class QYKApplication(tornado.web.Application):
def __init__(self, config, webpack_setting, _redis, session_class):
settings = dict()
settings["log_function"] = log_func
运行后日志文件内容:
在输出格式中,增加了访问url,ip,代理服务器,参数等信息
2)重写RequestHandler的_request_summary方法
def _request_summary(self):
return "%s %s (%s) %s\n%s" % (self.request.method, self.request.uri,
self.request.remote_ip, self.request.headers["User-Agent"],
self.request.arguments)
通过源码可以发现,tornado中也是大量使用了python中的logging模块来处理日志操作。
但tornado在处理日志的时候,特别是tornado.options.parse_command_line()时将根日志的级别设置为info,这点需要特别注意!
如果在tornado.options.parse_command_line()之前程序对logging的日志级别进行了设置,则很有可能会被tornado进行改写,这点需要特别注意。
通过下面的代码可以很容易就能看出tornado对跟logger的级别进行了调整:
import os
import ssl
import logging
from tornado.httpserver import HTTPServer
from tornado.web import Application, RequestHandler
from tornado.ioloop import IOLoop
import tornado.options
from tornado.options import define, options
LOG = logging.getLogger(__name__)
define("port", default=8000, help="run on the given port", type=int)
class TestHandler(RequestHandler):
def get(self):
self.write("Hello, World!\n")
application = Application([
(r"/", TestHandler),],
debug = True)
if __name__ == "__main__":
print "main 1 getEffectiveLevel = ", LOG.getEffectiveLevel()
print "tornado default log level = "tornado.options.options.logging
tornado.options.options.logging = "debug"
tornado.options.parse_command_line()
print "main getEffectiveLevel = ", LOG.getEffectiveLevel()
# server = HTTPServer(application,ssl_options={
# "certfile": os.path.join(os.path.abspath("."), "cert.pem"),
# "keyfile": os.path.join(os.path.abspath("."), "privatekey.pem"),
# })
server = HTTPServer(applicatio)
server.listen(options.port)
IOLoop.instance().start()
对根logger的日志级别进行更改主要是tornado的tornado.options所为,源码如下:
def parse_command_line(self, args=None):
if args is None:
args = sys.argv
remaining = []
for i in xrange(1, len(args)):
# All things after the last option are command line arguments
if not args[i].startswith("-"):
remaining = args[i:]
break
if args[i] == "--":
remaining = args[i + 1:]
break
arg = args[i].lstrip("-")
name, equals, value = arg.partition("=")
name = name.replace('-', '_')
if not name in self:
print_help()
raise Error('Unrecognized command line option: %r' % name)
option = self[name]
if not equals:
if option.type == bool:
value = "true"
else:
raise Error('Option %r requires a value' % name)
option.parse(value)
if self.help:
print_help()
sys.exit(0)
# Set up log level and pretty console logging by default
if self.logging != 'none':
logging.getLogger().setLevel(getattr(logging, self.logging.upper()))
enable_pretty_logging()
return remaining
# Default options
define("help", type=bool, help="show this help information")
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")
可以看到,tornado.options在parse_command_line的时候,会判断self.logging 是否为none,非none时执行logging.getLogger().setLevel(getattr(logging, self.logging.upper())),也就是对根logger的level进行设置,在tornado.options被import的时候定义了一个logging,parse_command_line的时候将logging的根级别设置为info。
总之,在tornado应用中,应该特别注意logging级别设置同tornado.options.parse_command_line的先后顺序。