[Golang] 標準ライブラリ "log" でログレベルを設定する
Goには3rdパーティ製のロギングライブラリが豊富にあるが、主要なものは構造化ロギングに注力を置いているものが多く、「ログレベルの設定と少しフォーマットができれば十分」みたいなライトなユースケースに合うライブラリは現状見当たらなかった。
なので今回、標準ライブラリのみでちょっとリッチなロギングができる構造体を作ったので共有。 logger.go
などと命名してこのファイルを1枚差し込めば、ログレベルを設定できるロギングが可能。
ソースコード
全体的に標準ライブラリの log
を最大限に活用したつくりになっている。
package logger
import (
"fmt"
"log"
"os"
"runtime"
)
const totalStep = 5
const (
ERROR = iota + 1
WARNING
INFO
DEBUG
)
func SetLogLevel() int {
logLevel := os.Getenv("LOG_LEVEL")
switch logLevel {
case "INFO", "info":
return INFO
case "DEBUG", "debug":
return DEBUG
case "ERROR", "error":
return ERROR
case "WARNING", "warning":
return WARNING
default:
return INFO
}
}
type BuiltinLogger struct {
logger *log.Logger
level int
step int
}
func NewBuiltinLogger() *BuiltinLogger {
return &BuiltinLogger{
logger: log.Default(),
level: SetLogLevel(),
step: 1,
}
}
func (l *BuiltinLogger) NextStep() {
l.step = l.step + 1
}
func (l *BuiltinLogger) Debug(format string, args ...interface{}) {
if l.level >= DEBUG {
prefix := fmt.Sprintf("[%s] [Step %d/%d] ", "DEBG", l.step, totalStep)
l.logger.SetOutput(os.Stdout)
l.logger.SetPrefix(prefix)
l.logger.SetFlags(log.Ldate | log.Ltime)
_, file, line, ok := runtime.Caller(1)
if ok {
caller := fmt.Sprintf("@%s:%d: ", file, line)
l.logger.Printf(caller+format, args...)
} else {
l.logger.Printf(format, args...)
}
}
}
func (l *BuiltinLogger) Info(format string, args ...interface{}) {
if l.level >= INFO {
prefix := fmt.Sprintf("[%s] [Step %d/%d] ", "INFO", l.step, totalStep)
l.logger.SetOutput(os.Stdout)
l.logger.SetPrefix(prefix)
l.logger.SetFlags(log.Ldate | log.Ltime)
l.logger.Printf(format, args...)
}
}
func (l *BuiltinLogger) Warning(format string, args ...interface{}) {
if l.level >= WARNING {
prefix := fmt.Sprintf("[%s] [Step %d/%d] ", "WARN", l.step, totalStep)
l.logger.SetOutput(os.Stdout)
l.logger.SetPrefix(prefix)
l.logger.SetFlags(log.Ldate | log.Ltime)
l.logger.Printf(format, args...)
}
}
func (l *BuiltinLogger) Error(format string, args ...interface{}) {
if l.level >= ERROR {
prefix := fmt.Sprintf("[%s] [Step %d/%d] ", "EROR", l.step, totalStep)
l.logger.SetOutput(os.Stdout)
l.logger.SetPrefix(prefix)
l.logger.SetFlags(log.Ldate | log.Ltime)
_, file, line, ok := runtime.Caller(1)
if ok {
caller := fmt.Sprintf("@%s:%d: ", file, line)
l.logger.Printf(caller+format, args...)
} else {
l.logger.Printf(format, args...)
}
}
}
func (l *BuiltinLogger) Fatal(format string, args ...interface{}) {
if l.level >= ERROR {
prefix := fmt.Sprintf("[%s] [Step %d/%d] ", "EROR", l.step, totalStep)
l.logger.SetOutput(os.Stdout)
l.logger.SetPrefix(prefix)
l.logger.SetFlags(log.Ldate | log.Ltime)
_, file, line, ok := runtime.Caller(1)
if ok {
caller := fmt.Sprintf("@%s:%d: ", file, line)
l.logger.Fatalf(caller+format, args...)
} else {
l.logger.Fatalf(format, args...)
}
}
}
解説
ログレベルは ERROR
WARNING
INFO
DEBUG
の4種類を環境変数 LOG_LEVEL
で決定する。足りなければお好みで追加。
const (
ERROR = iota + 1
WARNING
INFO
DEBUG
)
func SetLogLevel() int {
logLevel := os.Getenv("LOG_LEVEL")
switch logLevel {
case "INFO", "info":
return INFO
case "DEBUG", "debug":
return DEBUG
case "ERROR", "error":
return ERROR
case "WARNING", "warning":
return WARNING
default:
return INFO
}
}
NewBuiltinLogger()
は BuiltinLogger
を生成するための関数。内部では、標準ライブラリの log.Default()
でデフォルトのLoggerインスタンスを生成している。今回のロギングはバッチ処理用のプログラムに使用しているため、処理の順番に合わせて NextStep()
で step
を更新し、出力情報として追加するメソッドも追加した。このように、共通のデータは BuiltinLogger
に入れておくと取り回しがしやすい。もちろん不要であれば step
はなくても良い。
type BuiltinLogger struct {
logger *log.Logger
level int
step int
}
func NewBuiltinLogger() *BuiltinLogger {
return &BuiltinLogger{
logger: log.Default(),
level: SetLogLevel(),
step: 1,
}
}
func (l *BuiltinLogger) NextStep() {
l.step = l.step + 1
}
ログの出力には Info
や Error
といったメソッドを使用する。標準ライブラリの SetPrefix
や SetFlags
を活用しつつ、さらに追加したいフォーマットは format
の前に直接加えている。今回の場合は、呼び出した関数の情報を Debug
と Error
のみに加えている。
なお、できれば標準の log.Llongfile
や log.Lshortfile
で呼び出した関数の情報を付与したかったが、今回のケースだと出力内容が毎回 logger.go:63
などとなってしまうため、 runtime.Caller(1)
で呼び出した関数の情報を取得している。
func (l *BuiltinLogger) Info(format string, args ...interface{}) {
if l.level >= INFO {
prefix := fmt.Sprintf("[%s] [Step %d/%d] ", "INFO", l.step, totalStep)
l.logger.SetOutput(os.Stdout)
l.logger.SetPrefix(prefix)
l.logger.SetFlags(log.Ldate | log.Ltime)
l.logger.Printf(format, args...)
}
}
func (l *BuiltinLogger) Error(format string, args ...interface{}) {
if l.level >= ERROR {
prefix := fmt.Sprintf("[%s] [Step %d/%d] ", "EROR", l.step, totalStep)
l.logger.SetOutput(os.Stdout)
l.logger.SetPrefix(prefix)
l.logger.SetFlags(log.Ldate | log.Ltime)
_, file, line, ok := runtime.Caller(1)
if ok {
caller := fmt.Sprintf("@%s:%d: ", file, line)
l.logger.Printf(caller+format, args...)
} else {
l.logger.Printf(format, args...)
}
}
}
Error
メソッドはログをPrintするのみだが、合わせてプログラムをエラー終了させたい時用にFatal
メソッドも追加した。その名の通り Printf
の代わりに Fatalf
を使用している。
func (l *BuiltinLogger) Fatal(format string, args ...interface{}) {
if l.level >= ERROR {
prefix := fmt.Sprintf("[%s] [Step %d/%d] ", "EROR", l.step, totalStep)
l.logger.SetOutput(os.Stdout)
l.logger.SetPrefix(prefix)
l.logger.SetFlags(log.Ldate | log.Ltime)
_, file, line, ok := runtime.Caller(1)
if ok {
caller := fmt.Sprintf("@%s:%d: ", file, line)
l.logger.Fatalf(caller+format, args...)
} else {
l.logger.Fatalf(format, args...)
}
}
}
参照
- Goのロギングライブラリ 2021年冬
- Logging in Go: Choosing a System and Using it - Honeybadger Developer Blog
- Introduction on Performing Logging in Go applications | Engineering Education (EngEd) Program | Section
- Level based logging in Golang
- log package - log - Go Packages
- runtime package - runtime - Go Packages
Discussion