labstack/echoとmiddleware.Loggerとエラーレスポンス

github.com/labstack/echoのロギングについての備忘録です.
echoのログを活用していきたい...と考えてます。

# 前提

* go 1.19
* echo v4.10.0
* echoのErrorHandlerはDefaultHTTPErrorHandlerを使用
    * https://github.com/labstack/echo/blob/v4.10.0/echo.go#L389-L429

# はじめに、正常応答させてみる

echoの`middleware.Logger`をカスタマイズせずに使用します。

```go 
// ~省略

e := echo.New()
e.Use(middleware.Logger())

// ~省略

e.GET("/", func(ctx echo.Context) error {
  return ctx.String(http.StatusOK, "Hello world!")
})
```

curlで呼び出します
```sh 
#localhost, port=8888で実行しているとき
curl http://localhost:8888/
```

logの内容 
```json
{"time":"2023-01-22T03:52:21.307000008Z","id":"","remote_ip":"/*remote ip*/","host":"localhost:8888","method":"GET","uri":"/","user_agent":"curl/7.84.0","status":200,"error":"","latency":134542,"latency_human":"134.542µs","bytes_in":0,"bytes_out":12}
```

正常な振る舞いをしているので`error`は空です

# エラー応答について

正常時のログを確認したので、エラー応答のさせ方によるログやレスポンスの違いを見ていきます

## 1. echo.HTTPErrorを返してみる
``` go
e.GET("/", func(ctx echo.Context) error {
  // return ctx.String(http.StatusOK, "Hello world!")
  return &echo.HTTPError{Code: http.StatusNotFound, Message: "エラーメッセージ", Internal: errors.New("エラー内容")}
})
```

logの内容です
``` json
{"time":"2023-01-22T04:01:40.613879378Z","id":"","remote_ip":"/*remote ip*/","host":"localhost:8888","method":"GET","uri":"/","user_agent":"curl/7.84.0","status":404,"error":"code=404, message=エラーメッセージ, internal=エラー内容","latency":84958,"latency_human":"84.958µs","bytes_in":0,"bytes_out":39}
```

`error`の値として`echo.HTTPError`で定義した内容（Code, message, internal）が出力されてます。

この時のhttpレスポンスのBodyです
```json 
{"message":"エラーメッセージ"}
```

### （補足）echo.HTTPErrorを返すけどレスポンスのBodyを自前のstructで定義したい場合 

レスポンスに含まれるのがmessageだけでは物足りないという場合、`echo.HTTPError`のMessageに文字列ではなく構造体をセットすることでレスポンスBodyの内容を変えることができます。

```go 
e.GET("/", func(ctx echo.Context) error {
	// return ctx.String(http.StatusOK, "Hello world!")
    // return &echo.HTTPError{Code: http.StatusNotFound, Message: "エラーメッセージ", Internal: errors.New("エラー内容")}

	msgObj := struct {
		Prop1 int
		Prop2 string
	}{
		Prop1: 100,
		Prop2: "AAAAA",
	}
	return &echo.HTTPError{Code: http.StatusNotFound, Message: msgObj, Internal: errors.New("エラー内容")}
})
```

logの内容です
```json 
{"time":"2023-01-22T04:05:37.928444793Z","id":"","remote_ip":"/*remote ip*/","host":"localhost:8888","method":"GET","uri":"/","user_agent":"curl/7.84.0","status":404,"error":"code=404, message={100 AAAAA}, internal=エラー内容","latency":3021208,"latency_human":"3.021208ms","bytes_in":0,"bytes_out":30}
```

`error`のmessage部分が構造体に置き換わっていることが確認できます。

httpレスポンスのBody 

```json 
{"Prop1":100,"Prop2":"AAAAA"}
```

狙い通り、構造体をjson化して返してくれています。今回はProp1/Prop2といった形でレスポンスをさせましたが、`{"error_code":"~", "message":"~~"}`など、レスポンスBodyに`message`だけでなく、サービス固有のエラーIDなどを付け加えたい場合に使えそうです。

## 2. errorをそのまま返してみる

```go 
func e.GET("/", func(ctx echo.Context) error {
	// return ctx.String(http.StatusOK, "Hello world!")
	// return &echo.HTTPError{Code: http.StatusNotFound, Message: "エラーメッセージ", Internal: errors.New("エラー内容")}
	// msgObj := struct {
	// 	Prop1 int
	// 	Prop2 string
	// }{
	// 	Prop1: 100,
	// 	Prop2: "AAAAA",
	// }
	// return &echo.HTTPError{Code: http.StatusNotFound, Message: msgObj, Internal: errors.New("エラー内容")}

	return errors.New("エラー内容")
})
```

logの内容です
```json
{"time":"2023-01-22T04:13:47.412619673Z","id":"","remote_ip":"/*remote ip*/","host":"localhost:8888","method":"GET","uri":"/","user_agent":"curl/7.84.0","status":500,"error":"エラー内容","latency":832958,"latency_human":"832.958µs","bytes_in":0,"bytes_out":36}
```
`error`の値には`errors.New()`した内容が出力されています


httpレスポンスのBodyです
```json 
{"message":"Internal Server Error"}
```

InternalServerError（StatusCode=500)固定なので400, 401, 404など使い分けをしたい場合は要求を満たせないです。

## 3. 正常な挙動を装ってInternalServerErrorを返してみる 

```go 
func e.GET("/", func(ctx echo.Context) error {
	// return ctx.String(http.StatusOK, "Hello world!")
	// return &echo.HTTPError{Code: http.StatusNotFound, Message: "エラーメッセージ", Internal: errors.New("エラー内容")}
	// msgObj := struct {
	// 	Prop1 int
	// 	Prop2 string
	// }{
	// 	Prop1: 100,
	// 	Prop2: "AAAAA",
	// }
	// return &echo.HTTPError{Code: http.StatusNotFound, Message: msgObj, Internal: errors.New("エラー内容")}

	// return errors.New("エラー内容")

	msgObj := struct {
		Prop1 int
		Prop2 string
	}{
		Prop1: 100,
		Prop2: "AAAAA",
	}
	return ctx.JSON(http.StatusInternalServerError, msgObj)
})
```

logの内容です
```json 
{"time":"2023-01-22T04:30:00.244131346Z","id":"","remote_ip":"/*remote ip*/","host":"localhost:8888","method":"GET","uri":"/","user_agent":"curl/7.84.0","status":500,"error":"","latency":1925000,"latency_human":"1.925ms","bytes_in":0,"bytes_out":30}
```

`error`の値は空

httpレスポンスのBodyです
```json
{"Prop1":100,"Prop2":"AAAAA"}
```

# まとめ

DefaultHTTPErrorHandlerを使う前提でのエラー時のログについて

* `echo.HTTPError`を返す場合
* `error`を返す場合
* `ctx.JSON()`で返す場合

それぞれのログ出力を確認しました。

StatusCodeの使い分けやログ出力内容の充実など考えると、`echo.HTTPError`を返すようにするのが良さそうです