0
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?

More than 1 year has passed since last update.

チーム開発参加の記録【2023-10~2024-03】(7) slogでErrorレベルのログをSlackに飛ばしてみた

Last updated at Posted at 2023-12-24

本シリーズのリンク

本記事で行うこと

本シリーズの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の画面:

image.png

このライブラリ、使ってみます。

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変数は書き換える必要があります。

log.go
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レベルでログ出力するコードを加えました。

app.go
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

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

実行結果です。

image.png

ログを確認する

SlackにErrorレベルのログが飛んできました。

image.png

それと同時に、標準出力とログファイルに以下が出力されました。
本記事で処理を追加した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に飛ばすことができました。

0
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
0
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?