GoLang 日志模块使用介绍

2017-10-16 golang language

打印日志是最常规的需求,GoLang 的基础库只提供了简单的格式化功能,但是很多基础功能不存在,例如日志级别、文件切割等等。

这里介绍 Uber 开发的一个日志库 ZAP 。

简介

通常来说,一个日志系统需要具有如下的功能:

  • 将事件记录到文件中,而不是应用程序控制台。
  • 日志切割,可以根据文件大小、时间等来切割日志文件。
  • 支持不同的日志级别,例如 DEBUG、INFO、ERROR 等。
  • 能够打印基本信息,如调用文件、函数名、行号、时间等。

在 GoLang 中提供了基本的 log 模块,不过功能很简单。

log

官方标准的日志库,使用方式与 fmt 库类似,只是默认输出时会添加时间信息。

package main

import "log"

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

其输出类似 2019/03/01 11:15:08 Hello World

格式定制

可以在启动时通过 init() 函数进行一些初始化操作,其中 log 库提供了一些常见的配置项,可以通过 func SetFlags(flag int) 设置。

const (
	Ldate         = 1 << iota     // 日期示例: 2009/01/23
	Ltime                         // 时间示例: 01:23:23
	Lmicroseconds                 // 毫秒示例: 01:23:23.123123.
	Llongfile                     // 绝对路径和行号: /a/b/c/d.go:23
	Lshortfile                    // 文件和行号: d.go:23.
	LUTC                          // 日期时间转为UTC时区的
	LstdFlags     = Ldate | Ltime // 默认的格式输出
)

也可以通过 func SetPrefix(prefix string) 设置日志的输出前缀。

package main

import "log"

func init() {
    log.SetPrefix("[APP] ")
    log.SetFlags(log.Ldate | log.Lmicroseconds | log.Lshortfile)
}

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

最终的输出为 [APP] 2019/03/01 11:17:10.316400 test.go:11: Hello World

输出到文件

上述都只是打印到终端,当然,可以在初始化时指定文件。

package main

import (
    "fmt"
    "log"
    "os"
)

func init() {
    fileName := "/tmp/test.log"
    logFile, err := os.OpenFile(fileName, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0744)
    if err != nil {
            fmt.Printf("Open log file '%s' failed, %s.", fileName, err)
    } else {
            log.SetOutput(logFile)
    }

    log.SetPrefix("[APP] ")
    log.SetFlags(log.Ldate | log.Lmicroseconds | log.Lshortfile)
}

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

其它

最大的优势是简单,可以打印简单日志,提供了 Fatal (通过 os.Exit(1) 退出程序)和 Panic (消息写入日志后抛出 panic 报错)。前者会在打印日志后直接退出;后者如果没有使用 recover() 函数则会打印错误栈信息后退出。

ZAP

其它最常用的三方库有 logrus 以及 ZAP,后者的性能更好,而且通过三方库可以支持文件的自动切割,可以满足绝大部分的场景,这里就使用后者。

Zap 是 Uber 开源的高性能日志库,对性能和内存分配做了极致的优化,提供了日志常见的基础功能,而且配合一些三方库可以作为日志切割,这里简单介绍 ZAP 的使用。

安装

可以通过 go get -u go.uber.org/zap 命令安装,提供了两种类型的日志记录器,官网有关于两者的性能介绍。

  • Sugared Logger 通用日志格式,支持结构化和 printf 风格的日志记录。
  • Logger 性能更好,但是只支持强类型和结构化日志。

由于 fmt.Printf 之类的方法大量使用了 interface{} 和反射,会有性能损失,包括了内存分配次数,所以,zap 为了提供性能,没有使用反射,默认只支持强类型、结构化的日志。

package main

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

var logger *zap.Logger

func main() {
	logger, _ = zap.NewProduction()
	defer logger.Sync()
	logger.Info("Hello", zap.String("Name", "World"))
}

默认输出的是 JSON 结构,输出为。

{"level":"info","ts":1614570428.602909,"caller":"BasicAgent/main.go:38","msg":"Hello","Name":"World"}

上述就是所谓的强结构化类型,通过 zap.String 标识这个是字符串类型,还包括了一些其它的基础类型。

使用结构化类型就是复杂,所以也提供了 SugarLogger 方法,可以使用类似 fmt.Printf 的格式化。

package main

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

var logger *zap.Logger

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

	sugar := logger.Sugar()
	sugar.Infow("Hello", zap.String("Name", "World"))
	sugar.Infof("Hello %s", "World")
}

其输出为。

{"level":"info","ts":1614570676.1768792,"caller":"BasicAgent/main.go:39","msg":"Hello","Name":"World"}
{"level":"info","ts":1614570676.1769674,"caller":"BasicAgent/main.go:40","msg":"Hello World"}

可以支持格式化以及 printf 类似语法。

日志的格式可以进行定制,除了上述的 NewProduction 之外,还有 NewDevelopmentNewExample 总共三个函数,其输出的格式有所区别,分别用在生产、开发、单元测试中,还可以通过 New 进行高度定制。

日志定制

直接通过 New() 进行定制,其关键数据结构是 zapcore.Core,通过 NewCore() 函数创建,其关键参数为:

  • Encoder 决定如何写入日志,也即是日志格式。
  • WriterSyncer 日志写到哪里,文件还是终端等。
  • LogLevel 日志级别。

示例如下。

package main

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

var logger *zap.Logger

func main() {
	file, err := os.Create("/tmp/test.log")
	if err != nil {
		return
	}
	logger := zap.New(zapcore.NewCore(
		zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
		zapcore.AddSync(file),
		zapcore.DebugLevel,
		))
	defer logger.Sync()

	sugar := logger.Sugar()
	sugar.Infow("Hello", zap.String("Name", "World"))
	sugar.Infof("Hello %s", "World")
}

上述因为日志文件打开方式不是 APPEND 的,所以每次运行都会覆盖。

日志级别动态修改

Zap 允许动态修改日志级别,不过需要有些特殊设置。

package main

import (
	"os"

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

func main() {
	atom := zap.NewAtomicLevel()

	// To keep the example deterministic, disable timestamps in the output.
	encoderCfg := zap.NewProductionEncoderConfig()
	encoderCfg.TimeKey = ""

	logger := zap.New(zapcore.NewCore(
		zapcore.NewJSONEncoder(encoderCfg),
		zapcore.Lock(os.Stdout),
		atom,
	))
	defer logger.Sync()

	logger.Info("info logging enabled")

	atom.SetLevel(zap.ErrorLevel)
	logger.Info("info logging disabled")
}

日志切割

Zap 默认是不支持日志切割的,可以通过 github.com/natefinch/lumberjack 库实现,包括了标准的日志输出,也可以通过该库实现切割。

package main

import (
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"gopkg.in/natefinch/lumberjack.v2"
)

var logger *zap.Logger

func main() {
	logger := zap.New(zapcore.NewCore(
		zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
		zapcore.AddSync(&lumberjack.Logger{
			Filename: "/tmp/test.log",
			MaxSize: 50,
			MaxBackups: 4,
			MaxAge: 30,
			Compress: true,
		}),
		zapcore.DebugLevel,
		))
	defer logger.Sync()

	sugar := logger.Sugar()
	sugar.Infow("Hello", zap.String("Name", "World"))
	sugar.Infof("Hello %s", "World")
}

在使用时,可以将日志设置单独抽象一个包,然后其它模块直接使用即可。