Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 1559] ファイルの概要

このコミットは、Go言語の標準ライブラリであるlogパッケージのロギングフォーマット機能を改善するものです。具体的には、以下の2つのファイルが変更されています。

  • src/lib/log.go: logパッケージの主要な実装ファイルであり、ロギング機能のコアロジックが含まれています。このコミットでは、ログ出力のフォーマットを制御するための新しいフラグが導入され、ログヘッダーの生成ロジックが大幅に改善されています。
  • src/lib/log_test.go: logパッケージのテストファイルであり、変更されたロギングフォーマット機能が正しく動作するかを検証するためのテストコードが含まれています。テストの構造が改善され、正規表現を用いてより柔軟な検証が可能になっています。

コミット

このコミットは、Go言語の標準ロギングライブラリのフォーマット機能を大幅に改善し、より詳細で柔軟なログ出力オプションを提供します。以前はファイル名の短縮表示のみが可能でしたが、この変更により、日付、時刻(マイクロ秒単位)、完全なファイルパス、または短縮されたファイルパスなど、ログヘッダーに含める情報を細かく制御できるようになりました。これにより、開発者はアプリケーションのデバッグや監視において、より目的に合ったログ形式を選択できるようになります。

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/646b3b5c02f15fa057a0ba9dcff2f5ccb2ff11ed

元コミット内容

improved logging formats.

R=rsc
DELTA=210  (118 added, 60 deleted, 32 changed)
OCL=23508
CL=23518

変更の背景

Go言語の初期段階において、logパッケージは基本的なロギング機能を提供していましたが、ログメッセージのフォーマットに関する柔軟性は限られていました。特に、ログ出力に含める情報(日付、時刻、ファイル名、行番号など)の選択肢が少なく、開発者がデバッグやシステム監視のニーズに合わせてログ形式をカスタマイズすることが困難でした。

以前のlogパッケージでは、Lshortnameという単一のフラグしかなく、これによりファイル名が短縮されるかどうかが制御されるだけでした。しかし、実際の開発現場では、ログにタイムスタンプ(日付と時刻)、マイクロ秒単位の精度、完全なファイルパス、あるいは短縮されたファイルパスなど、より多様な情報を含めたいという要求がありました。

このコミットは、このような背景から、logパッケージのフォーマット機能を拡張し、開発者がより詳細かつ柔軟にログ出力を制御できるようにすることを目的としています。これにより、Goアプリケーションのロギングがより強力で実用的なものになります。

前提知識の解説

このコミットの変更内容を理解するためには、以下のGo言語の基本的な概念と関連技術についての知識が役立ちます。

  • Go言語のロギング: Go言語の標準ライブラリにはlogパッケージが含まれており、シンプルなロギング機能を提供します。通常、log.Print(), log.Printf(), log.Println()などの関数を使用してメッセージを出力します。これらの関数は、デフォルトで標準エラー出力にタイムスタンプとファイル/行番号を含む形式でログを出力します。
  • io.Writerインターフェース: Go言語におけるI/O操作の基本的なインターフェースです。Write([]byte) (n int, err error)メソッドを実装する型はio.Writerとして扱われます。logパッケージは、このインターフェースを通じてログメッセージを様々な出力先(標準出力、ファイル、ネットワークなど)に書き込むことができます。
  • timeパッケージ: Go言語で時間と日付を扱うためのパッケージです。time.Now()で現在の時刻を取得したり、time.Format()で時刻を特定の文字列形式に変換したりできます。このコミットでは、time.Nanoseconds()(現在のGoではtime.Now().UnixNano()に相当)やtime.SecondsToLocalTime()(現在のGoではtime.Unix()time.Localに相当)が使用されています。
  • runtime.Caller関数: Go言語のruntimeパッケージに含まれる関数で、呼び出し元の関数に関する情報を取得するために使用されます。runtime.Caller(skip int)は、スタックトレースを遡って、指定されたスキップ数だけ上位の呼び出し元のプログラムカウンタ、ファイル名、行番号、およびその情報が有効かどうかを返します。ロギングにおいて、ログが出力されたソースコードの場所(ファイル名と行番号)を特定するために利用されます。
  • ビットマスクとフラグ: 複数のブール値の状態を単一の整数値で表現するプログラミング手法です。各状態は2のべき乗(1, 2, 4, 8, ...)に対応するビット位置に割り当てられ、これらの値をビットOR演算子(|)で組み合わせることで、複数のフラグを同時に設定できます。このコミットでは、ログ出力のフォーマットオプション(日付、時刻、ファイル名など)を制御するために、このようなビットフラグが導入されています。
  • 正規表現(Regular Expression): 文字列のパターンを記述するための強力なツールです。このコミットのテストコードでは、ログ出力の文字列が期待されるフォーマットに合致するかどうかを検証するために正規表現が使用されています。これにより、厳密な文字列比較ではなく、柔軟なパターンマッチングが可能になります。

技術的詳細

このコミットにおける技術的な変更点は多岐にわたりますが、主なものは以下の通りです。

  1. ロギングフラグの拡張:

    • 以前はLshortnameという単一のフラグしかありませんでしたが、このコミットにより、ログ出力に含める情報を細かく制御するための新しい定数フラグが導入されました。
    • Ldate: 日付(例: 2009/0123)を含める。
    • Ltime: 時刻(例: 01:23:23)を含める。
    • Lmicroseconds: マイクロ秒の精度(例: .123123)を含める。Ltimeが設定されている場合にのみ有効。
    • Llongfile: 完全なファイル名と行番号(例: /a/b/c/d.go:23)を含める。
    • Lshortfile: 短縮されたファイル名と行番号(例: d.go:23)を含める。Llongfileが設定されている場合、Lshortfileが優先されます。
    • lAllBits: 上記すべてのフラグを組み合わせたビットマスクで、内部的なマスク処理に使用されます。
    • これらのフラグはビットOR演算子で組み合わせて使用され、Logger構造体のflagフィールドに設定されます。
  2. Logger構造体の変更:

    • Logger構造体にprefix stringフィールドが追加されました。これにより、ログメッセージの前に常に表示される固定の文字列を設定できるようになります。
    • NewLogger関数のシグネチャが変更され、prefix引数が追加されました。
    • デフォルトのロガー(stdout, stderr, exit, crash)の初期化時に、Ldate|Ltimeフラグがデフォルトで設定されるようになりました。これにより、特別な設定なしに日付と時刻がログに含まれるようになります。
  3. itoaヘルパー関数の導入:

    • 整数を固定幅の10進数ASCII文字列に変換するためのitoa(integer to ASCII)関数が新しく追加されました。これは、日付や時刻の各要素(年、月、日、時、分、秒、マイクロ秒)を整形された文字列としてログヘッダーに含めるために使用されます。負の幅を指定するとゼロパディングを回避する機能も持っています。
  4. formatHeaderメソッドの導入:

    • Logger構造体にformatHeader(ns int64, calldepth int) stringという新しいメソッドが追加されました。このメソッドは、ログメッセージの前に付加されるヘッダー文字列を生成する責任を持ちます。
    • このメソッド内で、Loggerflagフィールドに基づいて、日付、時刻、マイクロ秒、ファイル名、行番号などの情報が動的に構築されます。
    • ファイル名の短縮ロジック(Lshortfileが設定されている場合)もこのメソッド内に移動し、shortnamesマップを使用してキャッシュされます。
    • sys.Caller(現在のGoではruntime.Caller)を使用して、ログ呼び出し元のファイルと行番号を取得します。
  5. outputメソッドの変更とリネーム:

    • 以前のoutputメソッドはOutput(大文字始まり)にリネームされ、エクスポートされたメソッドになりました。これにより、外部からロガーのコア出力ロジックを呼び出すことが可能になります。
    • Outputメソッドは、ログヘッダーの生成を新しく導入されたformatHeaderメソッドに委譲するようになりました。これにより、コードの関心事が分離され、可読性と保守性が向上しています。
    • switch l.flag & ^Lshortnameの部分がswitch l.flag & ^lAllBitsに変更され、新しいフラグ体系に対応しました。
  6. テストコードの改善:

    • src/lib/log_test.goでは、テストの構造が大幅に改善されました。
    • Rdate, Rtime, Rmicroseconds, Rline, Rlongfile, Rshortfileといった正規表現の定数が導入され、ログ出力のフォーマットを柔軟に検証できるようになりました。
    • tester構造体とtests配列が導入され、様々なフラグの組み合わせやプレフィックスに対するテストケースを宣言的に定義できるようになりました。
    • testLogという汎用的なテスト関数が導入され、LogLogfの両方のパターンでテストを実行できるようになりました。
    • regexp.Match関数を使用して、生成されたログ出力が期待される正規表現パターンに一致するかどうかを検証するようになりました。これにより、行番号などの動的な部分を含むログ出力に対しても、より堅牢なテストが可能になりました。

これらの変更により、Goのlogパッケージは、より強力でカスタマイズ可能なロギング機能を提供するようになりました。

コアとなるコードの変更箇所

このコミットにおける主要なコード変更は、src/lib/log.gosrc/lib/log_test.goに集中しています。

src/lib/log.go

  1. ロギングフラグの定義変更:
    --- a/src/lib/log.go
    +++ b/src/lib/log.go
    @@ -18,73 +18,116 @@ import (
      	"time";
      )
    
    -// Lshortname can be or'd in to cause only the last element of the file name to be printed.
     const (
    +	// Flags
      	Lok = iota;
      	Lexit;	// terminate execution when written
      	Lcrash;	// crash (panic) when written
    -	Lshortname = 1 << 5;
    +	// Bits or'ed together to control what's printed. There is no control over the
    +	// order they appear (the order listed here) or the format they present (as
    +	// described in the comments).  A colon appears after these items:
    +	//	2009/0123 01:23:23.123123 /a/b/c/d.go:23: message
    +	Ldate = 1 << iota;	// the date: 2009/0123
    +	Ltime;	// the time: 01:23:23
    +	Lmicroseconds;	// microsecond resolution: 01:23:23.123123.  assumes Ltime.
    +	Llongfile;	// full file name and line number: /a/b/c/d.go:23
    +	Lshortfile;	// final file name element and line number: d.go:23. overrides Llongfile
    +	lAllBits = Ldate | Ltime | Lmicroseconds | Llongfile | Lshortfile;
     )
    
     type Logger struct {
      	out0	io.Write;
      	out1	io.Write;
    +	prefix string;
      	flag int;
      }
    
    -func NewLogger(out0, out1 io.Write, flag int) *Logger {
    -	return &Logger{out0, out1, flag}
    +func NewLogger(out0, out1 io.Write, prefix string, flag int) *Logger {
    +	return &Logger{out0, out1, prefix, flag}
     }
    
     var (
    -	stdout = NewLogger(os.Stdout, nil, Lok);
    -	stderr = NewLogger(os.Stderr, nil, Lok);
    -	exit = NewLogger(os.Stderr, nil, Lexit);
    -	crash = NewLogger(os.Stderr, nil, Lcrash);
    +	stdout = NewLogger(os.Stdout, nil, "", Lok|Ldate|Ltime);
    +	stderr = NewLogger(os.Stderr, nil, "", Lok|Ldate|Ltime);
    +	exit = NewLogger(os.Stderr, nil, "", Lexit|Ldate|Ltime);
    +	crash = NewLogger(os.Stderr, nil, "", Lcrash|Ldate|Ltime);
     )
    
    -func timestamp(ns int64) string {
    -	t := time.SecondsToLocalTime(ns/1e9);
    -	// why are time fields private?
    -	s := t.RFC1123();
    -	return s[5:12] + s[17:25];	// TODO(r): placeholder. this gives "24 Jan 15:50:18"
    +var shortnames = make(map[string] string)	// cache of short names to avoid allocation.
    +
    +// Cheap integer to fixed-width decimal ASCII.  Use a negative width to avoid zero-padding
    +func itoa(i int, wid int) string {
    +	var u uint = uint(i);
    +	if u == 0 && wid <= 1 {
    +		return "0"
    +	}
    +
    +	// Assemble decimal in reverse order.
    +	var b [32]byte;
    +	bp := len(b);
    +	for ; u > 0 || wid > 0; u /= 10 {
    +		bp--;
    +		wid--;
    +		b[bp] = byte(u%10) + '0';
    +	}
    +
    +	return string(b[bp:len(b)])
     }
    
    -var shortnames = make(map[string] string)	// cache of short names to avoid allocation.
    +func (l *Logger) formatHeader(ns int64, calldepth int) string {
    +	h := l.prefix;
    +	if l.flag & (Ldate | Ltime | Lmicroseconds) != 0 {
    +		t := time.SecondsToLocalTime(ns/1e9);
    +		if l.flag & (Ldate) != 0 {
    +			h += itoa(int(t.Year), 4) + "/" + itoa(t.Month, 2) + itoa(t.Day, 2) + " "
    +		}
    +		if l.flag & (Ltime | Lmicroseconds) != 0 {
    +			h += itoa(t.Hour, 2) + ":" + itoa(t.Minute, 2) + ":" + itoa(t.Second, 2);
    +			if l.flag & Lmicroseconds != 0 {
    +				h += "." + itoa(int(ns % 1e9)/1e3, 6);
    +			}
    +			h += " ";
    +		}
    +	}
    +	if l.flag & (Lshortfile | Llongfile) != 0 {
    +		pc, file, line, ok := sys.Caller(calldepth);
    +		if ok {
    +			if l.flag & Lshortfile != 0 {
    +				short, ok := shortnames[file];
    +				if !ok {
    +					short = file;
    +					for i := len(file) - 1; i > 0; i-- {
    +						if file[i] == '/' {
    +							short = file[i+1:len(file)];
    +							break;
    +						}
    +					}
    +					shortnames[file] = short;
    +				}
    +				file = short;
    +			}
    +		} else {
    +			file = "???";
    +			line = 0;
    +		}
    +		h += file + ":" + itoa(line, -1) + ": ";
    +	}
    +	return h;
    +}
    
     // The calldepth is provided for generality, although at the moment on all paths it will be 2.
    -func (l *Logger) output(calldepth int, s string) {
    +func (l *Logger) Output(calldepth int, s string) {
      	now := time.Nanoseconds();	// get this early.
      	newline := "\n";
      	if len(s) > 0 && s[len(s)-1] == '\n' {
      		newline = ""
      	}
    -	pc, file, line, ok := sys.Caller(calldepth);
    -	if ok {
    -		if l.flag & Lshortname == Lshortname {
    -			short, ok := shortnames[file];
    -			if !ok {
    -				short = file;
    -				for i := len(file) - 1; i > 0; i-- {
    -					if file[i] == '/' {
    -						short = file[i+1:len(file)];
    -						shortnames[file] = short;
    -						break;
    -					}
    -				}
    -			}
    -			file = short;
    -		}
    -	} else {
    -		file = "???";
    -		line = 0;
    -	}
    -	s = fmt.Sprintf("%s %s:%d: %s%s", timestamp(now), file, line, s, newline);
    +	s = l.formatHeader(now, calldepth+1) + s + newline;
      	io.WriteString(l.out0, s);
      	if l.out1 != nil {
      		io.WriteString(l.out1, s);
      	}
    -	switch l.flag & ^Lshortname {
    +	switch l.flag & ^lAllBits {
      	case Lcrash:
      		panic("log: fatal error");
      	case Lexit:
    @@ -94,42 +137,42 @@ func (l *Logger) output(calldepth int, s string) {
    
     // Basic methods on Logger, analogous to Printf and Print
     func (l *Logger) Logf(format string, v ...) {
    -	l.output(2, fmt.Sprintf(format, v))
    +	l.Output(2, fmt.Sprintf(format, v))
     }
    
     func (l *Logger) Log(v ...) {
    -	l.output(2, fmt.Sprintln(v))
    +	l.Output(2, fmt.Sprintln(v))
     }
    
     // Helper functions for lightweight simple logging to predefined Loggers.
     func Stdout(v ...) {
    -	stdout.output(2, fmt.Sprint(v))
    +	stdout.Output(2, fmt.Sprint(v))
     }
    
     func Stderr(v ...) {
    -	stdout.output(2, fmt.Sprintln(v))
    +	stderr.Output(2, fmt.Sprintln(v))
     }
    
     func Stdoutf(format string, v ...) {
    -	stdout.output(2, fmt.Sprintf(format, v))
    +	stdout.Output(2, fmt.Sprintf(format, v))
     }
    
     func Stderrf(format string, v ...) {
    -	stderr.output(2, fmt.Sprintf(format, v))
    +	stderr.Output(2, fmt.Sprintf(format, v))
     }
    
     func Exit(v ...) {
    -	exit.output(2, fmt.Sprintln(v))
    +	exit.Output(2, fmt.Sprintln(v))
     }
    
     func Exitf(format string, v ...) {
    -	exit.output(2, fmt.Sprintf(format, v))
    +	exit.Output(2, fmt.Sprintf(format, v))
     }
    
     func Crash(v ...) {
    -	crash.output(2, fmt.Sprintln(v))
    +	crash.Output(2, fmt.Sprintln(v))
     }
    
     func Crashf(format string, v ...) {
    -	crash.output(2, fmt.Sprintf(format, v))
    +	crash.Output(2, fmt.Sprintf(format, v))
     }
    

src/lib/log_test.go

  1. テストヘルパー関数の置き換えと正規表現定数の導入:
    --- a/src/lib/log_test.go
    +++ b/src/lib/log_test.go
    @@ -10,61 +10,76 @@ import (
      	"bufio";
      	"log";
      	"os";
    +	"regexp";
      	"testing";
      )
    
    -func test(t *testing.T, flag int, expect string) {
    -	fd0, fd1, err1 := os.Pipe();
    -	if err1 != nil {
    -		t.Error("pipe", err1);
    -	}
    -	buf, err2 := bufio.NewBufRead(fd0);
    -	if err2 != nil {
    -		t.Error("bufio.NewBufRead", err2);
    -	}
    -	l := NewLogger(fd1, nil, flag);
    -	l.Log("hello", 23, "world");	/// the line number of this line needs to be placed in the expect strings
    -	line, err3 := buf.ReadLineString('\n', false);
    -	if line[len(line)-len(expect):len(line)] != expect {
    -		t.Error("log output should be ...", expect, "; is " , line);
    -	}
    -	t.Log(line);
    -	fd0.Close();
    -	fd1.Close();
    -}
    +const (
    +	Rdate = `[0-9][0-9][0-9][0-9]/[0-9][0-9][0-9][0-9]`;
    +	Rtime = `[0-9][0-9]:[0-9][0-9]:[0-9][0-9]`;
    +	Rmicroseconds = `\.[0-9][0-9][0-9][0-9][0-9][0-9]`;
    +	Rline = `[0-9]+:`;
    +	Rlongfile = `/[A-Za-z0-9_/]+\.go:` + Rline;
    +	Rshortfile = `[A-Za-z0-9_]+\.go:` + Rline;
    +)
    
    -func TestRegularLog(t *testing.T) {
    -	test(t, Lok, "/go/src/lib/log_test.go:25: hello 23 world");
    +type tester struct {
    +	flag	int;
    +	prefix	string;
    +	pattern	string;	// regexp that log output must match; we add ^ and expected_text$ always
    +}
    +
    +var tests = []tester {
    +	// individual pieces:
    +	tester{ 0,	"", "" },
    +	tester{ 0, "XXX", "XXX" },
    +	tester{ Lok|Ldate, "", Rdate+" " },
    +	tester{ Lok|Ltime, "", Rtime+" " },
    +	tester{ Lok|Ltime|Lmicroseconds, "", Rtime+Rmicroseconds+" " },
    +	tester{ Lok|Lmicroseconds, "", Rtime+Rmicroseconds+" " },	// microsec implies time
    +	tester{ Lok|Llongfile, "", Rlongfile+" " },
    +	tester{ Lok|Lshortfile, "", Rshortfile+" " },
    +	tester{ Lok|Llongfile|Lshortfile, "", Rshortfile+" " },	// shortfile overrides longfile
    +	// everything at once:
    +	tester{ Lok|Ldate|Ltime|Lmicroseconds|Llongfile, "XXX", "XXX"+Rdate+" "+Rtime+Rmicroseconds+" "+Rlongfile+" " },
    +	tester{ Lok|Ldate|Ltime|Lmicroseconds|Lshortfile, "XXX", "XXX"+Rdate+" "+Rtime+Rmicroseconds+" "+Rshortfile+" " },
     }
    
    -func TestShortNameLog(t *testing.T) {
    -	test(t, Lok|Lshortname, " log_test.go:25: hello 23 world")
    +func testLog(t *testing.T, flag int, prefix string, pattern string, useLogf bool) {
      	fd0, fd1, err1 := os.Pipe();
      	if err1 != nil {
    -		t.Error("pipe", err1);
    +		t.Fatal("pipe", err1);
      	}
      	buf, err2 := bufio.NewBufRead(fd0);
      	if err2 != nil {
    -		t.Error("bufio.NewBufRead", err2);
    +		t.Fatal("bufio.NewBufRead", err2);
    +	}
    +	l := NewLogger(fd1, nil, prefix, flag);
    +	if useLogf {
    +		l.Logf("hello %d world", 23);
    +	} else {
    +		l.Log("hello", 23, "world");
      	}
    -	l := NewLogger(fd1, nil, flag);
    -	l.Logf("hello %d world", 23);	/// the line number of this line needs to be placed in the expect strings
      	line, err3 := buf.ReadLineString('\n', false);
    -	if line[len(line)-len(expect):len(line)] != expect {
    -		t.Error("log output should be ...", expect, "; is " , line);
    +	if err3 != nil {
    +		t.Fatal("log error", err3);
    +	}
    +	pattern = "^"+pattern+"hello 23 world$";
    +	matched, err4 := regexp.Match(pattern, line);
    +	if err4 != nil{
    +		t.Fatal("pattern did not compile:", err4);
    +	}
    +	if !matched {
    +		t.Errorf("log output should match %q is %q", pattern, line);
      	}
    -	t.Log(line);
      	fd0.Close();
      	fd1.Close();
     }
    
    -func TestRegularLogFormatted(t *testing.T) {
    -	testFormatted(t, Lok, "/go/src/lib/log_test.go:53: hello 23 world");
    -}
    -
    -func TestShortNameLogFormatted(t *testing.T) {
    -	testFormatted(t, Lok|Lshortname, " log_test.go:53: hello 23 world")
    +func TestAllLog(t *testing.T) {
    +	for i, testcase := range(tests) {
    +		testLog(t, testcase.flag, testcase.prefix, testcase.pattern, false);
    +		testLog(t, testcase.flag, testcase.prefix, testcase.pattern, true);
    +	}
     }
    

コアとなるコードの解説

src/lib/log.goの変更点

  • ロギングフラグの拡張: 以前はLshortnameという単一のフラグしかありませんでしたが、このコミットではLdate, Ltime, Lmicroseconds, Llongfile, Lshortfileといった複数のビットフラグが導入されました。これにより、開発者はログに含める情報の種類(日付、時刻、ファイルパスの長さなど)を細かく指定できるようになります。iotaを使用することで、各フラグにユニークなビット値が自動的に割り当てられ、ビット演算で簡単に組み合わせることが可能です。lAllBitsは、これらのフラグをまとめて扱うためのマスクとして機能します。

  • Logger構造体とNewLoggerの変更: Logger構造体にprefix stringフィールドが追加されました。これは、すべてのログメッセージの前に付加される固定の文字列を設定するためのものです。NewLogger関数もこのprefix引数を受け取るように変更され、ロガーの初期化時にプレフィックスを設定できるようになりました。これにより、例えば特定のモジュールからのログに一貫した識別子を付けることが可能になります。

  • デフォルトロガーの初期化: stdout, stderr, exit, crashといったデフォルトのロガーの初期化時に、NewLogger""(空のプレフィックス)とLok|Ldate|Ltimeフラグが渡されるようになりました。これは、特別な設定なしに、デフォルトで日付と時刻がログメッセージに含まれることを意味します。これにより、ログの可読性が向上し、タイムスタンプが常に利用可能になります。

  • timestamp関数の削除とitoa関数の追加: 以前のtimestamp関数は、日付と時刻のフォーマットが固定されており、柔軟性に欠けていました。この関数は削除され、代わりにitoa(integer to ASCII)という新しいヘルパー関数が導入されました。itoaは、整数を固定幅の文字列に変換する汎用的な関数であり、日付や時刻の各要素(年、月、日など)を整形して文字列に変換するために使用されます。これにより、日付と時刻のフォーマットをより細かく制御できるようになりました。

  • formatHeaderメソッドの導入: このコミットの最も重要な変更点の一つが、Logger構造体にformatHeaderメソッドが追加されたことです。このメソッドは、ログメッセージの先頭に付加されるヘッダー文字列(日付、時刻、ファイル名、行番号など)を生成する役割を担います。

    • l.prefixからヘッダーの構築を開始します。
    • l.flagの値に基づいて、Ldate, Ltime, Lmicrosecondsの各フラグが設定されているかを確認し、timeパッケージとitoa関数を使用して対応する日付と時刻の文字列を生成します。特にLmicrosecondsが設定されている場合は、マイクロ秒単位の精度で時刻が表示されます。
    • LlongfileまたはLshortfileが設定されている場合は、sys.Caller(現在のGoではruntime.Caller)を使用して呼び出し元のファイル名と行番号を取得します。Lshortfileが設定されている場合は、ファイルパスからファイル名のみを抽出するロジックが適用され、shortnamesマップでキャッシュされます。
    • 最終的に、構築されたヘッダー文字列が返されます。このメソッドの導入により、ログヘッダーの生成ロジックがOutputメソッドから分離され、コードのモジュール性と保守性が向上しました。
  • outputからOutputへの変更とロジックの委譲: 以前のoutputメソッドはOutput(大文字始まり)にリネームされました。Go言語の慣習では、大文字で始まる関数やメソッドはエクスポートされ、パッケージ外からアクセス可能になります。これにより、logパッケージのユーザーは、より低レベルなOutputメソッドを直接呼び出して、カスタムのロギングロジックを実装できるようになりました。

    • Outputメソッドは、ログヘッダーの生成を新しく導入されたformatHeaderメソッドに完全に委譲するようになりました。これにより、Outputメソッドの役割が、ヘッダーの取得、メッセージの結合、そして実際のI/O書き込みに限定され、コードの責務が明確になりました。
    • switch l.flag & ^Lshortnameの部分がswitch l.flag & ^lAllBitsに変更され、新しいフラグ体系に対応するようになりました。これは、LcrashLexitといったロガーの動作を制御するフラグを、フォーマット関連のフラグとは独立して扱うための変更です。
  • ロギングヘルパー関数の変更: Logf, Log, Stdout, Stderr, Stdoutf, Stderrf, Exit, Exitf, Crash, Crashfといったすべての公開ロギング関数は、内部的にl.outputを呼び出す代わりに、新しくエクスポートされたl.Outputを呼び出すように変更されました。これにより、すべてのロギングパスが新しいフォーマットロジックとformatHeaderメソッドを利用するようになります。

src/lib/log_test.goの変更点

  • テストヘルパー関数の刷新: 以前のtestおよびtestFormatted関数は、特定のフォーマット文字列に依存しており、新しい柔軟なフォーマットオプションをテストするには不十分でした。これらの関数は削除され、より汎用的なtestLog関数が導入されました。
  • 正規表現定数の導入: Rdate, Rtime, Rmicroseconds, Rline, Rlongfile, Rshortfileといった正規表現の定数が導入されました。これらの定数は、日付、時刻、ファイル名、行番号などのログ出力の各部分が期待されるパターンに一致するかどうかを検証するために使用されます。これにより、ログ出力の厳密な文字列比較ではなく、柔軟なパターンマッチングが可能になり、テストの堅牢性が向上しました。
  • tester構造体とテストケース配列: testerという構造体が定義され、各テストケースのフラグ、プレフィックス、そして期待される正規表現パターンをカプセル化するようになりました。testsというtester型の配列が定義され、様々なロギングフラグの組み合わせやプレフィックスに対するテストケースが宣言的に記述されています。これにより、テストの追加や管理が容易になりました。
  • testLog関数の実装: testLog関数は、tester配列の各要素を受け取り、指定されたフラグとプレフィックスでロガーを初期化し、LogまたはLogfを使用してログメッセージを出力します。その後、regexp.Match関数を使用して、出力されたログメッセージが期待される正規表現パターンに一致するかどうかを検証します。これにより、すべてのフォーマットオプションが網羅的にテストされるようになりました。
  • TestAllLog関数の導入: TestAllLog関数は、tests配列をループし、各テストケースに対してtestLog関数を呼び出します。これにより、すべての定義されたテストケースが実行され、新しいロギングフォーマット機能が正しく動作することが保証されます。

これらの変更により、logパッケージはより強力で柔軟なロギング機能を提供し、その機能が堅牢なテストスイートによって検証されるようになりました。

関連リンク

参考にした情報源リンク

  • この解説は、提供されたコミット情報とGo言語の一般的な知識に基づいて生成されました。