今回はログをファイルに書きだす方法の話題です。
70歳目前になり「ボケ防止」のためにGo言語プログラミングの学びなおしをしてて、とりあえず慣れるため、つまみ食い的にテーマを探してやってます。その過程で、テストの結果確認に、ログの確認が面倒になってきました。
Go言語には2種類のログ(JSON形式の構造化ログ出力「slog」、プレーンテキストの「log」)がありますが、どちらもデフォルトの出力先は「標準エラー出力」だからです。長いログだと、コンソールで遡って追いかけるのは大変だし、指定したログファイルへ出力するように変更できないかな?と考えたわけです。
Geminiに教えてもらいました。やり方としては「デフォルトの出力先」を初期化で書き換えれば良いようです。
ざっくりしたやり方としては。
- ログが2つあるので、一括で扱える構造体を用意する(LoggerCloser)。構造体の中身は、リソースを解放する Close メソッドのインターフェース(io.Closer)である。
- この構造体に属する「Close()」メソッドを用意する。例えば「func (lc *LoggerCloser) Close() error {」のように宣言することで「lcに属するメソッド(lc.Close()のように呼び出す)」を定義できる。
- slogの出力用のファイルを開いてJSONHandlerである「slogHandler」を生成し「 slog.SetDefault(slog.New(slogHandler)) 」でslogのデフォルト出力先のファイルにJSON形式で出力するようにします。(JSONHandlerはJSON形式データの変換や処理を一手に引き受けるコンポーネントです)
- logの出力用のファイルを開いて(logFile)「log.SetOutput(logFile)」でlogの出力先をそのファイルにする。
となります。
上記の3と4の処理を関数「InitLoggers」にまとめて、main()関数の中で呼び出し、上記の2で定義したClose()関数をその後で「defer」で宣言する感じです。
deferで宣言すると「即時実行されず、関数が終了する直前に実行される」ので、ログファイルのクローズが保証される・・と。そんな感じですね。
上記を実装し、僕が動作確認したコードを貼り付けておきます。ちょっと、ベタな感じはしますが、ファイルにログが書きだされるようになったので、各段に確認が楽になりました。
package mylog
import (
"io"
"log"
"log/slog"
"os"
)
// LoggerCloser は2つのファイルを一括で閉じるための構造体です
type LoggerCloser struct {
SLogFile io.Closer
LogFile io.Closer
}
// Close は両方のファイルを閉じます
func (lc *LoggerCloser) Close() error {
if lc.SLogFile != nil {
lc.SLogFile.Close()
}
if lc.LogFile != nil {
lc.LogFile.Close()
}
return nil
}
// InitLoggers は、slogと logの両方を初期化します
func InitLoggers(sLogPath, logPath string) (*LoggerCloser, error) {
// 1. slog用のファイル設定
sLogFile, err := os.OpenFile(sLogPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
return nil, err
}
slogHandler := slog.NewJSONHandler(sLogFile, &slog.HandlerOptions{
AddSource: true,
Level: slog.LevelInfo,
})
slog.SetDefault(slog.New(slogHandler)) // slogのデフォルトを差し替え
// 2. log用のファイル設定
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
sLogFile.Close()
return nil, err
}
// ★標準 log パッケージの出力先をファイルに切り替える
log.SetOutput(logFile)
// ログのプレフィックスや標準フラグ(日付、時刻、マイクロ秒など)を設定
log.SetPrefix("[TIMER] ")
log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds)
return &LoggerCloser{SLogFile: sLogFile, LogFile: logFile}, nil
}
これをmain関数の頭で以下のように宣言すると、全体のログ出力先が切り替わります
// ロガー(sLog、log)両方の出力先を指定のログファイルに切り替える
closers, err := tools.InitLoggers("C:/Users/11user/Documents/logs/slog.log", "C:/Users/11user/Documents/logs/log.log")
if err != nil {
panic(err)
}
defer closers.Close()
使ったGeminiのプロンプトです。
golang。言語の標準仕様にそって、自作関数で共用するログ処理です。ログの出力先はファイルにします。ログ処理は、mainとは別ファイルにまとめて、インポートして使う形式にします。slogとlogは、それぞれ別のログファイルに出力します。ログの初期化関数の中で、2つのログのデフォルトを差し替える方式のサンプルコードをお願いします
今回はGeminiの教えてくれたサンプルコードを手直しする部分も少なく、かつ、試行錯誤の余地も少なかったので、あんまり「ボケ防止」にはなってない気もしますが、必要な部分だし、まあいいかな。ではでは。