【Go】ロギングライブラリ zap の概要を掴む

2022/12/05に公開

この記事について

この記事はCyberAgent AI tech studio | Go Advent Calendar 2022 5日目の記事です。

対象読者

  • ✅ zapとは何か概要をざっくりと把握したい方
  • ✅ 普段goを使用して開発されている方

はじめに

業務で Go のロギングライブラリであるzapを使用する機会があったため、理解の促進の意味も込めて reference 等調べたことを記載します。

zapとは

⚡️ Blazing fast, structured, leveled logging in Go.

zap は Go での高速な構造化されたレベル別のロギングです。

github の README や referenceは以下です。

https://github.com/uber-go/zap/blob/master/README.md

https://pkg.go.dev/go.uber.org/zap

概要を掴む

以下では reference を読んで知ったことを記載します。

Logger の種類

Choosing a Logger

Choosing a Logger とあるように zapには2種類の Logger があります。

SugaredLogger

In contexts where performance is nice, but not critical, use the SugaredLogger.

パフォーマンスが重要でない場合は SugaredLogger を使用するのが良いようです。他のロギングパッケージよりも4~10倍速く構造化ロギングとprintfスタイルのロギングの両方をサポートしてくれます。

Logger

In the rare contexts where every microsecond and every allocation matter, use the Logger.

全てのマイクロ秒、アロケーションが重要な場合は Logger を使用するのが良いようです。 SugardedLogger よりもさらに速く、アロケーションも少ないですが、強く型付けされた構造化ロギングのみをサポートしてくれます。

Choosing between the Logger and SugaredLogger doesn't need to be an application-wide decision: converting between the two is simple and inexpensive.

上記のように LoggerSugaredLogger の変換は容易であるため、アプリケーション全体でどちらを使用するかを決める必要はないようです。

zapcoreとは

業務での実装を追うにあたり、zapcore packageを使用している部分も見られたため、調べてみます。zap の reference には以下のようにありました。

More unusual configurations (splitting output between files, sending logs to a message queue, etc.) are possible, but require direct use of go.uber.org/zap/zapcore.

より特殊な設定を可能にするべく zapcore を直接使用する必要があるようです。

The zap package itself is a relatively thin wrapper around the interfaces in go.uber.org/zap/zapcore.

とあるように、zap 自体は zapcore の薄いラッパーのようです。 zapcore.Encoder, zapcore.WriteSyncer, zapcore.Core インターフェースの実装を行えば独自のロガーの構築もできるようです。

Levelについて

A Level is a logging priority. Higher levels are more important.

zapcore package には7種類のロギングレベルがあり iota で管理されています。

zap/level.go
const (
	// DebugLevel logs are typically voluminous, and are usually disabled in
	// production.
	DebugLevel Level = iota - 1
	// InfoLevel is the default logging priority.
	InfoLevel
	// WarnLevel logs are more important than Info, but don't need individual
	// human review.
	WarnLevel
	// ErrorLevel logs are high-priority. If an application is running smoothly,
	// it shouldn't generate any error-level logs.
	ErrorLevel
	// DPanicLevel logs are particularly important errors. In development the
	// logger panics after writing the message.
	DPanicLevel
	// PanicLevel logs a message, then panics.
	PanicLevel
	// FatalLevel logs a message, then calls os.Exit(1).
	FatalLevel

	_minLevel = DebugLevel
	_maxLevel = FatalLevel
)

レベル別のメソッド

logger にはレベル別のメソッドが用意されており、それぞれメッセージを記録します。メッセージには渡されたフィールドがログに蓄積されたフィールドと同じように渡されます。
例えば Info レベルの実装は以下です。

logger.go
// Info logs a message at InfoLevel. The message includes any fields passed
// at the log site, as well as any fields accumulated on the logger.
func (log *Logger) Info(msg string, fields ...Field) {
	if ce := log.check(InfoLevel, msg); ce != nil {
		ce.Write(fields...)
	}
}

zap.Fieldについて

レベル別に用意されたメソッドの引数には zap.Field を渡していました。

Field is an alias for Field.

zap.Field は zapcore.Field の alias です。

zap/field.go
type Field = zapcore.Field

A Field is a marshaling operation used to add a key-value pair to a logger's context. Most fields are lazily marshaled, so it's inexpensive to add fields to disabled debug-level log statements.

Field は logger のコンテキストに key-value ペアを追加するために使用されるマーシャリングの操作に使用されるようです。

zapcore/field.go
type Field struct {
	Key       string
	Type      FieldType
	Integer   int64
	String    string
	Interface interface{}
}

reference には以下のような例があり、zap.String, zap.Int, zap.Duration はそれぞれ key と value を受け取って Field を返します。

logger, _ := zap.NewProduction()
defer logger.Sync()
logger.Info("failed to fetch URL",
  // Structured context as strongly typed Field values.
  zap.String("url", url),
  zap.Int("attempt", 3),
  zap.Duration("backoff", time.Second),
)

例えば String の実装を見てみると Field に Key, Type, String を充てて構造体を初期化していることが分かります。

zap/field.go
func String(key string, val string) Field {
	return Field{Key: key, Type: zapcore.StringType, String: val}
}

zap.Errorについて

error を受け取って Field を返す Error 関数もあります。

Error is shorthand for the common idiom NamedError("error", err).

Error は NamedError の省略記法のようです。

zap/error.go
func Error(err error) Field {
	return NamedError("error", err)
}

NamedError constructs a field that lazily stores err.Error() under the provided key. Errors which also implement fmt.Formatter (like those produced by github.com/pkg/errors) will also have their verbose representation stored under key+"Verbose". If passed a nil error, the field is a no-op.
For the common case in which the key is simply "error", the Error function is shorter and less repetitive.

NamedError は 与えられた key に基づいて err.Error() を格納するフィールドを構築します。
先の Field の Interface 部分に error を充てることで柔軟に value を格納することができます。

zap/NamedError.go
func NamedError(key string, err error) Field {
	if err == nil {
		return Skip()
	}
	return Field{Key: key, Type: zapcore.ErrorType, Interface: err}
}

zap.Syncについて

Sync calls the underlying Core's Sync method, flushing any buffered log entries. Applications should take care to call Sync before exiting.

Sync は基礎となる Core の Sync メソッドを呼び、バッファリングされたログのエントリを全てフラッシュします(蓄積されたログを全て吐き出します)。
アプリケーションが終了する前に Sync を呼び出す必要があるようです。

zap/logger.go
func (log *Logger) Sync() error {
	return log.core.Sync()
}

簡単に実装してみる

最後に簡単な実装を行ってみました。
コードは以下です。

https://go.dev/play/p/GVZX_y2MiBE

Input を想定した User 構造体に対して、バリデーションを施し、エラーを起こします。
Info レベルのログとして起こった err を表示してくれます。

package main

import (
	"github.com/go-playground/validator/v10"
	"go.uber.org/zap"
)

type User struct {
	Name     string `validate:"required"`
	Password string `validate:"required,min=8,max=20"`
}

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

	body := &User{
		Name:     "Taro",
		Password: "Pass",
	}

	validate := validator.New() //インスタンス生成
	err := validate.Struct(body)

	logger.Info("validation failed",
		zap.String("name", body.Name),
		zap.String("password", body.Password),
		zap.Error(err),
	)
}

結果として得られるログは以下です。

{
   "level":"info",
   "msg":"validation failed",
   "name":"Taro",
   "password":"Pass",
   "error":"Key: 'User.Password' Error:Field validation for 'Password' failed on the 'min' tag"
}

終わりに

今回は zap について簡単に reference を読んで概要を掴み、簡易的な実装を行いました。
今後は sugaredLogger, Loggerの違いや変換について, 独自のロガーの構築 等、更に学んでいければと思います。
誤り等ございましたら、自分の成長のために御指南いただけると幸いです。
ご精読ありがとうございました。

Discussion