Goにおいてログの設定を自分なりに作成してみた

# 概要

今回はGoを使用したログの設定を私なりに作成してみたのでアウトプットとしてこちらに記載させていただきます！

# 環境

構築にはDockerを使用します。

`Dockerfile`は下記を参照してください。

```dockerfile
FROM golang:1.20-alpine3.16 AS dev
ENV ROOT /app
WORKDIR ${ROOT}
RUN apk update && apk add --no-cache git
COPY go.mod go.sum ./
RUN go mod download
COPY . .
EXPOSE 8080
CMD ["go", "run", "main.go"]
```

```docker-compose.yml
version: '3.8'
services:
  db:
    image: mysql:8.0
    container_name: mysql
    env_file:
      - .env
    environment:
      MYSQL_ROOT_PASSWORD: $DB_ROOT_PASSWORD
      MYSQL_DATABASE: $DB_NAME
      MYSQL_USER: $DB_USER
      MYSQL_PASSWORD: $DB_PASSWORD
    volumes:
      - ./mysql:/var/lib/mysql
    ports:
      - 3306:3306

  api:
    build:
      context: .
      target: dev
    container_name: api
    working_dir: /app
    env_file:
      - .env
    environment:
      APP_ENV: $APP_ENV
    volumes:
      - .:/app
    ports:
      - 8080:8080
    depends_on:
      - db
```

# そもそもログがなぜ必要なのか

まず、そもそもなぜログが必要なのかについて簡単に解説します。

主な理由は下記です。

1. システムに問題が発生した際に原因を特定しやすくするため
1. システムの動きを追跡するため
1. システムに何が入ってきて、何が出ていくのかを把握するため

もしシステムに問題が起きた際に、ログがまったくなければどこでシステムがエラーになっているのかを特定することはかなり難しくなります。

エラー原因の発見が不可能ではないにしても非常に効率が悪く原因を発見するのにかなりの時間がかかってしまいます。

またシステムが裏側でどのように稼働しているのか、どのようなデータが入ってきて出ていくのかなど様々なことに役に立つためログは非常に重要な役割を持っています。

# ログの設定

それでは本題のログの設定を行なっていきます。

また今回はログを構造化できる`logrus`を使用していきます。

`logrus`について詳しく知りたい方は下記を参照してください！

https://github.com/sirupsen/logrus

```pkg/logging/logging.go
package logging

import (
	"os"

	log "github.com/sirupsen/logrus"
)

func SetupLogger() (*log.Logger, error) {
	logger := log.New()
	logger.SetFormatter(&log.JSONFormatter{})
	logger.SetReportCaller(true)

	if os.Getenv("APP_ENV") == "production" {
		logger.SetLevel(log.InfoLevel)
	} else {
		logger.SetLevel(log.DebugLevel)
	}

	file, err := getLogFile()
	if err != nil {
		return nil, err
	}

	logger.SetOutput(file)
	return logger, nil
}

func getLogFile() (*os.File, error) {
	file, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	if err != nil {
		return nil, err
	}

	return file, nil
}
```

それではひとつひとつ解説していきます。

- まず今回はログの設定を汎用的に利用したいので、`log.New()`でインスタンスを作成してます。

```go
logger := log.New()
```

- ロガーの設定を行う

```go
logger.SetFormatter(&log.JSONFormatter{}) // ログをJSONフォーマットで出力する
logger.SetReportCaller(true) // ログ内に呼び出し元の情報を追加する(/app/main.go:10などの情報)

if os.Getenv("APP_ENV") == "production" { // 環境によってログレベルを設定
    logger.SetLevel(log.InfoLevel)
} else {
    logger.SetLevel(log.DebugLevel)
}
```

- 出力先のファイルを作成

```go
file, err := getLogFile()
if err != nil {
    return nil, err
}

// 省略

func getLogFile() (*os.File, error) {
	file, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	if err != nil {
		return nil, err
	}

	return file, nil
}
```

`app.log`というファイルがない場合作成し、ある場合は末尾に書き込みを行うように設定。

`os`モジュールについてよくわからない場合は、下記を参照してみるといいかもしれません。

`os.OpenFile`のオプションフラグ(`os.O_CREATE`など)についても記載されています。

https://pkg.go.dev/os#pkg-constants:~:text=WriteFile-,Constants,-%C2%B6

- ログの出力先を指定

```go
logger.SetOutput(file)
```

先ほどの`getLogFile()`から取得したファイルオブジェクトを指定しています。

つまり、`app.log`にログが出力するように指定しています。

これで設定は終わりです。

次は実際に出力させてみましょう。

# ログの出力

トップのmain.goで呼び出して使ってみましょう。

```main.go
package main

import (
	"<モジュールのルートパス>/pkg/logging"
)

func main() {
	logger, err := logging.SetupLogger()
	if err != nil {
		panic(err)
	}

	logger.Info("Hello world!")
}
```

実行すると`app.log`が作成され、ファイル内に下記のようなログが出力されます。

```text
{"file":"/app/main.go:14","func":"main.main","level":"info","msg":"Hello World","time":"2023-02-18T09:25:05Z"}
```

もしタイムゾーンを変更したい場合、環境変数`TZ`に`Asia/Tokyo`を設定すれば変更できます。

# まとめ

いかがだったでしょうか。

まだまだGoにおける今回のログの設定は改善の余地があると思いますが、学習を進めつつ改善できたらなと思っています。

もし至らない点等ありましたら、ご指摘いただけると幸いです。

最後まで読んでいただきありがとうございました！