从 Flask 到 Gin —— Logging

文章目录

本文是 从 Flask 到 Gin 系列的第 2 篇。


MJP 项目中,我使用的是 Python 标准库中的 logging 模块。在 Flask 项目启动的时候,创建一个全局的 logger 对象,对其进行基本的设置。

在下面的代码中,_set_loggercreate_app 调用,初始化整个 MJP 系统的全局 logger 对象。这个 logger 对象与 Flask 中的 app.logger 是等同的。

Flask 中的全局 logger 实现

 1# package mjp.app
 2
 3import logging
 4from mjp import config
 5
 6# 就是 flas.app.logger https://flask.palletsprojects.com/en/1.1.x/logging/,放在这里不必引用 current_app
 7logger = logging.getLogger(__name__)
 8
 9def _set_logger(mjpapp):
10    """
11    设置 Flask app 的logger
12    """
13    # logger = mjpapp.logger
14    # 删除 Flask 的默认 Handler
15    del logger.handlers[:]
16    if mjpapp.config.get('DEBUG'):
17        hdr = logging.StreamHandler()
18        hdr.setLevel(logging.DEBUG)
19        logger.setLevel(logging.DEBUG)
20    else:
21        # logsdir 是一个 Path 实例
22        logsdir = config.getdir('logs')
23        # 创建或者设置 logs 文件夹的权限,让其他 user 也可以写入(例如nginx)
24        # 注意,要设置 777 权限,需要使用 0o40777 或者先设置 os.umask(0)
25        # 0o40777 是根据 os.stat() 获取到的 st_mode 得到的
26        if logsdir.exists():
27            logsdir.chmod(0o40777)
28        else:
29            logsdir.mkdir(mode=0o40777)
30        applog = logsdir.joinpath('app.log')
31        if not applog.exists():
32            applog.touch()
33        # python 3.6 的 FileHandler 才支持 Path 实例。因此这里要做处理
34        hdr = logging.FileHandler(str(applog.resolve()), encoding='utf8')
35        hdr.setLevel(logging.INFO)
36        logger.setLevel(logging.INFO)
37
38    LOG_FORMAT = """
39[%(asctime)s] %(levelname)s in %(module)s.%(funcName)s [%(pathname)s:%(lineno)d]:
40%(message)s"""
41    hdr.setFormatter(logging.Formatter(LOG_FORMAT))
42
43    for log in (logger, logging.getLogger('sqlalchemy')):
44        log.addHandler(hdr)
45
46def create_app(FlaskClass=MJPFlask, ResponseClass=MJPResponse, ConfigClass=FlaskConfig):
47    """
48    根据不同的配置创建 app
49    """
50    mjpapp = FlaskClass(__name__, static_url_path=config.getcfg('PATH', 'STATIC_URL_PATH'))
51    mjpapp.response_class = ResponseClass
52    mjpapp.config.from_object(ConfigClass(config.getcfg('FLASK')))
53    _set_logger(mjpapp)
54    return mjpapp

要使用这个全局 logger 对象,只需要进行下面的操作就可以了:

1
2from mjp.app import logger
3
4logger.info('Answer to Life, the Universe, and Everything: %s', 42)

我们看到,由于 Python 的动态特性和全局模块特性,这个使用方法是相当优雅和 Pythonic 的。

Golang 中的 log 包选择

在 Golang 实现中,我没有选择标准库中的 log 包,而是使用了 Logrus 这个 Golang 世界中被 Star 最多的 log 库,也是 docker 这个 Golang 明星项目使用的 logger 库。至于原因,看了下面的特性就清楚了 (via):

  1. 完全兼容golang标准库日志模块。logrus拥有六种日志级别:debug、info、warn、error、fatal和panic,这是golang标准库日志模块的API的超集。如果你的项目使用标准库日志模块,完全可以用最低的代价迁移到logrus上。
  2. 可扩展的Hook机制。允许使用者通过hook方式,将日志分发到任意地方,如本地文件系统、标准输出、logstash、elasticsearch或者mq等,或者通过hook定义日志内容和格式等。
  3. 可选的日志输出格式。logrus内置了两种日志格式,JSONFormatter和TextFormatter。 如果这两个格式不满足需求,可以自己动手实现接口Formatter,来定义自己的日志格式。
  4. Field机制。logrus鼓励通过Field机制进行精细化、结构化的日志记录,而不是通过冗长的消息来记录日志。
  5. logrus是一个可插拔的、结构化的日志框架。

可能是由于 Golang 标准库中的 log 包功能太弱,加上 Golang 社区的年轻活力,导致 Golang 世界中没有出现一个类似于 Java 世界的 log4j 这样一个具有统治力的库。这导致我们有大量优秀的 log 库可供选择,较真的话可能会挑花眼。有兴(Shi)趣(Jian)可以在 awesome-go 翻一下。

Logrus 已经帮我们处理的所有的事情,我要做的就是在项目中进行一下封装,把它做成一个单例:

 1package util
 2
 3import (
 4	"fmt"
 5	"os"
 6	"path"
 7	"sync"
 8
 9	"github.com/gin-gonic/gin"
10	"github.com/sirupsen/logrus"
11)
12
13func init() {
14	logsDir = GetDir("logs")
15	os.Mkdir(logsDir, os.ModePerm)
16}
17
18// Logger is a global logger
19type Logger struct {
20	filename string
21	*logrus.Logger
22}
23
24var logsDir string
25var appLogger *Logger
26var appLoggerOnce sync.Once
27
28// GetAppLogger will start a global logger use in app
29func GetAppLogger() *Logger {
30	appLoggerOnce.Do(func() {
31		appLogger = createLogger()
32	})
33	return appLogger
34}
35
36func makeFile() (*os.File, string) {
37    // 在调试状态的时候输出到 stdout
38	if gin.IsDebugging() {
39		return os.Stdout, "os.Stdout"
40    }
41    // 非调试状态的时候输出的库 app.log
42	appLogFile := path.Join(logsDir, "app.log")
43	file, err := os.OpenFile(appLogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
44	if err != nil {
45		panic(err)
46	}
47	return file, appLogFile
48}
49
50func createLogger() *Logger {
51	file, name := makeFile()
52	fmt.Printf("createLogger %s\n", name)
53	log := &logrus.Logger{
54		Out:       file,
55		Formatter: new(logrus.JSONFormatter),
56		Level:     logrus.DebugLevel,
57	}
58	return &Logger{name, log}
59}

在使用的时候,只需要这样处理:

1import "mjp/util"
2
3logger = util.GetAppLogger()
4logger.Infof("Answer to Life, the Universe, and Everything: %d", 42)

如果要频繁使用的话,可以把 logger 对象的赋值放到 init 中:

 1package middlewares
 2
 3import (
 4    "mjp/models"
 5	"mjp/util"
 6)
 7
 8var logger *util.Logger
 9
10func init() {
11	logger = util.GetAppLogger()
12}
13
14// 使用 mjstObj 中的数据获取一个 admin
15func getAdmin(mjstObj *app.MJST) *models.AdminModel {
16	admin := &models.AdminModel{}
17	adminFindErr := models.AdminDB.First(admin, mjstObj.Uid).Error
18	if adminFindErr != nil {
19		logger.Errorf("getAdmin Error %s", adminFindErr)
20		return nil
21	}
22	return admin
23}

阅读系列所有文章:从 Flask 到 Gin

全文完