vector

Activity, over time

Daily observations from the public event archive.

Attention

Daily stars & forks

Loading attention chart

Star accumulation

Cumulative stars

Loading star accumulation chart

Pull requests

Daily opens & merges

Loading pull requests chart

Conversations

Daily issues & comments

Loading conversations chart

Community

Daily accounts & reviews

Loading community chart

Releases

Daily releases

Loading releases chart

Mentions

1post · 30d
Nov 30 posts
  1. Qiita · · @yosshi_

    Kubernetesのログ入門 はじめに Kubernetesを普段使っていても、kubectl logsでPodのログが見られることは分かるものの、実際にどんな種類のログがあるのか、どういう形で管理されているのか、保存期間はどれくらいなのか、長期間保存するにはどうすればいいのか、といったところまではなかなか分からないと思います。 そこでこの記事では、Podのコンテナログとkubelet・コンテナランタイムなどのシステムコンポーネントログについて、ディレクトリ構成・確認方法・ローテーションの仕組みを中心に整理しました。EventとAudit Logについては、別の記事で扱っているのでそちらを参照してください。 対象コンポーネントとバージョン | コンポーネント | バージョン | リポジトリ | 本記事での役割 | |---|---|---|---| | Kubernetes | v1.37.1 | kubernetes/kubernetes | kubeletのコンテナログ管理・ローテーション・klog連携の実装をソースコードで確認 | | kind | v0.33.0 | kubernetes-sigs/kind | 検証環境。ノードイメージはkindest/node:v1.37.0 | | containerd | v2.3.4 | containerd/containerd | 検証環境のCRIランタイム(kindest/node:v1.37.0に同梱。ノード上でcontainerd --versionを実行して確認) | | eventrouter | commit 0d28dde(2026-09-16) | openshift/eventrouter | Eventを長期保存する実装例として、Event APIの監視方法と転送方式をソースコードで確認 | | Fluent Bit | v5.1.2 | fluent/fluent-bit | ノードレベルロギングエージェントの実装例として、in_tail・filter_kubernetes・out_lokiの実装をソースコードで確認 | | OpenTelemetry Collector Contrib | v0.162.0 | open-telemetry/opentelemetry-collector-contrib | Fluent Bitと並ぶノードレベルロギングエージェントの実装例として、filelog receiver・container operator・k8sattributesprocessorの実装をソースコードで確認 | | OpenTelemetry Collector | v0.162.0 | open-telemetry/opentelemetry-collector | otlphttpexporterの実装をソースコードで確認 | | Vector | v0.58.0 | vectordotdev/vector | Fluent Bit・OpenTelemetry Collectorと並ぶノードレベルロギングエージェントの実装例として、kubernetes_logs source・loki sinkの実装をソースコードで確認 | | Grafana Loki | v3.7.8 | grafana/loki | Fluent Bit・OpenTelemetry Collector・Vectorからのログ転送先として、実際にクラスタ外で動かして検証 | | stern | v1.34.0 | stern/stern | 複数Pod横断でのログ確認ツールの実装例として、Podの追跡方式をソースコードで確認 | TL;DR - コンテナログ・システムコンポーネントログ・Event・監査ログという4分類でログの全体像を整理できます。 - コンテナログは/var/log/pods/<namespace>_<name>_<uid>/<container>/<restartCount>.logに書き込まれ、/var/log/containers/に互換用のシンボリックリンクが張られます。 - コンテナログのローテーションは、kubelet自身がファイルをtruncateするのではなく、CRIランタイムのReopenContainerLogを呼んで新しいファイルへの書き込みを再開させる方式です。 - stdout/stderrを分離して取得できるPodLogsQuerySplitStreamsというFeature Gateもあります。 - kubeletやコンテナランタイムなど「コンテナの外で動くコンポーネント」自身のログには、Kubernetes側のローテーション機構がありません。 - kubectl get --rawでノードのkubeletログやその他のシステムサービスのログを直接取得できるLog Query機能がv1.36で安定版になりました。 - DaemonSetで動くログ収集エージェントは、kubeletのローテーション方式(ファイルのrename)に対応するため、パスを監視しつつファイルの実体を見分ける必要があり、Fluent Bitはinode比較、OpenTelemetry CollectorとVectorはファイル内容のチェックサムと、実装方式が異なります。 - Event(デフォルト1時間で消える)を長期間残したい場合は、Event APIをwatchして別の場所へ出力する専用のコンポーネントを立てます。 第1章: Kubernetesで扱うログの全体像 まず、この記事とEvent・監査ログの記事で扱う範囲を整理します。コンテナログ・システムコンポーネントログ・Event・監査ログという4種類のログについて、それぞれの位置づけを確認します。 1.1 4種類のログの位置づけ 公式ドキュメントのLogging Architectureは、ノード上のログをどう扱い・集約するかを説明するページで、Kubernetesのログ種別を網羅的に分類するものではありません。見出しも次の3つで、対象はノード上のログに限定されています。 - Pod and container logs - System component logs - Cluster-level logging architectures EventとAudit Logがここに登場しないのは、ノード上のファイルではなくAPIオブジェクトとして記録される別の仕組みだからです。 この記事では、ノード上のログ(コンテナログ+システムコンポーネントログ)とAPIオブジェクトのログ(Event、Audit Log)という区別で、それぞれ次の章で扱います。 | 系統 | 種類 | 記録される場所 | この記事での扱い | |---|---|---|---| | ノード上のログ | コンテナログ | ノードのファイルシステム(/var/log/pods等) | 第2章で詳説 | | ノード上のログ | システムコンポーネントログ(kubelet・コンテナランタイム・static Pod) | journald、/var/log配下のファイル | 第3章で詳説 | | APIオブジェクトのログ | Event | kube-apiserver(デフォルト--event-ttlで1時間保持) | 第4章では概要のみ。詳細はKubernetesのeventログの一覧に記載 | | APIオブジェクトのログ | Audit Log | Policyで指定したファイル/webhookバックエンド | 第4章では概要のみ。詳細はKubernetesの監査ログ(Audit Log)入門に記載 | 第2章: Podとコンテナのログ Podのコンテナがstdout/stderrに書いたログを、kubeletがどのようにノード上のファイルへ落とし込み、どうローテーションしているかを確認します。 2.1 ディレクトリ構成とファイルパスの決定 コンテナログの絶対パスは、Pod単位のディレクトリとコンテナ単位のサブディレクトリの組み合わせで決まります。 // BuildPodLogsDirectory builds absolute log directory path for a pod sandbox. func BuildPodLogsDirectory(podLogsDir, podNamespace, podName string, podUID types.UID) string { return filepath.Join(podLogsDir, strings.Join([]string{podNamespace, podName, string(podUID)}, logPathDelimiter)) } (参考: pkg/kubelet/kuberuntime/helpers.go#L209-L226) podLogsDirのデフォルト値は/var/log/podsで、KubeletConfigurationのpodLogsDirフィールド(デフォルトは空文字列扱いで、起動時にこのデフォルトが補完されます)で変更できます。 DefaultPodLogsDir = "/var/log/pods" (参考: pkg/kubelet/apis/config/v1beta1/defaults.go#L42、フィールド定義はstaging/src/k8s.io/kubelet/config/v1beta1/types.go#L132-L137) 変更する場合は、KubeletConfiguration(--configで指定するファイル、kindクラスタでは/var/lib/kubelet/config.yaml)に次のように指定します。 apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration podLogsDir: /mnt/log/pods コンテナ単位のファイル名は<コンテナ名>/<再起動回数>.logで、このパスがCRIのContainerConfig.LogPathとしてランタイムに渡され、Pod Sandbox単位で設定されるLogDirectoryと結合されて最終的な絶対パスになります。 containerLogsPath := buildContainerLogsPath(container.Name, restartCount) // ... config := &runtimeapi.ContainerConfig{ // ... LogPath: containerLogsPath, // ... } (参考: pkg/kubelet/kuberuntime/kuberuntime_container.go#L360-L382、Sandbox側のLogDirectory設定はpkg/kubelet/kuberuntime/kuberuntime_sandbox.go#L117-L118) 実際にkindクラスタ上でPodを起動して確認すると、/var/log以下は次のような階層になっていました。 $ tree /var/log/pods/default_demo-logtest_7f337cdd-9b2c-4cdd-839d-8336181f06a4 /var/log/pods/default_demo-logtest_7f337cdd-9b2c-4cdd-839d-8336181f06a4 `-- app |-- 0.log |-- 0.log.20261007-065856.gz |-- 0.log.20261007-065907.gz |-- 0.log.20261007-065917.gz `-- 0.log.20261007-065927 2 directories, 5 files pods/の下が<namespace>_<name>_<uid>、その下が<コンテナ名>というディレクトリになっており、ソースコードの実装どおりの階層が再現されています。 2.2 /var/log/containers/へのシンボリックリンク /var/log/containers/配下にも、コンテナログのシンボリックリンクが作られます。これはCRI導入前、kubeletがdockerdを直接操作していた頃に使われていたコンテナログの配置をそのまま残したものです。関数名もlegacyLogSymlinkと、ソースコード上はっきり「legacy」と位置づけられています。 // legacyLogSymlink composes the legacy container log path. It is only used for legacy cluster // logging support. func legacyLogSymlink(containerID string, containerName, podName, podNamespace string) string { return logSymlink(legacyContainerLogsDir, kubecontainer.BuildPodFullName(podName, podNamespace), containerName, containerID) } (参考: pkg/kubelet/kuberuntime/legacy.go#L31-L75) シンボリックリンクの作成自体は、コンテナ起動が成功した直後、実際のログファイルが存在することを確認してから行われます。 // Symlink container logs to the legacy container log location for cluster logging // support. // TODO(random-liu): Remove this after cluster logging supports CRI container log path. (参考: pkg/kubelet/kuberuntime/kuberuntime_container.go#L300-L317) :::note warn 上記のTODOコメントにあるとおり、このシンボリックリンクはいずれ削除される可能性があります。 ::: 実際の検証環境では、次のようなシンボリックリンクが張られていました。 $ ls -l /var/log/containers/ | grep demo-logtest lrwxrwxrwx 1 root root 81 Oct 7 06:58 demo-logtest_default_app-a33e8752ef7403a4970f2531374c62e9a657e5b945b3df05f1a8604da9f7d222.log -> /var/log/pods/default_demo-logtest_7f337cdd-9b2c-4cdd-839d-8336181f06a4/app/0.log 2.3 CRIログフォーマットとkubectl logs コンテナランタイムがファイルに書き込むログの各行は、CRIで定められた形式に従います。この形式はkubelet側のk8s.io/cri-clientモジュールでパースされており、ソースコード中のコメントに例が示されています。 // parseCRILog parses logs in CRI log format. CRI Log format example: // // 2016-10-06T00:17:09.669794202Z stdout P log content 1 // 2016-10-06T00:17:09.669794203Z stderr F log content 2 (参考: staging/src/k8s.io/cri-client/pkg/logs/logs.go#L123-L168) タイムスタンプ・ストリーム種別(stdout/stderr)に続く1文字は、そのログ行が1行の出力として完結しているか(F = Full)、まだ続きがあるか(P = Partial)を示すタグです。 LogTagPartial LogTag = "P" LogTagFull LogTag = "F" (参考: staging/src/k8s.io/cri-api/pkg/apis/runtime/v1/constants.go#L49-L52) :::note info このタグは、アプリケーションが改行区切りで書いた複数行のログ(スタックトレース等)を結合するためのものではありません。CRIランタイム側の1行あたりの読み取りバッファ上限(containerdではMaxContainerLogLineSize、デフォルト16KB)を、アプリケーションが1回の書き込みで超えた場合に、その1行を複数の物理行に分割して書き込むための仕組みです。分割された行のうち、最後の断片だけがFになります。 if maxLen > 0 && length > maxLen { // ... writeLineBuffer(partial, buf) // ... } (参考: internal/cri/io/logger.go#L114-L195、バッファ上限の設定はinternal/cri/config/config_unix.go#L102) ::: 検証環境のkube-apiserverのログを見ると、実際にこの形式で書き込まれていることが確認できます。 $ docker exec k8s-logging-demo-control-plane tail -c 300 /var/log/pods/kube-system_kube-apiserver-*/kube-apiserver/0.log 2026-09-30T06:07:44.320806626Z stderr F I0930 06:07:44.320423 1 controller.go:667] quota admission added evaluator for: controllerrevisions.apps 2026-09-30T06:07:44.563889999Z stderr F I0930 06:07:44.563773 1 controller.go:667] quota admission added evaluator for: replicasets.apps kubectl logsはこのCRI形式のファイルを直接読み、タグとタイムスタンプを除いた本文だけをクライアントに返します。 CRIログ形式がstdout/stderrをストリーム種別として行ごとに記録していることを利用して、Pod APIからstdout・stderrのどちらか一方だけを取得できる機能もあります。PodLogsQuerySplitStreamsというFeature Gateで、v1.32でアルファとして導入され、v1.37時点でもデフォルトで無効です(参考: pkg/features/kube_features.go#L899-L902、デフォルト状態は同ファイルL1978-L1980)。 kubelet側の実装では、PodLogOptionsのstreamフィールドの値に応じて、CRIログのstdout行・stderr行のどちらを応答に書き込むかを切り替えているだけです。 switch *wantedStream { case v1.LogStreamStdout: stdout, stderr = fw, nil case v1.LogStreamStderr: stdout, stderr = nil, fw case v1.LogStreamAll: stdout, stderr = fw, fw (参考: pkg/kubelet/server/server.go#L888-L901) Feature Gateを有効にして実際に試すと、streamクエリパラメータでstdout/stderrを分離して取得できました。 $ kubectl get --raw "/api/v1/namespaces/default/pods/demo-streams/log?stream=Stdout" stdout-line-1 stdout-line-2 done $ kubectl get --raw "/api/v1/namespaces/default/pods/demo-streams/log?stream=Stderr" stderr-line-1 stderr-line-2 普段のkubectl logsはstreamを指定しない(All)ため、これまでどおり両方のストリームがまとめて返ってきます。 2.4 ログローテーション コンテナログのローテーションはpkg/kubelet/logs/container_log_manager.goが担当します。デフォルト値は次のとおりです。 | 設定項目 | デフォルト値 | 役割 | |---|---|---| | containerLogMaxSize | 10Mi | 1ファイルあたりの上限サイズ | | containerLogMaxFiles | 5 | コンテナあたりの最大ファイル数(現行+ローテーション済み合計) | | containerLogMaxWorkers | 1 | ローテーションを並行実行するワーカー数 | | containerLogMonitorInterval | 10秒 | ログサイズを監視する間隔 | (参考: pkg/kubelet/apis/config/v1beta1/defaults.go#L256-L266) これらもKubeletConfigurationで変更できます。たとえばログの量が多いワークロードが多いノードで、1ファイルあたりの上限を大きくしつつ保持ファイル数を減らす場合は次のようになります。 apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration containerLogMaxSize: 50Mi containerLogMaxFiles: 3 containerLogMaxWorkers: 2 containerLogMonitorInterval: 10s ローテーションの実体は、containerLogMonitorIntervalごとに実行中の全コンテナを列挙し、ログファイルのサイズがcontainerLogMaxSize以上ならローテーション処理をキューに積む、というシンプルなポーリングです。 func (c *containerLogManager) processContainer(ctx context.Context, worker int) (ok bool) { // ... if info.Size() < c.policy.MaxSize { // rotate不要 return } if err := c.rotateLog(ctx, id, path); err != nil { // ... } return } (参考: pkg/kubelet/logs/container_log_manager.go#L218-L270) ここで重要なのは、kubeletがファイルをtruncateして使い回すのではなく、次の手順を踏む点です。 1. 現在のログファイルをタイムスタンプ付きの名前にrenameする(この時点ではまだ無圧縮)。これはkubelet自身が行う処理で、ファイルの中身ごと退避させるだけです。 2. CRIランタイムのReopenContainerLogを呼ぶ。ここから先はkubeletではなくランタイム側の処理です。 1. リネームによって空になった元のパスに、新しいログファイルを作成する。 2. コンテナのstdout/stderrから受け取ったバイトの書き込み先を、今作った新しいファイルに差し替える。 3. それまで書き込み先として使っていた古いファイルハンドル(リネームされた方のファイルを握っていたハンドル)を閉じる。 3. ReopenContainerLogが失敗したらリネームを元に戻し、次のローテーション周期(デフォルト10秒)で再試行する(ログを失わないための保険)。 コンテナプロセス自身は、自分のstdoutの書き込み先ファイルが新しいファイルに切り替わったことを一切意識しません。ファイルの実体を直接触っているのはコンテナプロセスではなくランタイム側であり、コンテナはあくまでランタイムが用意したパイプに書き込み続けているだけだからです。 (参考: containerd側のReopenContainerLog実装はinternal/cri/server/container_log_reopen.go#L29-L51、新しいログファイルをO_CREATEで作成している箇所はinternal/cri/server/helpers_linux.go#L58-L62) func (c *containerLogManager) rotateLatestLog(ctx context.Context, id, log string) error { // ... timestamp := c.clock.Now().Format(timestampFormat) rotated := fmt.Sprintf("%s.%s", log, timestamp) if err := c.osInterface.Rename(log, rotated); err != nil { return fmt.Errorf("failed to rotate log %q to %q: %w", log, rotated, err) } if err := c.runtimeService.ReopenContainerLog(ctx, id); err != nil { // rename back... } return nil } (参考: pkg/kubelet/logs/container_log_manager.go#L414-L436) リネーム後、まだ圧縮されていないローテーション済みファイルはgzipで非同期に圧縮され、圧縮後は元ファイルが削除されます。ファイル数がcontainerLogMaxFilesを超える場合は、古い順に削除されます(「現在のファイル」と「ローテーション中の1ファイル」を除いたMaxFiles - 2個までが、圧縮済みログとして保持される計算です)。 // removeExcessLogs removes old logs to make sure there are only at most MaxFiles log files. func (c *containerLogManager) removeExcessLogs(logs []string) ([]string, error) { sort.Strings(logs) maxRotatedFiles := c.policy.MaxFiles - 2 // ... } (参考: pkg/kubelet/logs/container_log_manager.go#L272-L371、gzip圧縮の実装は同ファイルL373-L411) 実際にローテーションを起こしてみる kindクラスタ上でstdoutに大量の行を書き続けるだけのPodを動かし、ローテーションの様子を観察しました。開始直後は、ローテーション済みファイルがまだ圧縮されていない状態が見えます。 $ tree -h /var/log/pods/default_demo-logtest_.../app /var/log/pods/default_demo-logtest_.../app |-- [ 17M] 0.log `-- [ 11M] 0.log.20260930-062154 1 directory, 2 files しばらく待つと、ローテーション済みファイルが.gzに置き換わり、さらに新しいローテーションも発生しています。 $ tree -h /var/log/pods/default_demo-logtest_.../app /var/log/pods/default_demo-logtest_.../app |-- [ 11M] 0.log |-- [1005K] 0.log.20260930-062154.gz `-- [ 17M] 0.log.20260930-062214 1 directory, 3 files さらにローテーションが繰り返されると、圧縮済み3ファイル+ローテーション中の1ファイル+現在のファイルの合計5ファイルで安定し、6ファイル目が作られるタイミングで最も古いファイルが削除されました。 $ tree -h /var/log/pods/default_demo-logtest_.../app /var/log/pods/default_demo-logtest_.../app |-- [2.9M] 0.log |-- [1005K] 0.log.20260930-062154.gz |-- [1.5M] 0.log.20260930-062214.gz |-- [1.4M] 0.log.20260930-062234.gz `-- [ 17M] 0.log.20260930-062254 1 directory, 5 files containerLogMaxFiles: 5が「現在のファイルを含めて合計5ファイル」を意味することが、実際の挙動としても確認できました。 :::note warn kubectl logsで読めるのは現在(最新)のログファイルだけです。ローテーション済みの.log.<timestamp>や.gzファイルの内容はkubectl logsでは取得できません。ノード上のファイルを直接見るか、ログ収集エージェント側でローテーション済みファイルまで拾う設計にしておく必要があります(参考: Logging Architecture#Log rotation)。 ::: 第3章: システムコンポーネントのログ Podのコンテナとは別に、kubeletやコンテナランタイム自身、そして制御コンポーネントにもログがあります。kubeletとコンテナランタイムはPodを管理する側なので、常にノードのOS上のプロセスとして動きます。一方、kube-apiserverなどの制御コンポーネントをsystemdのunitとして動かすかstatic Podとして動かすかは、クラスタの構築方法次第です。動かし方によってログの出力先・ローテーション方式が次のように変わります。 | パターン | 実体 | ログの出力先 | ローテーション | 確認コマンド | |---|---|---|---|---| | systemdのunit | ノードOS上で直接動くプロセス | journald | Kubernetes側の機構はなし。journald/OS側に依存(3.4節) | journalctl -u <unit名> | | static Pod | kubeletが管理する「ただのコンテナ」 | 通常のPodと同じ/var/log/pods(stdout) | 通常のコンテナログと同じcontainerLogMaxSize/MaxFiles(第2章2.4) | kubectl logs | この章では、それぞれどこに・どんな形式で出力されるかを以下で詳しく確認します。この記事の検証環境(kind)では、次のように動いています。 - systemd - kubelet - コンテナランタイム(containerd) - static Pod - kube-apiserver - kube-controller-manager - kube-scheduler - etcd 3.1 systemdで管理されるコンポーネント kubeletとコンテナランタイムは、通常コンテナの中では動かず、ノードのOS上で直接動くプロセスです。ノードの初期化・サービス管理の仕組みはsystemdであることが多いため、この記事もsystemdを前提に話を進めます。systemd環境では、デフォルトでjournaldにログを書き込みます。 今回の検証で用いたkindでも、kubeletはsystemdのunitとして動いています。 $ docker exec k8s-logging-demo-control-plane systemctl status kubelet --no-pager | head -8 ● kubelet.service - kubelet: The Kubernetes Node Agent Loaded: loaded (/etc/systemd/system/kubelet.service; enabled; preset: enabled) Active: active (running) since Wed 2026-09-30 06:07:36 UTC; 14min ago Main PID: 766 (kubelet) $ docker exec k8s-logging-demo-control-plane journalctl -u kubelet --no-pager | tail -3 Sep 30 06:21:41 k8s-logging-demo-control-plane kubelet[766]: I0930 06:21:41.349103 766 pod_startup_latency_tracker.go:144] "Observed pod startup duration" pod="default/demo-logtest" ... journalctl -u kubeletでkubelet自身のログが直接読めます。 3.2 static Podとして動くコンポーネント static Podの元になるマニフェストの置き場所は、kubeletのKubeletConfigurationではstaticPodPathというフィールドで指定します。 // staticPodPath is the path to the directory containing local (static) pods to // run, or the path to a single static pod file. // Default: "" StaticPodPath string `json:"staticPodPath,omitempty"` (参考: staging/src/k8s.io/kubelet/config/v1beta1/types.go#L127-L131) デフォルト値は空文字列で、kubelet自体は/etc/kubernetes/manifestsをハードコードしていません。この慣習的なパスはkubeadmなど、kubeletを実際にセットアップするツール側の取り決めで、実際にkubeadmが生成するKubeletConfigurationには次のように明示的に書き込まれています。 apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration staticPodPath: /etc/kubernetes/manifests 監視の実装はpkg/kubelet/config/file.goのNewSourceFileで、指定したディレクトリをファイルシステム通知で監視し、追加・削除されたマニフェストに応じてPodを起動・停止します。static Podは通常のAPIオブジェクトとしては作成されないため、kubeletはpkg/kubelet/pod/mirror_client.go経由で対応する「mirror Pod」をAPIサーバー上に作り、kubectl get pod等から見えるようにしています。 重要なのは、これらのコンポーネントがただのコンテナとして動いているという点です。static Podとして動くkube-apiserver等がstdoutに書いたログは、通常のPodのコンテナと全く同じ扱い(第2章のCRIログ管理・/var/log/pods・containerLogMaxSize/MaxFilesによるローテーション)を受けます。実際、検証環境でも次のとおり通常のPodと同じディレクトリ構成でした。 $ tree --matchdirs -P '*kube-apiserver*' --prune /var/log/pods /var/log/pods `-- kube-system_kube-apiserver-k8s-logging-demo-control-plane_aec712fda8c04555eaada4085f170287 `-- kube-apiserver 3 directories, 0 files 3.3 ログのフォーマットとklog システムコンポーネントのログは、同じ内容でも次の2つのフォーマットのどちらかで出力されます。 | 値 | 出力形式 | |---|---| | text(デフォルト) | 人間が読みやすいキーバリュー形式 | | json | 構造化されたJSON形式 | 実際にkube-schedulerのリーダー選出のログを、フォーマットを切り替えて比較してみます。text形式(デフォルト)では次のように出力されます。 I1008 08:56:19.685458 1 leaderelection.go:258] "Attempting to acquire leader lease..." lock="kube-system/kube-scheduler" I1008 08:56:19.688682 1 leaderelection.go:272] "Successfully acquired lease" lock="kube-system/kube-scheduler" 同じイベントをjson形式に切り替えて出力させると、次のようになります。 {"ts":1791449829380.4631,"caller":"leaderelection/leaderelection.go:258","msg":"Attempting to acquire leader lease...","v":0,"lock":"kube-system/kube-scheduler"} {"ts":1791449829383.8164,"caller":"leaderelection/leaderelection.go:272","msg":"Successfully acquired lease","v":0,"lock":"kube-system/kube-scheduler"} lockフィールドの中身は同じですが、text形式はメッセージと埋め込まれたキーバリューをそのまま1行に並べた形式、json形式はts/caller/msg/vといったキーを持つJSONオブジェクトです。このフォーマットの出し分けを担っているのが、kubeletを含むKubernetesのコンポーネントが共通で使っているロギングライブラリklogです。klogは出力形式だけでなく出力先についても規定しており、公式ドキュメントには次のように明記されています。 > Output will always be written to stderr, regardless of the output format. Output redirection is expected to be handled by the component which invokes a Kubernetes component. This can be a POSIX shell or a tool like systemd. (参考: System Logs#Klog) 出力形式に関わらず、出力先は常にstderr固定であり、リダイレクトはKubernetesコンポーネントを起動する側(POSIXシェルやsystemdなど)の責任だと明記されています。 この--logging-formatフラグは、次の4コンポーネントで利用できます。いずれもデフォルトはtext形式で、json形式にするには明示的な設定が必要です。 - kube-apiserver - kube-controller-manager - kube-scheduler - kubelet 実際、検証環境のstatic Podマニフェストを見ても--logging-formatは指定されておらず、ログはklogのテキスト形式のままでした。 $ docker exec k8s-logging-demo-control-plane grep -i logging-format /etc/kubernetes/manifests/*.yaml (該当なし) 有効にする場合、static Podとして動くものはマニフェストのcommandに以下のように引数を追加します。 apiVersion: v1 kind: Pod metadata: name: kube-apiserver namespace: kube-system spec: containers: - name: kube-apiserver command: - kube-apiserver - --logging-format=json # ...(他の引数は省略) kubelet自身は起動引数ではなくKubeletConfigurationのlogging.formatフィールドで指定します。 apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration logging: format: json JSON形式に加えて、Contextual LoggingとStructured Loggingという2つの仕組みも押さえておくとよいと思います。どちらも「有効かどうか」だけでなく、ログ出力箇所のコードが実際にそのAPIを使っているかによって見え方が変わります。kube-schedulerの実装を例に見てみます。 func (sched *Scheduler) scheduleOnePod(ctx context.Context, podInfo *framework.QueuedPodInfo) { logger := klog.FromContext(ctx) pod := podInfo.Pod // TODO(knelasevero): Remove duplicated keys from log entry calls // When contextualized logging hits GA // https://github.com/kubernetes/kubernetes/issues/111672 logger = klog.LoggerWithValues(logger, "pod", klog.KObj(pod)) ctx = klog.NewContext(ctx, logger) logger.V(4).Info("About to try and schedule pod", "pod", klog.KObj(pod)) (参考: pkg/scheduler/schedule_one.go#L93-L101) - Contextual Logging - 次の部分がこれです。 ```go klog.LoggerWithValues(logger, "pod", klog.KObj(pod)) ``` このPod用に作ったloggerにpodフィールドを紐付け、ctx = klog.NewContext(ctx, logger)でcontextに載せています。これ以降ctxを受け取って動く別の関数がklog.FromContext(ctx)でロガーを取り出すだけで、都度"pod", klog.KObj(pod)を書き直さなくてもpodフィールドが自動的に付いてきます。 - Structured Logging - 次の"pod", klog.KObj(pod)という部分がこれです。 ```go logger.V(4).Info("About to try and schedule pod", "pod", klog.KObj(pod)) ``` メッセージ文字列とは別に、キーと値の組をログ行に付与します。--logging-format=text(デフォルト)なら次のようにキーバリュー形式で、--logging-format=jsonならJSONのフィールドとして出力されます。 実際にkindクラスタのkube-scheduler(-v=4)でこの行を出力させてみると、次のようになりました。 I1007 09:47:56.151071 1 schedule_one.go:101] "About to try and schedule pod" pod="default/demo-schedule-test" コード中には"pod", klog.KObj(pod)という呼び出し側の明示的なキーがあります。loggerは直前のklog.LoggerWithValuesで既にpodフィールドを持っているので、本来この呼び出し側での指定は冗長なはずです。実際、klogの内部実装(internal/serialize/keyvalues.go)には、同じキーが複数回渡された場合に重複を取り除く処理があり、値が一致していれば後から渡された分は捨てられます。 if bytes.Compare(data[existing[i].start:existing[i].end], data[e.start:e.end]) == 0 { // The new entry gets obsoleted because it's identical. // ... obsolete = append(obsolete, e.interval) } (参考: vendor/k8s.io/klog/v2/internal/serialize/keyvalues.go#L100-L109) 試しに--feature-gates=ContextualLogging=falseでContextual Logging自体を無効化し、同じ行を出力させても、結果は変わりませんでした。 I1007 09:49:45.058261 1 schedule_one.go:101] "About to try and schedule pod" pod="default/demo-schedule-test3" これは、Contextual Loggingが無効ならklog.LoggerWithValuesが実質何もしなくなる一方で、呼び出し側が同じpodキーを明示的にも渡しているため、そちらだけでpodフィールドが出力され続けるからです。コード中のTODOコメントが示すとおり、 // TODO(knelasevero): Remove duplicated keys from log entry calls // When contextualized logging hits GA (参考: pkg/scheduler/schedule_one.go#L96-L97) これは「Contextual LoggingがGAになるまでの間、無効化されても出力が変わらないようにするための意図的な二重指定」だと考えられます。GA後にこの冗長な呼び出し側の指定が取り除かれれば、podフィールドが出力されるかどうかはContextual Loggingの有効/無効に完全に依存するようになると思います。 3.4 システムコンポーネントログのローテーション 第2章で見たコンテナログのローテーションは、あくまでkubeletが管理するContainerLogMaxSize/MaxFilesの仕組みです。journaldに書かれるkubelet・コンテナランタイム自身のログには、この仕組みは適用されません。k8s.io/component-base/logsパッケージ自体にも、サイズや時間を基準にしたローテーション処理は実装されていません。公式ドキュメントも次のように説明しています。 > Similar to the container logs, you should rotate system component logs in the /var/log directory. In Kubernetes clusters created by the kube-up.sh script, log rotation is configured by the logrotate tool. The logrotate tool rotates logs daily, or once the log size is greater than 100MB. (参考: System Logs#Log location) static Podとして動く制御コンポーネントについても、公式ドキュメントは次のように注意を促しています。 > If you deploy Kubernetes cluster components (such as the scheduler) to log to a volume shared from the parent node, you need to consider and ensure that those logs are rotated. Kubernetes does not manage that log rotation. (参考: Logging Architecture) これは、stdoutに書く(第2章のCRI管理下に入る)のではなく、あえて/var/logをhostPathでマウントしてそこにファイルを書かせるような構成を取った場合の注意で、stdoutに書く一般的な構成であれば、通常のコンテナログと同じくkubeletがローテーションを管理してくれます。 3.5 Log Queryでノードのシステムログを直接取得する 比較的新しい機能として、Log Queryがあります。v1.27でアルファ機能として導入され、v1.36で安定版に昇格しました(参考: System Logs#Log query)。ノードにSSHしたりjournalctlを叩いたりしなくても、kube-apiserver経由でノード上のシステムログを取得できます。 利用にはkubeletのenableSystemLogHandler(デフォルトfalse)を有効にする必要があります。検証環境で実際に有効化して試してみました。 # /var/lib/kubelet/config.yaml に追記 enableSystemLogHandler: true enableSystemLogQuery: true kubelet再起動後、次のようにkubectl get --rawでkubeletのログを直接取得できました。 $ kubectl get --raw "/api/v1/nodes/k8s-logging-demo-control-plane/proxy/logs/?query=kubelet&tailLines=5" Sep 30 06:29:29.628900 k8s-logging-demo-control-plane kubelet[1866838]: I0930 06:29:29.628821 ... "Finished populating initial desired state of world" Sep 30 06:29:29.644381 k8s-logging-demo-control-plane kubelet[1866838]: I0930 06:29:29.644327 ... "operationExecutor.VerifyControllerAttachedVolume started for volume \"xtables-lock\"..." pod="kube-system/kube-proxy-jqk52" patternクエリパラメータで正規表現による絞り込みもできます。 $ kubectl get --raw "/api/v1/nodes/k8s-logging-demo-control-plane/proxy/logs/?query=kubelet&pattern=broadcasted&tailLines=200" Sep 30 06:07:32.248053 k8s-logging-demo-control-plane kubelet[244]: I0930 06:07:32.248014 244 server.go:177] "Pod update broadcasted" podUID="aec712fda8c04555eaada4085f170287" type="ADDED" queryにはサービス名だけでなく/var/log配下のファイル名も指定でき、kubeletはjournaldを優先的に探し、無ければ/var/log/<servicename>等を探すヒューリスティックで動作します(参考: System Logs#Log query)。 :::note warn nodes/proxyへのget権限は、Log Query以外にもノード上で任意のコンテナに対してコマンドを実行できる強力なkubelet APIへのアクセスも同時に許可してしまいます。権限を付与する際はKubelet認証・認可のドキュメントにある注意点を確認しておくのが良いと思います。 ::: 第4章: EventとAudit Log EventとAudit Logは、それぞれ別の記事で扱っているのでそちらを参照してください。 4.1 Event Podのスケジューリング失敗やヘルスチェック失敗など、Kubernetesの各コンポーネントが状態変化に応じて発行するAPIオブジェクトです。kubectl describe podやkubectl get eventsで確認できますが、デフォルトで1時間(--event-ttl)しか保持されません。Reasonの種類ごとの詳しい整理は、以前書いた記事を参照してください。 参考: Kubernetesのeventログの一覧 Eventを長期間残したい場合は、Event APIをwatchして別の場所へ出力する専用のコンポーネントを用意します。その実装例として、eventrouterを見てみます。手元で確認したクローンは、Deploymentを使ってクラスタ全体で1つだけPodを用意し、そのPod内のプロセスがSharedInformerでEvent APIをwatchする実装でした。 func NewEventRouter(kubeClient kubernetes.Interface, eventsInformer coreinformers.EventInformer) *EventRouter { er := &EventRouter{ kubeClient: kubeClient, eSink: sinks.ManufactureSink(), } eventsInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: er.addEvent, UpdateFunc: er.updateEvent, DeleteFunc: er.deleteEvent, }) er.eLister = eventsInformer.Lister() er.eListerSynched = eventsInformer.Informer().HasSynced return er } (参考: eventrouter.go#L50-L63、Informer自体の生成元はmain.go#L112-L113) 受け取ったEventの転送先(sink)は、設定で切り替えられる作りになっています。 func ManufactureSink() (e EventSinkInterface) { s := viper.GetString("sink") switch s { case "glog": e = NewGlogSink() case "stdout": e = NewStdoutSink() case "http": // ... case "kafka": // ... } return e } (参考: sinks/interfaces.go#L35-L82) リポジトリに同梱されているKubernetesマニフェストでは、このsinkがstdoutに設定されています。 apiVersion: v1 data: config.json: |- { "sink": "stdout" } kind: ConfigMap (参考: yaml/eventrouter.yaml) stdout sinkの実装は、EventをJSONにシリアライズして標準出力に書き出すだけです。コード中のコメントに、その狙いが明記されています。 // StdoutSink is the other basic sink // By default, Fluentd/ElasticSearch won't index glog formatted lines // By logging raw JSON to stdout, we will get automated indexing which // can be queried in Kibana. (参考: sinks/stdoutsink.go#L27-L46) つまりEvent専用の転送手段を別途実装するのではなく、第5章・第6章で見たコンテナログ収集と同じ「標準出力に書く→ノードレベルエージェントが拾う」という仕組みにそのまま乗せる設計です。eventrouterのPod自体が標準出力にJSONを書き出すだけで、既存のFluent BitなどのDaemonSetがいつも通り収集し、Eventの長期保存が実現できます。 4.2 Audit Log 「誰が・いつ・何を・どうしようとしたか」というAPIリクエストそのものを記録する、kube-apiserverの仕組みです。Eventとは異なるaudit.k8s.ioという専用のAPI群で表現され、Policyでログの粒度や出力先(ファイル/webhook)を細かく制御できます。設定方法や実際に記録される内容については、以前書いた記事を参照してください。 参考: Kubernetesの監査ログ(Audit Log)入門 第5章: ログの収集方法 ノード上に散らばったログを、クラスタ全体で一元的に扱うための収集方法を整理します。 5.1 3つのアーキテクチャパターン 公式ドキュメントは、クラスタレベルのログ収集について3つのアプローチを挙げています。 - アプリケーションから直接バックエンドへログを送る。 - アプリケーションPodに専用のsidecarコンテナを追加する。 - ノードごとに動くノードレベルのロギングエージェントを使う。 | パターン | 概要 | 向いているケース | |---|---|---| | アプリケーションから直接送信 | Kubernetesの管轄外 | アプリケーション側で送信ライブラリを組み込める場合 | | sidecar(専用エージェント) | Fluentd等のロギングエージェント自体をPodにsidecarコンテナとして追加 | ノードレベルエージェントでは要件を満たせない場合。stdoutに書かない場合はkubectl logsでは読めない点に注意 | | ノードレベルエージェント(DaemonSet) | ノードごとに1つのエージェントが、そのノード上の全コンテナのログファイルを収集 | アプリケーション側の変更が不要。最も一般的 | 5.2 アプリケーションから直接送信する アプリケーション自身がログ送信用のライブラリを組み込み、ノードのエージェントを経由せず直接バックエンドへ送信する方法です。Kubernetes自体の管轄外の構成になります。 !Exposing logs directly from the application アプリケーションがノードやKubernetesを経由せず、直接ログバックエンドへ送信する構成 (参考: Logging Architecture#Exposing logs directly from the application) 5.3 専用のロギングエージェントをPodにsidecarとして追加する ノードレベルのエージェントでは要件を満たせない場合、Fluentd等のロギングエージェントをPodにsidecarコンテナとして追加し、アプリケーションコンテナのログを直接処理させる方法もあります。 !Sidecar container with a logging agent 専用のロギングエージェントをPodにsidecarコンテナとして追加し、直接バックエンドへ転送する構成 (参考: Logging Architecture#Sidecar container with a logging agent) 5.4 ノードレベルエージェントによるDaemonSetでの収集 最も一般的なのは、Fluent BitやOpenTelemetry CollectorのようなロギングエージェントをDaemonSetとしてすべてのノードにデプロイし、各ノードの/var/log/podsをhostPathでマウントして読む方法です。 !Using a node logging agent ノードごとに1つのロギングエージェントを配置し、そのノード上の全コンテナのログファイルを収集する構成 (参考: Logging Architecture#Using a node logging agent) DaemonSetにする理由は単純で、ノードごとに1つのエージェントだけを常駐させれば、そのノード上で動く全Podのログを横断的に拾えるためです。アプリケーション側のマニフェストを変更する必要がありません。 個人的には、まずこの方法から始めるのが良いと思います。アプリケーション側に手を入れる必要がなく、クラスタ側にDaemonSetを1つ用意するだけで、既存のワークロードも含めて横断的にログを収集できるためです。 第6章: ログ収集の実装例: Fluent BitとOpenTelemetry Collector 6.1 ログを収集して、蓄積する場合の例 ログを収集して蓄積する一例として、ここではノードレベルエージェントにFluent Bit・OpenTelemetry Collector・Vector、転送先にGrafana Lokiを使う構成を見ていきます。 - Fluent Bit (C言語, Apache License 2.0) - ロギングエージェント。CNCF(Cloud Native Computing Foundation)のプロジェクトで、複数のコントリビューターが開発しています(参考: README.md#Authors)。in_tail・filter_kubernetes・out_lokiを使います。 - OpenTelemetry Collector (Go言語, Apache License 2.0) - ロギングエージェント。同じくCNCFのプロジェクトです(参考: OpenTelemetry)。Collectorのメンテナーは特定の1社が全体の4分の1を超えて占めないようにする規約があり、複数ベンダーのコントリビューターがバランスを取りながら開発しています(参考: README.md#No Over-Representation)。filelog receiver・container operator・k8sattributesprocessor・otlphttp exporterを使います。 - Vector (Rust言語, Mozilla Public License 2.0) - ロギングエージェント。主にDatadogが開発しています(参考: README.md#What is Vector?)。設定ファイル自体にvector testでユニットテストを書けるように設計されているのが特徴で(参考: Unit tests)、Kubernetes向けに専用化されたkubernetes_logs sourceと、Loki向けのloki sinkを使います。 - Grafana Loki (Go言語, AGPL-3.0-only) - 収集したログの転送先・蓄積先(一部ファイルはApache License 2.0の例外あり、参考: README.md#License)。主にGrafana Labsが開発しています。Prometheusと同じラベル(key-value)でログの集まり(ストリーム)を識別します。 ロギングエージェントの実装には、大きく分けて次の工程があります。 - ログローテーションへの追従 - CRIログの再構成 - Podメタデータの付与 - Grafana Lokiへの転送 - チェックポイントの永続化 このうち最初の3つは第2章・第3章の内容と直接関わってきます。それぞれについて、Fluent Bit・OpenTelemetry Collector・Vectorがどういう考え方で実装しているかをソースコードを確認しながら見ていきます。続いて転送先であるGrafana Lokiへの送り方の違いを確認し、最後にDaemonSetの更新などでPodが入れ替わっても読み取り位置を引き継げるようにする、チェックポイントの永続化の仕組みも見ていきます。 6.2 ログローテーションへの追従 第2章2.4で見たとおり、kubeletは現在のログファイルをrenameしてから、CRIランタイムに元のパスへ新しいファイルを開き直させます。ログエージェントが単純にファイルパスだけをポーリングしていると、リネームの前後で「今開いているファイル」と「そのパスにある実際のファイル」がずれてしまい、ログを取りこぼす可能性があります。 Fluent Bitの場合 Fluent Bitのin_tailは、この問題をinode比較で解決しています。監視中のパスを定期的にstatし、記録しているinodeと食い違っていればローテーションされていると判断します。 /* Returns FLB_TRUE if a file has been rotated, otherwise FLB_FALSE */ int flb_tail_file_is_rotated(struct flb_tail_config *ctx, struct flb_tail_file *file) (参考: plugins/in_tail/tail_file.c#L2154-L2226、ローテーション後も元のファイルディスクリプタで読み切ってから新しいファイルを追加で監視する処理は同ファイルL2432-L2496) パスのinodeが変わらないまま存在し続けるファイルは同一ファイルとして読み続け、リネーム後に元のパスに現れたinodeの異なるファイルは新規ファイルとして扱われます。「ファイルパスだけでは同一性を保証できない」というkubeletのローテーション方式に対応するための設計で、inodeをそのまま使う直接的なアプローチです。 OpenTelemetry Collectorの場合 filelog receiverの実体はpkg/stanza/fileconsumerパッケージです。ログローテーションへの追従方式はFluent Bitのinode比較とは異なり、ファイル冒頭1000バイト(デフォルト)のチェックサムをフィンガープリントとして使っています。 // Fingerprint is used to identify a file // A file's fingerprint is the first N bytes of the file type Fingerprint struct { firstBytes []byte key string } (参考: pkg/stanza/fileconsumer/internal/fingerprint/fingerprint.go#L20-L29) フィンガープリントが完全一致すれば同一ファイル、新しいフィンガープリントが古いフィンガープリントで始まっていれば「書き込みが続いている同じファイル」と判定します。 func (f Fingerprint) StartsWith(old *Fingerprint) bool { l0 := len(old.firstBytes) // ... return bytes.Equal(old.firstBytes[:l0], f.firstBytes[:l0]) } (参考: 同ファイルL93-L113、ポーリングループとフィンガープリントの突き合わせはpkg/stanza/fileconsumer/file.go#L139-L256) inodeはPOSIX固有の概念のため、Windowsでは使えません。Windowsでも共通の仕組みで動作するように、この方式になっているようです(参考: design.md#Potential failure to consume files when file rotation via move/create is used on Windows)。 Vectorの場合 Vectorも中身はフィンガープリント方式ですが、DevInode(デバイス番号+inode)とFirstLinesChecksum(先頭数行のCRC64チェックサム)の2方式を切り替え可能な設定として両方持っています。 #[derive(Debug, Clone)] pub enum FingerprintStrategy { FirstLinesChecksum { ignored_header_bytes: usize, lines: usize, }, DevInode, } (参考: lib/file-source-common/src/fingerprinter.rs#L46-L53) そのうえで、Kubernetesのコンテナログを読むkubernetes_logs sourceはFirstLinesChecksumの方を明示的に選んでいます。コメントにその理由が書かれており、「Kubernetesのログファイルの形は決まっているので、専用に調整したフィンガープリンターを使う」としています。 // The shape of the log files is well-known in the Kubernetes // environment, so we pick the a specially crafted fingerprinter // for the log files. fingerprinter: Fingerprinter::new( FingerprintStrategy::FirstLinesChecksum { // Max line length to expect during fingerprinting, see the // explanation above. ignored_header_bytes: 0, lines: fingerprint_lines, }, resolved_max_line_bytes, true, ), (参考: src/sources/kubernetes_logs/mod.rs#L885-L895、fingerprint_linesのデフォルトは1行分です。同ファイルL1125-L1127) 「専用に調整した」の中身は、FirstLinesChecksumという汎用の仕組みに渡している2つの値です。 - ignored_header_bytes: 0: 読み飛ばすヘッダーが無いという指定です。CRIログの各行はヘッダーを持たず、いきなりタイムスタンプから始まるため、0で固定できます。 - lines: fingerprint_lines(デフォルト1行): 指紋に使う行数です。CRIログは1行1エントリの形式だと分かっているため、1行だけで十分に識別できます。 実際の読み取りは、ファイルの先頭からlines回目(デフォルトでは1回目)の改行が見つかった時点で打ち切り、そこまでのバイト列をCRC64でハッシュ化したものをフィンガープリントとします。 for (pos, &c) in buf[..read].iter().enumerate() { if c == delim { if count <= 1 { total_read += pos + 1; break 'main; } else { count -= 1; } } } (参考: fingerprinter.rs#L247-L274) さらに、1行目を読み込む最大サイズ(resolved_max_line_bytes)もデフォルトで32KBです(設定で変更可能)。CRIログの各行は必ずタイムスタンプから始まるため、たとえ1行目が32KBで打ち切られても、ファイルを識別する指紋としては十分な情報が確保できます。 このフィンガープリントは、ファイルパスとは別に、監視対象を管理するハッシュマップのキーとして使われます。ポーリングのたびに見つけたファイルのフィンガープリントを計算し、既存のキーと一致すれば同じファイル、一致しなければ新しいファイルとして扱います。OpenTelemetry CollectorのStartsWith(前方一致)のような特別な比較ロジックは無く、単純なハッシュマップの検索で済んでいます。改行を見つけた時点でハッシュが確定し、その後ファイルがどれだけ成長しても1行目の内容が変わらない限りフィンガープリントも変わらないためです。 if let Some(watcher) = fp_map.get_mut(&file_id) { // file fingerprint matches a watched file // ... } else { // untracked file fingerprint self.watch_new_file(path, file_id, &mut fp_map, &checkpoints, false).await; } (参考: file_server.rs#L189-L234) OpenTelemetry Collectorが「inodeのようなOS依存の概念は避ける」という一般方針のもと、ファイルの中身によらず固定の1000バイトという汎用的な値をフィンガープリントに使っているのに対し、Vectorは汎用のfile sourceではinode方式も選べるようにしたうえで、「Kubernetesのログ形式は決まっている」という前提が成り立つ場合に限り、行数・行長をCRI形式に合わせて決め打ったチェックサム方式を選んでいる、という違いがあります。 6.3 CRIログの再構成 第2章2.3で見たP(partial)/F(full)タグは、エージェント側で正しく結合しないと1つの長い行が複数行に分断されたまま扱われてしまいます。 Fluent Bitの場合 Fluent Bitはこれを専用のCマルチラインパーサーとして実装しており、_pフィールドがFになるまでlogフィールドの内容を連結し続けます。利用する側はmultiline.parser criを明示的に指定する必要があります。 #define FLB_ML_CRI_REGEX \ "^(?<time>.+?) (?<stream>stdout|stderr) (?<_p>F|P) (?<log>.*)$" (参考: src/multiline/flb_ml_parser_cri.c#L24-L74) 汎用のマルチラインパーサー機構にCRI用の定義を1つ追加する、という組み込み方です。 OpenTelemetry Collectorの場合 filelog receiverではtype: containerというoperatorを指定するだけで、ログの中身からDocker JSON・cri-o・containerdのどの形式かを自動判定します。 switch { case dockerMatcher.MatchString(raw): return dockerFormat, nil case crioMatcher.MatchString(raw): return crioFormat, nil case containerdMatcher.MatchString(raw): return containerdFormat, nil } (参考: pkg/stanza/operator/parser/container/parser.go#L292-L311) P(partial)/F(full)タグの再結合も、logtag == 'F'になるまで束ねるrecombineという内部トランスフォーマーが自動的に動くため、追加設定は不要です。 recombineSourceIdentifier = attrs.LogFilePath recombineIsLastEntry = "attributes.logtag == 'F'" (参考: pkg/stanza/operator/parser/container/config.go#L23-L24) CRIログを扱うための専用operatorとして、形式判定から再結合までを1つにまとめている点が、汎用パーサーにCRI向けの定義を追加するFluent Bitとの違いです。 Vectorの場合 kubernetes_logs sourceも、まずログの先頭1バイトだけを見て{ならDocker JSON形式、そうでなければCRI形式と判定します。一度判定すると、以降はその形式用のパーサーに固定されます。 self.state = if bytes.len() > 1 && bytes[0] == b'{' { ParserState::Docker(docker::Docker::new(self.log_namespace)) } else { ParserState::Cri(cri::Cri::new(self.log_namespace)) }; (参考: src/sources/kubernetes_logs/parser/mod.rs#L66-L70) P/Fタグの判定と、実際の結合は別の2つのコンポーネントに分かれています。CRI用パーサーはPタグの行に汎用的な_partialフィールドを立てるだけで、 // MULTILINE_TAG // If the MULTILINE_TAG is 'P' (partial), insert our generic `_partial` key. if parsed_log.multiline_tag[0] == b'P' { self.log_namespace.insert_source_metadata( Config::NAME, log, Some(LegacyKey::Overwrite(path!(event::PARTIAL))), path!(event::PARTIAL), true, ); } (参考: src/sources/kubernetes_logs/parser/cri.rs#L77-L89) 実際の結合は後段のpartial_events_mergerが、ファイルごとのバケツにメッセージを貯めてバイト列として連結する形で行います(肥大化防止の上限・切り詰め処理つき)。 (参考: src/sources/kubernetes_logs/partial_events_merger.rs#L28-L80) OpenTelemetry Collectorが形式判定と再結合を1つのoperatorにまとめているのに対し、Vectorは「タグを見て印を付ける」パーサーと「印に応じて結合する」マージャーに役割を分けている点が対照的です。 6.4 Podメタデータの付与 /var/log/podsのディレクトリ名(<namespace>_<name>_<uid>)や、/var/log/containersのシンボリックリンク名(<pod名>_<namespace>_<コンテナ名>-<コンテナID>.log)からは、Podのnamespace・名前・コンテナ名を読み取れますが、ラベルやannotationはここには含まれません。 Fluent Bitの場合 filter_kubernetesは、まずこのファイル名を正規表現でパースし、その後kube-apiserverに問い合わせてPodのメタデータを取得します(設定でkubeletへの問い合わせに切り替えることもできますが、namespaceの情報はAPIサーバーからしか取得できません)。ログ行1件ごとに問い合わせるとAPIサーバーへの負荷が大きくなるため、namespace・Pod名・コンテナ名から作ったキャッシュキーで結果を再利用します。 (参考: ファイル名の正規表現はplugins/filter_kubernetes/kube_regex.h#L25、APIサーバーへの問い合わせはplugins/filter_kubernetes/kube_meta.c#L607-L637、キャッシュキーの構成は同ファイルL1910-L1995) ファイル名パースとAPIサーバーへの問い合わせを1つのfilterプラグインの中にまとめて持たせる、フィルタチェーンの1段として組み込む設計です。 OpenTelemetry Collectorの場合 container operatorは、include_file_path: trueでログの実ファイルパスを取得できる状態にしておけば、デフォルト(add_metadata_from_filepath: true)でファイルパスからk8s.namespace.name/k8s.pod.name/k8s.container.name/k8s.pod.uidをその場で抽出します。Fluent Bitのfilter_kubernetesと違い、この時点ではAPIサーバーへの問い合わせが一切発生しません。 k8sMetadataMapping = map[string]string{ "container_name": "k8s.container.name", "namespace": "k8s.namespace.name", "pod_name": "k8s.pod.name", "restart_count": "k8s.container.restart_count", "uid": "k8s.pod.uid", } (参考: pkg/stanza/operator/parser/container/parser.go#L51-L57、デフォルト値はconfig.go#L42) :::note warn このファイルパスからの抽出は、正規表現が/var/log/pods/<namespace>_<name>_<uid>/<コンテナ名>/<再起動回数>.logという第2章2.1で見た構成を前提にしています。/var/log/containers/側のシンボリックリンクを指定すると、パースに失敗してメタデータが一切付与されません。 ::: ただしcontainer operator単体で取れるのはファイルパス由来の基本属性だけで、Podのラベルやannotationは含まれません。実運用ではラベルでログを絞り込みたい場面が多く、ラベルが無いと検索基盤側での利用に支障が出るため、k8sattributesprocessorを組み合わせるのがほぼ標準的な構成になります。こちらはFluent Bitのfilter_kubernetesと同じく、APIサーバーをSharedInformerでwatchしてローカルにPodを同期させる方式です。 informer := cache.NewSharedInformer( &cache.ListWatch{ ListWithContextFunc: informerListFuncWithSelectors(client, namespace, ls, fs), WatchFuncWithContext: informerWatchFuncWithSelectors(client, namespace, ls, fs), }, // ... ) (参考: processor/k8sattributesprocessor/internal/kube/informer.go#L54-L63、Podとの紐付けはk8s.pod.uid等のresource属性をキーに行います。pod_association.go#L20-L69) まずファイルパスという軽量な情報源から基本属性を抽出し、APIサーバーへの問い合わせが要るリッチな情報は別のprocessorに任せる、という段階的な構成です。receiverとプロセッサーを分けてパイプラインを組み立てるOpenTelemetry Collectorらしい役割分担だと言えます。 Vectorの場合 kubernetes_logs sourceは、ファイルパスのパースとAPIサーバーからの情報付与を1つの処理の中で両方行います。まず/var/log/pods/<namespace>_<name>_<uid>/<コンテナ名>/...というパスをパースして基本情報を取り出し、 pub(super) fn parse_log_file_path(path: &str) -> Option<LogFileInfo<'_>> { let mut components = path.rsplit(std::path::MAIN_SEPARATOR); let _log_file_name = components.next()?; let container_name = components.next()?; let pod_dir = components.next()?; let mut pod_dir_components = pod_dir.rsplit(LOG_PATH_DELIMITER); let pod_uid = pod_dir_components.next()?; let pod_name = pod_dir_components.next()?; let pod_namespace = pod_dir_components.next()?; // ... } (参考: src/sources/kubernetes_logs/path_helpers.rs#L32-L52) そのnamespace/Pod名をキーに、kube crateのreflector(APIサーバーをwatchしてローカルに同期させる、SharedInformerに相当する仕組み)が保持するPodのキャッシュを引きます。見つかればラベル・annotation・Pod spec/statusまで含めてフィールドを追加し、見つからなければ警告ログを出したうえでファイルパス由来の情報だけで処理を続行します。 pub fn annotate<'a>(&self, event: &mut Event, file: &'a str) -> Option<LogFileInfo<'a>> { let file_info = parse_log_file_path(file)?; let obj = ObjectRef::<Pod>::new(file_info.pod_name).within(file_info.pod_namespace); let resource = self.pods_state_reader.get(&obj); annotate_from_file_info(log, &self.fields_spec, &file_info, self.log_namespace); let Some(resource) = resource else { warn!(message = "Pod not found in API store, annotating from file path only.", ...); annotate_from_file_path(log, &self.fields_spec, &file_info, self.log_namespace); return Some(file_info); }; // ラベル・annotation・Pod spec/statusまで付与 // ... } (参考: src/sources/kubernetes_logs/pod_metadata_annotator.rs#L206-L238) Fluent BitとOpenTelemetry Collectorがそれぞれ「ファイルパス解析」と「APIサーバー問い合わせ」を別々のプラグイン・processorとして組み合わせる設計なのに対し、Vectorは1つのkubernetes_logs sourceの中に両方を組み込み、APIサーバー側の情報がまだ無い場合はファイルパス由来の情報だけで処理を続ける、というフォールバックまで内蔵しています。 6.5 Grafana Lokiへの転送 収集したログの転送先には、ログ専用のデータベースであるGrafana Lokiがよく使われます。LokiはPrometheusと同じラベル(key-value)でログの集まり(ストリーム)を識別する設計です。 Fluent Bitの場合 out_lokiという出力プラグインがF

    4 likes

Latest stargazers

The last 10 distinct accounts we observed this year.

Most active participants

PR, issue, comment & review events in the past year.

Packages

31
PackageVersionDownloads · 30dTrend · 30d
codecsv0.0.1
v0.0.1——
———
———
———
———
———
———
———
———
———
———
v0.33.1——
prometheus-parserv0.4.3-alpha9
v0.4.3-alpha9——
———
vectorv0.4.1
v0.4.1——
———
———
———
———
———
———
———
v0.10.0——
———
———
———
———
———
———
———
———

README badges

Embed a badge in your README or website.

  • vectordotdev/vector reached #3 in daily stars on gitwal · Jul 3, 2019
Live mentions · last 30 days
vectordotdev/vector mentions in the last 30 days