📽️

[Golang] 標準ライブラリ "log" でログレベルを設定する

2022/06/27に公開

Goには3rdパーティ製のロギングライブラリが豊富にあるが、主要なものは構造化ロギングに注力を置いているものが多く、「ログレベルの設定と少しフォーマットができれば十分」みたいなライトなユースケースに合うライブラリは現状見当たらなかった。

なので今回、標準ライブラリのみでちょっとリッチなロギングができる構造体を作ったので共有。 logger.go などと命名してこのファイルを1枚差し込めば、ログレベルを設定できるロギングが可能。

ソースコード

全体的に標準ライブラリの log を最大限に活用したつくりになっている。

https://pkg.go.dev/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
}

ログの出力には InfoError といったメソッドを使用する。標準ライブラリの SetPrefixSetFlags を活用しつつ、さらに追加したいフォーマットは format の前に直接加えている。今回の場合は、呼び出した関数の情報を DebugError のみに加えている。

なお、できれば標準の log.Llongfilelog.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...)
		}
	}
}

参照

Discussion