fluent-logger-golang の実戦的な使いかたまとめ

OSS紹介アドベントカレンダー の14日目の記事です。

Fluentd の 公式 Go 版 Logger である fluent-logger-golang はこのように使うのがよさそう、という使い方をまとめてみました。 元々社内で書いておいたドキュメントを編集したものです。

github.com

前提のユースケース

Webアプリケーション(APIサーバ) を Go で書いていて、そこから何らかのログを Fluentd に送信したい。

config のお勧めオプション

  • Timeout : Connect に対するタイムアウト。デフォルト3秒なのでそのままでよさそう
  • WriteTimeout : 書き込みのタイムアウト。デフォルトだとずっと待ってしまうので 3 秒とか?
  • BufferLimit : デフォルト 8MB これを超えると捨てられてしまう。送る流量によって調整が必要
  • MaxRetry : この回数だけリトライして失敗すると panic する。Exponential back off でリトライ間隔が延び続ける(上限なし)ので、リトライを繰り返し続けると間隔が広がりすぎてつらいが、panic されるよりはマシなので大きめに設定しておく
  • AsyncConnect : fluent.New() するときに connect できなくても error を返さず、裏でリトライする。 起動時に接続先の Fluentd がちゃんと動いてない場合に気がつけない可能性があるので、false (default) がよさそう

fluent.New() 時に接続できない場合

  • 特にコンテナで動作させる場合など、アプリケーション起動時に Fluentd がまだ Listen できていない状態があり得る。 fluent.New() 時に接続できないと error が返るので、そこで失敗扱いにしてしまうとアプリケーションの起動が失敗することになる
  • config.AsyncConnect = true にすると、設定ミスなどで Fluentd がいつまでも起動してこない場合に困る

アプリケーションで、適切にリトライ処理を行うほうがよい。

import "github.com/Songmu/Retry"

var client FluentClient
err := retry.Retry(3, time.Second, func() (err error) { // 3回試行。失敗したら1秒待つ
    client, err = fluent.New(fluent.Config{})
    return err
})

Post() がエラーを返してきたらどうすべきか

Post()net.Conn.Write() が正常に返ってくれば成功するので、この時点で Fluentd へ接続しているソケットに書き込みは成功している。本当に受け取れているかはまだ分からない。

error が返ってくるのは以下の場合。

  • オンメモリバッファが溢れた → ログは捨てられてしまう
  • 再接続試行中 errors.New("fluent#send: can't send logs, client is reconnecting")
  • net.Conn.Write() が error を返した
    • 自動的に net.Conn.Close() が行われ、再接続が裏 (別 goroutine) で走る

error が返ってきたとしてもバッファが溢れない限りは積まれて再送されるが、その再送が最終的に成功するかどうかは不明なため、この時点で対処する方がよい。

また、バッファが溢れた場合はその時点で対処しないとリカバリする方法はない。

ごくごく単純に書くとこう。

if err := logger.Post(tag, x); err != nil {
    log.Println(tag, x) // stderr にだしちゃう
}

実際はリカバリを考えて、再処理しやすい format で別の場所に書き出しましょう。

struct を投げるときの留意点

struct については何もしなくても msgpack でエンコードしてくれるが、public field が大文字はじまりなのでログの key も大文字になってしまう。

msgp を使って struct に対して MarshalMsg() を生成しておくと、それが使われる。

import "github.com/tinylib/msgp/msgp"

//go:generate msgp
type Foo struct {
    Foo int    `msg:"foo"`
    Bar string `msg:"bar"`
}

アプリケーションを shutdown する処理を書くときの留意点

Close() を呼ぶと、その時点で内部に持っているバッファがあればそれは送信を試みた上で終了する。ただし、そこで失敗した場合にリカバリする方法はない。 (バッファ自体は private なので、アプリケーションから触ることができない)

Post() で送信失敗してバッファに積まれるということは接続先が落ちている状況なので、その状況で Close() を呼んだとしても、再送に成功する可能性は高くない。

そのため、ロストしては困るログについては、Post() 失敗時点で他の箇所に書き込むなどの対応が必須。そうしていれば Close() は必ずしも呼ぶ必要はない。 (呼んで困ることはないので呼べるなら呼びましょう)


この元の文書を書いた時点では、go-fluent-client は同期書き込み (ソケットに書き込めてからアプリケーションに処理を戻す) がサポートされていなかったため、要件に合わずに採用しませんでしたが、最近同期モードもサポートされたようなので、go-fluent-logger も検討してみたいですね。

medium.com