zapのLoggerのメソッドError()のログ出力の仕組み②
概要
GoのロギングライブラリのzapのLoggerのメソッドError
のログ出力の仕組みが気になり、
コードを読んで実装内容を確認していきました。
この記事では個人の備忘録としてその時の内容をまとめていきます。
この下記記事の後編記事になります。
log.Error
再度zapの構造体Logger
のメソッドError
の実装内容について確認していきます。
上記コードのうち、前編の記事ではメソッドlog.check
の内容を確認していきました。
以降のこの記事ではlog.check
の戻り値であるCheckedEntry
のメソッドWrite
の実装内容について見ていきたいと思います。
CheckedEntry.Write
CheckedEntry.Write
の実装の全体は次のような内容になっています。
まず関数の冒頭でレシーバのCheckedEntry
型の変数ce
がnilかどうかをチェックしています。nilの場合は早期リターンしています。今回の場合は前の記事で見た通りnilではないのでこのif文部分は実行されません。
続けてみていきます。
レシーバーCheckedEntry
のフィールドdirty
の値がtrue
かどうかをチェックしています。このフィールドはpoolがミスユースされているかどうかをベストエフォートで確認するためのフィールドになっています。
今回の場合はfalse
なのでこの部分は実行されません。
続けて見ていきます。
レシーバーCheckedEntry
のプライベートフィールドでCore
のスライス型であるcores
の要素の数分for ~ range
でイテレートしています。
Core
は次のようにloggerのインターフェースとして実装されています。
▼インタフェースCore
このインターフェースで定義されているメソッドのうち、ここでは引数にce.Entry
とfields
をとりWrite
メソッドを呼び出しています。
Core
インターフェースのメソッドWrite
の定義部分のコメントを見ると、このメソッドはEntry
とfieleds
をログ出力用にシリアライズするとあります。
今回はloggerの生成に関数NewProduction
を利用しています。
この関数は内部でConfig
のBuild
メソッドを利用して指定されたOption
とレシーバーのConfig
を元にLogger
を生成します。
このBuild
メソッド内部ではLoggerの生成で関数zapcore.NewCore
を呼び出しています。
この関数NewCore
ではCore
としてioCore
を返却しています。
そのため今回のCheckedEntry
でもioCore
が利用されています。
そのため次章でioCore
のWriteメソッドの実装を確認していきます。
▼CheckedEntry.Write
中でのメソッドioCore.Write
の呼び出し部分(再掲)
ioCore.Write
メソッドioCore.Write
の全体の実装は次のようになっています。
まず関数の冒頭でレシーバioCore
のフィールドでEncoder
型のenc
のメソッドEncodeEntry
を呼び出しています。
▼構造体ioCore
このフィールドEncoder
は次のようにinterface型として定義されています。
▼インターフェースEncoder
このインターフェースはlog entryのmarshalersとして定義されています。
▼メソッドEncoder.EncodeEntry
今回のケースではこれまで見てきた通りLoggerの生成に関数NewProduction
を利用していて、その関数内部ではConfigの生成に関数NewProductionConfig
を利用しています。
▼関数NewProductionConfig
そしてこの関数ではEncoding
にjson
を指定しています。
▼関数NewProductionConfig
でEncoding
にjson
を指定
そのため今回ioCore
のフィールドEncoder
にはjsonEncoder
が利用されます。
▼構造体jsonEncoder
次節でjsonEncoder
のメソッドEncodeEntry
の実装内容について見ていきます。
▼ioCore.Write
メソッド内部でのEncoder.EncodeEntry
の呼び出し部分(再掲)
jsonEncoder.EncodeEntry
jsonEncoder.EncodeEntry
の実装全体は次のような内容になっています。
それでは関数の冒頭から順に見ていきます。
まず最初にjsonEncoder
のメソッドclone
を呼び出してその結果を変数final
に代入しています。
このメソッドはgetJsonEncoder
などを呼び出してjsonEncoder
のクローンを生成しています。
▼構造体EncoderConfig
続けて生成した変数final
のフィールドbuf
に対してメソッドAppendByte
を呼び出してjsonログの最初の{
をappendしています。
続けて変数final
のフィールドLevelKey
の値が空でないかつfinal
のフィールドEncodeLevel
がnil
でない場合にログの出力結果に"level"
を含めるようにjsonEncoder
のメソッドaddKey
を呼び出してbuf
に"level"
を追加しています。
▼メソッドaddKey
▼"level"
が追加されたログ
{"level":"error", ...
続いてfinal.TimeKey
の値の有無を見てlogにTimeKey
を出力するかどうかを判断しています。
今回の場合final.TimeKey
の値は"ts"
のためfinal
のメソッドAddTime
でログにTimeKey
を出力するように設定します。
▼メソッドAddTime
▼TimeKey
が追加されたログ
{"level":"error","ts":1672820212.1863208,
続けてメソッドEncodeEntry
の内容を見ていきます。
enc
のLoggerName
とfinal
のNameKey
の値が共に空でないかどうかをチェックしています。
今回の場合はenc.LoggerName
が空でないためこの部分はスキップされます。
enc.LoggerNameは今回は空
続けてメソッドEncodeEntry
の内容を見ていきます
xxxで見てきた通りent.Caller
は今回の場合はセットされているためent.Caller.Defined
がtrue
になり以降のif分部分が実行されます。
ent.Caller.Definedが今回はtrue
続くif分のif final.CallerKey != ""
の判定に関して今回のケースではfinal.CallerKey
の値には"caller"
の値がセットされているためこのKeyをログ出力に含めるように前述で見てきたようにメソッドaddKey
でログに追加しています。
▼CallerKey
が追加されたログ
{"level":"error","ts":1672820212.1863208,"caller":"my-logger/main.go:71", ...
続く行のif final.FunctionKey != ""
は今回の場合は空なのでスキップされログには追加されません。
続けてメソッドEncodeEntry
の内容を見ていきます
final.MessageKey
の値はlogger.Error
メソッドの引数で設定しており今回の場合は空でないのでif分部分が実行され、ログにMessageKeyの内容が出力されます。
▼MessageKey
が追加されたログ
{"level":"error","ts":1672820212.1863208,"caller":"my-logger/main.go:71","msg":"my test msg",
続けてメソッドEncodeEntry
の内容を見ていきます
enc.buf.Len()
の長さが0より大きいかをチェックしています。今回の場合は0のためこのif分のブロックはスキップされます。
続く行でプライベート関数addFields
を呼んでいます。
▼関数AddFields
今回の場合では引数fields
はKey
が"error"
のField
をもつ要素数1のField
のスライスになります。
関数AddFields
は引数fields
の要素数分イテレートしてその要素Field
のメソッドAddTo
を呼び出しています。
▼Field.AddTo
AddTo
メソッドではレシーバのField
のType
に応じてField
のKeyとその値をログに追加する処理を出し分けています。
先ほど見たように今回の場合はErrorType
のため下記部分のプライベート関数encodeError
が呼び出されます。
▼Field.Type
がErrorType
の場合
関数encodeError
▼関数encodeError
関数encodeError
のシグネチャは引数にstring
型のkey
、err
、そしてObjectEncoder
型のenc
を取り、戻り値としてerror
型のretErr
を返します。
関数冒頭のコメントに例が記載されてあるように、関数encodeError
は引数err
をログ出力用のObjectEncoder
のフィールドにエンコードします。
後続で見ますがログ出力用のkeyは"error"
になります。
さらに加えて、err
型に応じてログ出力に追加でフィールドを追加します。このコメントにあるようにerr
がfmt.Formatter
を実装していれば${key}Verbose
を、errorGroup
型であればerrorCauses
をログに追加します。
// Encodes the given error into fields of an object. A field with the given
// name is added for the error message.
//
// If the error implements fmt.Formatter, a field with the name ${key}Verbose
// is also added with the full verbose error message.
//
// Finally, if the error implements errorGroup (from go.uber.org/multierr) or
// causer (from github.com/pkg/errors), a ${key}Causes field is added with an
// array of objects containing the errors this error was comprised of.
//
// {
// "error": err.Error(),
// "errorVerbose": fmt.Sprintf("%+v", err),
// "errorCauses": [
// ...
// ],
// }
func encodeError(key string, err error, enc ObjectEncoder) (retErr error) {
それでは順番に処理内容を具体的に見ていきます。
まず最初に変数basic
にerr.Error()
エラーメッセージを代入します。
続く行でenc.AddString(key, basic)
でkey
に(今回の場合は"error"
)basic
,つまりエラーメッセージをログ出力用に設定しています。
▼"error"
が追加されたログ
{"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",
続いて引数err
の型に応じて処理を条件分岐しています。
今回のerr
はlogger.Error
の呼び出し側ではcustom errorとしてMyError
型として実装しており、Goの標準のfmt
パッケージのFormatter
インターフェースを満たすようにFormat
メソッドを実装していました。
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)
}
(参考:全体のコードは下記の前編の記事に記載しています)
Format
メソッドはgoのfmtパッケージのPrintf
などI/O系の関数で独自にフォーマット出力したい場合にこのインターフェースを満たすようにFormat
メソッドを実装するとその内容でフォーマットするようにこのFormat
メソッドを呼び出してくれます。
今回は詳細は長くなってしまうため別の記事でまとめていきたいと思います。
下記記事部分とも関連しています。
そのため、今回はfmt.Formatter:
の場合が実行されます。
case fmt.Formatter:
verbose := fmt.Sprintf("%+v", e)
if verbose != basic {
// This is a rich error type, like those produced by
// github.com/pkg/errors.
enc.AddString(key+"Verbose", verbose)
}
}
return nil
ここで行なっているのはkey
を"errorVerbose"
にエラーのstack trace用を出力しています。
▼"errorVerbose"
が追加されたログ
{"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"
最後にこの関数内でエラーが発生した場合に備えてdefer
でエラーのrecover処理を定義しています。
以上で関数encodeError
の内容は終わりです。
それではjsonEncoder.EncodeEntry
の内容に戻ります。
関数addFields
に続く行でfinal.closeOpenNamespaces
を呼び出しています。
enc.openNamespaces
の数だけfor文でイテレートしてログに"}"
を追加する処理です。
今回の場合はenc.openNamespaces
が0
なのでこの関数では処理はスキップされます。
jsonEncoder.EncodeEntry
の内容に戻ります。
ent.Stack
とfinal.StacktraceKey
の値が共にセットされている場合にログにKeyをfinal.StacktraceKey
に値ent.Stack
をログに追加します。
今回の場合はこれまで見てきたように共に値がセットされているためログに追加されます(今回はfinal.StacktraceKey
は"stacktrace"
になります)
▼"stacktrace"
が追加されたログ
{"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"
続く行ではログ出力するの全てフィールドのkeyの設定が完了したのでログの内容に"}"
とfinal.LineEnding
を追加しています。
そして最後に戻り値用の変数ret
にfinal.buf
の値を代入し、
putJSONEncoder(final)
を呼び出した後、ret
を戻り値として返しています。
関数PutJSONEncoder
ではjsonEncoder
のリソースクリーンアップ処理を行なっています。
▼関数PutJSONEncoder
jsonEncoder.EncodeEntry
の実装は以上になります。
では再びjsonEncoder.EncodeEntry
の呼び出しもとであるメソッドioCore.Write
の実装に戻ります。
func (c *ioCore) Write(ent Entry, fields []Field) error {
buf, err := c.enc.EncodeEntry(ent, fields)
if err != nil {
return err
}
_, err = c.out.Write(buf.Bytes())
buf.Free()
if err != nil {
return err
}
if ent.Level > ErrorLevel {
// Since we may be crashing the program, sync the output. Ignore Sync
// errors, pending a clean solution to issue #370.
c.Sync()
}
return nil
}
ログ出力の値でありjsonEncoder.EncodeEntry
の戻り値を変数buf
に代入し、
その結果をc.out.Write(buf.Bytes())
で書き込んでいます。
その後、buf.Free()
でクリーンアップ処理をして、ent.Level
がErrorLevel
より大きい場合にc.Sync()
を呼び出しています。今回はent.Level
はErrorLevel
のためこの部分は実行されません。
ioCore.Write
の実装も以上になります。
そしてioCore.Write
の呼び出しもとであるCheckedEntry.Write
の実装内容に戻ります。
残りはエラーハンドリングやpoolにcheckedEntryを戻したりなどのクリーンアップ処理を行なっています。
CheckedEntry.Write
実装の内容も以上です。
最後にCheckedEntry.Write
の呼び出しもとであるlog.Error
メソッドの内容を再掲します。
まとめ
この記事ではロギングライブラリのzapのLogger
のメソッドError
の実装の後半部分である
CheckedEntry.Write
の実装内容についてみていきました。
このメソッドは実際にログで出力する各フィールドのフォーマットを行なっていることがわかりました。
そしてフォーマットした内容をio.Writeメソッドで出力していました。
共通部分はinterfaceとして実装してLoggerやEncoderの種類に応じて各interfaceを実装したレシーバーである構造値がそれぞれのメソッドを具体的に実装して処理を出し分けていることも分かりました。
また、エラー発生までのstack traceの出力ではfmt
パッケージのFormatter
インタフェースの実装を満たしているかをチェックしてFormat
メソッドで実際にstack traceの内容をフォーマットしていることも分かりました。
前編の記事と共に今回学んだことを自分でログのフォーマットを変えたいなどのケースに活かしていけたらと思いました。
▼メソッドlog.Error
(再掲)
Discussion