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


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

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

Flask 中的全局 logger 实现

# package mjp.app

import logging
from mjp import config

# 就是 flas.app.logger https://flask.palletsprojects.com/en/1.1.x/logging/,放在这里不必引用 current_app
logger = logging.getLogger(__name__)

def _set_logger(mjpapp):
    """
    设置 Flask app 的logger
    """
    # logger = mjpapp.logger
    # 删除 Flask 的默认 Handler
    del logger.handlers[:]
    if mjpapp.config.get('DEBUG'):
        hdr = logging.StreamHandler()
        hdr.setLevel(logging.DEBUG)
        logger.setLevel(logging.DEBUG)
    else:
        # logsdir 是一个 Path 实例
        logsdir = config.getdir('logs')
        # 创建或者设置 logs 文件夹的权限,让其他 user 也可以写入(例如nginx)
        # 注意,要设置 777 权限,需要使用 0o40777 或者先设置 os.umask(0)
        # 0o40777 是根据 os.stat() 获取到的 st_mode 得到的
        if logsdir.exists():
            logsdir.chmod(0o40777)
        else:
            logsdir.mkdir(mode=0o40777)
        applog = logsdir.joinpath('app.log')
        if not applog.exists():
            applog.touch()
        # python 3.6 的 FileHandler 才支持 Path 实例。因此这里要做处理
        hdr = logging.FileHandler(str(applog.resolve()), encoding='utf8')
        hdr.setLevel(logging.INFO)
        logger.setLevel(logging.INFO)

    LOG_FORMAT = """
[%(asctime)s] %(levelname)s in %(module)s.%(funcName)s [%(pathname)s:%(lineno)d]:
%(message)s"""
    hdr.setFormatter(logging.Formatter(LOG_FORMAT))

    for log in (logger, logging.getLogger('sqlalchemy')):
        log.addHandler(hdr)

def create_app(FlaskClass=MJPFlask, ResponseClass=MJPResponse, ConfigClass=FlaskConfig):
    """
    根据不同的配置创建 app
    """
    mjpapp = FlaskClass(__name__, static_url_path=config.getcfg('PATH', 'STATIC_URL_PATH'))
    mjpapp.response_class = ResponseClass
    mjpapp.config.from_object(ConfigClass(config.getcfg('FLASK')))
    _set_logger(mjpapp)
    return mjpapp

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


from mjp.app import logger

logger.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 已经帮我们处理的所有的事情,我要做的就是在项目中进行一下封装,把它做成一个单例:

package util

import (
	"fmt"
	"os"
	"path"
	"sync"

	"github.com/gin-gonic/gin"
	"github.com/sirupsen/logrus"
)

func init() {
	logsDir = GetDir("logs")
	os.Mkdir(logsDir, os.ModePerm)
}

// Logger is a global logger
type Logger struct {
	filename string
	*logrus.Logger
}

var logsDir string
var appLogger *Logger
var appLoggerOnce sync.Once

// GetAppLogger will start a global logger use in app
func GetAppLogger() *Logger {
	appLoggerOnce.Do(func() {
		appLogger = createLogger()
	})
	return appLogger
}

func makeFile() (*os.File, string) {
    // 在调试状态的时候输出到 stdout
	if gin.IsDebugging() {
		return os.Stdout, "os.Stdout"
    }
    // 非调试状态的时候输出的库 app.log
	appLogFile := path.Join(logsDir, "app.log")
	file, err := os.OpenFile(appLogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
	if err != nil {
		panic(err)
	}
	return file, appLogFile
}

func createLogger() *Logger {
	file, name := makeFile()
	fmt.Printf("createLogger %s\n", name)
	log := &logrus.Logger{
		Out:       file,
		Formatter: new(logrus.JSONFormatter),
		Level:     logrus.DebugLevel,
	}
	return &Logger{name, log}
}

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

import "mjp/util"

logger = util.GetAppLogger()
logger.Infof("Answer to Life, the Universe, and Everything: %d", 42)

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

package middlewares

import (
    "mjp/models"
	"mjp/util"
)

var logger *util.Logger

func init() {
	logger = util.GetAppLogger()
}

// 使用 mjstObj 中的数据获取一个 admin
func getAdmin(mjstObj *app.MJST) *models.AdminModel {
	admin := &models.AdminModel{}
	adminFindErr := models.AdminDB.First(admin, mjstObj.Uid).Error
	if adminFindErr != nil {
		logger.Errorf("getAdmin Error %s", adminFindErr)
		return nil
	}
	return admin
}

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

全文完