ブログ

Common Expression Language(CEL):CEL入力がElastic Agent統合におけるデータ収集を改善する方法

Common Expression Languageと他のプログラミング言語との違い、FilebeatのCEL入力用の拡張方法、Elastic Agent統合でデータ収集ロジックを表現する上での柔軟性についてご紹介します。

Elastic Agent統合により、ユーザーは幅広いソースからデータをElasticsearchに取り込むことができます。コレクションロジック、取り込みパイプライン、ダッシュボード、その他のアーティファクトをパッケージにまとめ、Kibana Webインターフェースからインストールおよび管理できます。

統合では、データ収集を行うためにFilebeat入力を設定します。HTTP APIからデータを収集するために、私たちはしばしばHTTP JSON入力を使用してきました。しかし、基本的なリスティングAPIでさえ、細部において大きく異なることがあり、HTTP JSON入力のYAMLで構成された変換のモデルでは必要なコレクションロジックを表現するのが難しく、場合によっては不可能になることがあります。

Common Expression Language(CEL)入力は、HTTP APIとのより柔軟な相互作用を可能にするために導入されました。CELは、条件やデータ変換を高速、安全、かつ拡張性のある方法で表現するアプリケーションに組み込めるよう設計された言語です。CEL入力を使用すると、統合ビルダーは設定を読み取り、自身の状態を追跡し、リクエストを作成し、応答を処理し、最終的に取り込む準備が整ったイベントを返すことができる1つの式を記述できます。

この記事では、CELが他のプログラミング言語との違い、CEL入力用の拡張方法、そしてデータ収集ロジックを表現する上で提供するその柔軟性とパワーについて見ていきます。

CELと入力での動作の仕組み

CELは式言語で、ステートメントはありません。CELを記述する場合、ステートメントを記述して何を実行するかを指示するのではなく、式を記述してどのような値を生成するかを指示します。すべてのCEL式は値を生成し、小さな式を組み合わせて大きな式にすることで、より複雑なルールに従った結果を生成することができます。後ほど、他の言語のステートメントで記述できる内容に対して式を使用する方法について説明します。

CELは意図的に非チューリング完全言語であり、無限ループは許可されません。後ほど、マクロを使用してリストやマップを処理する方法を見ていきますが、無限ループを避けることで、この言語は個々の式に対して予測可能で制限された実行時間を保証します。

CEL入力は、CELプログラム(式)といくつかの初期状態で設定されます。状態はプログラムの入力として提供され、プログラムは出力状態を生成するために評価されます。出力状態にイベントのリストが含まれている場合、それらは削除されて公開されます。残りの出力状態は、次の評価の入力として使用されます。出力状態に1つ以上のイベントとフラグwant_more: trueが含まれている場合、次の評価はすぐに実行されます。それ以外の場合、設定された間隔の残りの時間スリープしてから続行します。以下は入力の制御フローの簡略的な図です。

Common Expression Language(CEL)入力制御フロー

各評価の出力は、入力が実行されている限り、次の評価への入力として順に渡されます。キー「cursor」下の出力データはディスクに永続化され、入力の再起動後に再ロードされますが、その他の状態は再起動をまたいで保存されません。

CEL言語自体は機能が制限されており、副作用を回避しますが、拡張可能です。cel-goの実装では、オプションの構文や型などの機能が追加されています。Mitoライブラリはcel-goを基盤とし、HTTPリクエストの機能を含むより多くの機能を追加しています。CEL入力はMito版のCELを使用しています。

Mitoの操作

CEL入力を使用して統合を構築またはデバッグする際、最も重要なことは、与えられた入力状態に対してCELプログラムがどのような出力状態を生成するかを理解することです。開発中は、完全なElasticスタックに囲まれた入力でCELプログラムを実行するのは面倒な場合があります。より高速なフィードバックループを実現する1つの方法は、Mitoのコマンドラインツールを使用することです。このツールを使用すると、CELプログラムを直接実行し、特定の入力に対して生成される出力を確認できます。

MitoはGoで記述されており、以下のようにインストールできます。

go install github.com/elastic/mito/cmd/mito@latest

MitoでCELプログラムを実行する場合、通常は2つのファイルを指定します。初期入力状態を含むJSONファイルと、CELプログラムのソースコードを含む別のファイルです。

mito -data state.json src.cel

コピー&ペーストを容易にするため、この記事の例は、シェルが<(echo '...content...')で各ファイルの内容をラップして、その場で一時ファイルを作成する単一のコマンドとして記述されています。独自の開発では、実際のファイルを操作する方が簡単になります。

GitHubからイシューデータを取得

以下の例には、GitHub APIからイシューに関するデータを取得する完全なCELプログラムが含まれています。その初期入力状態には、APIエンドポイントのURLと、ページネーションをどのように処理すべきかについての情報が含まれています。CELプログラムは、入力状態のデータを使用してリクエストを生成します。対応をデコードし、そこからイベントを生成し、出力状態の一部として返します。

mito -data <(echo '
  {
    "url": "https://api.github.com/repos/elastic/integrations/issues",
    "per_page": 3,
    "max_pages": 3
  }
') <(echo '
  int(state.?cursor.page.orValue(1)).as(page,
    (
      state.url + "?" + {
        "state": ["all"],
        "sort": ["created"],
        "direction": ["asc"],
        "per_page": [string(state.per_page)],
        "page": [string(page)],
      }.format_query()
    ).as(full_url,
      request("GET", full_url).with({
        "Header": {
          "Accept": ["application/vnd.github+json"],
          "X-GitHub-Api-Version": ["2022-11-28"],
        }
      }).do_request().as(resp,
        resp.Body.decode_json().as(data,
          state.with({
            "events": data.map(i, {
              "html_url": i.html_url,
              "title": i.title,
              "created_at": i.created_at,
            }),
            "cursor": { "page": page + 1 },
            "want_more": size(data) == state.per_page && page < state.max_pages,
          })
        )
      )
    )
  )
')

その最初の評価は次の出力を生成します。

{
  "cursor": {
    "page": 2
  },
  "events": [
    {
      "created_at": "2018-09-14T09:47:35Z",
      "html_url": "https://github.com/elastic/integrations/issues/3250",
      "title": "Increase support of log formats in haproxy filebeat module"
    },
    {
      "created_at": "2019-02-06T12:37:37Z",
      "html_url": "https://github.com/elastic/integrations/issues/487",
      "title": "ETCD Metricbeat module needs polishing and grooming"
    },
    {
      "created_at": "2019-08-13T11:33:11Z",
      "html_url": "https://github.com/elastic/integrations/pull/1",
      "title": "Initial structure"
    }
  ],
  "max_pages": 3,
  "per_page": 3,
  "url": "https://api.github.com/repos/elastic/integrations/issues",
  "want_more": true
}

イベントは削除され、CEL入力で実行されると、インジェストのために公開されます。残りの出力は、次のCELプログラム評価に入力状態として提供されます。

そのCELプログラムの仕組みを理解するために、いくつかの小さなCELの例で、CEL入力の仕組みについて詳しく説明します。

CELの基本

CEL言語にはステートメントはなく、式のみが存在します。成功したすべてのCEL式は最終値まで評価されます。以下は、記述できる最も小さなCEL式の1つと、その出力です。

mito <(echo '
  "hello" + " " + "world"
')
"hello world"

シンプルな表現は通常、直感的です。数学演算は同じタイプの値でのみサポートされます(例:intint など)。そのため、必要に応じてタイプを変換します(ここでは int から double へ)。

mito <(echo '
  double((1 + 2) * (3 + 4)) / 2.0
')
10.5

CEL言語には変数はありませんが、Mitoのasマクロを使用して、式に名前を付け、より大きな式で使用することができます。この例では、式(1 + 1)は値2に評価され、.as(n, ...)がその値に式"one plus one is "+string(n)で使用するための名前nを付けます。

mito <(echo '
  (1 + 1).as(n, "one plus one is "+string(n))
')
"one plus one is 2"

また、withを使用して示したように、マップに情報を蓄積し、それを後で式の中で使用することも可能です。

mito <(echo '
  { "key": "value" }.with({ "key2": "value2" }).as(data,
    {
      "data": data,
      "size": size(data),
    }
  )
')
{
  "data": {
    "key": "value",
    "key2": "value2"
  },
  "size": 2
}

その例をもう一度見てみましょう。ネストされた部分({ "data": data, "size": size(data), })が最終値の形状を示すことに注意してください。これは"data""size"をキーとするマップです。これらのキーの値はdataに依存し、式の外側部分によって定義されます。CEL式を内側から外側まで読み取ると、何が返されるかをすぐに把握できるようになります。

CELにはifのような制御フロー文はありませんが、条件分岐は三項演算子で実行可能です。

mito <(echo '
  1 + 1 < 12 ? "few" : "many"
')
"few"

CELはチューリング完全言語ではないため、無制限のループと再帰はサポートされていません。これにより、実行時間が予測可能になり、入力データのサイズと式の複雑さに比例するようになります。

個別のCEL式では無限ループはできませんが、mapのようなマクロを使ってリストやマップを処理できます。

mito <(echo '
  [1, 2, 3].map(x, x * 2)
')
[2, 4, 6]

このセクションでは、次の内容を説明しました。

  • 文字列、数値、リスト、マップ。

  • 文字列の連結。

  • 数学演算。

  • タイプキャスティング。

  • 条件文。

  • 部分式の命名。

  • コレクションの処理。

次に、HTTPリクエストを行う方法を見ていきます。

リクエスト

MitoはCELを拡張してHTTPリクエストを行う機能を提供します。

mito <(echo '
  get("https://example.com").as(resp, string(resp.Body))
')
"<!doctype html><html lang=\"en\"><head><title>Example Domain</title>..."

リクエストは実行前に明示的に構築することができます。これにより、さまざまなHTTPメソッドを使用したり、ヘッダーや本文を追加したりできるようになります。

この例では、 format_queryを使用してURLを構築し、リクエストにヘッダーを追加し、 decode_jsonを使用してレスポンス本文を解析します。-log_requestsオプションを指定すると、Mitoは各リクエストと対応に関する詳細情報をJSON形式でログに記録します。

mito -log_requests <(echo '
  request("GET",
    "https://postman-echo.com/get?" + {
        "q": ["query value"]
     }.format_query()
  ).with({
    "Header": { "Accept": ["application/json"] }
  }).do_request().as(resp, {
    "status": resp.StatusCode,
    "data": resp.Body.decode_json(),
  })
')
{"time":"...","level":"INFO","msg":"HTTP request",...}
{"time":"...","level":"INFO","msg":"HTTP response",...}
{
  "data": {
    "args": {
      "q": "query value"
    },
    "headers": {
      "accept": "application/json",
      "accept-encoding": "gzip, br",
      "host": "postman-echo.com",
      "user-agent": "Go-http-client/2.0",
      "x-forwarded-proto": "https"
    },
    "url": "https://postman-echo.com/get?q=query+value"
  },
  "status": 200
}

状態と評価の管理

ここまで、リクエストの作成方法と、目的の出力状態を生成するために必要なCELの基本について説明しました。次は、出力状態に何を入れるべきか、そしてそれによって後続の処理をどのように指示できるかについて詳しく見ていきましょう。

統合のCELプログラムでは、その出力状態が次の評価の入力として使用するのに適していることを確認する必要があります。構成では初期状態を設定し、適切な変更を加えて出力でそれを繰り返す必要があります。簡単な方法は state.with({ ... }) を使って、状態マップをオーバーライドして繰り返し表示することです。小規模プログラムの一般的なパターンは、state.with()でプログラム全体をラップすることです。これにより、出力データを生成する各分岐(例えば、成功、エラー)で状態の伝播を繰り返す必要がなくなります。

初期入力状態にハードコードされているのではなく、評価によって初期化される状態値がある場合、プログラムは初期値を設定する前に既存の値を確認する必要があります。これにはオプションの構文と型のサポートが役立ちます。マップキーのフィールド名の前に疑問符を使用すると、アクセスはオプションになります。値に解決される場合とされない場合がありますが、さらにオプションのアクセスが可能であり、値が存在しない場合にデフォルトを簡単に提供できます。

mito -data <(echo '{}') <(echo '
  int(state.?counter.orValue(0)).as(counter,
    state.with({
      "counter": counter + 1,
      "want_more": counter + 1 < 3,
    })
  )
')
{ "counter": 1, "want_more": true }
{ "counter": 2, "want_more": true }
{ "counter": 3, "want_more": false }

その例では、状態から読み取られたカウンター値はintにキャストされます。これは、JSONとJavaScriptのNumber型によって確立された規則に従って、状態内のすべての数値が浮動小数点数としてシリアライズされるためです。また、"want_more": trueはここでMitoによって尊重されますが、CEL入力で実行される場合、出力にもイベントが含まれている場合にのみ評価が繰り返されます。

CEL入力によって実行されるCELプログラムでは、出力マップに"events"キーを返すことが要件となります。その値はイベントマップのリスト、空のリスト、または単一のイベントマップである場合があります。単一イベントケースは通常、エラーに使用されます。イベントは入力によって公開されますが、その値もログに記録されます。error.message値が設定されている場合、その値は統合のFleetのヘルスステータスを更新するために使用されます。プログラムが単一の非エラーイベントを生成する場合は、それをリストにラップするのが最適です。

先ほどのGitHubイシュープログラムの出力をもう一度見てみましょう。

{
  "url": "https://api.github.com/repos/elastic/integrations/issues",
  "per_page": 3,
  "max_pages": 3,
  "cursor": {
    "page": 2
  },
  "events": [
    { ... },
    { ... },
    { ... }
  ],
  "want_more": true
}

プログラムは次のようにして状態を効果的に管理しました。

  • urlper_pagemax_pagesで初期状態値を繰り返します。

  • cursor.pageの再起動時に永続化されるべき状態を追加します。

  • eventsリストで公開する準備ができたイベントを返します。

  • want_more: trueで即時の再評価をリクエストしています。

オプションのアクセスと状態管理、CELの基本とHTTPリクエストを理解できたので、GitHubのイシュープログラム全体が読み取れるようになります。Mitoで実行し、いくつか変更を加えて実験してみてください。

レビューとリソース

この記事では、CEL言語とは何か、そしてそれがMitoライブラリでどのように拡張され、CEL入力で使用されるかを調べました。GitHub APIからイシュー情報を取得するサンプルプログラムでCELの柔軟性を確認し、初期状態での設定へのアクセス、HTTP APIとのやりとり、取り込むべきイベントのリターン、後のプログラム実行のための状態管理など、そのプログラムを理解するために必要なすべての詳細を説明しました。

CEL入力を使用してさらに学び、統合を構築ために役立つリソースは多数あります。

CEL入力を使用した統合を構築するための最も貴重なリソースは、GitHubで入手できる既存のElastic 統合のCELコードです。

cel.yml.hbs Elastic統合リポジトリ内のファイル - GitHub

関連記事

チャットボックスを超えたエージェントビルダー:Augmented Infrastructureの導入

Alexander Wert

Elasticsearchによるエージェントメモリの管理

Someshwaran Mohankumar

Elasticsearch Inference APIとHugging Faceモデルを組み合わせて使用

Jeffrey Rengifo

Elastic Agent Builder と GPT-OSS を使用した HR 向け AI エージェントの構築

Tomás Murúa

TypeScriptを使用したElasticsearch MCPサーバーの作成

Jeffrey Rengifo

最先端の検索体験を構築する準備はできましたか?

十分に高度な検索は 1 人の努力だけでは実現できません。Elasticsearch は、データ サイエンティスト、ML オペレーター、エンジニアなど、あなたと同じように検索に情熱を傾ける多くの人々によって支えられています。ぜひつながり、協力して、希望する結果が得られる魔法の検索エクスペリエンスを構築しましょう。

はじめましょう