Skip to content

日志配置 原创

1、说明

日志,用于记录整个业务系统的运行情况,比如:

  • HTTP 请求日志
  • SQL 日志
  • 错误日志

一个好的应用,都会有自己的日志记录以及保存方式。

本节课,我们使用 zap 来构建自己的日志库。

2、安装依赖

yaml
$ go get go.uber.org/zap
$ go get gopkg.in/natefinch/lumberjack.v2

3、开发组件

3.1、日志分级

我们将日志分为 6 个等级,如下:

  • debug:信息量大,可以记录非常详细的运行日志
  • info:业务级别的运行日志
  • warn:需要引起关注的信息,也称为警告信息
  • error:错误信息,需要引起重视
  • panic:记录异常日志,并抛出异常
  • fatal:致命错误,会导致应用程序退出

pkg/logger目录中创建logger.go文件,写入以下内容:

go
package logger

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

// 定义日志级别
type Level = zapcore.Level

const (
	DebugLevel Level = zap.DebugLevel
	InfoLevel  Level = zap.InfoLevel
	WarnLevel  Level = zap.WarnLevel
	ErrorLevel Level = zap.ErrorLevel
	PanicLevel Level = zap.PanicLevel
	FatalLevel Level = zap.FatalLevel
)

func WithLevel(level string) Level {
	switch level {
	case "debug":
		return DebugLevel
	case "info":
		return InfoLevel
	case "warn":
		return WarnLevel
	case "error":
		return ErrorLevel
	case "panic":
		return PanicLevel
	case "fatal":
		return PanicLevel
	default:
		return InfoLevel
	}
}

在上述代码中,预定义了应用日志的 Level 的具体类型,并且把日志分为 debug、info、warn、error、fatal 和 panic 六个等级,以便在不同的使用场景中记录不同级别的日志。

3.2、日志配置

首先,在 configs/config.yaml 中增加如下配置:

yaml
App:
  LogLevel: debug
  LogType: single
  LogFileName: storage/logs/app.log
  LogMaxSize: 64
  LogMaxBackup: 5
  LogMaxAge: 30
  LogCompress: false

然后,在 pkg/setting/section.go 中,添加日志对应的结构体,如下:

go
......
type AppSettingS struct {
	LogLevel     string
	LogType      string
	LogFileName  string
	LogMaxSize   int
	LogMaxBackup int
	LogMaxAge    int
	LogCompress  bool
}
......

再到 global/setting.go 中添加全局变量,如下:

go
package global

import "github.com/joker-bai/hawkeye/pkg/setting"

var (
	ServerSetting   *setting.ServerSettingS
	DatabaseSetting *setting.DatabaseSettingS
	AppSetting      *setting.AppSettingS
)

再者,到 initialize/setting.go 中把配置和结构体关联,如下:

go
func SetupSetting() error {
	......
	// 加载应用配置
	if err := s.ReadSection("App", &global.AppSetting); err != nil {
		return err
	}
	......
}

到此,日志配置相关操作完成。

3.2、日志初始化

在完成了日志的分级方法后,开始编写具体的方法,用来对日志的实例初始化和标准化参数进行绑定。

继续在 pkg/logger/logger.go 中写入日志初始化的内容:

go
type Logger struct {
	logger *zap.Logger
	level  Level
}

// RotateOptions 日志切割结构体
type RotateOptions struct {
	FileName   string
	MaxSize    int
	MaxAge     int
	MaxBackups int
	Compress   bool
}

// 其他选项
type Option = zap.Option

var (
	AddCaller     = zap.AddCaller
	AddStacktrace = zap.AddStacktrace
	AddCallerSkip = zap.AddCallerSkip
)

func NewLogger(writer io.Writer, level Level, ropt RotateOptions, options ...Option) *Logger {
	// 获取日志写入介质
	if writer == nil {
		panic("writer is nil")
	}
	cfg := zap.NewProductionConfig()

	// 处理日志输出时间
	cfg.EncoderConfig.EncodeTime = func(t time.Time, pae zapcore.PrimitiveArrayEncoder) {
		pae.AppendString(t.Format("2006-01-02T15:04:05"))
	}

	// 指定日志记录方式
	// 这里采取的是终端和文件两种输出方式
	writeSync := zapcore.NewMultiWriteSyncer(
		zapcore.AddSync(os.Stdout),
		zapcore.AddSync(&lumberjack.Logger{
			Filename:   ropt.FileName,
			MaxSize:    ropt.MaxSize,
			MaxBackups: ropt.MaxBackups,
			MaxAge:     ropt.MaxAge,
			Compress:   ropt.Compress,
		}),
	)

	core := zapcore.NewCore(
		zapcore.NewJSONEncoder(cfg.EncoderConfig),
		zapcore.AddSync(writeSync),
		zapcore.Level(level),
	)
	return &Logger{
		logger: zap.New(core, options...),
		level:  level,
	}
}

func (l *Logger) Sync() error {
	return l.logger.Sync()
}

然后,在 global 包中创建 logger.go 文件,声明全局的日志对象,如下:

go
package global

import "github.com/joker-bai/hawkeye/pkg/logger"

var (
	Log *logger.Logger
)

再到 initialize 包中创建 logger.go 文件,写入以下内容:

go
package initialize

import (
	"os"

	"github.com/joker-bai/kubemana/global"
	"github.com/joker-bai/kubemana/pkg/logger"
)

func SetupLogger() error {
	file, err := os.OpenFile(global.AppSetting.LogFileName, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
	if err != nil {
		return err
	}
	level := logger.WithLevel(global.AppSetting.LogLevel)
	options := []logger.Option{
		logger.AddCaller(),
		logger.AddCallerSkip(1),
		logger.AddStacktrace(logger.ErrorLevel),
	}
	global.Logger = logger.NewLogger(file, level, logger.RotateOptions{
		MaxSize:    global.AppSetting.LogMaxSize,
		MaxBackups: global.AppSetting.LogMaxBackup,
		MaxAge:     global.AppSetting.LogMaxAge,
		Compress:   global.AppSetting.LogCompress,
		FileName:   global.AppSetting.LogFileName,
	}, options...)
	defer global.Logger.Sync()
	return nil
}

最后,再将日志初始化的操作加入到 main.go 文件中,使之在服务启动过后能正常使用。

go
func init() {
	// 初始化配置
	if err := initialize.SetupSetting(); err != nil {
		log.Fatalf("Failed to init setting, error: %s", err)
	}

	// 初始化日志
	if err := initialize.SetupLogger(); err != nil {
		log.Fatalf("Failed to init logger, error: %s", err)
	}

	// 初始化数据库
	if err := initialize.SetupDB(); err != nil {
		log.Fatalf("Failed to init database, error: %s", err)
	}
}

到此,日志初始化操作完成。

3.3、添加日志操作方法

日志操作方法主要是对 zap 的封装,方法我们调用。

pkg/logger/logger.go 中写入如下代码:

go
// Dump 调试专用,不会中断程序,会在终端打印出 warning 消息。
// 第一个参数会使用 json.Marshal 进行渲染,第二个参数消息(可选)
//
//	logger.Dump(user.User{Name:"test"})
//	logger.Dump(user.User{Name:"test"}, "用户信息")
func (l *Logger) Dump(value interface{}, msg ...string) {
	valueString := l.jsonString(value)
	// 判断第二个参数是否传参 msg
	if len(msg) > 0 {
		l.logger.Warn("Dump", zap.String(msg[0], valueString))
	} else {
		l.logger.Warn("Dump", zap.String("data", valueString))
	}
}

// Debug 调试日志,详尽的程序日志
// 调用示例:
//
//	logger.Debug("Database", zap.String("sql", sql))
func (l *Logger) Debug(msg string, fields ...zap.Field) {
	l.logger.Debug(msg, fields...)
}

// Info 告知类日志
func (l *Logger) Info(msg string, fields ...zap.Field) {
	l.logger.Info(msg, fields...)
}

// Warn 警告类
func (l *Logger) Warn(msg string, fields ...zap.Field) {
	l.logger.Warn(msg, fields...)
}

// Error 错误时记录,不应该中断程序,查看日志时重点关注
func (l *Logger) Error(msg string, fields ...zap.Field) {
	l.logger.Error(msg, fields...)
}

func (l *Logger) Panic(msg string, fields ...zap.Field) {
	l.logger.Panic(msg, fields...)
}

func (l *Logger) Fatal(msg string, fields ...zap.Field) {
	l.logger.Fatal(msg, fields...)
}

func (l *Logger) jsonString(value interface{}) string {
	b, err := json.Marshal(value)
	if err != nil {
		l.logger.Error("Logger", zap.String("JSON marshal error", err.Error()))
	}
	return string(b)
}

3.4、测试一下

现在在 main.go 中添加一行日志打印记录,如下:

go
func main() {

	// 初始化
	engine := initialize.NewEngine()
	server := &http.Server{
		Addr:         ":" + global.ServerSetting.Port,
		WriteTimeout: time.Second * global.ServerSetting.WriteTimeout,
		ReadTimeout:  time.Second * global.ServerSetting.ReadTimeout,
		IdleTimeout:  time.Second * global.ServerSetting.IdleTimeout,
		Handler:      engine,
	}

	global.Log.Info("server start", zap.String("addr", server.Addr))

	// 运行服务
	if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
		log.Fatalf("Failed to start http server, error: %s", err)
	}
}

然后使用 go run main.go 启动服务,控制台输出如下:

ecaf5ccf23fa9b410e4fc8a215cccea0 MD5

然后日志文件也正常生成,记录如下:

c2990c8a36b6867d91cd8c040a27e813 MD5

3.5、添加.gitignore 文件

````
go
*
!.gitignore

3.6、代码版本

本节功能开发完成后,记得给代码标记版本:

go
$ git add .
$ git commit -m "日志配置"
最近更新