Go语言中日志统一处理详解

 更新时间:2024年01月15日 10:32:01   作者:吴佳浩  
在现代软件开发中,日志记录是一项至关重要的任务,它不仅帮助开发人员诊断问题,还有助于监控和维护应用程序,本文主要来和大家聊聊日志的统一处理,感兴趣的小伙伴可以了解下

简介

在现代软件开发中,日志记录是一项至关重要的任务,它不仅帮助开发人员诊断问题,还有助于监控和维护应用程序。在Go语言中,性能和内存分配是至关重要的考虑因素,因此选择一个高性能的日志库非常重要。本文将介绍Uber开源的zap日志库,它在性能和内存分配方面进行了极致的优化,成为Go语言中的一种理想选择。

快速使用

先安装:

$ go get go.uber.org/zap

后使用:

package main

import (
  "time"

  "go.uber.org/zap"
)

func main() {
  logger := zap.NewExample()
  defer logger.Sync()

  url := "http://example.org/api"
  logger.Info("failed to fetch URL",
    zap.String("url", url),
    zap.Int("attempt", 3),
    zap.Duration("backoff", time.Second),
  )

  sugar := logger.Sugar()
  sugar.Infow("failed to fetch URL",
    "url", url,
    "attempt", 3,
    "backoff", time.Second,
  )
  sugar.Infof("Failed to fetch URL: %s", url)
}

zap库的使用与其他的日志库非常相似。先创建一个logger,然后调用各个级别的方法记录日志(Debug/Info/Error/Warn)。zap提供了几个快速创建logger的方法,zap.NewExample()zap.NewDevelopment()zap.NewProduction(),还有高度定制化的创建方法zap.New()。创建前 3 个logger时,zap会使用一些预定义的设置,它们的使用场景也有所不同。Example适合用在测试代码中,Development在开发环境中使用,Production用在生成环境。

zap底层 API 可以设置缓存,所以一般使用defer logger.Sync()将缓存同步到文件中。

由于fmt.Printf之类的方法大量使用interface{}和反射,会有不少性能损失,并且增加了内存分配的频次。zap为了提高性能、减少内存分配次数,没有使用反射,而且默认的Logger只支持强类型的、结构化的日志。必须使用zap提供的方法记录字段。zap为 Go 语言中所有的基本类型和其他常见类型都提供了方法。这些方法的名称也比较好记忆,zap.TypeTypebool/int/uint/float64/complex64/time.Time/time.Duration/error等)就表示该类型的字段,zap.Typepp结尾表示该类型指针的字段,zap.Typess结尾表示该类型切片的字段。如:

  • zap.Bool(key string, val bool) Fieldbool字段
  • zap.Boolp(key string, val *bool) Fieldbool指针字段;
  • zap.Bools(key string, val []bool) Fieldbool切片字段。

当然也有一些特殊类型的字段:

  • zap.Any(key string, value interface{}) Field:任意类型的字段;
  • zap.Binary(key string, val []byte) Field:二进制串的字段。

当然,每个字段都用方法包一层用起来比较繁琐。zap也提供了便捷的方法SugarLogger,可以使用printf格式符的方式。调用logger.Sugar()即可创建SugaredLoggerSugaredLogger的使用比Logger简单,只是性能比Logger低 50% 左右,可以用在非热点函数中。调用SugarLoggerf结尾的方法与fmt.Printf没什么区别,如例子中的Infof。同时SugarLogger还支持以w结尾的方法,这种方式不需要先创建字段对象,直接将字段名和值依次放在参数中即可,如例子中的Infow

默认情况下,Example输出的日志为 JSON 格式:

{"level":"info","msg":"failed to fetch URL","url":"http://example.org/api","attempt":3,"backoff":"1s"}
{"level":"info","msg":"failed to fetch URL","url":"http://example.org/api","attempt":3,"backoff":"1s"}
{"level":"info","msg":"Failed to fetch URL: http://example.org/api"}

记录层级关系

前面我们记录的日志都是一层结构,没有嵌套的层级。我们可以使用zap.Namespace(key string) Field构建一个命名空间,后续的Field都记录在此命名空间中:

func main() {
  logger := zap.NewExample()
  defer logger.Sync()

  logger.Info("tracked some metrics",
    zap.Namespace("metrics"),
    zap.Int("counter", 1),
  )

  logger2 := logger.With(
    zap.Namespace("metrics"),
    zap.Int("counter", 1),
  )
  logger2.Info("tracked some metrics")
}

输出:

{"level":"info","msg":"tracked some metrics","metrics":{"counter":1}}
{"level":"info","msg":"tracked some metrices","metrics":{"counter":1}}

上面我们演示了两种Namespace的用法,一种是直接作为字段传入Debug/Info等方法,一种是调用With()创建一个新的Logger,新的Logger记录日志时总是带上预设的字段。With()方法实际上是创建了一个新的Logger

// src/go.uber.org/zap/logger.go
func (log *Logger) With(fields ...Field) *Logger {
  if len(fields) == 0 {
    return log
  }
  l := log.clone()
  l.core = l.core.With(fields)
  return l
}

定制Logger

调用NexExample()/NewDevelopment()/NewProduction()这 3 个方法,zap使用默认的配置。我们也可以手动调整,配置结构如下:

// src/go.uber.org/zap/config.go
type Config struct {
  Level AtomicLevel `json:"level" yaml:"level"`
  Encoding string `json:"encoding" yaml:"encoding"`
  EncoderConfig zapcore.EncoderConfig `json:"encoderConfig" yaml:"encoderConfig"`
  OutputPaths []string `json:"outputPaths" yaml:"outputPaths"`
  ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"`
  InitialFields map[string]interface{} `json:"initialFields" yaml:"initialFields"`
}
  • Level:日志级别;
  • Encoding:输出的日志格式,默认为 JSON;
  • OutputPaths:可以配置多个输出路径,路径可以是文件路径和stdout(标准输出);
  • ErrorOutputPaths:错误输出路径,也可以是多个;
  • InitialFields:每条日志中都会输出这些值。

其中EncoderConfig为编码配置:

// src/go.uber.org/zap/zapcore/encoder.go
type EncoderConfig struct {
  MessageKey    string `json:"messageKey" yaml:"messageKey"`
  LevelKey      string `json:"levelKey" yaml:"levelKey"`
  TimeKey       string `json:"timeKey" yaml:"timeKey"`
  NameKey       string `json:"nameKey" yaml:"nameKey"`
  CallerKey     string `json:"callerKey" yaml:"callerKey"`
  StacktraceKey string `json:"stacktraceKey" yaml:"stacktraceKey"`
  LineEnding    string `json:"lineEnding" yaml:"lineEnding"`
  EncodeLevel    LevelEncoder    `json:"levelEncoder" yaml:"levelEncoder"`
  EncodeTime     TimeEncoder     `json:"timeEncoder" yaml:"timeEncoder"`
  EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"`
  EncodeCaller   CallerEncoder   `json:"callerEncoder" yaml:"callerEncoder"`
  EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"`
}
  • MessageKey:日志中信息的键名,默认为msg
  • LevelKey:日志中级别的键名,默认为level
  • EncodeLevel:日志中级别的格式,默认为小写,如debug/info

调用zap.ConfigBuild()方法即可使用该配置对象创建一个Logger

func main() {
  rawJSON := []byte(`{
    "level":"debug",
    "encoding":"json",
    "outputPaths": ["stdout", "server.log"],
    "errorOutputPaths": ["stderr"],
    "initialFields":{"name":"dj"},
    "encoderConfig": {
      "messageKey": "message",
      "levelKey": "level",
      "levelEncoder": "lowercase"
    }
  }`)

  var cfg zap.Config
  if err := json.Unmarshal(rawJSON, &cfg); err != nil {
    panic(err)
  }
  logger, err := cfg.Build()
  if err != nil {
    panic(err)
  }
  defer logger.Sync()

  logger.Info("server start work successfully!")
}

上面创建一个输出到标准输出stdout和文件server.logLogger。观察输出:

{"level":"info","message":"server start work successfully!","name":"dj"}

使用NewDevelopment()创建的Logger使用的是如下的配置:

// src/go.uber.org/zap/config.go
func NewDevelopmentConfig() Config {
  return Config{
    Level:            NewAtomicLevelAt(DebugLevel),
    Development:      true, 
    Encoding:         "console",
    EncoderConfig:    NewDevelopmentEncoderConfig(),
    OutputPaths:      []string{"stderr"},
    ErrorOutputPaths: []string{"stderr"},
  }
}

func NewDevelopmentEncoderConfig() zapcore.EncoderConfig {
  return zapcore.EncoderConfig{
    // Keys can be anything except the empty string.
    TimeKey:        "T",
    LevelKey:       "L",
    NameKey:        "N",
    CallerKey:      "C",
    MessageKey:     "M",
    StacktraceKey:  "S",
    LineEnding:     zapcore.DefaultLineEnding,
    EncodeLevel:    zapcore.CapitalLevelEncoder,
    EncodeTime:     zapcore.ISO8601TimeEncoder,
    EncodeDuration: zapcore.StringDurationEncoder,
    EncodeCaller:   zapcore.ShortCallerEncoder,
  }
}

NewProduction()的配置可自行查看。

选项

NewExample()/NewDevelopment()/NewProduction()这 3 个函数可以传入若干类型为zap.Option的选项,从而定制Logger的行为。又一次见到了选项模式!!

zap提供了丰富的选项供我们选择。

输出文件名和行号

调用zap.AddCaller()返回的选项设置输出文件名和行号。但是有一个前提,必须设置配置对象Config中的CallerKey字段。也因此NewExample()不能输出这个信息(它的Config没有设置CallerKey)。

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

  logger.Info("hello world")
}

输出:

{"level":"info","ts":1587740198.9508286,"caller":"caller/main.go:9","msg":"hello world"}

Info()方法在main.go的第 9 行被调用。AddCaller()zap.WithCaller(true)等价。

有时我们稍微封装了一下记录日志的方法,但是我们希望输出的文件名和行号是调用封装函数的位置。这时可以使用zap.AddCallerSkip(skip int)向上跳 1 层:

func Output(msg string, fields ...zap.Field) {
  zap.L().Info(msg, fields...)
}

func main() {
  logger, _ := zap.NewProduction(zap.AddCaller(), zap.AddCallerSkip(1))
  defer logger.Sync()

  zap.ReplaceGlobals(logger)

  Output("hello world")
}

输出:

{"level":"info","ts":1587740501.5592482,"caller":"skip/main.go:15","msg":"hello world"}

输出在main函数中调用Output()的位置。如果不指定zap.AddCallerSkip(1),将输出"caller":"skip/main.go:6",这是在Output()函数中调用zap.Info()的位置。因为这个Output()函数可能在很多地方被调用,所以这个位置参考意义并不大。试试看!

输出调用堆栈

有时候在某个函数处理中遇到了异常情况,因为这个函数可能在很多地方被调用。如果我们能输出此次调用的堆栈,那么分析起来就会很方便。我们可以使用zap.AddStackTrace(lvl zapcore.LevelEnabler)达成这个目的。该函数指定lvl和之上的级别都需要输出调用堆栈:

func f1() {
  f2("hello world")
}

func f2(msg string, fields ...zap.Field) {
  zap.L().Warn(msg, fields...)
}

func main() {
  logger, _ := zap.NewProduction(zap.AddStacktrace(zapcore.WarnLevel))
  defer logger.Sync()

  zap.ReplaceGlobals(logger)

  f1()
}

zapcore.WarnLevel传入AddStacktrace(),之后Warn()/Error()等级别的日志会输出堆栈,Debug()/Info()这些级别不会。运行结果:

{"level":"warn","ts":1587740883.4965692,"caller":"stacktrace/main.go:13","msg":"hello world","stacktrace":"main.f2\n\td:/code/golang/src/github.com/darjun/go-daily-lib/zap/option/stacktrace/main.go:13\nmain.f1\n\td:/code/golang/src/github.com/darjun/go-daily-lib/zap/option/stacktrace/main.go:9\nmain.main\n\td:/code/golang/src/github.com/darjun/go-daily-lib/zap/option/stacktrace/main.go:22\nruntime.main\n\tC:/Go/src/runtime/proc.go:203"}

stacktrace单独拉出来:

很清楚地看到调用路径。

全局Logger

为了方便使用,zap提供了两个全局的Logger,一个是*zap.Logger,可调用zap.L()获得;另一个是*zap.SugaredLogger,可调用zap.S()获得。需要注意的是,全局的Logger默认并不会记录日志!它是一个无实际效果的Logger。看源码:

// go.uber.org/zap/global.go
var (
  _globalMu sync.RWMutex
  _globalL  = NewNop()
  _globalS  = _globalL.Sugar()
)

我们可以使用ReplaceGlobals(logger *Logger) func()logger设置为全局的Logger,该函数返回一个无参函数,用于恢复全局Logger设置:

func main() {
  zap.L().Info("global Logger before")
  zap.S().Info("global SugaredLogger before")

  logger := zap.NewExample()
  defer logger.Sync()

  zap.ReplaceGlobals(logger)
  zap.L().Info("global Logger after")
  zap.S().Info("global SugaredLogger after")
}

{"level":"info","msg":"global Logger after"}
{"level":"info","msg":"global SugaredLogger after"}

可以看到在调用ReplaceGlobals之前记录的日志并没有输出。

预设日志字段

如果每条日志都要记录一些共用的字段,那么使用zap.Fields(fs ...Field)创建的选项。例如在服务器日志中记录可能都需要记录serverIdserverName

func main() {
  logger := zap.NewExample(zap.Fields(
    zap.Int("serverId", 90),
    zap.String("serverName", "awesome web"),
  ))

  logger.Info("hello world")
}

输出:

{"level":"info","msg":"hello world","serverId":90,"serverName":"awesome web"}

与标准日志库搭配使用

如果项目一开始使用的是标准日志库log,后面想转为zap。这时不必修改每一个文件。我们可以调用zap.NewStdLog(l *Logger) *log.Logger返回一个标准的log.Logger,内部实际上写入的还是我们之前创建的zap.Logger

func main() {
  logger := zap.NewExample()
  defer logger.Sync()

  std := zap.NewStdLog(logger)
  std.Print("standard logger wrapper")
}

输出:

{"level":"info","msg":"standard logger wrapper"}

很方便不是吗?我们还可以使用NewStdLogAt(l *logger, level zapcore.Level) (*log.Logger, error)让标准接口以level级别写入内部的*zap.Logger

如果我们只是想在一段代码内使用标准日志库log,其它地方还是使用zap.Logger。可以调用RedirectStdLog(l *Logger) func()。它会返回一个无参函数恢复设置:

func main() {
  logger := zap.NewExample()
  defer logger.Sync()

  undo := zap.RedirectStdLog(logger)
  log.Print("redirected standard library")
  undo()

  log.Print("restored standard library")
}

看前后输出变化:

{"level":"info","msg":"redirected standard library"}
2020/04/24 22:13:58 restored standard library

当然RedirectStdLog也有一个对应的RedirectStdLogAt以特定的级别调用内部的*zap.Logger方法。

惊喜来啦 通用zap封装拿走直接用

package common
/*
Package common provides a logging utility that utilizes the Zap logger library
for structured and performant logging. It includes functions for logging at
different log levels and is configured to write log entries to a file using
the Lumberjack log rotation mechanism.

Author: wujiahao

Initial Description:
This package sets up a structured logging system using Uber's Zap logger and
Lumberjack for log rotation. It allows you to log messages at different
severity levels, such as Debug, Info, Warn, Error, DPanic, and Fatal, and
supports both plain and formatted log messages. The log output is directed to
a file with rotation based on size, and each log entry includes a timestamp
in ISO8601 format. Additionally, caller information can be included in log
entries for debugging purposes.

Usage:
To use this logging utility, simply import the package and make calls to the
logging functions as needed. The logger is initialized with default
configuration, but you can customize it by modifying the init() function in
this package.
*/
import (
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"gopkg.in/natefinch/lumberjack.v2"
)




var (
	logger *zap.SugaredLogger
)

func init()  {
	//log file name
	fileName :="micro.log"
	writeSyncer := zapcore.AddSync(&lumberjack.Logger{
		Filename: fileName, //file name
		MaxSize:  521,      //the file max size *MB
		//MaxAge:     0,		//the destroy time
		MaxBackups: 0,    //the max back up
		LocalTime:  true, //start local time
		Compress:   true, //is zip
	})
	//encode
	encoder := zap.NewProductionEncoderConfig()
	//time format
	encoder.EncodeTime = zapcore.ISO8601TimeEncoder
	core := zapcore.NewCore(
		//encoder
		zapcore.NewJSONEncoder(encoder),
		writeSyncer,
		zap.NewAtomicLevelAt(zap.DebugLevel))
	log := zap.New(
		core,
		zap.AddCaller(),
		zap.AddCallerSkip(1))//有时我们稍微封装了一下记录日志的方法,但是我们希望输出的文件名和行号是调用封装函数的位置。这时可以使用zap.AddCallerSkip(skip int)向上跳 1 层:
	logger= log.Sugar()
}
func Debug(args ...interface{})  {
	logger.Debug(args)
}
func Debugf(template string,args ...interface{}) {
	logger.Debugf(template, args)
}

func Info(args ...interface{})  {
     logger.Info(args...)
}
func Infof(template string,arg ...interface{})  {
	logger.Infof(template,arg...)
}
func Warn(args ...interface{})  {
	logger.Warn(args...)
}
func Warnf(template string,args ...interface{})  {
	logger.Warnf(template,args...)
}
func Error(args ...interface{})  {
	logger.Error(args...)
}
func Errorf(template string,args ...interface{})  {
	logger.Errorf(template,args)
}
func DPanic(args ...interface{})  {
	logger.DPanic(args...)
}
func DPanicf(template string,args ...interface{})  {
	logger.DPanicf(template,args...)
}
func Fatal(args ...interface{})  {
	logger.Fatal(args...)
}
func FatalF(tempalte string,args ...interface{})  {
	logger.Fatalf(tempalte,args...)
}

总结

使用zap库,我们可以轻松地实现高性能、结构化的日志记录,同时减少内存分配和性能损失。

本文从安装、快速使用、配置、全局Logger等方面介绍了zap的基本用法。

它的性能和优势使得它成为处理热点函数中日志记录的首选工具。

通过掌握zap库,开发人员可以更好地管理和分析应用程序的日志,提高开发和维护效率,确保应用程序的稳定性和可维护性。

因此,我们鼓励开发人员深入学习和应用zap库,以提高Go语言应用程序的日志记录质量和性能。

以上就是Go语言中日志统一处理详解的详细内容,更多关于Go日志处理的资料请关注脚本之家其它相关文章!

相关文章

  • Go语言中的goroutine和channel如何协同工作

    Go语言中的goroutine和channel如何协同工作

    在Go语言中,goroutine和channel是并发编程的两个核心概念,它们协同工作以实现高效、安全的并发执行,本文将详细探讨goroutine和channel如何协同工作,以及它们在并发编程中的作用和优势,需要的朋友可以参考下
    2024-04-04
  • go实现一个分布式限流器的方法步骤

    go实现一个分布式限流器的方法步骤

    项目中需要对api的接口进行限流,本文主要介绍了go实现一个分布式限流器的方法步骤,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-01-01
  • Golang标准库之errors包应用方式

    Golang标准库之errors包应用方式

    Go语言的errors包提供了基础的错误处理能力,允许通过errors.New创建自定义error对象,error在Go中是一个接口,通过实现Error方法来定义错误文本,对错误的比较通常基于对象地址,而非文本内容,因此即使两个错误文本相同
    2024-10-10
  • go语言区块链学习调用智能合约

    go语言区块链学习调用智能合约

    这篇文章主要为大家介绍了go语言区块链学习中如何调用智能合约的实现示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步
    2021-10-10
  • Golang PHP 数据绑定示例分析

    Golang PHP 数据绑定示例分析

    这篇文章主要为大家介绍了Golang PHP 数据绑定示例分析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-08-08
  • Golang通过小程序获取微信openid的方法示例

    Golang通过小程序获取微信openid的方法示例

    这篇文章主要介绍了Golang通过小程序获取微信openid的方法示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-03-03
  • 让Go反射变快的方法实例探究

    让Go反射变快的方法实例探究

    反射允许你在运行时获得有关 Go 类型的信息,如果你曾经愚蠢地尝试编写 json.Unmarshal 之类的新版本,本文将探讨的就是如何使用反射来填充结构体值
    2024-01-01
  • Go语言中内存泄漏的常见案例与解决方法

    Go语言中内存泄漏的常见案例与解决方法

    Go虽然是自动GC类型的语言,但在编码过程中如果不注意,很容易造成内存泄漏的问题,本文为大家整理了一些内存泄漏的常见Case与解决方法,希望对大家有所帮助
    2024-03-03
  • golang获取网卡信息操作

    golang获取网卡信息操作

    这篇文章主要介绍了golang获取网卡信息操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • 图文详解Go中的channel

    图文详解Go中的channel

    Channel是go语言内置的一个非常重要的特性,也是go并发编程的两大基石之一,下面这篇文章主要给大家介绍了关于Go中channel的相关资料,需要的朋友可以参考下
    2023-02-02

最新评论