ErrorsWithStack という Goa v3 プラグインを作った

errorswithstackGoa v3プラグインで、オリジナルのサービスエラーにスタックトレースを追加します。このプラグインgithub.com/cockroachdb/errors/withstack に依存しています。

github.com

プラグインの有効化

プラグインを有効にするには、下記のように errorswithstack を import します。

import (
  _ "github.com/tchssk/goaplugins/v3/errorswithstack"
  . "goa.design/goa/v3/dsl"
)

コード生成への影響

プラグインを有効化すると goa ツールの gen コマンドの挙動が変わります。

gen コマンドの出力は次のように変更されます:

  1. すべてのエラー初期化用ヘルパー関数は、 WithStackDepth() を使ってオリジナルのサービスエラーにスタックトレースを追加するよう変更されます。

     func MakeInternalError(err error) *goa.ServiceError {
     -   return goa.NewServiceError(err, "internal_error", false, false, true)
     +   return goa.NewServiceError(withstack.WithStackDepth(err, 1), "internal_error", false, false, true)
     }
    

ミドルウェア

Goa エンドポイントミドルウェアを使ってエラーをキャプチャーすることができます。 GetOneLineSource() を使って呼び出し元を抽出することができます:

func ErrorLogger(logger *log.Logger) func(goa.Endpoint) goa.Endpoint {
    return func(e goa.Endpoint) goa.Endpoint {
        return goa.Endpoint(func(ctx context.Context, req any) (any, error) {
            res, err := e(ctx, req)
            if err != nil {
                file, line, _, ok := withstack.GetOneLineSource(err)
                if ok {
                    logger.Printf("%s:%d: %v", file, line, err) // file.go:15 something went wrong
                }
            }
            return res, err
        })
    }
}

もしくは GetReportableStackTrace() を使うこともできます;

func ErrorLogger(logger *log.Logger) func(goa.Endpoint) goa.Endpoint {
    return func(e goa.Endpoint) goa.Endpoint {
        return goa.Endpoint(func(ctx context.Context, req any) (any, error) {
            res, err := e(ctx, req)
            if err != nil {
                if st := withstack.GetReportableStackTrace(errors.Unwrap(err)); st != nil {
                    if len(st.Frames) >= 1 {
                        frame := st.Frames[len(st.Frames)-1]
                        logger.Printf("%s:%d: %v", frame.AbsPath, frame.Lineno, err) // /path/to/file.go:15 something went wrong
                    }
                }
            }
            return res, err
        })
    }
}

エラーの基になる具体的な値は ServiceError です。型アサーションして条件を作ることもできます:

func ErrorLogger(logger *log.Logger) func(goa.Endpoint) goa.Endpoint {
    return func(e goa.Endpoint) goa.Endpoint {
        return goa.Endpoint(func(ctx context.Context, req any) (any, error) {
            res, err := e(ctx, req)
            if err != nil {
                if serviceError, ok := err.(*goa.ServiceError); ok {
                    if serviceError.Fault {
                        file, line, _, ok := withstack.GetOneLineSource(err)
                        if ok {
                            logger.Printf("%s:%d: %v", file, line, err) // file.go:15 something went wrong
                        }
                    }
                }
            }
            return res, err
        })
    }
}

また report.ReportError を使って Sentry にエラーを送ることもできます:

func ErrorReporter() func(goa.Endpoint) goa.Endpoint {
    return func(e goa.Endpoint) goa.Endpoint {
        return goa.Endpoint(func(ctx context.Context, req any) (any, error) {
            res, err := e(ctx, req)
            if err != nil {
                report.ReportError(err)
            }
            return res, err
        })
    }
}