zapのLoggerのメソッドError()のログ出力の仕組み①
概要
Goのロギングライブラリとしてzap
を使うケースがよくあると思います。
今回はzap
の二種類のLoggerのうち高パフォーマンスで構造化ログのみに対応しているLogger
に焦点を当てて、zap
のLogger
のメソッドの1つError
メソッドがエラーをどのようにログ出力しているのかを確認するためzapの実装を見てみました。
この記事ではその時の個人の備忘録としてまとめていきます。
長くなるので前後編で二つの記事に分ける予定です。
全体のコード
まずサンプルコードとして次のようなコードを今回は使用しました。
(実行環境はMacOSになります。)
package main
import (
"fmt"
"go.uber.org/zap"
"golang.org/x/xerrors"
"log"
)
var (
errInfra = xerrors.New("infra error")
)
type MyError struct {
msg string
err error
frame xerrors.Frame
}
func (m *MyError) Error() string {
return fmt.Sprint(m)
}
// Format implements fmt.Format
func (m *MyError) Format(s fmt.State, c rune) {
xerrors.FormatError(m, s, c)
}
// FormatError implements xerrors.FormatError
func (m *MyError) FormatError(p xerrors.Printer) error {
p.Print(m.msg)
m.frame.Format(p)
return m.err
}
func NewMyError(err error, msg string) error {
return &MyError{
msg: msg,
err: err,
frame: xerrors.Caller(1),
}
}
func ui() error {
if err := usecase(); err != nil {
return NewMyError(err, "failed to exec usecase()")
}
return nil
}
func usecase() error {
if err := infra(); err != nil {
return NewMyError(err, "failed to exec infra()")
}
return nil
}
func infra() error {
return errInfra
}
func main() {
logger, err := zap.NewProduction()
if err != nil {
log.Fatalf("can't initialize zap logger: %v", err)
}
defer logger.Sync()
if err := ui(); err != nil {
msg := "my test msg"
logger.Error(
msg,
zap.Error(err),
)
}
}
上記コードを実行すると次のような結果が出力されます。
(ログのキーのうちerrorVerbose
やstacktrace
の値で実行環境のパスの部分はxxx
やyyy
などとしています)
{"level":"error","ts":1672820212.1863208,"caller":"my-logger/main.go:71","msg":"my test msg","error":"failed to exec usecase(): failed to exec infra(): infra error","errorVerbose":"failed to exec usecase():\n main.ui\n /Users/xxx/yyy/my-logger/main.go:46\n - failed to exec infra():\n main.usecase\n /Users/xxx/yyy/my-logger/main.go:53\n - infra error:\n main.init\n /Users/xxx/yyy/my-logger/main.go:11",,"stacktrace":"main.main\n\t/Users/xxx/yyy/my-logger/main.go:71\nruntime.main\n\t/usr/local/Cellar/go/1.19.4/libexec/src/runtime/proc.go:250"
上記コード中で今回見ていくのは下記部分になります。
logger.Error(
msg,
zap.Error(err),
)
loggerインスタンス生成部分のzap.NewProduction()
などの処理は割愛します。
それでは次章以降で順にlogger.Error
の実装を見ていきます。
log.Error
logger
のメソッドError
は引数に指定されたメッセージをlevel ErrorLevel
でログ出力します。
▼メソッドlog.Error
Error
メソッドの実装を確認すると、メソッド内部でloggerのプライベートメソッドcheck()
を呼び出しています。その後その戻りce
がnilでない場合にce.Write(fields...)
を呼び出しています。
以降ではこの記事ではメソッドlog.check
の実装内容について見ていきます。
後編の記事でce.Write(fields...)
の実装を見ていきます。
log.check
check
メソッドのシグネチャを確認すると、引数にzapcore.Level
とmsg
(string)を受け取り、
戻り値としてzapcore.CheckedEntry
のポインタを返しています。
▼zapcore.CheckedEntry
コメントを確認するとCheckedEntry
はログのアウトプットに許可したEntryのコレクションであることがわかります。
詳細は実装の確認時に随時見ていきます。
それではcheck
メソッドの実装について見ていきます。
まず最初にint型の定数callerSkipOffset
を宣言して値として2
を代入しています。
コメントにある通り、これはlogger.check
とこのメソッドの呼び出しもとであるlogger.Error
のcallerをスキップするために2を指定しています。
後にStackTraceのcapture部分で参照されます。
続いて無効化されているログ呼び出しによるコストを減らすためにこちらで確認しています。
今回の場合!log.core.Enabled(lvl)
がtrueのためこの部分は実行されません。
続いてlog.check
の引数のmsg
,ログレベルのlvl
、log.name
、そして現在時刻をフィールドにセットしてzapcore.Entry
を生成しています。
zapcore.Entry
の定義を見るとEntry
はcomple log message
を表しているとのことです。
ここでセットされているフィールドの他にzapcore.EntryCaller
型のCaller
やstring型のStack
などがあります。
続けてlog.core
のCheck
メソッドを呼び出しています。
メソッドCheck
は次のようにCore
インターフェースで定義されています。
コメントを確認するとこのCheck
メソッドは指定されたEntry
がログ出力されるべきかどうかを判定するメソッドです。もしここでログ出力対象として判定されると、Entry
はCheckedEntry
にappendされてその結果がCheck
の戻り値として返却されます。
今回はcore
にはSampler
がセットされているのでSampler
のCheck
メソッドが呼び出されます。
この結果、ce
にはCheckedEntry
が入ります。
続けてlog.Check
の処理に戻ってwillWrite
にはtrue
がそれぞれ代入されます。
▼zapcore.Entry
zapcore.EntryCaller
の定義を確認するとロギング関数のcallerを表す構造体になっています。
フィールドはスタックトレース用にプログラムカウンタのPC
、File
やLile
、Function
などが定義されています。
続けてcheck
メソッドのコードを見ていきます。Entry
のLevel
に応じて振る舞いをセットアップしています。今回はErrorLevel
のためここでのswitchブロックには当てはまらずにスキップされます。
log.check
の内容を続けて見ていきます。
willWrite
の値は今回の場合はtrue
が入るのでここの処理はスキップされて処理は継続します。
続けてlog.check
を見ていきます。
CheckedEntry
のフィールドのErrorOutput
設定と変数addStack
の定義を行なっています。
変数addStack
にはlog.addStack.Enabled(ce.Level)
の結果true
が代入されます。
続けてlog.check
の内容を見ていきます。
変数stackDepth
の定義を行なっています。先ほど見た通りaddStack
にはtrue
がセットされているためstackDepth
にはstacktraceFull
が代入されます。
続けてプライベート関数のcaptureStacktrace
を呼び出して結果を変数stack
に代入しています。
captureStacktrace
captureStacktrace
のシグネチャを確認すると、引数にint
型のskip
とstacktraceDepth
型(intのtype alias)のdepth
を受け取って、プライベートの構造体stacktrace
が戻り値となっています。
▼stacktraceDepth
型
▼構造体stacktrace
コメントを確認するとcaptureStacktrace
指定されたdepth
のスタックトレースをキャプチャする関数とのことです。引数skip
で指定された分スタックフレームをスキップします。
それでは順に実装をみていきます。
まず関数の冒頭で_stacktracePool
からメソッドGet()
でpoolからstacktraceを取得するようにしています。
続けて引数depth
に応じてstack.pcs
に値をセットしています。
前述で見てきたようにcaptureStacktrace
の呼び出し側でdepth
にはstacktraceFull
が渡されているのでstack.pcs
にstack.storage
が代入されます。
Go標準のruntime
パッケージの関数Callers
を呼び出しています。
コメントにある通り引数skip
にskip+2
を指定してruntime.Callers
とcaptureStacktrace
自身の呼び出しのスタックフレームをスキップするように指定しています。
runtime.Callers
の実装の詳細についてはここでは長くなるので割愛して別記事で見ていきたいと思います。
そしてdepth
の値に応じて処理を分岐しています。
今回の場合のようにstacktraceFull
の場合、
stack.storage
にpcs
を、stack.pcs
に実際のスタックフレーム数のstack.pcs[:numFrames]
を代入しています。
続けでcaptureStacktrace
の実装を見ていきます。
関数の最後にstack.frames
にruntime
パッケージの関数CallersFrames
の呼び出した結果を代入して、最後に stack
をreturnしています。
関数runtime.CallersFrames
は[]uintptr
型の引数callers
を受け取ってruntime.Frames
を返却します。
runtime.Frames
はスタックフレームの関数、ファイル、実行行数などの情報を保持している構造体になります。
以上がcaptureStacktrace
の実装内容になります。
ここから再びlog.check
メソッドの実装に戻ります。
captureStacktrace
の呼び出し後はdefer
でメソッドFree
を呼び出してstacktrace
のリソースをリリースして、poolに戻します。
続けてlog.capture
メソッドの実装をみていきます。
stacktrace.Count
メソッドでstacktraceのプログラムカウンタの数をチェックしています。
今回の場合はプログラムカウンタ数は3
で0
ではないのここの処理はスキップされます。
続くlog.check
の行では次のようにstacktrace
のメソッドNext
を呼び出しています。
これはstack trace中の次のstack frameを返す関数です。次のstack frameがあれば、メソッドNext
の第一引数でそのframeを、第二引数more
はtrue
を返します。
続いてのlog.check
メソッドの内容を見ていきます。
構造体Logger(log)
のbool型のフィールドaddCaller
の値がtrueの場合にzapcore.CheckedEntry
型の変数ce
のフィールドCaller
にzapcore.EntryCaller
を代入しています。
コメントにある通りzapcore.EntryCaller
はロギング関数のcallerを表す構造体になっています。
ここではstack.Next()
で取得したFrame
のPC
,File
,Line
,Function
などの値をセットしています。
続けてlog.check
メソッドの内容を確認していきます。
今回の場合では変数addStack
の値がtrue
なのでこの部分の処理は実行されます。
bufferpool.Get()
でpoolからbuffer
を取得して変数buffer
に代入しています。
続けてブライベート関数newStackFormatter
を呼び出してプライベート構造体のstackFormatter
を生成しています。
構造体stackFormatter
の定義を確認するとstack traceを読みやすいようにフォーマットするための構造体とコメントされています。
▼構造体stackFormatter
log.check
メソッドの内容を続けて確認していくと生成したstackFormatter
のメソッドFormatFrame
を呼び出しています。
メソッドFormatFrame
はruntime.Frame
型の引数frame
をフォーマットします。
メソッド内部で構造体Buffer
のメソッドAppendString
やメソッドAppendByte
などを利用してスタックフレームの関数やファイル、実行行などの情報をフォーマットしています。
続けてlog.check
メソッドの内容を見ていきます。
先ほど見たframe, more := stack.Next()
の行の結果more
にはtrue
がセットされており、
その結果stackFormatter
のメソッドFormatStack
を呼び出します。
メソッドFormatStack
は引数にstacktrace
を受け取り、そのstack traceのまだ処理していない残りのスタックフレームをフォーマットします。
実装内容としてはfor
分で前述で見てきたメソッドstack.Next()
を呼び、値が変える限りその戻り値stack frameのメソッドで先ほどもみたFormatFrame
を呼び出してstack frameをフォーマットします。
log.check
メソッドの内容に戻ります。
CheckedEntry
のフィールドStack
にbuffer
のメソッドString()
の結果を代入しています。
最後にメソッドのlog.check
戻り値として変数ce
を返却しています。
まとめ
この記事でzap
のLogger.Error
メソッドの内容の前半部分であるメソッドlog.check
の実装について見ていきます。
メソッドlog.check
ではログ出力用に利用する構造体zapcore.Entry
やzapcore.CheckedEntry
の生成や初期化、ログレベルに応じたログ処理の前設定、ログでstack traceの出力が有効化されている場合はstack traceのキャプチャなどの処理を行なっていました。
▼メソッドLogger.Error
(再掲)
▼メソッドlog.check
(再掲)
後編の記事ではLogger.Error
メソッドのもう一つのメソッドであるce.Write(fields...)
の内容を見ていきます。このメソッドは今回生成したCheckedEntry
を利用してログ出力する内容のフォーマット処理を行なっています。
Discussion