(七)集成zap
- 分类
- 未分类
文章目录 6 节
开始前的准备
$ go get go.uber.org/zap
$ go get gopkg.in/natefinch/lumberjack.v2
这一部分内容我们需要上面两个包的内容,zap是一个功能强大而且完善的日志包,lumberjack则是一个日志分割的包。
这一部分的内容由于较到了较为全面的个性化配置,所以新内容会比较多,难度上会稍大,但是慢慢来,我们尽可能地梳理这里面的内容:smirk:
开始编码
配置文件
配置的方法与前面的方法类似
config/log.go
func init() {
config.Add("log", func() map[string]interface{} {
return map[string]interface{}{
// 日志级别,必须是以下这些选项:
// "debug" —— 信息量大,一般调试时打开。系统模块详细运行的日志,例如 HTTP 请求、数据库请求、发送邮件、发送短信
// "info" —— 业务级别的运行日志,如用户登录、用户退出、订单撤销。
// "warn" —— 感兴趣、需要引起关注的信息。 例如,调试时候打印调试信息(命令行输出会有高亮)。
// "error" —— 记录错误信息。Panic 或者 Error。如数据库连接错误、HTTP 端口被占用等。一般生产环境使用的等级。
// 以上级别从低到高,level 值设置的级别越高,记录到日志的信息就越少
// 开发时推荐使用 "debug" 或者 "info" ,生产环境下使用 "error"
"level": config.Env("LOG_LEVEL", "debug"),
// 日志的类型,可选:
// "single" 独立的文件
// "daily" 按照日期每日一个
"type": config.Env("LOG_TYPE", "single"),
/* ------------------ 滚动日志配置 ------------------ */
// 日志文件路径
"filename": config.Env("LOG_NAME", "storage/logs/logs.log"),
// 每个日志文件保存的最大尺寸 单位:M
"max_size": config.Env("LOG_MAX_SIZE", 64),
// 最多保存日志文件数,0 为不限,MaxAge 到了还是会删
"max_backup": config.Env("LOG_MAX_BACKUP", 5),
// 最多保存多少天,7 表示一周前的日志会被删除,0 表示不删
"max_age": config.Env("LOG_MAX_AGE", 30),
// 是否压缩,压缩日志不方便查看,我们设置为 false(压缩可节省空间)
"compress": config.Env("LOG_COMPRESS", false),
}
})
}
.env
.
.
.
LOG_TYPE=daily
LOG_LEVEL=debug
zap工具包
pkg/app/app.go
func IsLocal() bool {
return config.Get("app.env") == "local"
}
func IsProduction() bool {
return config.Get("app.env") == "production"
}
func IsTesting() bool {
return config.Get("app.env") == "testing"
}
区别本地和生成环境
helpers/helpers.go
func MicrosecondsStr(elapsed time.Duration) string {
return fmt.Sprintf("%.3fms", float64(elapsed.Nanoseconds())/1e6)
}
把时间转换成纳秒,可以对程序的运行时间有比较精细的查看
pkg/logger/logger.go
package logger
import (
"encoding/json"
"fmt"
"go-api-practice/pkg/app"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
"os"
"strings"
"time"
)
var Logger *zap.Logger
func InitLogger(filename string, maxSize, maxBackups, maxAge int, compress bool, logType string, level string) {
writeSyncer := getLoggerWriter(filename, maxSize, maxBackups, maxAge, compress, logType)
logLevel := new(zapcore.Level)
if err := logLevel.UnmarshalText([]byte(level)); err != nil {
fmt.Println("日志级别设置错误")
}
core := zapcore.NewCore(getEncoder(), writeSyncer, logLevel)
Logger = zap.New(core,
zap.AddCaller(),
zap.AddCallerSkip(1),
zap.AddStacktrace(zap.ErrorLevel))
zap.ReplaceGlobals(Logger)
}
func getEncoder() zapcore.Encoder {
encoderConfig := zapcore.EncoderConfig{
TimeKey: "time",
LevelKey: "level",
NameKey: "logger",
CallerKey: "caller",
FunctionKey: zapcore.OmitKey,
MessageKey: "message",
StacktraceKey: "stacktrace",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.CapitalLevelEncoder,
EncodeTime: customTimeEncoder,
EncodeDuration: zapcore.SecondsDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
}
if app.IsLocal() {
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
return zapcore.NewConsoleEncoder(encoderConfig)
}
return zapcore.NewJSONEncoder(encoderConfig)
}
func customTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString(t.Format("2006-01-02 15:04:05"))
}
func getLoggerWriter(filename string, maxSize, maxBackups, maxAge int, compress bool, logType string) zapcore.WriteSyncer {
if logType == "daily" {
logname := time.Now().Format("2006-01-02.log")
filename = strings.ReplaceAll(filename, "logs.log", logname)
}
lumberJackLogger := &lumberjack.Logger{
Filename: filename,
MaxSize: maxSize,
MaxBackups: maxBackups,
MaxAge: maxAge,
Compress: compress,
}
if app.IsLocal() {
return zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(lumberJackLogger))
} else {
return zapcore.AddSync(lumberJackLogger)
}
}
// Dump 调试专用,不会中断程序,会在终端打印出 warning 消息。
// 第一个参数会使用 json.Marshal 进行渲染,第二个参数消息(可选)
// logger.Dump(user.User{Name:"test"})
// logger.Dump(user.User{Name:"test"}, "用户信息")
func Dump(value interface{}, msg ...string) {
valueString := jsonString(value)
// 判断第二个参数是否传参 msg
if len(msg) > 0 {
Logger.Warn("Dump", zap.String(msg[0], valueString))
} else {
Logger.Warn("Dump", zap.String("data", valueString))
}
}
// LogIf 当 err != nil 时记录 error 等级的日志
func LogIf(err error) {
if err != nil {
Logger.Error("Error Occurred:", zap.Error(err))
}
}
// LogWarnIf 当 err != nil 时记录 warning 等级的日志
func LogWarnIf(err error) {
if err != nil {
Logger.Warn("Error Occurred:", zap.Error(err))
}
}
// LogInfoIf 当 err != nil 时记录 info 等级的日志
func LogInfoIf(err error) {
if err != nil {
Logger.Info("Error Occurred:", zap.Error(err))
}
}
// Debug 调试日志,详尽的程序日志
// 调用示例:
// logger.Debug("Database", zap.String("sql", sql))
func Debug(moduleName string, fields ...zap.Field) {
Logger.Debug(moduleName, fields...)
}
// Info 告知类日志
func Info(moduleName string, fields ...zap.Field) {
Logger.Info(moduleName, fields...)
}
// Warn 警告类
func Warn(moduleName string, fields ...zap.Field) {
Logger.Warn(moduleName, fields...)
}
// Error 错误时记录,不应该中断程序,查看日志时重点关注
func Error(moduleName string, fields ...zap.Field) {
Logger.Error(moduleName, fields...)
}
// Fatal 级别同 Error(), 写完 log 后调用 os.Exit(1) 退出程序
func Fatal(moduleName string, fields ...zap.Field) {
Logger.Fatal(moduleName, fields...)
}
// DebugString 记录一条字符串类型的 debug 日志,调用示例:
// logger.DebugString("SMS", "短信内容", string(result.RawResponse))
func DebugString(moduleName, name, msg string) {
Logger.Debug(moduleName, zap.String(name, msg))
}
func InfoString(moduleName, name, msg string) {
Logger.Info(moduleName, zap.String(name, msg))
}
func WarnString(moduleName, name, msg string) {
Logger.Warn(moduleName, zap.String(name, msg))
}
func ErrorString(moduleName, name, msg string) {
Logger.Error(moduleName, zap.String(name, msg))
}
func FatalString(moduleName, name, msg string) {
Logger.Fatal(moduleName, zap.String(name, msg))
}
// DebugJSON 记录对象类型的 debug 日志,使用 json.Marshal 进行编码。调用示例:
// logger.DebugJSON("Auth", "读取登录用户", auth.CurrentUser())
func DebugJSON(moduleName, name string, value interface{}) {
Logger.Debug(moduleName, zap.String(name, jsonString(value)))
}
func InfoJSON(moduleName, name string, value interface{}) {
Logger.Info(moduleName, zap.String(name, jsonString(value)))
}
func WarnJSON(moduleName, name string, value interface{}) {
Logger.Warn(moduleName, zap.String(name, jsonString(value)))
}
func ErrorJSON(moduleName, name string, value interface{}) {
Logger.Error(moduleName, zap.String(name, jsonString(value)))
}
func FatalJSON(moduleName, name string, value interface{}) {
Logger.Fatal(moduleName, zap.String(name, jsonString(value)))
}
func jsonString(value interface{}) string {
b, err := json.Marshal(value)
if err != nil {
Logger.Error("Logger", zap.String("JSON marshal error", err.Error()))
}
return string(b)
}
代码分成两部分,第一部分是zap的初始化,后一部分是对zap方法的封装来用于多种场景
zap的初始化
内容有点多:innocent:,慢慢解析initLogger()
函数参数
filename:日志文件生成的位置
maxSize:每个日志文件的最大大小
maxBackups:保存日志文件的最大数量
maxAge:保存每个日志文件的最长时间
compress:是否压缩日志文件,会变成压缩文件
logType:有"daily"和"single"两个选项,"daily"表示每天生成一个日志文件,"single"表示全部日志集中在一个文件中
level:打印的日志文件级别
getLoggerWriter
if logType == "daily" {
logname := time.Now().Format("2006-01-02.log")
filename = strings.ReplaceAll(filename, "logs.log", logname)
}
原来默认生成的日志是一个日志文件logs.log,如果我们设置日志的种类是daily,那么就重新设置文件的位置和名称
lumberJackLogger := &lumberjack.Logger{
Filename: filename,
MaxSize: maxSize,
MaxBackups: maxBackups,
MaxAge: maxAge,
Compress: compress,
}
初始化lumberjack
zap.AddSync
if app.IsLocal() {
return zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(lumberJackLogger))
} else {
return zapcore.AddSync(lumberJackLogger)
}
添加日志的输出流,如果是在本地,我们要求在本地和日志文件中同时打印,生成环境只需要打印在日志文件中就可以了
获取日志等级
logLevel := new(zapcore.Level)
if err := logLevel.UnmarshalText([]byte(level)); err != nil {
fmt.Println("日志级别设置错误")
}
我们来看看这个方法可以解析哪些等级
func (l *Level) UnmarshalText(text []byte) error {
if l == nil {
return errUnmarshalNilLevel
}
if !l.unmarshalText(text) && !l.unmarshalText(bytes.ToLower(text)) {
return fmt.Errorf("unrecognized level: %q", text)
}
return nil
}
func (l *Level) unmarshalText(text []byte) bool {
switch string(text) {
case "debug", "DEBUG":
*l = DebugLevel
case "info", "INFO", "": // make the zero value useful
*l = InfoLevel
case "warn", "WARN":
*l = WarnLevel
case "error", "ERROR":
*l = ErrorLevel
case "dpanic", "DPANIC":
*l = DPanicLevel
case "panic", "PANIC":
*l = PanicLevel
case "fatal", "FATAL":
*l = FatalLevel
default:
return false
}
return true
}
解析的等级包括了debug, info,warn等,解析过程中发现它会把字符串都变成小写,所以我们输入时不必关心大小写
zapcore.NewCore()
初始化zap.Core
getEncoder()
func getEncoder() zapcore.Encoder {
encoderConfig := zapcore.EncoderConfig{
TimeKey: "time",
LevelKey: "level",
NameKey: "logger",
CallerKey: "caller",
FunctionKey: zapcore.OmitKey,//忽悠这个字段
MessageKey: "message",
StacktraceKey: "stacktrace",
LineEnding: zapcore.DefaultLineEnding,//默认换行"\n"
EncodeLevel: zapcore.CapitalLevelEncoder,//以大写字母输出
EncodeTime: customTimeEncoder,//自定义打印的时间格式
EncodeDuration: zapcore.SecondsDurationEncoder,//把时间序列化位以秒为单位的浮点数
EncodeCaller: zapcore.ShortCallerEncoder,//只打印调信息发生的最终目录
}
if app.IsLocal() {
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
return zapcore.NewConsoleEncoder(encoderConfig)
}
return zapcore.NewJSONEncoder(encoderConfig)
}
主要作用是在这里配置了要打印哪些内容以及打印的效果,像TimeKey,LevelKey等key字段,加入我们没有配置这些选项,那么最后打印的结果中将没有这些内容。
zap.New()
Logger = zap.New(core,
zap.AddCaller(),//打印信息发生的位置
zap.AddCallerSkip(1),//打印位置时从调用堆栈中回溯一级
zap.AddStacktrace(zap.ErrorLevel))//只有在Error以上的错误时才会打印堆栈追踪
zap.ReplaceGlobals(Logger)//替换全局logger
caller
在上述的解释中,caller讲的比较抽象,我们来举个例子说明一下,接下来我们通过删除zap.New()中的参数来看看这些参数的效果,我们采用Dump来测试,这个函数暂时还没有讲到,你可以理解这个函数的作用就是用户调试时打印日志的。
func logInfo() {
logger.Dump("test")
}
func main() {
var env string
flag.StringVar(&env, "env", "", "")
flag.Parse()
config.InitConfig(env)
bootstrap.SetupLogger()
logInfo()
router := gin.New()
bootstrap.SetupDB()
bootstrap.SetupRoute(router)
gin.SetMode(gin.ReleaseMode)
err := router.Run(":" + config.Get("app.port"))
if err != nil {
fmt.Println(err)
}
}
删除AddCaller()和AddCallerSkip()
2022-08-28 09:25:46 WARN Dump {"data": "\"test\""}
删除AddCallerSkip()
2022-08-28 09:22:45 WARN logger/logger.go:94 Dump {"data": "\"test\""}
添加了AddCaller()之后,那么调用这个函数的位置就被打印出来了,我们不难发现,因为打印日志调用的是pkg包中的函数,在实际debug过程中没有什么作用,但是我们如果跳过这一级,那么这时的日志就有所作用了
全部保留
2022-08-28 09:21:54 WARN go-api-practice/main.go:18 Dump {"data": "\"test\""}
很明显发现添加了AddCallerSkip(1)的日志更具实际意义
zap函数的封装
不难发现所有的工具类都是对zap中一组相似方法的封装,我就挑选一个来讲解
Logger.Warn()
func (log *Logger) Warn(msg string, fields ...Field)
type Field struct {
Key string
Type FieldType
Integer int64
String string
Interface interface{}
}
这类函数的第一个参数就是message,第二个参数是zap.Field结构体,内容的核心就是键值对,在最后的日志打印中,zap会把Field字段全部序列化成JSON
其它的函数都是在利用这些打印功能做封装,我就不一一讲解了:stuck_out_tongue_closed_eyes: