💎

Golang on CloudRunでのCustom Logger実装

2021/12/19に公開

背景

Golang on CloudRun で、builtinのlogライブラリをwrapしてcustom Loggerを実装してみた。

Loggingライブラリいくつか見てみたが、GCP面倒見いいしそれほどリッチな機能はいらないかなと思いつつ、最低限必要な機能をcustom loggerで実装してみた。

Loggingの目的

  • アプリケーションのコードのバグを見つける
  • サーバーのエラー前後の動作を確認する
  • パフォーマンスの問題を見つける

Loggingで表示してはいけないもの

  • 名前
  • IPアドレス
  • クレジットカード番号

実装

GolangのLogライブラリ

Golangでは、biltinでlogライブラリがある。
ログレベル(DEBUG, WARN, ERROR)は無いが、基本的なLogging機能は備えてる。デフォルトでは、logパッケージは標準出力(stderr)に出力される。

CloudRunの場合、標準出力がそのままStackdriverに送られる。なので、Stackdriverのloglevelに合わせたJsonフォーマットでLog.Print()すれば、Stackdriver側でLogレベルを判別してくれる。

main.go
package main
import "log"

func main() {
    log.Println("Hello world!")
}
Terminal
$ go run main.go
2021/19/09 17:21:53 Hello world!

Logger

interfaceで、使用したいログレベルのメソッドを定義する。
初期化時にLoggerを差し替えたりと、融通は効くかなと。

sdlogger/print.go
package sdlogger

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
	"time"
)

const (
	DEBUG = "DEBUG"
	INFO  = "INFO"
	WARN  = "WARN"
	ERROR = "ERROR"
)

type Logger interface {
	Debugf(format string, v ...interface{})
	Infof(format string, v ...interface{})
	Warnf(format string, v ...interface{})
	Errorf(format string, v ...interface{})
}

var logger Logger = &BasicLogger{
	Logger: log.New(os.Stderr, "", log.LstdFlags),
}

func Initialize(l Logger) {
	logger = l
}

type BasicLogger struct {
	Logger *log.Logger
}

func stackdriverFmtJSON(level string, message string) string {
	entry := map[string]string{
		"severity": level,
		"message":  message,
		"time":     time.Now().Format(time.RFC3339Nano),
	}
	bytes, _ := json.Marshal(entry)
	return string(bytes)
}

func localFmt(level string, message string) string {
	return fmt.Sprintf("%s %s", level, message)
}

func (bl *BasicLogger) Printf(level string, format string, v ...interface{}) {
	m := fmt.Sprintf(format, v...)
	if IS_GCP {
		bl.Logger.Println(stackdriverFmtJSON(level, m))
	} else {
		bl.Logger.Println(localFmt(level, m))
	}
}

func (bl *BasicLogger) Debugf(format string, v ...interface{}) {
	bl.Printf(DEBUG, format, v...)
}

func (bl *BasicLogger) Infof(format string, v ...interface{}) {
	bl.Printf(INFO, format, v...)
}

func (bl *BasicLogger) Warnf(format string, v ...interface{}) {
	bl.Printf(WARN, format, v...)
}

func (bl *BasicLogger) Errorf(format string, v ...interface{}) {
	bl.Printf(ERROR, format, v...)
}

使うときは、mainなどの早いタイミングで初期化させる。

main.go
package main

func init() {
	sdlogger.Initialize(...)
}

main() {
	err := fmt.Errorf("error something.")
	sdlogger.Warnf("waring something... %v", err)
}

まとめ

Golangのfmtではログレベルが存在しないのは、システムによって扱うべきログレベルは違うという考え方から来ているらしい。確かに、cli toolであれば、細かなロギングなんていらないけど、APIだと環境によって細かにロギングしたかったり。

今回は、自前でCustomLogger を実装してみたが、APIの要件によっては、別のlogライブラリ入れるのも手かなと。自前で実装すると、テスト書く手間もかかるし。
zap、zerolog辺りが気になるので触ってみたい。
https://github.com/uber-go/zap
https://github.com/rs/zerolog

参考

https://pkg.go.dev/log
https://www.honeybadger.io/blog/golang-logging/
https://www.kaoriya.net/blog/2018/12/16/

Discussion