背景
记录日志,在任何项目中,都是很重要的。在Flask项目中,即有Flask提供的logger可以用来记录log,也可以通过直接使用Python的logging模块自定义logger来记录。那么这两者是什么关系,又该怎么使用呢?
思路
Python的logging模块先看下对于
logging模块的官方介绍Loggers have the following attributes and methods. Note that Loggers are never instantiated directly, but always through the module-level function
logging.getLogger(name). Multiple calls togetLogger()with the same name will always return a reference to the same Logger object.The
nameis potentially a period-separated hierarchical value, likefoo.bar.baz(though it could also be just plainfoo, for example). Loggers that are further down in the hierarchical list are children of loggers higher up in the list. For example, given a logger with a name offoo, loggers with names offoo.bar,foo.bar.baz, andfoo.bamare all descendants offoo. The logger name hierarchy is analogous to the Python package hierarchy, and identical to it if you organise your loggers on a per-module basis using the recommended constructionlogging.getLogger(__name__). That’s because in a module,__name__is the module’s name in the Python package namespace.https://docs.python.org/3/library/logging.html#logger-objects
上面主要告诉我们两点,
- 可以通过
logging.getLogger(name)来获取一个logger,相同名字的logger,其实是同一个logger。 logger是通过name进行继承的,比如foo.bar就是foo的子logger。就可以是实现我们通过配置一个rootLogger,然后直接使用rootLogger.sublogger来记录一下内容,而不需要单独再配置一遍。- 当使用
logging.getLogger(__name__)时,__name__就是这个模块所在的python package的namespace。
- 可以通过
flask提供的logger
再看下flask中的logging模块:
Flask uses standard Python
logging. All Flask-related messages are logged under the'flask'logger namespace.Flask.loggerreturns the logger named'flask.app', and can be used to log messages for your application.Depending on the situation, an extension may choose to log to
app.loggeror its own named logger. Consult each extension’s documentation for details.http://flask.pocoo.org/docs/1.0/logging/
我们可以知道flask的logger就是一个标准的Python logging,它的命名是
flask。我们既可以使用app.logger,也可以自己定义一个logger。那么如何使用
app.logger呢?有两种方式:
直接调用
1
2logger = logging.getLogger('flask.app')
logger.info('flask.app')使用
Flask提供的接口1
2from flask import current_app
current_app.logger.info('logged by current_app from main')
这里推荐还是使用第二种,
current_app是一个单例,可以直接引用到app.logger。通过修改
app.logger的name,可以实现子logger的继承么?答案是否定的。
修改
app.logger的name:1
2# app/__init__.py
app.logger.name = 'app'然后在子模块中定义一个
app.module的logger来记录:1
2
3
4
5
6
7
8
9from flask import current_app
import logging
logger = logging.getLogger('app.module')
def test():
logger.info('logged by app.module')
current_app.logger.info('logged by current_app.logger')输出结果:
1
2019-02-01 10:56:01,877 - Thread-2 - app - INFO - logged by current_app.logger
只有
current_app.logger的输出。修改
app.logger的name是不是无效呢?我们把子模块中的
logger的name修改为flask.app.module:1
2
3
4
5
6
7
8
9from flask import current_app
import logging
logger = logging.getLogger('flask.app.module')
def test():
logger.info('logged by flask.app.module')
current_app.logger.info('logged by current_app.logger')输出结果:
1
22019-02-01 11:00:10,944 - Thread-2 - flask.app.module - INFO - logged by flask.app.module
2019-02-01 11:00:10,946 - Thread-2 - app - INFO - logged by current_app.logger两个
logger均输出了。
可见,通过修改
app.logger.name可以在记录的时候显示为我们设置的名称,但实际上这个logger还是flask.app。__name__的使用在自定义
logger的情况下,为了方便起见,我们可以利用__name__这个参数。前面说到:当使用
logging.getLogger(__name__)时,__name__就是这个模块所在的python package的namespace。一般
Flask的工厂模式结构如下:1
2
3
4
5
6
7
8
9
10app
├── __init__.py
├── main
│ ├── __init__.py
│ ├── functions.py
│ └── views.py
└── module
├── __init__.py
├── functions.py
└── views.py那么我们在先在
app.__init__中定义rootLogger,然后再在app.module.functions.py中定义子Logger,均使用logging.getLogger(__name__):1
2
3
4
5
6
7
8
9
10
11
12
13
14# app.__init__.py 初始化rootlogger
rootLogger = logging.getLogger(__name__)
rootLogger.setLevel(logging.DEBUG)
socketHandler = logging.handlers.SocketHandler('localhost',logging.handlers.DEFAULT_TCP_LOGGING_PORT)
rootLogger.addHandler(socketHandler)
rootLogger.setLevel(logging.DEBUG)
# app.module.functions.py
import logging
logger = logging.getLogger(__name__)
def record_from_logging():
logger.info('logged by logging from __name__')输出:
1
22019-02-01 12:18:34,743 - MainThread - app - INFO - register root logger by __name__
2019-02-01 12:19:24,954 - Thread-4 - app.module.functions - INFO - logged by logging from __name__可以发现输出的
logger.name就是所在的文件目录,logger之间的继承关系与整个程序包保持一致。
总结
根据上面分析,那么怎么优雅的记录logger呢?
如果没有对模块进行分
logger记录要求的话。可以直接使用在程序初始化的时候配置app.logger(可以自行设置logger.name)。在模块中通过import current_app来记录:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18# app.__init__.py
def register_logging(app):
app.logger.name = 'app'
# logstash_handler
stashHandler = logstash.LogstashHandler('app.config.get('ELK_HOST')', 'app.config.get('ELK_PORT')')
app.logger.addHandler(stashHandler)
# socket_handler
socketHandler = logging.handlers.SocketHandler('localhost', logging.handlers.DEFAULT_TCP_LOGGING_PORT)
app.logger.addHandler(socketHandler)
# app.module.function.py
from flask import current_app
def test():
current_app.logger.info('logging someting')
return 'logged by current_app.logger'输出效果:
1
22019-02-01 13:49:28,998 - Thread-2 - app - INFO - logged by current_app from main
2019-02-01 13:49:38,346 - Thread-3 - app - INFO - logged by current_app of functions注意: 对于
current_app.logger的引用不能通过如下方式,会有RuntimeError的报错。1
2
3
4
5
6
7
8
9
10
11
12from flask import current_app
logger = current_app.logger
## 异常
raise RuntimeError(_app_ctx_err_msg)
RuntimeError: Working outside of application context.
This typically means that you attempted to use functionality that needed
to interface with the current application object in some way. To solve
this, set up an application context with app.app_context(). See the
documentation for more information.如果希望按自己的实际需求,对模块进行分
logger记录要求的话。那么建议自己设置logger。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22# app.__init__.py
def register_logging():
# set own root logger
rootLogger = logging.getLogger(__name__)
rootLogger.setLevel(logging.DEBUG)
# socketHandler
socketHandler = logging.handlers.SocketHandler('localhost',logging.handlers.DEFAULT_TCP_LOGGING_PORT)
rootLogger.addHandler(socketHandler)
# logstash_handler
stashHandler = logstash.LogstashHandler('app.config.get('ELK_HOST')', 'app.config.get('ELK_PORT')')
rootLogger.addHandler(stashHandler)
rootLogger.setLevel(logging.DEBUG)
# app.module.function.py
import logging
logger = logging.getLogger(__name__)
def test():
logger.info('logging someting')
return 'logged by logging module'输出效果:
1
22019-02-01 13:49:49,297 - Thread-5 - app.module.views - INFO - logged by flask.app.module
2019-02-01 13:50:01,013 - Thread-7 - app.module.functions - INFO - logged by logging module of functions
完整代码可参考:https://github.com/keejo125/flask_logging_demo
注意
关于python中logging的配置可参考官网:
https://docs.python.org/3/library/logging.config.html?highlight=logging
在配置handler时,经常会希望日志可以按时间分割(TimedRotatingFileHandler)或者按大小分割(RotatingFileHandler).
但是在flask项目中,尤其开启多线程之后,在分割日志(doRollover())时会有文件读写的异常:
1 | WindowsError: [Error 32] |
建议使用SocketHandler,将日志发送给单独的LogServer来进行二次处理。
简易的接收socketlog的LogServer可参考:https://github.com/keejo125/flask_logging_demo/blob/master/LogServer.py
或者现在流行的stashHandler,将日志发送给ELK来进行二次处理。