手把手带你从0到1封装Gin框架:06 日志封装

576 阅读7分钟

项目源码

Github

在软件开发和系统运维中,日志是一种不可或缺的工具,它在故障排查、性能分析、问题追踪以及监控报警等方面发挥着重要作用。

log 标准库

golang官方提供的log标准库,支持基础的日志操作,不仅可以将信息输出到控制面板,还能输出到文件,并且可通过不同的日志级别来控制输出的详细程度。

基本使用
package main

import (
    "log"
)

func main() {
    log.Println("Hello World")
}

运行上述代码后,控制台会有如下输出:

2024/07/16 16:41:43 Hello World
日志写入文件

上边演示了日志的基本使用,但是正式开发中我们一般会把日志写入文件中:

package main

import (
    "log"
    "fmt"
)

func main() {
    // 创建一个用于记录日志的文件
    file, err := os.OpenFile("sys.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
    if err!= nil {
        fmt.Println(err)
        return
    }

    logger := log.New(file, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)

    logger.Println("hello world")
}

运行上述代码后,日志会被写入sys.log文件,并在日志前加上INFO: 前缀。

可以看出,log库使用起来非常简单,但也确实简单得有些过头了,多余的功能一点都没有。一般来说,分布式系统的日志都是以json格式存储的,这样便于日志被统一收录到ELK等日志分析平台。此外,还需要支持多种日志级别,以便于检索、报警,同时也能节省一些存储空间。

zap

zapuber 开源的一个高性能、结构化、分级记录的日志记录包。

安装
go get -u go.uber.org/zap
基本使用
package logger

import (
    "go.uber.org/zap"
)

func main() {
    logger, _ := zap.NewProduction()

    logger.Info("hello world", zap.String("name", "eve"),
        zap.Any("user", struct {
            Name string
            Age  int
        }{Name: "eve", Age: 18}))
}

运行上述代码后,控制台会输出如下内容:

{"level":"info","ts":1721121236.311571,"caller":"logger/logger.go:27","msg":"hello world","name":"eve","user":{"Name":"eve","Age":18}}

这正是我们想要的日志结构

封装到系统中

新增internal/logger/logger.go文件:

package logger

import (
	"go.uber.org/zap"
)

func New() (logger *zap.Logger, err error) {
	logger, err = zap.NewProduction()

	return
}

修改internal/global/global.go文件:

package global

import (
	"eve/internal/config"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

var (
	Config *config.Config
	Db     *gorm.DB
	Logger *zap.Logger
)

修改internal/bootstrap/init文件:

package bootstrap

import (
	"eve/internal/config"
	"eve/internal/global"
	"eve/internal/logger"
	"eve/internal/mysql"
	"fmt"
	"go.uber.org/zap"
)

func init() {
	var err error

	// 初始化配置文件
	global.Config = config.GetConfig()

	// 初始化数据库
	global.Db = mysql.GetConnection()

	// 初始化日志
	if global.Logger, err = logger.New(); err != nil {
		fmt.Println("初始化日志错误:", err)
		return
	}

	// 下边是测试代码,用完删除
	global.Logger.Info("log_init", zap.String("name", "eve"))
}

然后运行项目会发现控制台有日志输出:


running...
{"level":"info","ts":1721203124.044276,"caller":"bootstrap/init.go:28","msg":"log_init","name":"eve"}

...
日志记录到文件中

上边的日志是直接输出在控制台的,现在我们改为记录到文件中

修改config.yaml文件,新增logger对象:

logger:
  level: debug # 日志级别
  file_path: "/tmp/log/" # 日志存放路径
  file_name: "eve.log" # 日志文件名称
  max_size: 10 # 以M为单位对日志进行切割
  max_age: 30 # 保留旧文件最大天数
  max_backups: 5 # 保留旧文件最大份数
  compress: true # 是否压缩归档文件

新增internal/config/logger.go文件:

package config

type Logger struct {
	Level      string `mapstructure:"level"`
	FilePath   string `mapstructure:"file_path"`
	FileName   string `mapstructure:"file_name"`
	Format     string `mapstructure:"format"`
	MaxSize    int    `mapstructure:"max_size"`
	MaxAge     int    `mapstructure:"max_age"`
	MaxBackups int    `mapstructure:"max_backups"`
	Compress   bool   `mapstructure:"compress"`
}

修改internal/config/config.go文件中Config结构体:

...

// Config 配置文件集合
type Config struct {
	App      App
	Database Database
	Logger   Logger
}

...

修改internal/logger/logger.go文件:

package logger

import (
	"eve/internal/config"
	"eve/internal/global"
	"eve/internal/tool"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"os"
	"strings"
	"time"
)

var (
	options []zap.Option
	conf    config.Logger
)

func New() (logger *zap.Logger, err error) {
	// 获取配置文件
	conf = global.Config.Logger

	// 创建日志存放目录
	rootDir, _ := tool.GetRootDir()
	logDir := rootDir + conf.FilePath
	err = os.MkdirAll(logDir, os.ModePerm)
	if err != nil {
		return
	}

	// 打开日志文件
	file, err := os.OpenFile(logDir+conf.FileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
	if err != nil {
		return
	}

	loggerConf := genConfig()

	loggerConf.EncoderConfig = genEncodeConfig()

	core := zapcore.NewCore(
		zapcore.NewJSONEncoder(loggerConf.EncoderConfig),
		zapcore.AddSync(file),
		loggerConf.Level,
	)

	// 添加调用位置
	options = append(options, zap.AddCaller())

	logger = zap.New(core, options...)

	return
}

// 生成配置
func genConfig() (config zap.Config) {
	config = zap.NewProductionConfig()

	config.EncoderConfig = genEncodeConfig()
	config.Level = zap.NewAtomicLevelAt(getLevel())

	return

}

// 生成编码配置
func genEncodeConfig() (c zapcore.EncoderConfig) {
	c = zap.NewProductionEncoderConfig()

	c.EncodeTime = func(time time.Time, encoder zapcore.PrimitiveArrayEncoder) {
		encoder.AppendString(time.Format("2006-01-02 15:04:05.000"))
	}

	c.EncodeLevel = func(l zapcore.Level, encoder zapcore.PrimitiveArrayEncoder) {
		encoder.AppendString(strings.ToUpper(l.String()))
	}

	c.TimeKey = "time"

	return
}

// 配置文件的level转换为zapcore的level
func getLevel() (level zapcore.Level) {
	switch conf.Level {
	case "debug":
		level = zap.DebugLevel
	case "info":
		level = zap.InfoLevel
	case "warn":
		level = zap.WarnLevel
	case "error":
		level = zap.ErrorLevel
	case "dpanic":
		level = zap.DPanicLevel
	case "panic":
		level = zap.PanicLevel
	case "fatal":
		level = zap.FatalLevel
	default:
		level = zap.InfoLevel
	}

	return
}

修改完之后运行项目,发现日志已经写入了tmp/log/eve.log文件中了

日志归档切割

经过上边的改动,日志已经封装好了,但是我们发现日志是写在一个文件中,那在生产环境中一直往一个文件中写入日志,服务器磁盘肯定会被撑满,肯定是不行的,所以我们还需要可以做日志切割和自动归档的工具,zap当前是不支持的,这里我们使用lumberjack

安装
go get -u github.com/natefinch/lumberjack@v2
使用

官网可以看到,使用时实例化一个Logger结构体,它支持一些初始化的参数:

// Logger 结构体
type Logger struct {
	// Filename 字段用于指定日志文件的名称。如果为空,则默认为 os.TempDir() 目录下的 <processname>-lumberjack.log
	Filename string `json:"filename" yaml:"filename"`

	// MaxSize 字段指定日志文件的最大大小(以兆字节为单位)。如果达到这个大小,文件将被轮循。默认为 100 兆字节
	MaxSize int `json:"maxsize" yaml:"maxsize"`

	// MaxAge 字段指定保留旧日志文件的最大天数。根据文件名中编码的时间戳来判断。请注意,一天定义为 24 小时,并可能由于夏令时、闰秒等因素而与日历日不完全对应。默认为不根据年龄删除旧日志文件
	MaxAge int `json:"maxage" yaml:"maxage"`

	// MaxBackups 字段指定要保留的旧日志文件的最大数量。默认情况下,将保留所有旧日志文件(尽管 MaxAge 仍可能导致它们被删除)
	MaxBackups int `json:"maxbackups" yaml:"maxbackups"`

	// LocalTime 字段决定格式化备份文件时间戳时是否使用计算机的本地时间。默认是使用 UTC 时间
	LocalTime bool `json:"localtime" yaml:"localtime"`

	// Compress 字段决定是否使用 gzip 压缩轮转后的日志文件。默认情况下不执行压缩
	Compress bool `json:"compress" yaml:"compress"`
}

它是返回一个Writer,我们这里可以用它来替换上边用到的file,修改internal/logger/logger.go文件:

package logger

import (
	"eve/internal/config"
	"eve/internal/global"
	"eve/internal/tool"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"gopkg.in/natefinch/lumberjack.v2"
	"os"
	"strings"
	"time"
)

var (
	options []zap.Option
	conf    config.Logger
)

func New() (logger *zap.Logger, err error) {
	// 获取配置文件
	conf = global.Config.Logger

	// 创建日志存放目录
	rootDir, _ := tool.GetRootDir()
	logDir := rootDir + conf.FilePath
	err = os.MkdirAll(logDir, os.ModePerm)
	if err != nil {
		return
	}

	loggerConf := genConfig()

	loggerConf.EncoderConfig = genEncodeConfig()

	writer, err := genWriteSyncer()
	if err != nil {
		return nil, err
	}

	core := zapcore.NewCore(
		zapcore.NewJSONEncoder(loggerConf.EncoderConfig),
		writer,
		loggerConf.Level,
	)

	// 添加调用位置
	options = append(options, zap.AddCaller())

	logger = zap.New(core, options...)

	return
}

// 生成WriteSyncer
func genWriteSyncer() (writeSyncer zapcore.WriteSyncer, err error) {
	// 创建日志存放目录
	rootDir, _ := tool.GetRootDir()
	logDir := rootDir + conf.FilePath
	err = os.MkdirAll(logDir, os.ModePerm)
	if err != nil {
		return
	}

	lumberJack := &lumberjack.Logger{
		Filename:   logDir + conf.FileName,
		MaxSize:    conf.MaxSize, // megabytes
		MaxBackups: conf.MaxBackups,
		MaxAge:     conf.MaxAge, //days
	}

	writeSyncer = zapcore.AddSync(lumberJack)
	return
}

// 生成配置
func genConfig() (config zap.Config) {
	config = zap.NewProductionConfig()

	config.EncoderConfig = genEncodeConfig()
	config.Level = zap.NewAtomicLevelAt(getLevel())

	return

}

// 生成编码配置
func genEncodeConfig() (c zapcore.EncoderConfig) {
	c = zap.NewProductionEncoderConfig()

	c.EncodeTime = func(time time.Time, encoder zapcore.PrimitiveArrayEncoder) {
		encoder.AppendString(time.Format("2006-01-02 15:04:05.000"))
	}

	c.EncodeLevel = func(l zapcore.Level, encoder zapcore.PrimitiveArrayEncoder) {
		encoder.AppendString(strings.ToUpper(l.String()))
	}

	c.TimeKey = "time"

	return
}

// 配置文件的level转换为zapcore的level
func getLevel() (level zapcore.Level) {
	switch conf.Level {
	case "debug":
		level = zap.DebugLevel
	case "info":
		level = zap.InfoLevel
	case "warn":
		level = zap.WarnLevel
	case "error":
		level = zap.ErrorLevel
	case "dpanic":
		level = zap.DPanicLevel
	case "panic":
		level = zap.PanicLevel
	case "fatal":
		level = zap.FatalLevel
	default:
		level = zap.InfoLevel
	}

	return
}
测试

修改完之后我们测试一下,修改internal/bootstrap/init文件:

package bootstrap

import (
	"eve/internal/config"
	"eve/internal/global"
	"eve/internal/logger"
	"eve/internal/mysql"
	"go.uber.org/zap"
)

func init() {
	var err error

	// 初始化配置文件
	global.Config = config.GetConfig()

	// 初始化数据库
	global.Db = mysql.GetConnection()

	// 初始化日志
	if global.Logger, err = logger.New(); err != nil {
		panic(err)
		return
	}

	// 下边是测试代码,用完删除
	for {

		global.Logger.Info("info", zap.String("name", "eve"))
		global.Logger.Debug("debug", zap.String("name", "eve"))
		global.Logger.Error("error", zap.String("name", "eve"))
	}
	
}

运行代码一段时间之后可以看到tmp/log目录下:

➜  eve_api git:(main) ✗ ls -lh tmp/log/ 
total 115688
-rw-r--r--  1 lining  staff    10M Jul 18 10:38 eve-2024-07-18T02-38-06.278.log
-rw-r--r--  1 lining  staff    10M Jul 18 10:38 eve-2024-07-18T02-38-06.549.log
-rw-r--r--  1 lining  staff    10M Jul 18 10:38 eve-2024-07-18T02-38-06.820.log
-rw-r--r--  1 lining  staff    10M Jul 18 10:38 eve-2024-07-18T02-38-07.095.log
-rw-r--r--  1 lining  staff    10M Jul 18 10:38 eve-2024-07-18T02-38-07.361.log
-rw-r--r--  1 lining  staff   6.5M Jul 18 10:38 eve.log

可以看到下目录下有5个归档文件以及一个当前写入的文件,归档文件大小为配置文件所配置的10M

这样我们的日志就封装好了,业务中可以通过global直接调用使用

总结

  • 官方日志库简单使用
  • zap安装
  • zap初始化
  • 日志文件归档切割

commit-hash: 369a678