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

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

このコミットは、Go言語の標準ライブラリである log/syslog パッケージにおける、ログメッセージへの改行コードの二重追加(\n)の問題を修正するものです。具体的には、log パッケージが既にメッセージの末尾に改行を追加している場合、syslog パッケージがさらに改行を追加しないように変更されました。これにより、ログ出力が意図しない二重改行によって乱れることを防ぎます。

コミット

commit 88e858ac80e4adaa9e37db4268f49091d739dd55
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Fri Jul 27 14:22:27 2012 -0400

    log/syslog: don't append \n if there is one
       pkg log already appends a linefeed to the log message,
    so log/syslog doesn't need to append another.
    
    R=golang-dev, bradfitz, r
    CC=golang-dev
    https://golang.org/cl/6441048

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

https://github.com/golang/go/commit/88e858ac80e4adaa9e37db4268f49091d739dd55

元コミット内容

log/syslog パッケージがログメッセージに改行コード (\n) を追加する際に、既にメッセージの末尾に改行が存在する場合でも、さらに改行を追加してしまう問題を修正します。これは、log パッケージ自体がログメッセージに改行を追加する動作をするため、syslog パッケージ側で二重に改行が追加されてしまうことが原因でした。

変更の背景

Go言語の標準 log パッケージは、log.Printlog.Println などの関数を通じてログメッセージを出力する際に、自動的にメッセージの末尾に改行コードを追加します。一方、log/syslog パッケージは、システムログデーモン(syslog)にメッセージを送信するためのインターフェースを提供します。

このコミット以前の log/syslog パッケージの実装では、fmt.Fprintf を使用してsyslogメッセージをフォーマットする際に、メッセージの末尾に無条件で \n を追加していました。このため、log パッケージから渡されたメッセージが既に改行を含んでいる場合、syslog パッケージがさらに改行を追加し、結果としてログファイルやsyslogサーバーに二重の改行が出力されるという問題が発生していました。

二重改行は、ログの可読性を低下させるだけでなく、ログ解析ツールやスクリプトが正しく動作しない原因となる可能性がありました。このコミットは、この冗長な改行を防ぎ、ログ出力の一貫性と正確性を保つことを目的としています。

前提知識の解説

Go言語の log パッケージ

Go言語の標準ライブラリには、基本的なロギング機能を提供する log パッケージが含まれています。このパッケージは、アプリケーションのイベントやエラーメッセージを標準出力、標準エラー出力、またはファイルに書き出すために使用されます。 log.Print(), log.Printf(), log.Println() などの関数があり、Println 系は自動的に改行を追加します。Print 系も、通常は最終的に改行が追加されるように設計されています。

Go言語の log/syslog パッケージ

log/syslog パッケージは、GoプログラムからUnix系システムで広く利用されているsyslogプロトコルを通じてログメッセージを送信するためのインターフェースを提供します。これにより、アプリケーションのログを中央のsyslogサーバーに集約したり、システムのログ管理機能と統合したりすることが可能になります。

syslogメッセージは通常、特定のフォーマットに従って送信されます。これには、優先度(Severity + Facility)、タイムスタンプ、ホスト名、アプリケーション名、プロセスID、そしてメッセージ本文が含まれます。メッセージ本文の末尾には、通常、単一の改行コードが期待されます。

fmt.Fprintf 関数

fmt.Fprintf は、Go言語の fmt パッケージに含まれる関数で、指定された io.Writer インターフェースにフォーマットされた文字列を書き込むために使用されます。このコミットでは、netConn 型(syslog接続を表す)が io.Writer として機能し、syslogメッセージをネットワーク接続に書き出すために fmt.Fprintf が利用されています。

改行コード (\n) の重要性

改行コード (\n) は、テキストファイルやログメッセージにおいて、行の終わりを示すために使用される特殊文字です。ログメッセージにおいて改行が適切に管理されることは非常に重要です。

  • 可読性: 各ログエントリが独立した行として表示されることで、ログの読みやすさが向上します。
  • 解析の容易さ: 多くのログ解析ツールやスクリプトは、改行コードを行の区切りとして利用します。二重改行や改行の欠如は、これらのツールの動作を妨げる可能性があります。
  • 標準への準拠: syslogプロトコルなどの標準では、メッセージの末尾に単一の改行が期待されることが一般的です。

技術的詳細

このコミットの技術的な核心は、log/syslog パッケージ内の netConn 型の writeBytes および writeString メソッドにおける改行コードの追加ロジックの変更です。

変更前: 変更前は、これらのメソッド内で fmt.Fprintf を使用してsyslogメッセージをフォーマットする際に、メッセージ本文の後に無条件で \n を追加していました。

// 変更前の writeBytes (抜粋)
_, err := fmt.Fprintf(n.conn, "<%d>%s: %s\\n", p, prefix, b)

// 変更前の writeString (抜粋)
_, err := fmt.Fprintf(n.conn, "<%d>%s: %s\\n", p, prefix, s)

この \\n が、log パッケージから渡されたメッセージが既に改行を含んでいる場合に、二重改行を引き起こしていました。

変更後: 変更後では、メッセージ本文の末尾に改行が必要かどうかを動的に判断するロジックが導入されました。

  1. nl という新しい文字列変数が導入されます。
  2. 入力されたバイトスライス b (または文字列 s) の長さが0でない、かつ、その最後の文字が \n でない場合にのみ、nl"\n" が代入されます。
  3. それ以外の場合(メッセージが空であるか、既に \n で終わっている場合)、nl は空文字列のままになります。
  4. fmt.Fprintf のフォーマット文字列の末尾に、この nl 変数が追加されます。
// 変更後の writeBytes (抜粋)
nl := ""
if len(b) == 0 || b[len(b)-1] != '\n' {
    nl = "\n"
}
_, err := fmt.Fprintf(n.conn, "<%d>%s: %s%s", p, prefix, b, nl)

// 変更後の writeString (抜粋)
nl := ""
if len(s) == 0 || s[len(s)-1] != '\n' {
    nl = "\n"
}
_, err := fmt.Fprintf(n.conn, "<%d>%s: %s%s", p, prefix, s, nl)

この変更により、メッセージが既に改行で終わっている場合は nl が空文字列となり、余分な改行が追加されなくなります。メッセージが改行で終わっていない場合は nl"\n" となり、適切な改行が追加されます。これにより、常に単一の改行がメッセージの末尾に付与されるようになります。

テストコード (syslog_test.go) も、この新しい動作を検証するために更新されました。特に、メッセージが既に改行で終わっている場合のテストケースが追加され、二重改行が発生しないことを確認しています。

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

src/pkg/log/syslog/syslog.go ファイルの netConn 型の writeBytes および writeString メソッドが変更されました。

--- a/src/pkg/log/syslog/syslog.go
+++ b/src/pkg/log/syslog/syslog.go
@@ -138,7 +138,11 @@ func (w *Writer) Debug(m string) (err error) {
 }
 
 func (n netConn) writeBytes(p Priority, prefix string, b []byte) (int, error) {
-	_, err := fmt.Fprintf(n.conn, "<%d>%s: %s\\n", p, prefix, b)
+	nl := ""
+	if len(b) == 0 || b[len(b)-1] != '\n' {
+		nl = "\n"
+	}
+	_, err := fmt.Fprintf(n.conn, "<%d>%s: %s%s", p, prefix, b, nl)
 	if err != nil {
 		return 0, err
 	}
@@ -146,7 +150,11 @@ func (n netConn) writeBytes(p Priority, prefix string, b []byte) (int, error) {
 }
 
 func (n netConn) writeString(p Priority, prefix string, s string) (int, error) {
-	_, err := fmt.Fprintf(n.conn, "<%d>%s: %s\\n", p, prefix, s)
+	nl := ""
+	if len(s) == 0 || s[len(s)-1] != '\n' {
+		nl = "\n"
+	}
+	_, err := fmt.Fprintf(n.conn, "<%d>%s: %s%s", p, prefix, s, nl)
 	if err != nil {
 		return 0, err
 	}

また、src/pkg/log/syslog/syslog_test.go ファイルの TestWrite 関数も、新しいロジックを検証するために変更されました。

--- a/src/pkg/log/syslog/syslog_test.go
+++ b/src/pkg/log/syslog/syslog_test.go
@@ -98,20 +98,32 @@ func TestUDPDial(t *testing.T) {
 }
 
 func TestWrite(t *testing.T) {
-\tdone := make(chan string)\n-\tstartServer(done)\n-\tl, err := Dial(\"udp\", serverAddr, LOG_ERR, \"syslog_test\")\n-\tif err != nil {\n-\t\tt.Fatalf(\"syslog.Dial() failed: %s\", err)\n+\ttests := []struct {\n+\t\tpri Priority\n+\t\tpre string\n+\t\tmsg string\n+\t\texp string\n+\t}{\n+\t\t{LOG_ERR, \"syslog_test\", \"\", \"<3>syslog_test: \\n\"},\n+\t\t{LOG_ERR, \"syslog_test\", \"write test\", \"<3>syslog_test: write test\\n\"},\n+\t\t// Write should not add \\n if there already is one\n+\t\t{LOG_ERR, \"syslog_test\", \"write test 2\\n\", \"<3>syslog_test: write test 2\\n\"},\n \t}\n-\tmsg := \"write test\"\n-\t_, err = io.WriteString(l, msg)\n-\tif err != nil {\n-\t\tt.Fatalf(\"WriteString() failed: %s\", err)\n-\t}\n-\texpected := \"<3>syslog_test: write test\\n\"\n-\trcvd := <-done\n-\tif rcvd != expected {\n-\t\tt.Fatalf(\"s.Info() = \'%q\', but wanted \'%q\'\", rcvd, expected)\n+\n+\tfor _, test := range tests {\n+\t\tdone := make(chan string)\n+\t\tstartServer(done)\n+\t\tl, err := Dial(\"udp\", serverAddr, test.pri, test.pre)\n+\t\tif err != nil {\n+\t\t\tt.Fatalf(\"syslog.Dial() failed: %s\", err)\n+\t\t}\n+\t\t_, err = io.WriteString(l, test.msg)\n+\t\tif err != nil {\n+\t\t\tt.Fatalf(\"WriteString() failed: %s\", err)\n+\t\t}\n+\t\trcvd := <-done\n+\t\tif rcvd != test.exp {\n+\t\t\tt.Fatalf(\"s.Info() = \'%q\', but wanted \'%q\'\", rcvd, test.exp)\n+\t\t}\n \t}\n }\n```

## コアとなるコードの解説

`syslog.go` の `writeBytes` および `writeString` 関数は、syslogメッセージを実際にネットワークに書き込む役割を担っています。これらの関数は、ログの優先度、プレフィックス、そして実際のメッセージ本文を受け取ります。

変更の核心は、以下の `if` 文と `nl` 変数の導入です。

```go
nl := ""
if len(b) == 0 || b[len(b)-1] != '\n' {
    nl = "\n"
}
  • nl := "":まず、nl (newlineの略) という文字列変数を空文字列で初期化します。これは、メッセージの末尾に追加する改行コードを保持するためのものです。
  • if len(b) == 0 || b[len(b)-1] != '\n':この条件文が、改行を追加する必要があるかどうかを判断します。
    • len(b) == 0:入力されたメッセージ b (または s) が空である場合。空のメッセージであっても、syslogプロトコルでは通常、末尾に改行が期待されるため、この場合は改行を追加します。
    • b[len(b)-1] != '\n':入力されたメッセージの最後の文字が改行 (\n) でない場合。つまり、メッセージが改行で終わっていない場合は、改行を追加する必要があります。
  • nl = "\n":上記の条件のいずれかが真である場合、nl"\n" を代入します。これにより、メッセージの末尾に改行が追加される準備が整います。

最終的に、fmt.Fprintf のフォーマット文字列は "%s%s" となり、メッセージ本文の後に nl の内容が結合されます。

_, err := fmt.Fprintf(n.conn, "<%d>%s: %s%s", p, prefix, b, nl)

このロジックにより、メッセージが既に改行で終わっている場合は nl が空文字列のままとなり、余分な改行が追加されることはありません。メッセージが改行で終わっていない場合や空の場合は、nl"\n" が設定され、適切な改行が一度だけ追加されるようになります。これにより、ログ出力の整合性が保たれます。

syslog_test.go の変更は、この新しいロジックが期待通りに動作することを保証するためのものです。特に、メッセージが改行で終わっている場合のテストケースが追加されたことで、この修正が正しく機能していることが確認できます。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (上記「関連リンク」に記載)
  • コミットメッセージと差分情報 (./commit_data/13511.txt)
  • 一般的なsyslogプロトコルの知識