How to collect, standardize, and centralize Golang logs

tip / logs /golang /microservices

分散システムに依存している組織は、チャネルやgoroutinesなどの並行処理の機能を利用するためにGo言語でしばしばアプリケーションを書いています(例: Goroutines, GOLANGなど.., Heroku、Basecamp、Cockroach Labs、および Datadog など)。 Go アプリケーションの構築またはサポートを担当している場合、よく考えられたログ記録戦略は、ユーザーの行動を理解し、エラーを特定し、アプリケーションのパフォーマンスを監視するのに役立ちます。

この投稿では、Golang ログを管理するためのいくつかのツールとテクニックを紹介します。 まず、さまざまな種類の要件に対してどのログ記録パッケージを使用するかという疑問から始めます。 次に、ログをより検索しやすく、信頼できるものにし、ロギング設定のリソースのフットプリントを減らし、ログメッセージを標準化するためのいくつかのテクニックについて説明します。

ロギング パッケージを知る

Go はロギング パッケージを選択する際に、豊富な選択肢を提供し、以下では、これらのうちのいくつかについて説明します。 logrus は、私たちがカバーするライブラリの中で最も人気があり、一貫したロギング フォーマットの実装を支援しますが、他のライブラリには言及する価値のある特別な使用事例があります。 このセクションでは log、logrus、および glog ライブラリについて調査します。

Use log for simplicity

Golang の組み込みロギングライブラリである log は、標準エラーに書き込み、設定の必要なくタイムスタンプを追加できるデフォルトロガーを持っています。 リッチで構造化されたログを生成するよりも、 コードから素早くフィードバックを得ることが重要な場合、 ローカル開発ではこれらの荒削りなログを使うことができます。

例えば、ゼロで割ろうとしたときにプログラムを終了させずに呼び出し元に エラーを返す除算関数を定義できます。

package mainimport ( "log" "errors" "fmt" )func divide(a float32, b float32) (float32, error) { if b == 0 { return 0, errors.New("can't divide by zero") } return a / b, nil}func main() { var a float32 = 10 var b float32 ret, err := divide(a,b) if err != nil{ log.Print(err) } fmt.Println(ret)}

この例はゼロで割るので、次のログメッセージを出力します:

2019/01/31 11:48:00 can't divide by zero

Use logrus for formated logs

私たちは logrus、JSON でログを取るためによく適している構造化ロギング用に設計されたログパッケージを使って Golang ログを書くことを推奨します。 JSONフォーマットはマシンがあなたのGolangログを簡単にパースすることを可能にします。 そして、JSON はよく定義された標準なので、新しいフィールドを含めることによってコンテキストを追加するのが簡単です。

logrus を使用して、以下に示すように、WithFields 関数を使用して JSON ログに追加する標準フィールドを定義することができます。 そして、Info()Warn()Error() などの異なるレベルでロガーへの呼び出しを行うことができます。 logrus ライブラリは、ログを自動的に JSON として書き出し、標準のフィールドと、その場で定義したフィールドを挿入します。

package mainimport ( log "github.com/sirupsen/logrus")func main() { log.SetFormatter(&log.JSONFormatter{}) standardFields := log.Fields{ "hostname": "staging-1", "appname": "foo-app", "session": "1ce3f6v", } log.WithFields(standardFields).WithFields(log.Fields{"string": "foo", "int": 1, "float": 1.1}).Info("My first ssl event from Golang")}

結果として得られるログは、メッセージ、ログ レベル、タイムスタンプ、および JSON オブジェクト内の標準フィールドを含みます:

{"appname":"foo-app","float":1.1,"hostname":"staging-1","int":1,"level":"info","msg":"My first ssl event from Golang","session":"1ce3f6v","string":"foo","time":"2019-03-06T13:37:12-05:00"}

Use glog if you’re concerned about volume

いくつかのログ ライブラリは特定のレベルでのログを有効または無効にすることができ、開発と生産の間を移行する際にログ量をチェックするために役に立ちます。 このようなライブラリのひとつに glog があり、コマンドラインでフラグ (例: -v は冗長性) を使用して、コード実行時にロギングレベルを設定することができます。

例えば、先ほどと同じ “Can’t divide by zero” エラーを書くために glog を使うことができますが、それは 2 のverbosityレベルでログをとっている場合だけです。 冗長度は任意の符号付き 32 ビット整数に設定することができ、また関数 Info()Warning()Error()Fatal() を使用して冗長レベル 0から 3(それぞれ)を割り当てることができます。

 if err != nil && glog.V(2){ glog.Warning(err) }

生産時に特定のレベルのみロギングすることにより、アプリケーションにあまりリソースがかからないようにすることが可能です。 同時に、ユーザーへの影響がない場合、アプリケーションとのできるだけ多くのインタラクションをログに記録することはしばしば良いアイデアであり、その後、Datadog のようなログ管理ソフトウェアを使用して、調査に必要なデータを見つけます

Best practices for writing and storing Golang logs

いったんログライブラリを選択したら、コード内のどこでロガーへの呼び出しを行い、ログを格納し、それらを理解する方法についても計画したいと思うでしょう。 このセクションでは、Golang ログを整理するための一連のベスト・プラクティスを推奨します:

  • Goroutines 内ではなく、メイン アプリケーション プロセス内から logger を呼び出します。
  • Send your logs to a central platform so you can analyze and aggregate them.
  • Use HTTP headers and unique IDs to log user behavior across microservices.

Avoid declaring goroutines for logging

There are two reasons to avoid you own goroutines creating to handle writing logs.Specialization for logs. まず、ロガーの重複が同じ io.Writer にアクセスしようとするため、並行処理の問題につながる可能性があります。 第二に、ログ記録ライブラリは通常、並行性の問題を内部で管理するゴルーチン自体を開始し、独自のゴルーチンを開始しても干渉するだけです。

Write your logs to a file

たとえログを中央プラットフォームに送信していても、最初にローカルマシンのファイルにそれらを書き込むことを推奨します。 ログがネットワークで失われることなく、常にローカルで利用可能であることを確認したいと思うでしょう。 また、ファイルに書き出すことで、ログを書き出す作業と、セントラル・プラットフォームへ送信する作業を切り離すことができます。 アプリケーション自身が接続を確立したり、ログをストリームする必要はなく、これらの作業はDatadog Agentのような専門ソフトウェアに任せることができます。 永続的ストレージをまだ含まないコンテナ化されたインフラストラクチャ内で Go アプリケーションを実行している場合 (AWS Fargate で実行しているコンテナなど)、コンテナの STDOUT および STDERR ストリームから直接ログを収集するようにログ管理ツールを構成することができます (これは Docker と Kubernetes では異なって処理されます)。 一貫性のない属性は、ユーザーを混乱させ、同じ画像の一部を形成するはずのログを相関させることを不可能にします。

Golang は異なる場所からの異なるメッセージで同じエラーについてログを記録します。

標準化を強制する良い方法は、アプリケーション コードとログ記録ライブラリの間にインターフェイスを作成することです。 インターフェイスは特定のフォーマットを実装した定義済みのログメッセージを含み、 ログメッセージを検索、グループ化、フィルタリングできることを保証することにより、 問題の調査を容易にします。

Golang は一貫したメッセージを作成するために標準インターフェイスを使ってエラーに対してログを記録します。
Golang は一貫したメッセージを作成するために、標準的なインターフェイスを使用してエラーに対してログを記録します。 そして、ロガーへの呼び出しを行うために Event メッセージを使用します。

最初に、開発者がコード内に含めることができる logwrapper パッケージを記述します。

package logwrapperimport ( "github.com/sirupsen/logrus")// Event stores messages to log later, from our standard interfacetype Event struct { id int message string}// StandardLogger enforces specific log message formatstype StandardLogger struct { *logrus.Logger}// NewLogger initializes the standard loggerfunc NewLogger() *StandardLogger {var baseLogger = logrus.New()var standardLogger = &StandardLogger{baseLogger}standardLogger.Formatter = &logrus.JSONFormatter{}return standardLogger}// Declare variables to store log messages as new Eventsvar ( invalidArgMessage = Event{1, "Invalid arg: %s"} invalidArgValueMessage = Event{2, "Invalid value for argument: %s: %v"} missingArgMessage = Event{3, "Missing arg: %s"})// InvalidArg is a standard error messagefunc (l *StandardLogger) InvalidArg(argumentName string) { l.Errorf(invalidArgMessage.message, argumentName)}// InvalidArgValue is a standard error messagefunc (l *StandardLogger) InvalidArgValue(argumentName string, argumentValue string) { l.Errorf(invalidArgValueMessage.message, argumentName, argumentValue)}// MissingArg is a standard error messagefunc (l *StandardLogger) MissingArg(argumentName string) { l.Errorf(missingArgMessage.message, argumentName)}

このロギングインターフェースを使用するには、コード内にそれを含めて、StandardLogger のインスタンスを呼び出すだけです。

package mainimport ( li "<PATH_TO_PACKAGE>/logwrapper")func main() { var standardLogger = := li.NewLogger() // You can then call a method of our standard logger in the context of an error // you would like to log. standardLogger.InvalidArgValue("client", "nil")}

コードを実行すると、次の JSON ログが得られます。

{"level":"error","msg":"Invalid value for argument: client: nil","time":"2019-03-04T11:21:07-05:00"}

Centralize Golang logs

アプリケーションがホスト群にわたって展開されているなら、ログを尾行、grep、および調査するためにそれぞれのホストへ SSH することは持続不可能です。

1 つの解決策は、Golang syslog パッケージを使用して、インフラストラクチャ全体から単一の syslog サーバーにログを転送することです。

Datadog で Golang ログ管理をカスタマイズおよび合理化する。 たとえば、Datadog は、ログ ファイルをテーリングし、処理と分析のためにログを中央のプラットフォームに転送できます。

Datadog Log Explorer ビューは、さまざまなソースからの Golang ログを表示することができます。 たとえば、service によってエラーの数を追跡して、サービスの 1 つでインシデントが発生した場合にそれを知らせることができます。 5229>

Golang ログをステータスでグループ化。

Track Golang logs across microservices

エラーをトラブルシューティングするとき、たとえその動作が多くのマイクロサービスを含む場合でも、どの動作パターンがそれにつながったかを見ることはしばしば役に立ちます。 これは、アプリケーションが関数、データベース クエリ、およびその他のタスクを実行する順序を視覚化し、ネットワークを介して道を作るようにこれらの実行ステップを追跡する、分散トレースで実現できます。

この例では、1 つのマイクロサービスがリクエストを受信し、x-trace ヘッダーでトレース ID をチェックし、存在しない場合は生成します。 別のマイクロサービスにリクエストを行うとき、新しい spanID を生成し、このリクエストとすべてのリクエストのために、それをヘッダー x-span に追加します。

func microService2(w http.ResponseWriter, r *http.Request) { trace := r.Header.Get("x-trace") span := generateSpanId() parent := r.Header.Get("x-span") if (trace == "") { w.Header().Set("x-parent", parent) } w.Header().Set("x-trace", trace) w.Header().Set("x-span", span) if (parent == "") { w.Header().Set("x-parent", span) } w.WriteHeader(http.StatusOK) io.WriteString(w, fmt.Sprintf(aResponseMessage, 2, trace, span, parent))}

マイクロサービスのいずれかでエラーが発生した場合、traceparent、および span 属性を使用して、リクエストが通ったルートを確認し、どのホストおよびおそらくアプリケーション コードのどの部分を調査すればよいかを知ることができます。

最初のマイクロサービスでは:

{"appname":"go-logging","level":"debug","msg":"Hello from Microservice One","trace":"eUBrVfdw","time":"2017-03-02T15:29:26+01:00","span":"UzWHRihF"}

2番目では:

{"appname":"go-logging","level":"debug","msg":"Hello from Microservice Two","parent":"UzWHRihF","trace":"eUBrVfdw","time":"2017-03-02T15:29:26+01:00","span":"DPRHBMuE"}

Golang トレースの可能性をさらに深く知りたい場合、Go アプリケーション用の分散トレースをサポートする OpenTracing または監視プラットフォームなどのトレース ライブラリを使用することができます。 たとえば、Datadog は、Golang トレース ライブラリからのデータを使用してサービスのマップを自動的に構築し、時間の経過とともにトレースの傾向を視覚化し、異常な要求率、エラー率、またはレイテンシーのあるサービスについて知ることができます。

A an example of a visualization showing the traces of requests between microservices.

Clean and comprehensive Golang logs

この投稿では、いくつかの Go ログ ライブラリの利点とトレードオフをハイライトしてきました。 また、ログが必要なときに利用可能でアクセス可能であること、および、ログに含まれる情報が一貫していて分析しやすいことを保証する方法を推奨しました。

Datadog ですべての Go ログを分析し始めるには、無料トライアルにサインアップしてください。

コメントを残す

メールアドレスが公開されることはありません。