呑気にOSをアップデートしたら今までjournalに出ていたコンテナのログが出なくなってしまった。 今までコンテナのログもjournaldから取ってOpenTelemetry Collectorで集約していたが、これを機にファイルから取る方法を試すことにした。
今回はPodmanを使っている*1が、後述するように使う道具の全てがDocker用なのでDockerでも同じようにできるはず。
Podmanのログファイル
Podmanでコンテナを起動する際、log-driver
にk8s-file
を指定すると、コンテナIDごとに分かれたディレクトリにログファイルが作られる。
ログファイルの位置は$ podman inspect (コンテナ名) --format {{.HostConfig.LogConfig.Path}}
で確認できる。
このログファイルはcontainerdのログと同じ形式になっている*2ので、containerdのログファイルを読む仕組みを流用できる。
また、この格納方法から、Podmanのコンテナの生成と破棄を検知して、ファイルからログを読み出す設定をコンテナIDに同期させればうまくログを収集できそうということがわかる。
Receiver Creator
OpenTelemetry Collectorには以下のようなコンポーネントが存在する。
- Receiver: テレメトリを収集する
- Processor: テレメトリを加工する
- Exporter: テレメトリを出力する
- Provider: 設定ファイルに介入して情報を展開する
- Extension: その他全て
これらのコンポーネント(主にReceiver/Processor/Exporter)を組み合わせてパイプラインを組むのが作法だが、基本的にこれらのコンポーネントは静的に定義され、Collectorが起動している間に変化することはない。 しかしReceiver Creatorは特殊なコンポーネントで、Observerという一種のExtensionからコンテナなどの情報を取得し、そこからテンプレートを使って動的にReceiverを増減させることができる*3。
Docker Observer
Docker Observerは実際にDockerを監視してコンテナの状態を検知する。PodmanはDockerの互換APIを実装している*4ため、Docker用のObserverをそのまま使うことができる。
File Log Receiver
OpenTelemetry Collectorでファイルからログを読み出して加工するためにはFile Log Receiverを使う。
File Log ReceiverはOperatorを使って取得したログをパースしたり加工したりできる。
コンテナログの場合、type: container
というOperatorを使えばいい感じにパースしてくれる。
レシピ
最終形(ログ取得に関わる部分)としてはこんな感じ。他にも書きたいことは色々ある*5が、今日は疲れたのでここまで。
extensions: docker_observer/podman: endpoint: unix:///var/run/podman/podman.sock excluded_images: - (Collector自身のイメージ名を入れて自分を除外するようにしておく) receivers: receiver_creator/podman: watch_observers: [docker_observer/podman] resource_attributes: container: # type: container のときの設定 container.id: "`container_id`" container.name: "`name`" container.image.name: "`image`" receivers: filelog: rule: type == "container" config: include: - "/path/to/containers/`container_id`/userdata/*.log" include_file_path: true # ログストリームの連続性の判定に必要 operators: - type: container add_metadata_from_filepath: false service: extensions: - docker_observer/podman pipelines: logs/podman: receivers: [receiver_creator/podman] processors: ... exporters: ...
おまけ: OpenTelemetry Collectorで構造化ログをいい感じにパースする設定
JSONやlogfmtを区別せずに放りこみたいし、severityやトレース周りをフィールドから読んで埋めてほしいということはよくある。
- type: router routes: - output: json_parser expr: hasPrefix(body, '{"') - output: key_value_parser expr: body matches '^[a-zA-Z0-9]{1,16}=' default: end - type: json_parser parse_from: body parse_to: body output: structured - type: key_value_parser parse_from: body parse_to: body output: structured - id: structured type: noop # Severity - type: copy if: body.level != nil from: body.level to: attributes.level - type: severity_parser if: attributes.level != nil parse_from: attributes.level on_error: send_quiet # Traces - type: move if: body.traceID != nil from: body.traceID to: body.trace_id - type: move if: body.spanID != nil from: body.spanID to: body.span_id - type: trace_parser - type: remove if: body.trace_id != nil field: body.trace_id - type: remove if: body.span_id != nil field: body.span_id - id: end type: noop
*1:Podmanが好きで使っているかと聞かれればあまりそうではなく、使ってるVyOSが自由にプロセスを動かせる手段をコンテナしか提供していなくて、それがPodmanで……
*2:正確にはCRI-Oの形式になっていて、微妙に違う
*3:Observerの概念はここでしか使われていないのが残念。OpenTelemetry Collectorにおけるサービスディスカバリの抽象として働いているはずで、うまくやればK8s Attribute Processorとか置き換えられそうな気がする。
*4:イベントの取得やコンテナの一覧などは実装されているがメトリックなど一部は互換性がない
*5:Observerがコンテナの生成を検知するのは非同期なのでログは本当は頭から読む必要があるとか、それをするとCollectorを再起動する度に大量のログが流れるのでどこまで読んだかを記録するためにstorage extensionが必要とか……