ロギングの仕組み
Caddyには強力で柔軟なロギング機能がありますが、特に古くなった共有ホスティングや他のレガシーWebサーバーからの移行の場合は、慣れているものとは異なる場合があります。
概要
ロギングには、出力と消費という2つの主要な側面があります。
出力とは、メッセージを生成することです。これは3つのステップで構成されます。
- 関連情報の収集(コンテキスト)
- 有用な表現の構築(エンコーディング)
- その表現を出力に送信する(書き込み)
この機能はCaddyのコアに組み込まれており、Caddyコードベースのどの部分やモジュール(プラグイン)でもログを出力できます。
消費とは、メッセージの受信と処理です。有用であるためには、出力されたログを消費する必要があります。単に書き込まれて読み取られないログは、何の価値も提供しません。ログの消費は、管理者がコンソール出力を読むほど単純なものでも、ログ集約ツールやクラウドサービスを接続してログメッセージをフィルタリング、カウント、インデックス付けするほど高度なものでもかまいません。
Caddyの役割
Caddyはログ出力ツールです。 ログをエンコードして書き込むために必要な最小限の処理を除き、ログを消費しません。これは、Caddyのコアをよりシンプルに保ち、バグやエッジケースを減らし、メンテナンスの負担を軽減するため重要です。最終的に、ログ処理はCaddyコアの範囲外です。
ただし、ログを消費するCaddyアプリモジュールを作成する可能性は常にあります。(私たちの知る限り、まだ存在していませんが。)
構造化ログ
ほとんどの最新のアプリケーションと同様に、Caddyのログは構造化されています。これは、メッセージ内の情報が単なる不透明な文字列やバイトスライスではないことを意味します。代わりに、データは強く型付けされたままになり、メッセージをエンコードして書き出すまで、個々のフィールド名によってキー付けされます。
従来の非構造化ログ(従来のHTTPサーバーで一般的に使用される古くなったCommon Log Format(CLF)など)と比較してみましょう。
127.0.0.1 - - [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.1" 200 2326
この形式は「構造化されている」ものの「構造化された」ものではありません。HTTPリクエストのログ記録にのみ使用できます。不透明なバイト文字列であるため、異なる方法でエンコードする(効率的な)方法はありません。また、多くの情報が欠けています。リクエストのHostヘッダーさえ含まれていません!このログ形式は、単一のサイトをホストする場合、およびリクエストに関する最も基本的な情報を取得する場合にのみ役立ちます。
今度は、JSONでエンコードされ、表示のために適切にフォーマットされたCaddyからの同等の構造化ログメッセージと比較してみましょう。
{
"level": "info",
"ts": 1646861401.5241024,
"logger": "http.log.access",
"msg": "handled request",
"request": {
"remote_ip": "127.0.0.1",
"remote_port": "41342",
"client_ip": "127.0.0.1",
"proto": "HTTP/2.0",
"method": "GET",
"host": "localhost",
"uri": "/",
"headers": {
"User-Agent": ["curl/7.82.0"],
"Accept": ["*/*"],
"Accept-Encoding": ["gzip, deflate, br"],
},
"tls": {
"resumed": false,
"version": 772,
"cipher_suite": 4865,
"proto": "h2",
"server_name": "example.com"
}
},
"bytes_read": 0,
"user_id": "",
"duration": 0.000929675,
"size": 10900,
"status": 200,
"resp_headers": {
"Server": ["Caddy"],
"Content-Encoding": ["gzip"],
"Content-Type": ["text/html; charset=utf-8"],
"Vary": ["Accept-Encoding"]
}
}
構造化ログの方がはるかに有用であり、はるかに多くの情報が含まれていることがわかります。このログメッセージの豊富な情報は、有用であるだけでなく、事実上パフォーマンスオーバーヘッドはありません。Caddyのログはゼロアロケーションです。構造化ログには、データ型やコンテキストの制限がありません。どのコードパスでも使用でき、あらゆる種類の情報を含めることができます。
ログは構造化され、強く型付けされているため、あらゆる形式にエンコードできます。そのため、JSONを使用したくない場合、ログは他の表現にエンコードできます。ログエンコーダーモジュールを介してCaddyは他のエンコーダーをサポートしており、さらに追加することもできます。
最も重要なことは、構造化ログとレガシー形式の違いにおいて、パフォーマンスのペナルティを伴う構造化ログはレガシーCommon Log Formatに変換できます が、その逆はできません。CLFから構造化形式への変換は自明ではありません(少なくとも非効率的であり)、情報の不足を考慮すると不可能です。
本質的に、効率的な構造化ロギングは一般的にこれらの哲学を促進します。
- ログが多すぎるよりも少ない方が良い
- 破棄するよりもフィルタリングする方が良い
- より大きな柔軟性と相互運用性のためにエンコーディングを遅らせる
出力
コードでは、ログ出力は次のようになります。
logger.Debug("proxy roundtrip",
zap.String("upstream", di.Upstream.String()),
zap.Object("request", caddyhttp.LoggableHTTPRequest{Request: req}),
zap.Object("headers", caddyhttp.LoggableHTTPHeader(res.Header)),
zap.Duration("duration", duration),
zap.Int("status", res.StatusCode),
)
この1つの関数呼び出しには、ログレベル、メッセージ、およびいくつかのデータフィールドが含まれていることがわかります。これらはすべて強く型付けされており、Caddyはゼロアロケーションロギングライブラリを使用しているため、ログ出力はオーバーヘッドがほとんどなく迅速かつ効率的です。
logger
変数は、名前とデータフィールドの両方を含む、任意の量のコンテキストを関連付けることができるzap.Logger
です。これにより、ロガーは親コンテキストから非常にうまく「継承」できるようになり、高度なトレースとメトリクスの実現が可能になります。
そこから、メッセージは非常に効率的な処理パイプラインを通して送信され、エンコードされて書き込まれます。
ロギングパイプライン
上記のように、メッセージはロガーによって出力されます。次に、メッセージは処理のためにログに送信されます。
Caddyでは、メッセージを処理できる複数のログを設定できます。ログは、エンコーダー、ライター、最小レベル、サンプリング比率、および含めるか除外するロガーのリストで構成されます。Caddyでは、常にdefault
という名前のデフォルトログがあります。設定のこのオブジェクトで"default"
としてキー付けされたログを指定することで、カスタマイズできます。
- エンコーダー:ログの形式。メモリ内のデータ表現をバイトスライスに変換します。エンコーダーは、ログメッセージのすべてのフィールドにアクセスできます。
- ライター:ログ出力。ファイルやネットワークソケットなど、任意のログライターモジュールにすることができます。単にバイトを書き込みます。
- レベル:ログには、DEBUGからFATALまでさまざまなレベルがあります。指定されたレベルよりも低いメッセージは、ログによって無視されます。
- サンプリング:非常にホットなパスでは、効果的に処理できるよりも多くのログが出力される可能性があります。サンプリングを有効にすることで、メッセージの代表的なサンプルを生成しながら、負荷を軽減できます。
- 含める/除外する:各メッセージは、名前(通常はモジュールIDから派生)を持つロガーによって出力されます。ログは、特定のロガーからのメッセージを含めるか除外できます。
Caddyからログメッセージが出力されるとき
- 発信元のロガーの名前は、各ログの含める/除外リストに対してチェックされます。含まれている場合(または除外されていない場合)、そのログに承認されます。
- サンプリングが有効になっている場合、簡単な計算によってログメッセージを保持するかどうかが決定されます。
- ログの構成済みエンコーダーを使用してメッセージがエンコードされます。
- 次に、エンコードされたバイトがログの構成済みライターに書き込まれます。
デフォルトでは、すべてのメッセージがすべての構成済みログに送信されます。これは、上記で説明した構造化ロギングの値に準拠しています。含める/除外リストを設定することで、どのメッセージをどのログに送信するかを制限できますが、これは主に異なるモジュールからのメッセージをフィルタリングするためのものであり、ログ集約サービスのように使用することを意図したものではありません。Caddyのロギングパイプラインを効率的に維持するために、ログメッセージの高度な処理は消費に遅延されます。
消費
メッセージが出力に送信された後、コンシューマーがそれらを読み取り、解析し、それに応じて処理します。
これは、ログの出力とは非常に異なる問題領域であり、Caddyのコアは消費を処理しません(ただし、Caddyアプリモジュールは確かに処理できます)。JSONメッセージ(または他の形式)のストリームを処理し、ログの表示、フィルタリング、インデックス付け、クエリを行うために使用できるツールは数多くあります。自分で作成することもできます。
たとえば、特定のフィールド(例:ホスト名)に基づいて別々のファイルにCLFを必要とするレガシーソフトウェアを実行している場合、JSONを読み取り、sprintf()
を呼び出してCLF文字列を作成し、request.host
フィールドの値に基づいてファイルに書き込む簡単なツールを使用または作成できます。
Caddyのロギング機能は、メトリクスとトレースの実装にも使用できます。メトリクスは基本的に特定の特性を持つメッセージをカウントし、トレースはそれらの間の共通点に基づいて複数のメッセージをリンクします。
Caddyのログを消費することで実行できる可能性は無限にあります!