freeeの開発情報ポータルサイト

Vectorの魅力を語る!ログ収集ツールとしての可能性について

Vectorのロゴ vector.devより引用
Vectorのロゴ vector.devより引用

こんにちは! SREのzakiです。この記事は、freee Developers Advent Calendar 2025 の 4日目の記事です。

はじめに

Vectorという名前のツールはいろいろ存在しますが、今回取り上げるのは「ログ収集ツール」のVectorです。Vectorは、もともとTimber.ioにより開発されていましたが、Datadogによって買収され、現在はVector By Datadogとして開発が活発に続けられています。なお、Vectorについて詳しくは、以下の公式ドキュメントを参照して下さい。

vector.dev

Vectorの検証を始めた動機

これまでfreeeでは、Fluentdを使ってログをS3に送信していました。このFluentdの仕組みは、freeeのインフラがEC2からEKSに移行した際に構築されたもので、そのログの仕組みが現在も使われているものです。また、FluentdをDaemonSetやSidecarとして、さまざまな箇所にデプロイして、さまざまな種類のログをS3に送っています。Fluentdは多機能かつ信頼性高くデータを転送してくれるソフトウェアである反面、メモリー使用量が大きいという課題感がありました。特に、Nodeに一台しか立たないDaemonSetならともかく、Podの数分だけ立つSidecarのFluentdにコストへの課題感が強くありました。そこで、現在のFluentdのログ機能を保ったまま、メモリー使用量を抑えることが可能な代替ツールを探すことにしました。

今回、その代替ツールの一候補として、Vectorの検証を行いました。検証をしていく中で分かった、Vectorというログ収集ツールの素晴らしさと課題を、この記事で紹介します。

Vectorの使い方速習

まずは、Vectorのコンセプトや動かし方を簡単に説明します。実際に手元で動かしてみたい方は、是非 Vector quickstart | Vector documentation も参照して下さい。

基本コンセプト

Vectorは以下の3つのコンポーネントで構成されます

  • Sources: データを収集するインプット箇所
  • Transforms: データを加工する箇所
  • Sinks: データを送信するアウトプット箇所

また、Vectorは、VRL (= Vector Remap Language) と呼ばれるVectorのために特別に設計されたDSLを使って、設定を記述します。後述しますが、このVRLは特に使い勝手が良くて最高です。

動かし方

サンプルコードとして、標準入力からログを読み込んで、JSON形式でS3 Bucketにログを送る最もシンプルな設定ファイルは、以下のような構成になります。本記事では設定ファイルをYAMLで表記しますが、他にもTOMLやJSONでも書くことができます。

sources:  
  in:  
    type: "stdin"  
  
sinks:
  out:
    type: aws_s3
    inputs:
      - "in"
    encoding:
      codec: json
    bucket: test-bucket

設定ファイルを例えば test_s3.yamlとして保存すると、vector -c test_s3.yaml コマンドでVectorを起動することができます。ここまでは簡単ですね!

Vectorの良さ

ここからは早速、検証を通じて分かったVectorの良さを説明していきます。

設定ファイルをテストできる

ログツールをバージョンアップする、もしくは設定に変更を加えないといけなくなった際に、予想外の変更が入っていないか不安になったことはないでしょうか?Vectorを使うと、なんと設定ファイルに対してユニットテストを記述することができます。

Unit testing Vector configurations | Vector documentation

例えば、以下のようなログをS3に送る設定ファイルを書いたとします(一部簡略化しています)。この設定は、標準入力から渡されるログを読み取り、JSONとしてparse処理を行い、hostnameを追加・timestampから日付を計算してdateカラムに追加する例です。また、エラートレースのような非JSON形式のログも破棄せずにS3に送る設定になっています。

sources:
  log_source:
    type: stdin

transforms:
  log_parser:
    type: remap
    inputs:
      - log_source
    source: |
      parsed, err = parse_json(.message)
      # JSON parseに失敗しても、破棄せずに後続の処理を行う
      if err == null {
        . = parsed
        .date = format_timestamp!(parse_timestamp!(.timestamp, format: "%+"), format: "%Y/%m/%d")
        .hostname = get_hostname!()
      } else {
        .original_message = .message
        .timestamp = to_float(now())
      }

sinks:
  log_output:
    type: aws_s3
    inputs:
      - log_parser
    bucket: test-bucket

この設定ファイルに対して、意図した挙動をすることを保証するにはどうしたらいいでしょうか? そこでVectorのテスト機能の出番です!

まずは、一番簡単な正常系のテストを書いてみましょう。以下のテストは、単純なJSONをinputとして渡して、その結果出力されるログがちゃんとJSONとしてparseされて各フィールドに保存されているか、チェックしています。また、最後にhostnameフィールドが存在することを確認しています。

tests:
  - name: minimal_test_case
    inputs:
      - insert_at: log_parser
        type: log
        log_fields:
          # ↓ これがテストのinput
          message: '{"timestamp":"2025-07-31T06:55:52.339Z","service_name":"productA","method":"GET","status":200,"user_id":"123"}'
    outputs:
      - extract_from: log_parser
        conditions:
          - type: vrl
            source: |
              # ↓ inputに対して、outputの値をassertする dateがちゃんと変換できているかも確認できる
              assert_eq!(.date, "2025/07/31")
              assert_eq!(.method, "GET")
              assert_eq!(.status, 200)
              assert_eq!(.service_name, "productA")
              assert_eq!(.user_id, "123")
              assert!(exists(.hostname))

このテストをvector testコマンドで実行すると、以下のように成功します。

$ vector test test.yaml
Running tests
test minimal_test_case ... passed

また、user_idをわざと"124"に変えてテストすると、以下のようにちゃんと失敗を検知してくれます。

$ vector test test.yaml
Running tests
test minimal_test_case ... failed

failures:

test minimal_test_case:

check[0] for transforms ["log_parser"] failed conditions:

  condition[0]: source execution failed: 
error[E000]: function call error for "assert_eq" at (142:169): assertion failed: "123" == "124"
  ┌─ :5:1
  │
5 │ assert_eq!(.user_id, "124")
  │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^ assertion failed: "123" == "124"
  │

雰囲気を掴めたところで、他にもテストを書いていきましょう。例えば、非JSON形式のエラーログが入力された場合、破棄されずに出力されることを確認するテストは以下のようになります。

  - name: plain_error_message_test
    inputs:
      - insert_at: log_parser
        type: log
        log_fields:
          message: 'エラーが発生しました: Database connection failed - データベース接続に失敗しました'
    outputs:
      - extract_from: log_parser
        conditions:
          - type: vrl
            source: |
              assert_eq!(.original_message, "エラーが発生しました: Database connection failed - データベース接続に失敗しました")
              assert!(exists(.timestamp))

この場合は、エラーが発生しました: Database connection failed - データベース接続に失敗しました という平のエラー文が入力された際のテストです。

ちょっと極端な例でしたが、このようにいくつかの入力/出力をセットとして渡して、意図通りの挙動をするか確認するブラックボックステストが行えます。Vectorのバージョンアップや、処理の変更を行いたい際には、テストを整備しておけば、安全安心に行うことができます。また、テストを揃えておけば、初めて見る人でも、この設定はこんな感じの処理をしたいんだな、というのが一目瞭然で伝わる点も素晴らしいと思います。設定が複雑になればなるほど、テストの有り難みも増していきますね。

エラーメッセージが非常に分かりやすい

テストの続きになってしまいますが、DSLの使い勝手、特にエラーメッセージの分かりやすさ、を検証を通して感じました。ただのエラーメッセージではなく、直し方、さらには悪影響を及ぼす可能性がある箇所を事前に指摘してくれます。

例えば、公式ブログ では、split関数を使った時のエラーハンドリングの例が紹介されています。もしsplit対象が厳密な文字列であることを保証できないときは、適切なエラーハンドリングを求めるガイドと共に、エラーメッセージを表示してくれます。本番運用を見据える際は、この厳密かつ親切なエラーメッセージに、しっかり対応すると良いでしょう。

error[E110]: invalid argument type
  ┌─ :1:7
  │
1 │ split(msg, " ")
  │       ^^^
  │       │
  │       this expression resolves to one of string or null
  │       but the parameter "value" expects the exact type string
  │
  = try: ensuring an appropriate type at runtime
  =
  =     msg = string!(msg)
  =     split(msg, " ")
  =
  = try: coercing to an appropriate type and specifying a default value as a fallback in case coercion fails
  =
  =     msg = to_string(msg) ?? "default"
  =     split(msg, " ")

メモリ使用量が少ない

公式Doc の Why Vector?の欄に、 Built in Rust, Vector is blistering fast, memory efficientと書いてあるように、Vectorを使うとメモリ使用量が抑えることができます。負荷試験を行なってさまざまなログ量でメモリ使用量を測定しましたが、Fluentdと比べて30%ほどのメモリ使用量に抑えることができそうでした。

開発が活発

開発が非常に活発です。また、Datadog社がバックに付いていて実際にDatadog内部で使われている、というのもユーザーから見ると安心材料です。

(懸念点)

もちろん良いところばかりではなく、何個か懸念点もありました。

例えば、Vectorの最新バージョンは2025年11月現在でv0.51.1が最新で、まだv1がリリースされていません。そのため、バージョンアップに伴う破壊的変更がまだまだ多いという印象があります。とはいえ、上述したテストをしっかり整備しておけば、破壊的変更に気づかずにうっかり本番環境に出してしまった。。!ということは防げそうです。また、Vectorの日本語の情報が少ない、という懸念点もありましたが、検証時はAIを積極的に活用することで、この点はあまりしんどさを感じませんでした。特に、コードベースを読みながら正確に質問に回答してくれる deepwiki は大変重宝しました。

しかしながら。。

ここまでは、Vectorの良さを力説してきましたが、実はVectorの導入を一旦見送る決定をしました。以下のGitHub Issueが直接的な原因です。簡単に要約すると、disk bufferを使用した際、シャットダウン時にbufferに残っているログがflushされずに、結果的にログが欠損してしまうというバグです。手元で検証したところ、このバグは確かに再現しました。

Disk buffers should close at shut-down · Issue #19600 · vectordotdev/vector · GitHub

Vectorを使用してS3にログを送る際、Bufferは以下のような仕組みで動作します。以下は、公式doc から引用したBufferの仕組みの図です。BufferとBatchという機構が存在し、Bufferにどんどんログを貯めていき、条件が満たされるとBatchが起動して溜まったログをS3に送信します。

vectorのbufferの仕組み
vectorのbufferの仕組み

Vectorは、デフォルトでは以下のどちらかの条件が満たされると、batchが起動してその時点でbufferに溜まっているログをまとめてS3にログを送信します。

disk bufferを使っている場合、podの終了処理に入ってからkillされるまでに、上の条件のどちらかを満たさなければ、bufferに残っているログはflushされずに、ログは欠損してしまうようです。

頑張って検証結果を Issueにコメント してみたり、VectorのDiscordにコメントを投稿してみたりしてみましたが、中々すぐには直せなさそうでした。AIを使って自力で修正PRを作成することも検討しましたが、中々ハードルが高く難しかったです。

残念なことに、このbugは今回検証して置き換えようとしている箇所にとって致命的だったので、一旦Vector導入を見送ることにしました。

最後に

最後の最後に、致命的な事象でVector導入を見送ることになってしまいましたが、それと同時にVectorには良さがたくさんあるのもまた事実だと思います。興味ある方はぜひ触ってみて下さい!

明日は、gonさんから、freeeのAI開発マニア制度についての記事が公開されます。お楽しみに!