BOKUのブログ

ジャンルにこだわらず、ノートの感じで書いてます

ボケ防止にGO言語の勉強/2種類のログ(slog、log)のデフォルト出力先を指定ファイルに変更する

今回はログをファイルに書きだす方法の話題です。

70歳目前になり「ボケ防止」のためにGo言語プログラミングの学びなおしをしてて、とりあえず慣れるため、つまみ食い的にテーマを探してやってます。その過程で、テストの結果確認に、ログの確認が面倒になってきました。

Go言語には2種類のログ(JSON形式の構造化ログ出力「slog」、プレーンテキストの「log」)がありますが、どちらもデフォルトの出力先は「標準エラー出力」だからです。長いログだと、コンソールで遡って追いかけるのは大変だし、指定したログファイルへ出力するように変更できないかな?と考えたわけです。

 

Geminiに教えてもらいました。やり方としては「デフォルトの出力先」を初期化で書き換えれば良いようです。

ざっくりしたやり方としては。

  1. ログが2つあるので、一括で扱える構造体を用意する(LoggerCloser)。構造体の中身は、リソースを解放する Close メソッドのインターフェース(io.Closer)である。
  2. この構造体に属する「Close()」メソッドを用意する。例えば「func (lc *LoggerCloser) Close() error {」のように宣言することで「lcに属するメソッド(lc.Close()のように呼び出す)」を定義できる。
  3. slogの出力用のファイルを開いてJSONHandlerである「slogHandler」を生成し「 slog.SetDefault(slog.New(slogHandler)) 」でslogのデフォルト出力先のファイルにJSON形式で出力するようにします。(JSONHandlerはJSON形式データの変換や処理を一手に引き受けるコンポーネントです)
  4. 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の教えてくれたサンプルコードを手直しする部分も少なく、かつ、試行錯誤の余地も少なかったので、あんまり「ボケ防止」にはなってない気もしますが、必要な部分だし、まあいいかな。ではでは。