Go 日志处理

麦浪4个月前技术文章96

1、背景

日志文件一般用于记录操作系统或其他软件运行时发生的事件,或通信软件不同用户之间的消息。如果有一些问题需要对程序进行调试或故障排查时,日志是必不可少的,这是我们分析程序问题常用的手段。

2、操作前了解相关配置和要求

zap + lumberjack 实现日志切割归档的机制:


3、操作步骤

3.1 配置 Logger

zap 提供了两种类型的日志记录器 Logger 和 Sugared Logger,区别是:

  1. 在每一微秒和每一次内存分配都很重要的上下文中,使用 Logger。它比 Sugared Logger 更快,内存分配次数也更少,但它只支持强类型的结构化日志记录。

  2. 在性能很好但不是很关键的上下文中,使用 SugaredLogger。它比其他结构化日志记录包快 4~10 倍,并且支持结构化和 printf 风格的日志记录。


3.1.1 Logger

  • 通过调用 zap.NewProduction(), zap.NewDevelopment() 或者 zap.NewExample() 创建一个 Logger。

  • 上面的每一个函数都将创建一个 Logger,唯一的区别在于它将记录的信息不同。

  • 我们可以通过 Logger 调用 Debug, Info, Warn, Error 等方法来记录不同级别的日志。


3.1.1.1 NewExample
package main

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

func main() {
	log := zap.NewExample()

	log.Debug("this is debug message")
	log.Info("this is info message")
	log.Info("this is info message with fileds", zap.Int("age", 24), zap.String("agender", "man"))
	log.Warn("this is warn message")
	log.Error("this is error message")
	log.Panic("this is panic message")
}
{"level":"debug","msg":"this is debug message"}
{"level":"info","msg":"this is info message"}
{"level":"info","msg":"this is info message with fileds","age":24,"agender":"man"}
{"level":"warn","msg":"this is warn message"}
{"level":"error","msg":"this is error message"}
{"level":"panic","msg":"this is panic message"}
panic: this is panic message


3.1.1.2 NewDevelopment
package main

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

func main() {
	log, _ := zap.NewDevelopment()
	log.Debug("this is debug message")
	log.Info("this is info message")
	log.Info("this is info message with fileds", zap.Int("age", 24), zap.String("agender", "man"))
	log.Warn("this is warn message")
	log.Error("this is error message")
	// log.DPanic("This is a DPANIC message")
	// log.Panic("this is panic message")
	// log.Fatal("This is a FATAL message")
}
2023-01-02T16:24:07.904+0800    DEBUG   main/demo.go:9  this is debug message
2023-01-02T16:24:07.905+0800    INFO    main/demo.go:10 this is info message
2023-01-02T16:24:07.905+0800    INFO    main/demo.go:11 this is info message with fileds        {"age": 24, "agender": "man"}
2023-01-02T16:24:07.905+0800    WARN    main/demo.go:12 this is warn message
main.main
        /Users/zhangyy/Documents/Gitlab/yannic/golang/zhangyydev.com/golang-basics/main/demo.go:12
runtime.main
        /opt/homebrew/Cellar/go/1.19.4/libexec/src/runtime/proc.go:250
2023-01-02T16:24:07.905+0800    ERROR   main/demo.go:13 this is error message
main.main
        /Users/zhangyy/Documents/Gitlab/yannic/golang/zhangyydev.com/golang-basics/main/demo.go:13
runtime.main
        /opt/homebrew/Cellar/go/1.19.4/libexec/src/runtime/proc.go:250


3.1.1.3 NewProduction
package main

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

func main() {
	log, _ := zap.NewProduction()
	log.Debug("this is debug message")
	log.Info("this is info message")
	log.Info("this is info message with fileds", zap.Int("age", 24), zap.String("agender", "man"))
	log.Warn("this is warn message")
	log.Error("this is error message")
	// log.DPanic("This is a DPANIC message")
	// log.Panic("this is panic message")
	// log.Fatal("This is a FATAL message")
}
{"level":"info","ts":1672648032.9237802,"caller":"main/demo.go:10","msg":"this is info message"}
{"level":"info","ts":1672648032.923952,"caller":"main/demo.go:11","msg":"this is info message with fileds","age":24,"agender":"man"}
{"level":"warn","ts":1672648032.9239638,"caller":"main/demo.go:12","msg":"this is warn message"}
{"level":"error","ts":1672648032.9239721,"caller":"main/demo.go:13","msg":"this is error message","stacktrace":"main.main\n\t/Users/zhangyy/Documents/Gitlab/yannic/golang/zhangyydev.com/golang-basics/main/demo.go:13\nruntime.main\n\t/opt/homebrew/Cellar/go/1.19.4/libexec/src/runtime/proc.go:250"}


3.1.1.4 对比总结
  • Example 和 Production 使用的是 JSON 格式输出,Development 使用行的形式输出。

  • Example

    • Debug 级别日志不记录

    • Caller 信息、时间信息不记录

    • 以小写形式打印级别名称

  • Production

    • Debug 级别日志不记录

    • Error, Dpanic 级别的记录,会在堆栈中跟踪文件,Warn 不会

    • 始终将调用者添加到文件中

    • 以时间戳格式打印日期

    • 以小写形式打印级别名称

  • Development

    • 从警告级别向上打印到堆栈中来跟踪

    • 始终打印包/文件/行(方法)

    • 在行尾添加任何额外字段作为 JSON 字符串

    • 以大写形式打印级别名称

    • 以毫秒为单位打印 ISO8601 格式的时间戳

3.1.2 Sugared Logger

Sugared Logger 对 Logger 进行了封装,以提供更符合人体工程学但速度稍慢的 API。Sugaring Logger 的成本非常低,因此单个应用程序同时使用 Loggers 和 SugaredLoggers 是合理的,并在性能敏感代码的边界上在它们之间进行转换。

默认的 zap 记录器需要结构化标签,需要使用特定值类型的函数:

log := zap.NewExample()
log.Info("this is info message with fields", zap.Int("age",24), zap.String("agender","man"))

这样虽然会显的很长,但是对性能要求较高的话,这是最快的选择。

当然我们也可以使用 Sugared Logger,它基于 printf 分割的反射类型检测,提供更简单的语法来添加混合类型的标签。

package main

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

func main() {
	logger, _ := zap.NewDevelopment()
	slogger := logger.Sugar()

	slogger.Debugf("debug message age is %d, agender is %s", 19, "man")
	slogger.Info("Info() uses sprint")
	slogger.Infof("Infof() uses %s", "sprintf")
	slogger.Infow("Infow() allows tags", "name", "Legolas", "type", 1)
}
2023-01-02T17:12:55.345+0800    DEBUG   main/demo.go:11 debug message age is 19, agender is man
2023-01-02T17:12:55.345+0800    INFO    main/demo.go:12 Info() uses sprint
2023-01-02T17:12:55.345+0800    INFO    main/demo.go:13 Infof() uses sprintf
2023-01-02T17:12:55.346+0800    INFO    main/demo.go:14 Infow() allows tags     {"name": "Legolas", "type": 1}


如果需要,可以随时使用记录器上的 slogger.Desugar() 方法从 sugar logger 切换到标准记录器。

logger, _ := zap.NewDevelopment()
// Logger >>> Sugared Logger
slogger := logger.Sugar()

// Sugared Logger >>> Logger
logger = slogger.Desugar()


3.2 日志写入文件

我们将使用 zap.New() 方法来手动传递所有配置,而不是使用像 zap.NewProduction() 这样的预置方法来创建 Logger。

func New(core zapcore.Core, options ...Option) *Logger

zapcore.Core 需要三个配置:Encoder, WriteSyncer, LogLevel

  • Encoder(编码器),我们将使用开箱即用的 NewConsoleEncoder(),并使用预先设置的 ProductionEncoderConfig()

zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig())
  • writeSyncer,指定日志将写到那里去,我们使用 zapcore.AddSync() 函数并且将打开的文件句柄传进去。

file,_ := os.Create("./test.log")
writeSyncer := zapcore.AddSync(file)
  • Log Level,定义哪种级别的日志将被写入。


实例:

package main

import (
	"os"

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

var sugarLogger *zap.SugaredLogger

func InitLogger() {
	encoder := getEncoder()
	writeSyncer := getLogWriter()
	core := zapcore.NewCore(encoder, writeSyncer, zapcore.DebugLevel)

	logger := zap.New(core, zap.AddCaller()) // 添加将调用函数信息记录到日志中的功能
	sugarLogger = logger.Sugar()
}

func getEncoder() zapcore.Encoder {
	encoderConfig := zap.NewProductionEncoderConfig()
	encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder   // 修改时间编码器
	encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder // 在日志文件中使用大写字母记录日志级别

	return zapcore.NewConsoleEncoder(encoderConfig) // NewConsoleEncoder 打印更符合人们观察的方式
}

func getLogWriter() zapcore.WriteSyncer {
	file, _ := os.Create("./test.log")
	return zapcore.AddSync(file)
}

func main() {
	InitLogger()
	sugarLogger.Info("this is info message")
	sugarLogger.Infof("this is info message, %s, %d", "Infof", 666)
	sugarLogger.Error("this is error message")
	sugarLogger.Info("this is info message")
}
2023-01-02T17:42:36.952+0800	INFO	main/demo.go:36	this is info message
2023-01-02T17:42:36.953+0800	INFO	main/demo.go:37	this is info message, Infof, 666
2023-01-02T17:42:36.953+0800	ERROR	main/demo.go:38	this is error message
2023-01-02T17:42:36.953+0800	INFO	main/demo.go:39	this is info message


3.3 日志切割归档

因为 zap 本身不支持切割归档日志文件,为了添加日志切割归档功能,我们将使用第三方库 lumberjack 来实现。

3.3.1 安装

命令:go get github.com/natefinch/lumberjack


3.3.2 lumberjack + zap

要在 zap 中加入 lumberjack 支持,我们需要修改 WriteSyncer 代码。我们将按照下面的代码修改 getLogWriter() 函数:

func getLogWriter() zapcore.WriteSyncer {
	lumberJackLogger := &lumberjack.Logger{
		Filename:   "./test.log", // 日志文件的位置
		MaxSize:    10,           // 在进行切割之前,日志文件的最大大小(以MB为单位)
		MaxBackups: 5,            // 保留旧文件的最大个数
		MaxAge:     30,           // 保留旧文件的最大天数
		Compress:   false,        // 是否压缩/归档旧文件
	}
	return zapcore.AddSync(lumberJackLogger)
}

完整代码:

package main

import (
	"github.com/natefinch/lumberjack"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
)

var sugarLogger *zap.SugaredLogger

func InitLogger() {

	encoder := getEncoder()
	writeSyncer := getLogWriter()
	core := zapcore.NewCore(encoder, writeSyncer, zapcore.DebugLevel)

	logger := zap.New(core, zap.AddCaller()) // 添加将调用函数信息记录到日志中的功能
	sugarLogger = logger.Sugar()
}

func getEncoder() zapcore.Encoder {
	encoderConfig := zap.NewProductionEncoderConfig()
	encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder   // 修改时间编码器
	encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder // 在日志文件中使用大写字母记录日志级别

	return zapcore.NewConsoleEncoder(encoderConfig) // NewConsoleEncoder 打印更符合人们观察的方式
}

func getLogWriter() zapcore.WriteSyncer {
	lumberJackLogger := &lumberjack.Logger{
		Filename:   "./test.log", // 日志文件的位置
		MaxSize:    10,           // 在进行切割之前,日志文件的最大大小(以MB为单位)
		MaxBackups: 5,            // 保留旧文件的最大个数
		MaxAge:     30,           // 保留旧文件的最大天数
		Compress:   false,        // 是否压缩/归档旧文件
	}
	return zapcore.AddSync(lumberJackLogger)
}

func main() {
	InitLogger()
	sugarLogger.Info("this is info message")
	sugarLogger.Infof("this is info message, %s, %d", "Infof", 666)
	sugarLogger.Error("this is error message")
	sugarLogger.Info("this is info message")
}


4、注意事项

应用程序同时使用 Loggers 和 SugaredLoggers 是合理的,但请注意在性能敏感代码的边界上在它们之间进行转换。

5、结果检查

对所配置的日志系统进行测试运行。


相关文章

Hive 重新编译-解决Tez JobName的问题

Hive 重新编译-解决Tez JobName的问题

本文采用linux编译首先下载源码https://dlcdn.apache.org/hive/hive-3.1.2/apache-hive-3.1.2-src.tar.gz源码位置ql/src/jav...

MySQL优化器特性(二)MRR优化

MySQL优化器特性(二)MRR优化

Index Range Scan索引范围扫描的一般步骤:1、根据where条件,从B+树定位到第一条记录。2、从索引页子节点中获取到行号(rowid),根据rowid回表查询数据。3、使用额外的whe...

Flume使用案例之实时读取本地文件到HDFS

Flume实时读取本地文件到HDFS1.  创建flume-hdfs.conf文件# 1 agenta2.sources = r2a2.sinks = k2a2.channels = c2 # 2 s...

Linux分区动态扩容/缩容

Linux分区动态扩容/缩容

xfs与ext文件系统类型xfs:XFS一种高性能的日志文件系统,几乎具备所有EXT4支持的功能。但不支持文件系统收缩ext:支持度最广、但格式化慢,有ext2、ext3、ext4基础命令先查看一下目...

MySQL性能优化(一)索引缺失引起的全表扫描

MySQL性能优化(一)索引缺失引起的全表扫描

索引缺失是引起数据库性能问题的第一大原因。一个例子这是一个非常简单的SQL,SELECT * FROM template WHERE templet_id ...

docker安装及常用操作

docker安装及常用操作

一、安装docker1、移除以前docker相关包sudo yum remove docker \      ...

发表评论    

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。