以下の内容はhttps://christina04.hatenablog.com/entry/openfeature-evalution-contextより取得しました。


OpenFeature で Evaluation Context を使う

概要

Feature Flagを使いたいモチベーションとして、

  • Fractional Evaluation で一部のユーザにだけロールアウトしたい
  • 年齢・性別によって表示させる物を変更したい

のようなターゲティング機能があります。

OpenFeatureでターゲティングを実現する際には Evaluation Context を使います。

今回はこれを利用してFeature Flag の評価結果の出し分けを行います。

環境

  • Go v1.24.0
  • open-feature/go-sdk v1.14.1
  • flagd v0.11.1

Evaluation Context

Evaluation Context とは Dynamic evaluation(実行時にさまざまな条件に基づいて、フラグのオン・オフをリアルタイムに決定する) に使うコンテキストデータのコンテナ(データの塊)です。 例として

  • ユーザID
  • クライアントIP
  • リクエストパラメータ
  • ホスト名

など、クライアントを識別するなどしてターゲティングする際に使います。

基本的な使い方

Evaluation Contextの基本的な使い方として以下の3通りがあります。 通常は最後の評価単位で使うことが多いでしょう。

グローバル単位

// add a value to the global context
openfeature.SetEvaluationContext(openfeature.NewEvaluationContext(
    "",
    map[string]interface{}{
        "myGlobalKey":  "myGlobalValue",
    },
))

アプリケーション全体で共通して利用される情報を一元管理する際に使えます。

  • dev, prdなど環境情報
  • バージョン情報

クライアント単位

// add a value to the client context
client := openfeature.NewClient("my-app")
client.SetEvaluationContext(openfeature.NewEvaluationContext(
    "", 
    map[string]interface{}{
        "myGlobalKey":  "myGlobalValue",
    },
))

複数のクライアントが必要なユースケース(モジュラーモノリス、マルチテナントサービス)で有用です。

  • モジュラーモノリス構成においてモジュール毎にクライアントを分ける際のサービス名
  • マルチテナントのシステムにおける各テナント(顧客)に対して個別の設定

評価単位

// add a value to the invocation context
evalCtx := openfeature.NewEvaluationContext(
    "",
    map[string]interface{}{
        "myInvocationKey": "myInvocationValue",
    },
)
boolValue, err := client.BooleanValue("boolFlag", false, evalCtx)

個々の評価呼び出し(リクエスト)ごとに動的に変わる情報を設定します。

  • ユーザーID
  • セッション情報
  • リクエストパラメータ

具体的な対応

2つのユースケースで実装してみます。

Fractional Evaluation

最初は割合ベースの出し分けです。

Flag設定

次の条件をflagdで設定します。

  • ユーザIDをTargeting Keyに
  • OFFとONのbool値
  • OFF:ONを3:1で出し分け
{
  "flags": {
    "fractional-evaluation": {
      "variants": {
        "on": true,
        "off": false
      },
      "state": "ENABLED",
      "defaultVariant": "off",
      "targeting": {
        "fractional": [
          {
            "var": "userId"
          },
          ["on", 25],
          ["off", 75]
        ]
      }
    }
  }
}

Goによる実装

Goで実装すると次のようになります。

func registerFractionEndpoint(engine *gin.Engine, client *openfeature.Client) {
        engine.GET("/hello", func(c *gin.Context) {
                userId := c.Query("userId")

                evalCtx := openfeature.NewEvaluationContext(
                        "",
                        map[string]interface{}{
                                "userId": userId,
                        })

                flagResult, err := client.BooleanValue(
                        context.Background(), fraExpKey, false, evalCtx,
                )
                if err != nil {
                        c.JSON(http.StatusInternalServerError, err.Error())
                        return
                }

                if flagResult {
                        c.JSON(http.StatusOK, BoolFlag{Result: true})
                        return
                }
                c.JSON(http.StatusOK, BoolFlag{Result: false})
        })
}

ユーザIDをTargeting keyにするようにAttributeに入れています。

Targeting key引数の直指定

また理由は後述しますが、 Fractional Evaluation については次のような書き方も可能です。

evalCtx := openfeature.NewEvaluationContext(userId,    nil)

動作確認

以下の様にユーザIDによって評価が変わります。

$ curl http://localhost:8080/hello?userId=2
{"result":true}
$ curl http://localhost:8080/hello?userId=5
{"result":false}

Targeting Keyのハッシュ値で評価されるので、同じユーザIDは同じ結果になります。

$ curl http://localhost:8080/hello?userId=2
{"result":true}
$ curl http://localhost:8080/hello?userId=2
{"result":true}

割合も期待通りに出ています。
3:1の設定に対して1500件:500件になっています。

$ k6 run k6/test.js

         /\      Grafana   /‾‾/
    /\  /  \     |\  __   /  /
   /  \/    \    | |/ /  /   ‾‾\
  /          \   |   (  |  ()  |
 / __________ \  |_|\_\  \_____/

     execution: local
        script: k6/test.js
        output: -

     scenarios: (100.00%) 1 scenario, 100 max VUs, 1m0s max duration (incl. graceful stop):
              * constant_request_rate: 67.00 iterations/s for 30s (maxVUs: 50-100, gracefulStop: 30s)status is 200

     checks.........................: 100.00% 2010 out of 2010
     data_received..................: 279 kB  9.3 kB/s
     data_sent......................: 203 kB  6.8 kB/s
     http_req_blocked...............: avg=18.43µs min=1µs     med=9µs    max=1.05ms  p(90)=16.1µs p(95)=24µs
     http_req_connecting............: avg=6.35µs  min=0s      med=0s     max=828µs   p(90)=0s     p(95)=0s
     http_req_duration..............: avg=3.83ms  min=837µs   med=3.36ms max=33.92ms p(90)=5.77ms p(95)=7.27ms
       { expected_response:true }...: avg=3.83ms  min=837µs   med=3.36ms max=33.92ms p(90)=5.77ms p(95)=7.27ms
     http_req_failed................: 0.00%   0 out of 2010
     http_req_receiving.............: avg=84.09µs min=10µs    med=70µs   max=12.04ms p(90)=119µs  p(95)=143.54µs
     http_req_sending...............: avg=29.9µs  min=2µs     med=28µs   max=469µs   p(90)=44µs   p(95)=52µs
     http_req_tls_handshaking.......: avg=0s      min=0s      med=0s     max=0s      p(90)=0s     p(95)=0s
     http_req_waiting...............: avg=3.72ms  min=809µs   med=3.25ms max=33.87ms p(90)=5.62ms p(95)=7.11ms
     http_reqs......................: 2010    66.998962/s
     iteration_duration.............: avg=4.18ms  min=947.7µs med=3.7ms  max=34.39ms p(90)=6.24ms p(95)=7.84ms
     iterations.....................: 2010    66.998962/s
     result_false_count.............: 1511    50.365886/s
     result_true_count..............: 499     16.633076/s
     vus............................: 0       min=0            max=1
     vus_max........................: 50      min=50           max=50

年齢による表示分け

次は年齢による表示分けのケースです。

Flag設定

次の条件をflagdで設定します。

  • 年齢(age)をTargeting Keyに
  • 赤、青、緑、グレーの4つのVariant
  • 年齢毎に色を分ける
{
  "flags": {
    "color-experiment": {
      "variants": {
         "red": "#b91c1c",
         "blue": "#0284c7",
         "green": "#16a34a",
         "grey": "#4b5563"
      },
      "state": "ENABLED",
      "defaultVariant": "grey",
      "targeting": {
        "if": [
          {
            "<": [
             {"var":"age"},
             10
            ]
          },
          "red",
          {
            "<": [
             {"var":"age"},
             30
            ]
          },
          "blue",
          {
            "<": [
             {"var":"age"},
             50
            ]
          },
          "green",
          "grey"
        ]
      }
    }
  }
}

Goによる実装

Goで実装すると次のようになります。

func registerColorEndpoint(engine *gin.Engine, client *openfeature.Client) {
        engine.GET("/color", func(c *gin.Context) {
                age := c.Query("age")

                evalCtx := openfeature.NewEvaluationContext(
                        "",
                        map[string]interface{}{
                                "age": age,
                        })

                flagResult, err := client.StringValue(
                        context.Background(), colorExpKey, "", evalCtx,
                )
                if err != nil {
                        c.JSON(http.StatusInternalServerError, err.Error())
                        return
                }
                c.JSON(http.StatusOK, StringFlag{Color: flagResult})
        })
}

クエリでageパラメータを受け取り、それを Exvaluation Context に入れています。

動作確認

ageクエリパラメータを変更すると、期待通りの色が返ってきます。

$ curl http://localhost:8080/color?age=5
{"color":"#b91c1c"}
$ curl http://localhost:8080/color?age=90
{"color":"#4b5563"}

その他

サンプルコード

今回のサンプルコードはこちらです。

github.com

Targeting Keyは直接引数で? Attributeで?

Go SDKのシグネチャは次のようになっています。

func NewEvaluationContext(targetingKey string, attributes map[string]interface{}) EvaluationContext

これは背景として、Providerによっては識別子名なしで Fractional Evaluation 用の Targeting key を設定するケースがあり、それとの互換性を保つためです。

例えば flagd は両方考慮した処理になっています。

ref: https://flagd.dev/reference/specifications/custom-operations/fractional-operation-spec/?h=targeting

またそのサポートはSDKによって異なり、PHPなどはGoと同じくTargeting Keyを別途持ちますが、Javaなどはありません。

// add a value to the invocation context  
EvaluationContext context = new MutableContext();  
context.addStringAttribute("myInvocationKey", "myInvocationValue")  
Boolean boolValue = client.getBooleanValue("boolFlag", false, context);

まとめ

OpenFeature における Evaluation Context の説明と具体的な使い方を説明しました。

参考




以上の内容はhttps://christina04.hatenablog.com/entry/openfeature-evalution-contextより取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14