golang常用庫包:log日誌記錄-uber的Go日誌庫zap使用詳解

九卷發表於2023-04-11

Go 日誌記錄庫:uber-go 的日誌操作庫 zap 使用

一、簡介

zapuber 開源的一個高效能,結構化,分級記錄的日誌記錄包。

go1.20.2

zap v1.24.0

zap的特性

  • 高效能:zap 對日誌輸出進行了多項最佳化以提高它的效能

  • 日誌分級:有 Debug,Info,Warn,Error,DPanic,Panic,Fatal 等

  • 日誌記錄結構化:日誌內容記錄是結構化的,比如 json 格式輸出

  • 自定義格式:使用者可以自定義輸出的日誌格式

  • 自定義公共欄位:使用者可以自定義公共欄位,大家輸出的日誌內容就共同擁有了這些欄位

  • 除錯:可以列印檔名、函式名、行號、日誌時間等,便於除錯程式

  • 自定義呼叫棧級別:可以根據日誌級別輸出它的呼叫棧資訊

  • Namespace:日誌名稱空間。定義名稱空間後,所有日誌內容就在這個名稱空間下。名稱空間相當於一個資料夾

  • 支援 hook 操作

高效能介紹

與其它日誌庫對比

github官網的對比圖,下面的對比圖來自:https://github.com/uber-go/zap#performance

Log a message and 10 fields:

Package Time Time % to zap Objects Allocated
⚡ zap 2900 ns/op +0% 5 allocs/op
⚡ zap (sugared) 3475 ns/op +20% 10 allocs/op
zerolog 10639 ns/op +267% 32 allocs/op
go-kit 14434 ns/op +398% 59 allocs/op
logrus 17104 ns/op +490% 81 allocs/op
apex/log 32424 ns/op +1018% 66 allocs/op
log15 33579 ns/op +1058% 76 allocs/op

Log a message with a logger that already has 10 fields of context:

Package Time Time % to zap Objects Allocated
⚡ zap 373 ns/op +0% 0 allocs/op
⚡ zap (sugared) 452 ns/op +21% 1 allocs/op
zerolog 288 ns/op -23% 0 allocs/op
go-kit 11785 ns/op +3060% 58 allocs/op
logrus 19629 ns/op +5162% 70 allocs/op
log15 21866 ns/op +5762% 72 allocs/op
apex/log 30890 ns/op +8182% 55 allocs/op

Log a static string, without any context or printf-style templating:

Package Time Time % to zap Objects Allocated
⚡ zap 381 ns/op +0% 0 allocs/op
⚡ zap (sugared) 410 ns/op +8% 1 allocs/op
zerolog 369 ns/op -3% 0 allocs/op
standard library 385 ns/op +1% 2 allocs/op
go-kit 606 ns/op +59% 11 allocs/op
logrus 1730 ns/op +354% 25 allocs/op
apex/log 1998 ns/op +424% 7 allocs/op
log15 4546 ns/op +1093% 22 allocs/op

做了哪些最佳化

基於反射的序列化和字串格式化,它們都是 CPU 密集型計算且分配很多小的記憶體。具體到 Go 語言中,使用 encoding/json 和 fmt.Fprintf 格式化 interface{} 會使程式效能降低。

Zap 咋解決呢?Zap 使用一個無反射、零分配的 JOSN 編碼器,基礎 Logger 儘可能避免序列化開銷和記憶體分配開銷。在此基礎上,zap 還構建了更高階的 SuggaredLogger。

二、quickstart快速開始

zap 安裝:

go get -u go.uber.org/zap

zap 提供了 2 種日誌記錄器:SugaredLoggerLogger

在需要效能但不是很重要的情況下,使用 SugaredLogger 較合適。它比其它結構化日誌包快 4-10 倍,包括 結構化日誌和 printf 風格的 API。看下面使用 SugaredLogger 例子:

logger, _ := zap.NewProduction()
defer logger.Sync() // zap底層有緩衝。在任何情況下執行 defer logger.Sync() 是一個很好的習慣
sugar := logger.Sugar()
sugar.Infow("failed to fetch URL",
 // 欄位是鬆散型別,不是強型別
  "url", url,
  "attempt", 3,
  "backoff", time.Second,
)
sugar.Infof("Failed to fetch URL: %s", url)

當效能和型別安全很重要時,請使用 Logger。它比 SugaredLogger 更快,分配的資源更少,但它只支援結構化日誌和強型別欄位。

logger, _ := zap.NewProduction()
defer logger.Sync()
logger.Info("failed to fetch URL",
  // 欄位是強型別,不是鬆散型別
  zap.String("url", url),
  zap.Int("attempt", 3),
  zap.Duration("backoff", time.Second),
)

三、NewExample/NewDevelopment/NewProduction使用

zap 為我們提供了三種快速建立 logger 的方法: zap.NewProduction()zap.NewDevelopment()zap.NewExample()

見名思義,Example 一般用在測試程式碼中,Development 用在開發環境中,Production 用在生成環境中。這三種方法都預先設定好了配置資訊。

NewExample()使用

NewExample 構建一個 logger,專門為在 zap 的測試示例使用。它將 DebugLevel 及以上日誌用 JSON 格式標準輸出,但它省略了時間戳和呼叫函式,以保持示例輸出的簡短和確定性。

為什麼說 zap.NewExample() 是 zap 為我們提供快速建立 logger 的方法呢?

因為在這個方法裡,zap 已經定義好了日誌配置項部分預設值。來看它的程式碼:

// https://github.com/uber-go/zap/blob/v1.24.0/logger.go#L127
func NewExample(options ...Option) *Logger {
	encoderCfg := zapcore.EncoderConfig{
        MessageKey:     "msg",  // 日誌內容key:val, 前面的key設為msg
		LevelKey:       "level", // 日誌級別的key設為level
		NameKey:        "logger", // 日誌名
		EncodeLevel:    zapcore.LowercaseLevelEncoder, //日誌級別,預設小寫
		EncodeTime:     zapcore.ISO8601TimeEncoder, // 日誌時間
		EncodeDuration: zapcore.StringDurationEncoder,
	}
	core := zapcore.NewCore(zapcore.NewJSONEncoder(encoderCfg), os.Stdout, DebugLevel)
	return New(core).WithOptions(options...)
}

使用例子:

package main

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

func main() {
	logger := zap.NewExample()
	logger.Debug("this is debug message")
	logger.Info("this is info message")
	logger.Info("this is info message with fileds",
		zap.Int("age", 37), 
        zap.String("agender", "man"),
    )
	logger.Warn("this is warn message")
	logger.Error("this is error 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":37,"agender":"man"}
{"level":"warn","msg":"this is warn message"}
{"level":"error","msg":"this is error message"}

NewDevelopment()使用

NewDevelopment() 構建一個開發使用的 Logger,它以人性化的格式將 DebugLevel 及以上日誌資訊輸出。它的底層使用

NewDevelopmentConfig().Build(...Option) 構建。它的日誌格式各種設定在函式 NewDevelopmentEncoderConfig() 裡,想檢視詳情設定,請點進去檢視。

使用例子:

package main

import (
	"time"

	"go.uber.org/zap"
)

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

	logger.Info("failed to fetch url",
		// 強型別欄位
		zap.String("url", "http://example.com"),
		zap.Int("attempt", 3),
		zap.Duration("duration", time.Second),
	)

	logger.With(
		// 強型別欄位
		zap.String("url", "http://development.com"),
		zap.Int("attempt", 4),
		zap.Duration("duration", time.Second*5),
	).Info("[With] failed to fetch url")
}

輸出:

2023-03-22T16:02:45.760+0800    INFO    zapdemos/newdevelopment1.go:13  failed to fetch url     {"url": "http://example.com", "attempt": 3, "duration": "1s"}
2023-03-22T16:02:45.786+0800    INFO    zapdemos/newdevelopment1.go:25  [With] failed to fetch url      {"url": "http://development.com", "attempt": 4, "duration": "5s"}

NewProduction()使用

NewProduction() 構建了一個合理的 Prouction 日誌記錄器,它將 info 及以上的日誌內容以 JSON 格式記寫入標準錯誤裡。

它的底層使用 NewProductionConfig().Build(...Option) 構建。它的日誌格式設定在函式 NewProductionEncoderConfig 裡。

使用例子

package main

import (
	"time"

	"go.uber.org/zap"
)

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

	url := "http://zap.uber.io"
	sugar := logger.Sugar()
	sugar.Infow("failed to fetch URL",
		"url", url,
		"attempt", 3,
		"time", time.Second,
	)

	sugar.Infof("Failed to fetch URL: %s", url)

	// 或更簡潔 Sugar() 使用
	// sugar := zap.NewProduction().Sugar()
	// defer sugar.Sync()
}

輸出:

{"level":"info","ts":1679472893.2944522,"caller":"zapdemos/newproduction1.go:16","msg":"failed to fetch URL","url":"http://zap.uber.io","attempt":3,"time":1}
{"level":"info","ts":1679472893.294975,"caller":"zapdemos/newproduction1.go:22","msg":"Failed to fetch URL: http://zap.uber.io"}

使用配置

在這 3 個函式中,可以傳入一些配置項。為什麼能傳入配置項?我們來看看 NewExample() 函式定義:

func NewExample(options ...Option) *Logger

它的函式傳參有一個 ...Option 選項,是一個 interface 型別,它關聯的是 Logger struct。只要返回 Option 就可以傳進 NewExample() 裡。在 zap/options.go 檔案中可以看到很多返回 Option 的函式,也就是說這些函式都可以傳入 NewExample 函式裡。這裡用到了 Go 裡面的一個編碼技巧,函式選項模式。

zap.Fields() 新增欄位到 Logger 中:

package main

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

func main() {
	logger, _ := zap.NewProduction(zap.Fields(
		zap.String("log_name", "testlog"),
		zap.String("log_author", "prometheus"),
	))
	defer logger.Sync()

	logger.Info("test fields output")

	logger.Warn("warn info")
}

輸出:

{"level":"info","ts":1679477929.842166,"caller":"zapdemos/fields.go:14","msg":"test fields output","log_name":"testlog","log_author":"prometheus"}
{"level":"warn","ts":1679477929.842166,"caller":"zapdemos/fields.go:16","msg":"warn info","log_name":"testlog","log_author":"prometheus"}

zap.Hook() 新增回撥函式:

Hook (鉤子函式)回撥函式為使用者提供一種簡單方法,在每次日誌內容記錄後執行這個回撥函式,執行使用者需要的操作。也就是說記錄完日誌後你還想做其它事情就可以呼叫這個函式。

package main

import (
	"fmt"

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

func main() {
	logger := zap.NewExample(zap.Hooks(func(entry zapcore.Entry) error {
		fmt.Println("[zap.Hooks]test Hooks")
		return nil
	}))
	defer logger.Sync()

	logger.Info("test output")

	logger.Warn("warn info")
}

輸出:

{"level":"info","msg":"test output"}
[zap.Hooks]test Hooks
{"level":"warn","msg":"warn info"}
[zap.Hooks]test Hooks

四、logger和sugaredlogger區別

從上面例子中看出,zap 有 2 種格式化日誌方式:logger 和 sugared logger。

  • sugared logger:
  1. 它有很好的效能,比一般日誌包快 4-10 倍。
  2. 支援結構化的日誌。
  3. 支援 printf 風格的日誌。
  4. 日誌欄位不需要定義型別
  • logger(沒有sugar)
  1. 它的效能比 sugared logger 還要快
  2. 它只支援強型別的結構化日誌。

它應用在對效能更加敏感日誌記錄中,它的記憶體分配次數更少。

比如如果每一次記憶體分配都很重要的話可以使用這個。對型別安全有嚴格要求也可以使用這個。

logger 和 sugaredlogger 相互轉換:

// 建立 logger
logger := zap.NewExample()
defer logger.Sync()

// 轉換 SugaredLogger
sugar := logger.Sugar()

// 轉換 logger
plain := sugar.Desugar()

怎麼快速構建一個 logger 呢?有下面種幾種方法:

  • zap.NewProduction()
  • zap.NewDevelopment()
  • zap.Example()

主要區別:

  • 記錄日誌資訊和結構不同。

    Example 和 Production 是 json 格式輸出,Development 是普通一行格式輸出,如果後面帶有欄位輸出話用json格式。

相同點:

  • 預設情況下都會列印日誌資訊到 console 介面
  • 都是透過 logger 呼叫 Info、Error 等方法

怎麼選擇:

  • 需要不錯的效能但不是很重要的情況下,可以選擇 sugaredlogger。它支援結構化日誌和 printf 風格的日誌記錄。sugaredlogger 的日誌記錄是鬆散型別的,不是強型別,能接受可變數量的鍵值對。如果你要用強型別欄位記錄,可以使用 SugaredLogger.With 方法。
  • 如果是每次或每微秒記錄日誌都很重要情況下,可以使用 logger,它比 sugaredlogger 每次分配記憶體更少,效能更高。但它僅支援強型別的結構化日誌記錄。

五、自定義配置

快速構建 logger 日誌記錄器最簡單的方法就是用 zap 預定義了配置的方法:NewExample(), NewProduction()NewDevelopment(),這 3 個方法透過單個函式呼叫就可以構建一個日誌計記錄器,也可以簡單配置。

但是有的專案需要更多的定製,怎麼辦?zap 的 Config 結構和 zapcore 的 EncoderConfig 結構可以幫助你,讓你能夠進行自定義配置。

配置結構說明

Config 配置項原始碼:

// zap v1.24.0
type Config struct {
    // 動態改變日誌級別,在執行時你可以安全改變日誌級別
	Level AtomicLevel `json:"level" yaml:"level"`
    // 將日誌記錄器設定為開發模式,在 WarnLevel 及以上級別日誌會包含堆疊跟蹤資訊
	Development bool `json:"development" yaml:"development"`
    // 在日誌中停止呼叫函式所在檔名、行數
	DisableCaller bool `json:"disableCaller" yaml:"disableCaller"`
    // 完全禁止自動堆疊跟蹤。預設情況下,在 development 中,warnlevel及以上日誌級別會自動捕獲堆疊跟蹤資訊
    // 在 production 中,ErrorLevel 及以上也會自動捕獲堆疊資訊
	DisableStacktrace bool `json:"disableStacktrace" yaml:"disableStacktrace"`
    // 設定取樣策略。沒有 SamplingConfing 將禁止取樣
	Sampling *SamplingConfig `json:"sampling" yaml:"sampling"`
    // 設定日誌編碼。可以設定為 console 和 json。也可以透過 RegisterEncoder 設定第三方編碼格式
	Encoding string `json:"encoding" yaml:"encoding"`
    // 為encoder編碼器設定選項。詳細設定資訊在 zapcore.zapcore.EncoderConfig
	EncoderConfig zapcore.EncoderConfig `json:"encoderConfig" yaml:"encoderConfig"`
    // 日誌輸出地址可以是一個 URLs 地址或檔案路徑,可以設定多個
	OutputPaths []string `json:"outputPaths" yaml:"outputPaths"`
    // 錯誤日誌輸出地址。預設輸出標準錯誤資訊
	ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"`
    // 可以新增自定義的欄位資訊到 root logger 中。也就是每條日誌都會攜帶這些欄位資訊,公共欄位
	InitialFields map[string]interface{} `json:"initialFields" yaml:"initialFields"`
}

EncoderConfig 結構原始碼,它裡面也有很多配置選項,具體請看 這裡:

// zapcore@v1.24.0
type EncoderConfig struct {
    // 為log entry設定key。如果 key 為空,那麼在日誌中的這部分資訊也會省略
	MessageKey     string `json:"messageKey" yaml:"messageKey"`//日誌資訊的健名,預設為msg
	LevelKey       string `json:"levelKey" yaml:"levelKey"`//日誌級別的健名,預設為level
	TimeKey        string `json:"timeKey" yaml:"timeKey"`//記錄日誌時間的健名,預設為time
	NameKey        string `json:"nameKey" yaml:"nameKey"`
	CallerKey      string `json:"callerKey" yaml:"callerKey"`
	FunctionKey    string `json:"functionKey" yaml:"functionKey"`
	StacktraceKey  string `json:"stacktraceKey" yaml:"stacktraceKey"`
	SkipLineEnding bool   `json:"skipLineEnding" yaml:"skipLineEnding"`
	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"`
    // 配置 interface{} 型別編碼器。如果沒設定,將用 json.Encoder 進行編碼
	NewReflectedEncoder func(io.Writer) ReflectedEncoder `json:"-" yaml:"-"`
    // 配置 console 中欄位分隔符。預設使用 tab 
	ConsoleSeparator string `json:"consoleSeparator" yaml:"consoleSeparator"`
}
type Entry struct {
	Level      Level
	Time       time.Time
	LoggerName string
	Message    string
	Caller     EntryCaller
	Stack      string
}

例子1:基本配置

  1. zap.Config 自定義配置,看官方的一個基本例子:
package main

import (
	"encoding/json"

	"go.uber.org/zap"
)

// https://pkg.go.dev/go.uber.org/zap@v1.24.0#hdr-Configuring_Zap
func main() {
	// 表示 zap.Config 的 json 原始編碼
	// outputPath: 設定日誌輸出路徑,日誌內容輸出到標準輸出和檔案 logs.log
	// errorOutputPaths:設定錯誤日誌輸出路徑
	rawJSON := []byte(`{
      "level": "debug",
      "encoding": "json",
      "outputPaths": ["stdout", "./logs.log"],
      "errorOutputPaths": ["stderr"],
      "initialFields": {"foo": "bar"},
      "encoderConfig": {
        "messageKey": "message-customer",
        "levelKey": "level",
        "levelEncoder": "lowercase"
      }
    }`)

	// 把 json 格式資料解析到 zap.Config struct
	var cfg zap.Config
	if err := json.Unmarshal(rawJSON, &cfg); err != nil {
		panic(err)
	}
	// cfg.Build() 為配置物件建立一個 Logger
	// zap.Must() 封裝了 Logger,Must()函式如果返回值不是 nil,就會報 panic。也就是檢查Build是否錯誤
	logger := zap.Must(cfg.Build())
	defer logger.Sync()

	logger.Info("logger construction succeeded")
}

/*
Must() 函式
//  var logger = zap.Must(zap.NewProduction())
func Must(logger *Logger, err error) *Logger {
    if err != nil {
        panic(err)
    }

    return logger
}
*/

consol 輸出如下:

{"level":"info","message-customer":"logger construction succeeded","foo":"bar"}

並且在程式目錄下生成了一個檔案 logs.log,裡面記錄的日誌內容也是上面consol輸出內容。每執行一次就在日誌檔案末尾append一次內容。

例子2:高階配置

上面的配置只是基本的自定義配置,如果有一些複雜的需求,比如在多個檔案之間分割日誌。

或者輸出到不是 file 的檔案中,比如輸出到 kafka 中,那麼就需要使用 zapcore 包。

在下面的例子中,我們將把日誌輸出到 kafka 中,並且也輸出到 console 裡。並且我們對 kafka 不同主題進行編碼設定,對輸出到 console 編碼進行設定,也希望處理高優先順序的日誌。

官方例子:

package main

import (
	"io"
	"os"

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

func main() {
	// 首先,定義不同級別日誌處理邏輯
	highPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
		return lvl >= zapcore.ErrorLevel
	})
	lowPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
		return lvl < zapcore.ErrorLevel
	})

	// 假設有2個kafka 的 topic,一個 debugging,一個 errors

	// zapcore.AddSync 新增一個檔案控制程式碼。
	topicDebugging := zapcore.AddSync(io.Discard)
	topicErrors := zapcore.AddSync(io.Discard)

	// 如果他們對併發使用不安全,我們可以用 zapcore.Lock 新增一個 mutex 互斥鎖。
	consoleDebugging := zapcore.Lock(os.Stdout)
	consoleErrors := zapcore.Lock(os.Stderr)

	// 設定 kafka 和 console 輸出配置
	kafkaEncoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
	consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())

	// 把上面的設定加入到 zapcore.NewCore() 函式裡,然後再把他們加入到 zapcore.NewTee() 函式裡
	core := zapcore.NewTee(
		zapcore.NewCore(kafkaEncoder, topicErrors, highPriority),
		zapcore.NewCore(consoleEncoder, consoleErrors, highPriority),
		zapcore.NewCore(kafkaEncoder, topicDebugging, lowPriority),
		zapcore.NewCore(consoleEncoder, consoleDebugging, lowPriority),
	)

	// 最後呼叫 zap.New() 函式
	logger := zap.New(core)
	defer logger.Sync()
	logger.Info("constructed a logger")
}

例子3:日誌寫入檔案

與上面例子2相似,但是比它簡單

package main

import (
	"os"

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

func main() {
	writetofile()
}

func writetofile() {
	// 設定一些配置引數
	config := zap.NewProductionEncoderConfig()
	config.EncodeTime = zapcore.ISO8601TimeEncoder
	fileEncoder := zapcore.NewJSONEncoder(config)
	defaultLogLevel := zapcore.DebugLevel // 設定 loglevel

	logFile, _ := os.OpenFile("./log-test-zap.json", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 06666)
	// or os.Create()
	writer := zapcore.AddSync(logFile)

	logger := zap.New(
		zapcore.NewCore(fileEncoder, writer, defaultLogLevel),
		zap.AddCaller(),
		zap.AddStacktrace(zapcore.ErrorLevel),
	)
	defer logger.Sync()

	url := "http://www.test.com"
	logger.Info("write log to file",
		zap.String("url", url),
		zap.Int("attemp", 3),
	)
}

例子4:根據日誌級別寫入不同檔案

這個與上面例子2相似

package main

import (
	"os"

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

func main() {
	writeToFileWithLogLevel()
}

func writeToFileWithLogLevel() {
	// 設定配置
	config := zap.NewProductionEncoderConfig()
	config.EncodeTime = zapcore.ISO8601TimeEncoder
	fileEncoder := zapcore.NewJSONEncoder(config)

	logFile, _ := os.OpenFile("./log-debug-zap.json", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) //日誌記錄debug資訊

	errFile, _ := os.OpenFile("./log-err-zap.json", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) //日誌記錄error資訊

	teecore := zapcore.NewTee(
		zapcore.NewCore(fileEncoder, zapcore.AddSync(logFile), zap.DebugLevel),
		zapcore.NewCore(fileEncoder, zapcore.AddSync(errFile), zap.ErrorLevel),
	)

	logger := zap.New(teecore, zap.AddCaller())
	defer logger.Sync()

	url := "http://www.diff-log-level.com"
	logger.Info("write log to file",
		zap.String("url", url),
		zap.Int("time", 3),
	)

	logger.With(
		zap.String("url", url),
		zap.String("name", "jimmmyr"),
	).Error("test error ")
}

主要是設定日誌級別,和把 2 個設定的 NewCore 放入到方法 NewTee 中。

六、Hook和Namespace

zap.Hook()

Hook (鉤子函式)回撥函式為使用者提供一種簡單方法,在每次日誌內容記錄後執行這個回撥函式,執行使用者需要的操作。也就是說記錄完日誌後你還想做其它事情就可以呼叫這個函式。

package main

import (
	"fmt"

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

func main() {
	logger := zap.NewExample(zap.Hooks(func(entry zapcore.Entry) error {
		fmt.Println("[zap.Hooks]test Hooks")
		return nil
	}))
	defer logger.Sync()

	logger.Info("test output")

	logger.Warn("warn info")
}

zap.Namespace():

建立一個名稱空間,後面的欄位都在這名字空間中。Namespace 就像一個資料夾,後面檔案都放在這個資料夾裡。

package main

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

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

	logger.Info("some message",
		zap.Namespace("shop"),
		zap.String("name", "LiLei"),
		zap.String("grade", "No2"),
	)

	logger.Error("some error message",
		zap.Namespace("shop"),
		zap.String("name", "LiLei"),
		zap.String("grade", "No3"),
	)
}

輸出:

{"level":"info","msg":"some message","shop":{"name":"LiLei","grade":"No2"}}
{"level":"error","msg":"some error message","shop":{"name":"LiLei","grade":"No3"}}

七、日誌切割歸檔

lumberjack 這個庫是按照日誌大小切割日誌檔案。

安裝 v2 版本:

go get -u github.com/natefinch/lumberjack@v2

Code:

log.SetOutput(&lumberjack.Logger{
    Filename:   "/var/log/myapp/foo.log", // 檔案位置
    MaxSize:    500,  // megabytes,M 為單位,達到這個設定數後就進行日誌切割
    MaxBackups: 3,    // 保留舊檔案最大份數
    MaxAge:     28,   //days , 舊檔案最大儲存天數
    Compress:   true, // disabled by default,是否壓縮日誌歸檔,預設不壓縮
})

參照它的檔案和結合上面自定義配置的例子,寫一個例子:

package main

import (
	"fmt"

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

func main() {
	lumberjacklogger := &lumberjack.Logger{
		Filename:   "./log-rotate-test.json",
		MaxSize:    1, // megabytes
		MaxBackups: 3,
		MaxAge:     28,   //days
		Compress:   true, // disabled by default
	}
	defer lumberjacklogger.Close()

	config := zap.NewProductionEncoderConfig()

	config.EncodeTime = zapcore.ISO8601TimeEncoder // 設定時間格式
	fileEncoder := zapcore.NewJSONEncoder(config)

	core := zapcore.NewCore(
		fileEncoder,                       //編碼設定
		zapcore.AddSync(lumberjacklogger), //輸出到檔案
		zap.InfoLevel,                     //日誌等級
	)

	logger := zap.New(core)
	defer logger.Sync()

    // 測試分割日誌
	for i := 0; i < 8000; i++ {
		logger.With(
			zap.String("url", fmt.Sprintf("www.test%d.com", i)),
			zap.String("name", "jimmmyr"),
			zap.Int("age", 23),
			zap.String("agradege", "no111-000222"),
		).Info("test info ")
	}

}

八、zap使用總結

  • zap 的使用,先建立 logger,再呼叫各個日誌級別方法記錄日誌資訊。比如 logger.Info()。

  • zap 提供了三種快速建立 logger 的方法: zap.Newproduction()zap.NewDevelopment()zap.NewExample()。見名思義,Example 一般用在測試程式碼中,Development 用在開發環境中,Production 用在生成環境中。這三種方法都預先設定好了配置資訊。它們的日誌資料型別輸出都是強型別。

  • 當然,zap 也提供了給使用者自定義的方法 zap.New()。比如使用者可以自定義一些配置資訊等。

  • 在上面的例子中,幾乎都有 defer logger.Sync() 這段程式碼,為什麼?因為 zap 底層 API 允許緩衝日誌以提高效能,在預設情況下,日誌記錄器是沒有緩衝的。但是在程式退出之前呼叫 Sync() 方法是一個好習慣。

  • 如果你在 zap 中使用了 sugaredlogger,把 zap 建立 logger 的三種方法用 logger.Sugar() 包裝下,那麼 zap 就支援 printf 風格的格式化輸出,也支援以 w 結尾的方法。如 Infow,Infof 等。這種就是通用型別日誌輸出,不是強型別輸出,不需要強制指定輸出的資料型別。它們的效能區別,通用型別會比強型別下降 50% 左右。

比如 Infow 的輸出形式,Infow 不需要 zap.String 這種指定欄位的資料型別。如下程式碼:

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

強型別輸出,比如 Info 方法輸出欄位和值就需要指定資料型別:

logger.Info("failed to fetch url",
		// 強型別欄位
		zap.String("url", "http://example.com"),
		zap.Int("attempt", 3),
		zap.Duration("backoff", time.Second),
)
  • 強型別輸出和通用型別輸出區別

    通用型別輸出,經過 interface{} 轉換會有效能損失,標準庫的 fmt.Printf 為了通用性就用了 interface{} 這種”萬能型“的資料型別,另外它還使用了反射,效能進一步降低。

    zap 強型別輸出,zap 為了提供日誌輸出效能,zap 的強型別輸出沒有使用 interface{} 和反射。zap 預設輸出就是強型別。

    上面介紹,zap 中 3 種建立 logger 方式(zap.Newproduction()zap.NewDevelopment()zap.NewExample())就是強型別日誌欄位,當然,也可以轉化為通用型別,用 logger.Sugar() 方法建立 SugaredLogger。

  • zap.Namespace() 建立一個名稱空間,後面的欄位都在這名字空間中。Namespace 就像一個資料夾,後面檔案都放在這個資料夾裡。

logger.Info("some message",
    zap.Namespace("shop"),
    zap.String("shopid", "s1234323"),
  )
{"level":"info","msg":"some message","shop":{"shopid":"s1234323"}}

九、Demo原始碼地址

zap demos

十、參考

相關文章