本シリーズのリンク
- チーム開発参加の記録【2023-10~2024-03】(1) Go言語用ORM「Bun」をDBファーストで使う試み(SQLite使用)
- チーム開発参加の記録【2023-10~2024-03】(2) Go言語からTursoを使ってみた
- チーム開発参加の記録【2023-10~2024-03】(3) slogを使ってリクエスト・レスポンス情報をログ出力してみた
- チーム開発参加の記録【2023-10~2024-03】(4) Go言語用ORM「Bun」をDBファーストで使う試み(PostgreSQL使用)
- チーム開発参加の記録【2023-10~2024-03】(5) Go言語用ORM「Bun」でトランザクション、UPSERT、JOINを使ってみた
- チーム開発参加の記録【2023-10~2024-03】(6) Go言語用ORM「Bun」で複数のクエリーをまとめてDBサーバーで実行
- チーム開発参加の記録【2023-10~2024-03】(7) slogでErrorレベルのログをSlackに飛ばしてみた
本記事で行うこと
本シリーズの3番目の記事で、slogを使って標準出力とログファイルにログを出力しました。
本記事ではそれに加えて、ErrorレベルのログをSlackに飛ばしてみます。
サードパーティーのライブラリを探す
Slackにログを飛ばすライブラリ
「golang slog slack」でググったところ、「slog: Slack handler」というライブラリが見つかりました。
exampleコードがあったので動かしてみたところ、あれ、Slackにログが飛ばない..
原因を探ってみると、handler.goに以下の記述がありました。
go func() {
_ = h.postMessage(ctx, message)
}()
どうやら、ゴルーチンでSlackにログを飛ばそうとしたけど、その前にメインルーチン自体が終わってしまったようです。
試しにexample.goの最後に以下を追加したら、無事にSlackにログが飛びました。
time.Sleep(3 * time.Second)
Slackの画面:
このライブラリ、使ってみます。
slogで複数のHandlerを使うライブラリ
今まで使っていたJSONHandlerに加えて、上記のSlackHandlerも使いたいので、slogで複数のHandlerを使えるライブラリを探します。
「golang slog multiple handlers」でググったところ、「slog: Handler chaining, fanout, routing, failover, load balancing...」というライブラリが見つかりました。
先ほどの「slog: Slack handler」と作者が同じですね。
これも使ってみることにします。
ソースコード
それでは、今回書いたソースコード全体を載せてしまいます。
3番目の記事のコードを少しアレンジしました。
ディレクトリ構成とファイル一覧
log/ディレクトリはログファイルの出力先です。
Project Root
├── log/
│ └── *.log
├── utils/
│ ├── middlewares/
│ │ └── LoggingMiddleware.go
│ └── log.go
├── app.go
└── go.mod
utils/middlewares/LoggingMiddleware.go
LoggingMiddleware.goは、3番目の記事と全く同じなので割愛します。
utils/log.go
SlackHandlerも使うように記述します。
コード中のwebhook変数は書き換える必要があります。
package utils
import (
"io"
"log/slog"
"os"
"path/filepath"
"time"
"github.com/labstack/echo/v4"
slogmulti "github.com/samber/slog-multi"
slogslack "github.com/samber/slog-slack/v2"
)
// GetRootLogger は以下の設定を持つロガーを返します:
// - ログを標準出力とログファイルの両方に出力します
// - ログファイルは "./log" ディレクトリ配下に作成されます
// - "./log" ディレクトリがなければ、0755の権限で作成されます
// - ログファイル名は "YYYY-MM-DD-HH-MM.log" 形式です
// - ロガーはINFO以上のログをJSON形式で書き込みます
// - コード中のAddSourceをtrueにすると、ログ出力したソースコードの場所も出力されます
func GetRootLogger() *slog.Logger {
// ログ出力先
dir := "./log/"
// パスが存在しなければディレクトリを作成
if _, err := os.Stat(dir); os.IsNotExist(err) {
err = os.MkdirAll(dir, 0755) // ディレクトリがない場合は作成
if err != nil { // ディレクトリの作成に失敗したらエラー
panic(err)
}
}
filename := time.Now().Format("2006-01-02-15-04") + ".log"
path := filepath.Join(dir, filename)
logfile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
panic(err)
}
// JSONHandlerで標準出力とログファイルの両方に出力
multiWriter := io.MultiWriter(os.Stdout, logfile)
// SlackHandlerでSlackに出力
webhook := "https://hooks.slack.com/services/書き換えてください"
return slog.New(slogmulti.Fanout(
slog.NewJSONHandler(multiWriter, &slog.HandlerOptions{AddSource: true, Level: slog.LevelInfo}),
slogslack.Option{Level: slog.LevelError, WebhookURL: webhook}.NewSlackHandler(),
))
}
// GetApiLogger はAPI用のロガーを返す関数です。
// 各APIのハンドラから呼ばれることを想定しています。
// 引数としてecho.Contextを受け取り、その中から"trace"値を抽出して
// 自動で出力ログに加えます。
//
// 使用例:
//
// logger := GetApiLogger(c)
// logger.Info("こんにちは")
//
// 上記の例では、GetApiLogger関数を使用してロガーを取得し、
// "こんにちは"とメッセージをログ出力します。
func GetApiLogger(c echo.Context) *slog.Logger {
getLoggingTrace := func() string {
trace := c.Get("trace")
if trace != nil {
return trace.(string)
} else {
return "nil"
}
}
return slog.With(slog.String("trace", getLoggingTrace()))
}
3番目の記事のlog.goとの差分は以下の通りです。
@@ -8,6 +8,8 @@
"time"
"github.com/labstack/echo/v4"
+ slogmulti "github.com/samber/slog-multi"
+ slogslack "github.com/samber/slog-slack/v2"
)
// GetRootLogger は以下の設定を持つロガーを返します:
@@ -37,12 +39,14 @@
panic(err)
}
- // ログを標準出力とログファイルの両方に出力
+ // JSONHandlerで標準出力とログファイルの両方に出力
multiWriter := io.MultiWriter(os.Stdout, logfile)
- return slog.New(slog.NewJSONHandler(multiWriter, &slog.HandlerOptions{
- AddSource: true,
- Level: slog.LevelInfo,
- }))
+ // SlackHandlerでSlackに出力
+ webhook := "https://hooks.slack.com/services/書き換えてください"
+ return slog.New(slogmulti.Fanout(
+ slog.NewJSONHandler(multiWriter, &slog.HandlerOptions{AddSource: true, Level: slog.LevelInfo}),
+ slogslack.Option{Level: slog.LevelError, WebhookURL: webhook}.NewSlackHandler(),
+ ))
}
// GetApiLogger はAPI用のロガーを返す関数です。
@@ -68,4 +72,3 @@
}
return slog.With(slog.String("trace", getLoggingTrace()))
}
app.go
動作確認用に、Errorレベルでログ出力するコードを加えました。
package main
import (
"exercise_log/utils"
"exercise_log/utils/middlewares"
"log/slog"
"net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
type (
httpError struct {
Error string `json:"error"`
}
)
func handler(c echo.Context) error {
type (
requestBodyType struct {
No int `json:"no"`
}
responseType struct {
PathParam string `json:"pathParam"`
QueryParam string `json:"queryParam"`
Body int `json:"body"`
}
)
// ハンドラ先頭でGetApiLogger()を呼ぶ
logger := utils.GetApiLogger(c)
// ハンドラへの出入りをログ出力
logger.Info(".. in")
defer logger.Info(".. out")
// Errorレベルでログ出力
logger.Error("エラーです")
// パスパラメータ
pathParam := c.Param("pathParam")
// クエリパラメータ
queryParam := c.QueryParam("queryParam")
// リクエストボディ
reqBody := new(requestBodyType)
if err := c.Bind(reqBody); err != nil {
return c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
}
return c.JSON(http.StatusOK, responseType{PathParam: pathParam, QueryParam: queryParam, Body: reqBody.No})
}
func main() {
slog.SetDefault(utils.GetRootLogger())
// EchoでAPIサーバー
e := echo.New()
e.Use(middleware.Recover())
e.Use(middlewares.LoggingMiddleware)
// Routing
api := e.Group("/api")
api.POST("/foo/:pathParam", handler)
e.Logger.Fatal(e.Start(":1323"))
}
3番目の記事のapp.goとの差分は以下の通りです。
@@ -35,6 +35,9 @@
logger.Info(".. in")
defer logger.Info(".. out")
+ // Errorレベルでログ出力
+ logger.Error("エラーです")
+
// パスパラメータ
pathParam := c.Param("pathParam")
@@ -64,4 +67,3 @@
e.Logger.Fatal(e.Start(":1323"))
}
go.mod
module exercise_log
go 1.21
require (
github.com/labstack/echo/v4 v4.11.3
github.com/matoous/go-nanoid/v2 v2.0.0
github.com/samber/slog-multi v1.0.2
github.com/samber/slog-slack/v2 v2.2.0
)
require (
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/labstack/gommon v0.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/samber/lo v1.38.1 // indirect
github.com/samber/slog-common v0.11.0 // indirect
github.com/slack-go/slack v0.12.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/time v0.3.0 // indirect
)
動かしてみる
3番目の記事と同じ手順で動かしてみます。
APIを呼んでみる
http://localhost:1323/api/foo/bar?queryParam=baz
実行結果です。
ログを確認する
SlackにErrorレベルのログが飛んできました。
それと同時に、標準出力とログファイルに以下が出力されました。
本記事で処理を追加したErrorレベルのログは、2行目に出力されています。
{"time":"2023-12-24T15:54:28.665017123+09:00","level":"INFO","source":{"function":"main.handler","file":"/home/user/GolandProjects/exercise_log/app.go","line":35},"msg":".. in","trace":"fL5pBUnAE_tzLQOnt9JjE"}
{"time":"2023-12-24T15:54:28.665120317+09:00","level":"ERROR","source":{"function":"main.handler","file":"/home/user/GolandProjects/exercise_log/app.go","line":39},"msg":"エラーです","trace":"fL5pBUnAE_tzLQOnt9JjE"}
{"time":"2023-12-24T15:54:28.66519141+09:00","level":"INFO","source":{"function":"exercise_log/utils/middlewares.reqLogging","file":"/home/user/GolandProjects/exercise_log/utils/middlewares/LoggingMiddleware.go","line":127},"msg":"[request]","trace":"fL5pBUnAE_tzLQOnt9JjE","request":{"remoteIp":"::1","method":"POST","path":"/api/foo/bar","queryParams":{"queryParam":["baz"]},"body":{"no":12345}}}
{"time":"2023-12-24T15:54:28.665271209+09:00","level":"INFO","source":{"function":"main.handler","file":"/home/user/GolandProjects/exercise_log/app.go","line":53},"msg":".. out","trace":"fL5pBUnAE_tzLQOnt9JjE"}
{"time":"2023-12-24T15:54:28.66611766+09:00","level":"INFO","source":{"function":"exercise_log/utils/middlewares.respLogging","file":"/home/user/GolandProjects/exercise_log/utils/middlewares/LoggingMiddleware.go","line":166},"msg":"[response]","trace":"fL5pBUnAE_tzLQOnt9JjE","response":{"method":"POST","path":"/api/foo/bar","status":200,"contentType":"application/json; charset=UTF-8","body":{"body":12345,"pathParam":"bar","queryParam":"baz"}}}
まとめ
Errorレベルのログはすぐに確認したいです。
サードパーティーのライブラリを利用して、ErrorレベルのログをSlackに飛ばすことができました。