[インデックス 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): 文字列のパターンを記述するための強力なツールです。このコミットのテストコードでは、ログ出力の文字列が期待されるフォーマットに合致するかどうかを検証するために正規表現が使用されています。これにより、厳密な文字列比較ではなく、柔軟なパターンマッチングが可能になります。
技術的詳細
このコミットにおける技術的な変更点は多岐にわたりますが、主なものは以下の通りです。
-
ロギングフラグの拡張:
- 以前は
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
フィールドに設定されます。
- 以前は
-
Logger
構造体の変更:Logger
構造体にprefix string
フィールドが追加されました。これにより、ログメッセージの前に常に表示される固定の文字列を設定できるようになります。NewLogger
関数のシグネチャが変更され、prefix
引数が追加されました。- デフォルトのロガー(
stdout
,stderr
,exit
,crash
)の初期化時に、Ldate|Ltime
フラグがデフォルトで設定されるようになりました。これにより、特別な設定なしに日付と時刻がログに含まれるようになります。
-
itoa
ヘルパー関数の導入:- 整数を固定幅の10進数ASCII文字列に変換するための
itoa
(integer to ASCII)関数が新しく追加されました。これは、日付や時刻の各要素(年、月、日、時、分、秒、マイクロ秒)を整形された文字列としてログヘッダーに含めるために使用されます。負の幅を指定するとゼロパディングを回避する機能も持っています。
- 整数を固定幅の10進数ASCII文字列に変換するための
-
formatHeader
メソッドの導入:Logger
構造体にformatHeader(ns int64, calldepth int) string
という新しいメソッドが追加されました。このメソッドは、ログメッセージの前に付加されるヘッダー文字列を生成する責任を持ちます。- このメソッド内で、
Logger
のflag
フィールドに基づいて、日付、時刻、マイクロ秒、ファイル名、行番号などの情報が動的に構築されます。 - ファイル名の短縮ロジック(
Lshortfile
が設定されている場合)もこのメソッド内に移動し、shortnames
マップを使用してキャッシュされます。 sys.Caller
(現在のGoではruntime.Caller
)を使用して、ログ呼び出し元のファイルと行番号を取得します。
-
output
メソッドの変更とリネーム:- 以前の
output
メソッドはOutput
(大文字始まり)にリネームされ、エクスポートされたメソッドになりました。これにより、外部からロガーのコア出力ロジックを呼び出すことが可能になります。 Output
メソッドは、ログヘッダーの生成を新しく導入されたformatHeader
メソッドに委譲するようになりました。これにより、コードの関心事が分離され、可読性と保守性が向上しています。switch l.flag & ^Lshortname
の部分がswitch l.flag & ^lAllBits
に変更され、新しいフラグ体系に対応しました。
- 以前の
-
テストコードの改善:
src/lib/log_test.go
では、テストの構造が大幅に改善されました。Rdate
,Rtime
,Rmicroseconds
,Rline
,Rlongfile
,Rshortfile
といった正規表現の定数が導入され、ログ出力のフォーマットを柔軟に検証できるようになりました。tester
構造体とtests
配列が導入され、様々なフラグの組み合わせやプレフィックスに対するテストケースを宣言的に定義できるようになりました。testLog
という汎用的なテスト関数が導入され、Log
とLogf
の両方のパターンでテストを実行できるようになりました。regexp.Match
関数を使用して、生成されたログ出力が期待される正規表現パターンに一致するかどうかを検証するようになりました。これにより、行番号などの動的な部分を含むログ出力に対しても、より堅牢なテストが可能になりました。
これらの変更により、Goのlog
パッケージは、より強力でカスタマイズ可能なロギング機能を提供するようになりました。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、src/lib/log.go
とsrc/lib/log_test.go
に集中しています。
src/lib/log.go
- ロギングフラグの定義変更:
--- 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
- テストヘルパー関数の置き換えと正規表現定数の導入:
--- 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
に変更され、新しいフラグ体系に対応するようになりました。これは、Lcrash
やLexit
といったロガーの動作を制御するフラグを、フォーマット関連のフラグとは独立して扱うための変更です。
-
ロギングヘルパー関数の変更:
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言語の
log
パッケージ公式ドキュメント: https://pkg.go.dev/log - Go言語の
time
パッケージ公式ドキュメント: https://pkg.go.dev/time - Go言語の
runtime
パッケージ公式ドキュメント: https://pkg.go.dev/runtime - Go言語の
regexp
パッケージ公式ドキュメント: https://pkg.go.dev/regexp
参考にした情報源リンク
- この解説は、提供されたコミット情報とGo言語の一般的な知識に基づいて生成されました。