概要
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"}
その他
サンプルコード
今回のサンプルコードはこちらです。
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 の説明と具体的な使い方を説明しました。