1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

独自エラーにスタックトレースをつけて構造化ログ(slog)で出力する

Last updated at Posted at 2024-12-19

題名の通り、独自エラーにスタックトレース情報をもたせ、
slogで構造化ログとして出力できるようにします

goでスタックトレースを取得するとパフォーマンスへの影響もあるとかないとか
詳しい説明はこちらの記事が大変わかりやすかったです

Goのerrorがスタックトレースを含まない理由

実装

スタックトレースの取得

エラーの生成とともにスタック情報を取得しています

type MyError struct {
	Msg   string
	stack []uintptr
	err   error
}

func NewMyError(msg string) *MyError {
	if !hasStack {
		return &MyError{Msg: msg}
	}
	stack := make([]uintptr, maxDepth)
    // skip 2 frames [NewMyError, runtime.Callers]
	length := runtime.Callers(2, stack)
	return &MyError{Msg: msg, stack: stack[:length]}
}

履歴(スタックトレース)を格納する箱を用意して、その箱に履歴(スタックトレース)を積むイメージです
runtime.Callers(2, stack)の第一引数は取得するスタックトレースの開始位置を指定しており、
この場合2なので、自分自身(runtime.Callers)と呼び出し関数(NewMyError)をスキップします

スタックトレースの出力

エラー発生時に取得したスタックトレースを出力します
slogで出力した際に以下のように出力したいので、スライスで返すようにします

{
	"time": "2024-12-19T08:33:23.232035471Z",
	"level": "ERROR",
	"msg": "occured error",
	"stackTrace": [
		{
			"file": "/workspaces/caller_sample/cmd/main.go",
			"function": "main.f3",
			"line": 40
		},
		{
			"file": "/workspaces/caller_sample/cmd/main.go",
			"function": "main.f2",
			"line": 33
		},
	]
}
func (e *MyError) StackTrace() []map[string]interface{} {
	if !hasStack {
		return nil
	}

	if e.err != nil {
		if myErr, ok := e.err.(*MyError); ok {
			return myErr.StackTrace()
		}
	}

	frames := runtime.CallersFrames(e.stack)
	res := make([]map[string]interface{}, 0)
	for {
		frame, more := frames.Next()
		res = append(res, map[string]interface{}{
			"file":     frame.File,
			"line":     frame.Line,
			"function": frame.Function,
		})

		if !more {
			break
		}
	}
	return res
}

runtime.CallersFrames(e.stack)でスタックトレースを解析し、フレーム情報(関数名、ファイル名、行番号など)を出力する準備をしています

上記で取得できるフレーム情報をループで回し、各フレーム情報を取得しスライスに格納していきます

エラーがラップされている場合も考慮して、ラップされている場合は再起的に処理を繰り返します

コード全体

package main

import (
	"fmt"
	"log/slog"
	"os"
	"runtime"
)

const (
	maxDepth = 50
)

var hasStack = true

type MyError struct {
	Msg   string
	stack []uintptr
	err   error
}

func NewMyError(msg string) *MyError {
	if !hasStack {
		return &MyError{Msg: msg}
	}
	stack := make([]uintptr, maxDepth)
	length := runtime.Callers(2, stack)
	return &MyError{Msg: msg, stack: stack[:length]}
}

func (e *MyError) Error() string {
	return e.Msg
}

func (e *MyError) Unwrap() error {
	return e.err
}

func (e *MyError) Wrap(err error) {
	e.err = err
}

func (e *MyError) StackTrace() []map[string]interface{} {
	if !hasStack {
		return nil
	}

	if e.err != nil {
		if myErr, ok := e.err.(*MyError); ok {
			return myErr.StackTrace()
		}
	}

	frames := runtime.CallersFrames(e.stack)
	res := make([]map[string]interface{}, 0)
	for {
		frame, more := frames.Next()
		res = append(res, map[string]interface{}{
			"file":     frame.File,
			"line":     frame.Line,
			"function": frame.Function,
		})

		if !more {
			break
		}
	}
	return res
}

使ってみる

func main() {
    // slogの出力設定
	logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

	err := f1()
 
	if myErr, ok := err.(*MyError); ok {
		logger.Error("occured error", slog.Any("stackTrace", myErr.StackTrace()))
	} else {
		fmt.Println("error is not of type *MyError")
	}
}

func f1() error {
	return f2()
}

func f2() error {
	err := f3()
	newErr := NewMyError("this is f2 error")
    // f3で起きたエラーをラップ
	newErr.Wrap(err)
	return newErr
}

func f3() error {
    // エラー発生
	err := NewMyError("this is f3 error")
	return err
}

ログ

※JSONは整形しています

{
	"time": "2009-11-10T23:00:00Z",
	"level": "ERROR",
	"msg": "occured error",
	"stackTrace": [
		{
			"file": "/tmp/sandbox1721138770/prog.go",
			"function": "main.f3",
			"line": 40
		},
		{
			"file": "/tmp/sandbox1721138770/prog.go",
			"function": "main.f2",
			"line": 33
		},
		{
			"file": "/tmp/sandbox1721138770/prog.go",
			"function": "main.f1",
			"line": 29
		},
		{
			"file": "/tmp/sandbox1721138770/prog.go",
			"function": "main.main",
			"line": 19
		},
		{
			"file": "/usr/local/go-faketime/src/runtime/proc.go",
			"function": "runtime.main",
			"line": 272
		},
		{
			"file": "/usr/local/go-faketime/src/runtime/asm_amd64.s",
			"function": "runtime.goexit",
			"line": 1700
		}
	]
}

ベンチマーク

パフォーマンスへの影響がどれほどあるのかも確認してみます

テスコード

func BenchmarkWithStackTrace(b *testing.B) {
	hasStack = true
	output, _ := os.Create("output_withstack.log")
	logger := slog.New(slog.NewJSONHandler(output, nil))
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		logger.Error("occured error", slog.Any("stackTrace", NewMyError("occured error").StackTrace()))
	}

}

func BenchmarkWithoutStackTrace(b *testing.B) {
	hasStack = false
	output, _ := os.Create("output_withoutstack.log")
	logger := slog.New(slog.NewJSONHandler(output, nil))
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		logger.Error("occured error", slog.Any("stackTrace", NewMyError("occured error")))
	}
}

結果

BenchmarkWithStackTrace-10                 16770             70639 ns/op            3733 B/op         56 allocs/op
BenchmarkWithoutStackTrace-10              20995             49570 ns/op             112 B/op          2 allocs/op
  • 実行時間: スタックトレースを含む場合、約1.43倍の時間がかかる
  • メモリ使用量: スタックトレースを含む場合、約33.34倍のメモリを使用
  • アロケーション数: スタックトレースを含む場合、28倍のアロケーションが発生

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?