From e7d07d3fb2ec317380529555a3d906a725b20e25 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Mon, 21 Aug 2023 12:51:13 +0200 Subject: [PATCH 01/18] docs: add specification for in-process flagd provider implementations Signed-off-by: Florian Bacher --- .../evaluators/fractional_evaluation.md | 211 +++++++++++++++ .../evaluators/semver_evaluation.md | 43 ++++ .../string_comparison_evaluation.md | 39 +++ .../in-process-providers/specification.md | 240 ++++++++++++++++++ 4 files changed, 533 insertions(+) create mode 100644 docs/other_resources/in-process-providers/evaluators/fractional_evaluation.md create mode 100644 docs/other_resources/in-process-providers/evaluators/semver_evaluation.md create mode 100644 docs/other_resources/in-process-providers/evaluators/string_comparison_evaluation.md create mode 100755 docs/other_resources/in-process-providers/specification.md diff --git a/docs/other_resources/in-process-providers/evaluators/fractional_evaluation.md b/docs/other_resources/in-process-providers/evaluators/fractional_evaluation.md new file mode 100644 index 000000000..50fd43d18 --- /dev/null +++ b/docs/other_resources/in-process-providers/evaluators/fractional_evaluation.md @@ -0,0 +1,211 @@ +# Fractional Evaluatior + +This evaluator allows to split the returned variants of a feature flag into different buckets, +where each bucket can be assigned a percentage, representing how many requests will resolve to the corresponding +variant. The sum of all weights must be 100, and the distribution must be performed by using the value of a referenced +from the evaluation context to hash that value and map it to a value between [0, 100]. It is important to note +that evaluations MUST be sticky, meaning that flag resolution requests containing the same value for the +referenced property in their context MUST always resolve to the same variant. For calculating the hash value of the +referenced evaluation context property, the [MurmurHash3](https://github.com/aappleby/smhasher/blob/master/src/MurmurHash3.cpp) +hash function should be used. This is to ensure that flag resolution requests yield the same result, +regardless of which implementation of the in-process flagd provider is being used. + +The implementation of this evaluator should accept the object containing the `fractionalEvaluation` evaluator +configuration, and a `data` object containing the evaluation context. The evaluator configuration should be an +array containing at least two items, with the first item being a `string` +value representing the target property to base the distribution of values on, and the remaining items +being `arrays` with two values, with the first being `string` item representing the name of the variant, and the +second being a `float` item representing the percentage for that variant. The percentages of all items must add up to +100.0, otherwise unexpected behavior can occur during the evaluation. The `data` object can be an arbitrary +JSON object. Below is an example for a targetingRule containing a `fractionalEvaluation`: + +```json +{ + "flags": { + "headerColor": { + "variants": { + "red": "#FF0000", + "blue": "#0000FF", + "green": "#00FF00" + }, + "defaultVariant": "red", + "state": "ENABLED", + "targeting": { + "fractionalEvaluation": [ + "email", + [ + "red", + 50 + ], + [ + "blue", + 20 + ], + [ + "green", + 30 + ] + ] + } + } + } +} +``` + +The following flow chart depicts the logic of this evaluator: + +```mermaid +flowchart TD +A[Parse targetingRule] --> B{Is an array containing at least two items?}; +B -- Yes --> C{Is targetingRule at index 0 a string?}; +B -- No --> D[Return nil]; +C -- Yes --> E[targetProperty := targetingRule at index 0]; +C -- No --> D; +E --> F[targetPropertyValue := evaluationContext at key 'targetProperty', empty string otherwise] +F --> G{Is targetPropertyValue a string?}; +G -- No --> D; +G -- Yes --> H[Iterate through the remaining elements of the targetingRule array and parse the variants and their percentages]; +H --> I{Parsing successful?}; +I -- No --> D; +I -- Yes --> J{Does percentage of variants add up to 100?}; +J -- No --> D; +J -- Yes --> K[hash := murmur3Hash of targetPropertyValue divided by Int64.MaxValue] +K --> L[Iterate through the variant and increment the threshold by the percentage of each variant. Return the first variant where the bucket is smaller than the threshold.] +``` + +As a reference, below is a simplified version of the actual implementation of this evaluator in Go. + +```go + +type fractionalEvaluationDistribution struct { + variant string + percentage int +} + +/* + values: contains the targeting rule object; e.g.: + [ + "email", + [ + "red", + 50 + ], + [ + "blue", + 20 + ], + [ + "green", + 30 + ] + ] + + data: contains the evaluation context; e.g.: + { + "email": "test@faas.com" + } +*/ +func FractionalEvaluation(values, data interface{}) interface{} { + // 1. Check if the values object contains at least two elements: + valuesArray, ok := values.([]interface{}) + if !ok { + log.Error("fractional evaluation data is not an array") + return nil + } + if len(valuesArray) < 2 { + log.Error("fractional evaluation data has length under 2") + return nil + } + + // 2. Get the target property used for bucketing the values and retrieve its value from the evaluation context: + bucketBy, ok := valuesArray[0].(string) + if !ok { + log.Error("first element of fractional evaluation data isn't of type string") + return nil + } + + dataMap, ok := data.(map[string]interface{}) + if !ok { + log.Error("data isn't of type map[string]interface{}") + return nil + } + + v, ok := dataMap[bucketBy] + if !ok { + // if the target property is not part of the evaluation context, use an empty string for calculating the hash + v = "" + } + + valueToDistribute, ok := v.(string) + if !ok { + // the target property must have a string value + log.Error("var: %s isn't of type string", bucketBy) + return nil + } + + // 3. Parse the fractionalEvaluation values distribution + sumOfPercentages := 0 + var feDistributions []fractionalEvaluationDistribution + + // start at index 1, as the first item of the values array is the target property + for i := 1; i < len(valuesArray); i++ { + distributionArray, ok := values[i].([]interface{}) + if !ok { + log.Error("distribution elements aren't of type []interface{}") + return nil + } + + if len(distributionArray) != 2 { + log.Error("distribution element isn't length 2") + return nil + } + + variant, ok := distributionArray[0].(string) + if !ok { + log.Error("first element of distribution element isn't a string") + return nil + } + + percentage, ok := distributionArray[1].(float64) + if !ok { + log.Error("second element of distribution element isn't float") + return nil + } + + sumOfPercentages += int(percentage) + + feDistributions = append(feDistributions, fractionalEvaluationDistribution{ + variant: variant, + percentage: int(percentage), + }) + } + + // check if the sum of percentages adds up to 100, otherwise log an error + if sumOfPercentages != 100 { + log.Error("percentages must sum to 100, got: %d", sumOfPercentages) + return nil + } + + // 4. Calculate the hash of the target property and map it to a number between [0, 99] + hashValue := murmur2.HashString(value) + + // divide the hash value by the largest possible value, integer 2^64 + hashRatio := float64(hashValue) / math.Pow(2, 64) + + // integer in range [0, 99] + bucket := int(hashRatio * 100) + + // 5. Iterate through the variant and increment the threshold by the percentage of each variant. + // return the first variant where the bucket is smaller than the threshold. + rangeEnd := 0 + for _, dist := range feDistribution { + rangeEnd += dist.percentage + if bucket < rangeEnd { + // return the matching variant + return dist.variant + } + } + + return "" +} +``` \ No newline at end of file diff --git a/docs/other_resources/in-process-providers/evaluators/semver_evaluation.md b/docs/other_resources/in-process-providers/evaluators/semver_evaluation.md new file mode 100644 index 000000000..0d72ab79c --- /dev/null +++ b/docs/other_resources/in-process-providers/evaluators/semver_evaluation.md @@ -0,0 +1,43 @@ +# Semantic Versioning Evaluation + +This evaluator checks if the given property within the evaluation context matches a semantic versioning condition. +It returns 'true', if the value of the given property meets the condition, 'false' if not. + +The implementation of this evaluator should accept the object containing the `sem_ver` evaluator +configuration, and a `data` object containing the evaluation context. +The 'sem_ver' evaluation rule contains exactly three items: + +1. Target property value: the resolved value of the target property referenced in the targeting rule +2. Operator: One of the following: `=`, `!=`, `>`, `<`, `>=`, `<=`, `~` (match minor version), `^` (match major version) +3. Target value: this needs to resolve to a semantic versioning string + +The `sem_ver` evaluation returns a boolean, indicating whether the condition has been met. + +```js +{ + "if": [ + { + "sem_ver": [{"var": "version"}, ">=", "1.0.0"] + }, + "red", null + ] +} +``` + +Please note that the implementation of this evaluator can assume that instead of `{"var": "version"}`, it will receive +the resolved value of that referenced property, as resolving the value will be taken care of by JsonLogic before +applying the evaluator. + +The following flow chart depicts the logic of this evaluator: + +```mermaid +flowchart TD +A[Parse targetingRule] --> B{Is an array containing exactly three items?}; +B -- Yes --> C{Is targetingRule at index 0 a semantic version string?}; +B -- No --> D[Return nil]; +C -- Yes --> E{Is targetingRule at index 1 a supported operator?}; +C -- No --> D; +E -- Yes --> F{Is targetingRule at index 2 a semantic version string?}; +E -- No --> D; +F --> G[Compare the two versions using the operator and return a boolean value indicating if they match]; +``` \ No newline at end of file diff --git a/docs/other_resources/in-process-providers/evaluators/string_comparison_evaluation.md b/docs/other_resources/in-process-providers/evaluators/string_comparison_evaluation.md new file mode 100644 index 000000000..bb12dfb5c --- /dev/null +++ b/docs/other_resources/in-process-providers/evaluators/string_comparison_evaluation.md @@ -0,0 +1,39 @@ +# StartsWith/EndsWith evaluation + +This evaluator selects a variant based on whether the specified property within the evaluation context +starts/ends with a certain string. + +The implementation of this evaluator should accept the object containing the `starts_with` or `ends_with` evaluator +configuration, and a `data` object containing the evaluation context. +The `starts_with`/`ends_with` evaluation rule contains exactly two items: + +1. The resolved string value from the evaluation context +2. The target string value + +The `starts_with`/`ends_with` evaluation returns a boolean, indicating whether the condition has been met. + +```js +// starts_with property name used in a targeting rule +"starts_with": [ + // Evaluation context property the be evaluated + {"var": "email"}, + // prefix that has to be present in the value of the referenced property + "user@faas" +] +``` + +Please note that the implementation of this evaluator can assume that instead of `{"var": "email"}`, it will receive +the resolved value of that referenced property, as resolving the value will be taken care of by JsonLogic before +applying the evaluator. + +The following flow chart depicts the logic of this evaluator: + +```mermaid +flowchart TD +A[Parse targetingRule] --> B{Is an array containing exactly two items?}; +B -- Yes --> C{Is targetingRule at index 0 a string?}; +B -- No --> D[Return nil]; +C -- Yes --> E{Is targetingRule at index 1 a string?}; +C -- No --> D; +E --> F[Return a boolean value indicating if the first string starts/ends with the second string]; +``` \ No newline at end of file diff --git a/docs/other_resources/in-process-providers/specification.md b/docs/other_resources/in-process-providers/specification.md new file mode 100755 index 000000000..517d0c149 --- /dev/null +++ b/docs/other_resources/in-process-providers/specification.md @@ -0,0 +1,240 @@ +# Creating an in-process flagd provider + +The in-process flagd provider is responsible for creating an abstraction between the (JsonLogic)[https://jsonlogic.com] based evaluation of flag configurations following the [flag configuration scheme](https://github.com/open-feature/schemas/blob/main/json/flagd-definitions.json) used by `flagd` and the OpenFeature SDK (for the [chosen technology](https://openfeature.dev/docs/reference/technologies/)). + +Prerequisites: + +- Understanding of [general provider concepts](https://openfeature.dev/docs/reference/concepts/provider/) +- Proficiency in the chosen programming language (check the language isn't already covered by the [existing providers](../usage/flagd_providers.md)) + +The Flag Configuration containing the feature flags and JsonLogic based targeting rules shall be retrieved by the in-process flagd provider via a gRPC client connection to a sync server, such as (flagd-proxy)[https://github.com/open-feature/flagd/flagd-proxy]. + +# Sync sources + +An implementation of an in-process flagd-provider must accept the following environment variables which determine the sync source: + +- `FLAGD_SOURCE_URI`: The URI identifying the sync source. Depending on the sync provider type, this can be the URI of a gRPC service providing the `sync` API required by the in-process flagd provider, or the name of a [core.openfeature.dev/v1alpha2.FeatureFlagConfiguration](https://github.com/open-feature/open-feature-operator/blob/main/docs/crds.md#featureflagconfiguration-1) Custom Resource containing the flag configuration. + +- `FLAGD_SOURCE_PROVIDER_TYPE`: The type of the provider. E.g. `grpc` or `kubernetes`. + +- `FLAGD_SOURCE_SELECTOR`: Optional selector for the feature flag configuration of interest. This is used as a `selector` for the flagd-proxie's sync API to identify a flag configuration within a collection of feature flag configurations. + + +An implementation of an in-process flagd provider should provide a source for retrieving the flag configuration, namely a gRPC source. +Other sources may be desired eventually, so separation of concerns should be mantained between the abstractions evaluating flags and those retreving confitation. + +## gRPC sources + +gRPC sync sources are identified by the `provider` field set to `grpc`. +When such a sync source is specified, the in-process flagd provider should connect to the gRPC service located at the `uri` of the sync source, and use its `sync` API ([see here](https://github.com/open-feature/schemas)) to retrieve the feature flag configurations. If the `selector` field of the sync source is set, that selector should be passed through to the `Sync` and `FetchAllFlags` requests sent to the gRPC server. + +### Protobuf + +Protobuf schemas define the contract between a client (flagd or the in-process provider implementation) and server (`flagd-proxy`). `flagd-proxy`'s schemas are defined [here](https://github.com/open-feature/schemas/tree/main/protobuf/sync/v1). + +#### Code generation for gRPC sync + +Leverage the [buf CLI](https://docs.buf.build/installation) or protoc to generate a `flagd-proxy` client in the chosen technology: + +Add the [open-feature schema repository](https://github.com/open-feature/schemas) as a submodule + +```shell +git submodule add --force https://github.com/open-feature/schemas.git +``` + +Create a `buf.gen.{chosen language}.yaml` for the chosen language in `schemas/protobuf` (if it doesn't already exist) using one of the other files as a template (find a plugin for the chosen language [here](https://buf.build/protocolbuffers/plugins)) and create a pull request with this file. + +Generate the code (this step ought to be automated in the build process for the chosen technology so that the generated code is never committed) + +```shell +cd schemas/protobuf +buf generate --template buf.gen.{chosen language}.yaml +``` + +As an alternative to buf, use the .proto file directly along with whatever protoc-related tools or plugins avaialble for your language. + +Move the generated code (following convention for the chosen language) and add its location to .gitignore + +Note that for the in-process provider only the `sync` package will be relevant, as it does not communicate with `flagd`, but only with compliant gRPC services such as `flagd-proxy`. + +## JsonLogic evaluation + +An in-process flagd provider should provide the feature set offered by [JsonLogic](https://jsonlogic.com) to evaluate flag resolution requests for a given context. If available, the JsonLogic library for the chosen technology should be used. + +### Custom JsonLogic evaluators + +In addition to the built-in evaluators provided by JsonLogic, the following custom targeting rules should be implemented by the provider: + +- [Fractional evaluation](https://github.com/open-feature/flagd/blob/main/docs/configuration/fractional_evaluation.md): +This evaluator allows to split the returned variants of a feature flag into different buckets, where each bucket +can be assigned a percentage, representing how many requests will resolve to the corresponding variant. +The sum of all weights must be 100, and the distribution must be performed by using the value of a referenced +from the evaluation context to hash that value and map it to a value between [0, 100]. +It is important to note that evaluations MUST be sticky, meaning that flag resolution requests containing the +same value for the referenced property in their context MUST always resolve to the same variant. +For calculating the hash value of the referenced evaluation context property, +the [MurmurHash3](https://github.com/aappleby/smhasher/blob/master/src/MurmurHash3.cpp) hash function should be used. +This is to ensure that flag resolution requests yield the same result, regardless of which implementation of +the in-process flagd provider is being used. For more specific implementation guidelines, please refer to +[this document](./evaluators/fractional_evaluation.md). +- [SemVer evaluation](https://github.com/open-feature/flagd/blob/main/docs/configuration/sem_ver_evaluation.md): +This evaluator checks if the given property within the evaluation context matches a semantic versioning condition. +It returns 'true', if the value of the given property meets the condition, 'false' if not. +For more specific implementation guidelines, please refer to [this document](./evaluators/semver_evaluation.md). +- [StartsWith/EndsWith evaluation](https://github.com/open-feature/flagd/blob/main/docs/configuration/string_comparison_evaluation.md): +This evaluator selects a variant based on whether the specified property within the evaluation context +starts/ends with a certain string. +For more specific implementation guidelines, please refer to [this document](./evaluators/string_comparison_evaluation.md). + +## Provider construction + +(__using Go as an example__) + +Create a provider struct/class/type (whichever is relevant to the chosen language) with an exported (public) constructor allowing configuration (e.g. `flagd` host). +Give the provider an un-exported (private) client field, set this field as the client generated by the previous step. + +Create methods for the provider to satisfy the chosen language SDK's provider interface. +These methods ought to wrap the built client's methods. + +```go +type Provider struct { + evaluator IJsonEvaluator +} + +type ProviderOption func(*Provider) + +func NewProvider(options ...ProviderOption) *Provider { + provider := &Provider{} + for _, opt := range opts { + opt(provider) + } + + // create a store that is responsible for retrieving the flag configurations + // from the sources that are given to the provider via the options + s := store.NewFlags() + for _, provider := range config.SyncProviders { + s.FlagSources = append(s.FlagSources, provider.URI) + s.SourceMetadata[provider.URI] = store.SourceDetails{ + Source: provider.URI, + Selector: provider.Selector, + } + } + + // derive evaluator + provider.evaluator := setupJSONEvaluator(logger, s) + + return provider +} + +func WithHost(host string) ProviderOption { + return func(p *Provider) { + p.flagdHost = host + } +} + +func (p *Provider) BooleanEvaluation( + ctx context.Context, flagKey string, defaultValue bool, evalCtx of.FlattenedContext, +) of.BoolResolutionDetail { + + res, err := p.evaluator.ResolveBoolean(ctx, flagKey, context) + + if err != nil { + return of.BoolResolutionDetail{ + Value: defaultValue, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.NewGeneralResolutionError(err.Error()), + Reason: of.Reason(res.Reason), + Variant: res.Variant, + }, + } + } + + return of.BoolResolutionDetail{ + Value: defaultValue, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + Reason: of.Reason(res.Reason), + Variant: res.Variant, + }, + } +} + +// ... +``` + +## Provider lifecycle, initialization and shutdown + +With the release of the v0.x.0 spec, OpenFeature now outlines a lifecycle for in-process flagd provider initialization and shutdown. + +In-process flagd providers should do the following to make use of OpenFeature v0.6.0 features: + +- start in a `NOT_READY` state +- fetch the flag configurations specified in the sync provider sources and set `state` to `READY` or `ERROR` in the `initialization` function + - note that the SDK will automatically emit `PROVIDER_READY`/`PROVIDER_ERROR` according to the termination of the `initialization` function +- throw an exception or terminate abnormally if a connection cannot be established during `initialization` +- For gRPC based sources (i.e. flagd-proxy), attempt to restore the streaming connection to flagd-proxy (if the connection cannot be established or is broken): + - If flag configurations have been retrieved previously, go into `STALE` state to indicate that flag resolution responsees are based on potentially outdated Flag Configurations. + - reconnection should be attempted with an exponential back-off, with a max-delay of `maxSyncRetryInterval` (see [configuration](#configuration)) + - reconnection should be attempted up to `maxSyncRetryDelay` times (see [configuration](#configuration)) + - `PROVIDER_READY` and `PROVIDER_CONFIGURATION_CHANGED` should be emitted, in that order, after successful reconnection +- For Kubernetes sync sources, retry to retrieve the FlagConfiguration resource, using an exponential back-off strategy, with a max-delay of `maxSyncRetryInterval` (see [configuration](#configuration)) +- emit `PROVIDER_CONFIGURATION_CHANGED` event and update ruleset when a `configuration_change` message is received on the streaming connection +- close the streaming connection in the`shutdown` function + +```mermaid +stateDiagram-v2 + [*] --> NOT_READY + NOT_READY --> READY: initialize(), stream connected, flag configurations retrieved + NOT_READY --> ERROR: initialize(), unable to connect (retry) + READY --> STALE: previously retrieved flag configurations can not be retrieved anymore (emit stale*) + STALE --> READY: connection to flag source reestablished, and latest flag configurations retrieved (emit ready*, changed*) + STALE --> ERROR: connection reattempt failed after maxSyncRetries reached (emit error*) + READY --> READY: configuration_change (emit changed*) + ERROR --> READY: reconnect successful (emit ready*, changed*) + ERROR --> ERROR: maxSyncRetries reached + ERROR --> [*]: shutdown(), stream disconnected +``` + +\* ready=`PROVIDER_READY`, changed=`PROVIDER_CONFIGURATION_CHANGED`, stale=`PROVIDER_STALE`, error=`PROVIDER_ERROR` + +## Configuration + +Expose means to configure the provider aligned with the following priority system (highest to lowest). + +```mermaid +flowchart LR + constructor-parameters -->|highest priority| environment-variables -->|lowest priority| defaults +``` + +### Explicit declaration + +This takes the form of parameters to the provider's constructor, it has the highest priority. + +### Environment variables + +Read environment variables with sensible defaults (before applying the values explicitly declared to the constructor). + +| Option name | Environment variable name | Type | Options | Default | +| --------------------------- | ------------------------------------- | ------- | ------------ | -------------------------------------- | +| host | FLAGD_PROXY_HOST | string | | localhost | +| port | FLAGD_PROXY_PORT | number | | 8013 | +| tls | FLAGD_PROXY_TLS | boolean | | false | +| socketPath | FLAGD_PROXY_SOCKET_PATH | string | | | +| certPath | FLAGD_PROXY_SERVER_CERT_PATH | string | | | +| sourceURI | FLAGD_SOURCE_URI | string | | | +| sourceProviderType | FLAGD_SOURCE_PROVIDER_TYPE | string | | grpc | +| sourceSelector | FLAGD_SOURCE_SELECTOR | string | | | +| maxSyncRetries | FLAGD_MAX_SYNC_RETRIES | int | | 0 (0 means unlimited) | +| maxSyncRetryInterval | FLAGD_MAX_SYNC_RETRY_INTERVAL | int | | 60s | + +## Error handling + +Handle flag evaluation errors by using the error constructors exported by the SDK (e.g. `openfeature.NewProviderNotReadyResolutionError(ConnectionError)`), thereby allowing the SDK to parse and handle the error appropriately. + +## Post creation + +The following steps will extend the reach of the newly created provider to other developers of the chosen technology. + +### Open an issue to document the provider + +Create an issue in openfeature.dev [here](https://github.com/open-feature/openfeature.dev/issues/new?assignees=&labels=provider&template=document-provider.yaml&title=%5BProvider%5D%3A+). +This will ensure the provider is added to OpenFeature's website. \ No newline at end of file From 47cdf1b40695c7bd7ac5623c1ab3386d53976948 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Mon, 21 Aug 2023 13:33:42 +0200 Subject: [PATCH 02/18] fix linting errors Signed-off-by: Florian Bacher --- .../evaluators/fractional_evaluation.md | 120 +++++++++--------- .../evaluators/semver_evaluation.md | 2 +- .../string_comparison_evaluation.md | 2 +- .../in-process-providers/specification.md | 30 ++--- 4 files changed, 75 insertions(+), 79 deletions(-) diff --git a/docs/other_resources/in-process-providers/evaluators/fractional_evaluation.md b/docs/other_resources/in-process-providers/evaluators/fractional_evaluation.md index 50fd43d18..9401ba154 100644 --- a/docs/other_resources/in-process-providers/evaluators/fractional_evaluation.md +++ b/docs/other_resources/in-process-providers/evaluators/fractional_evaluation.md @@ -78,8 +78,8 @@ As a reference, below is a simplified version of the actual implementation of th ```go type fractionalEvaluationDistribution struct { - variant string - percentage int + variant string + percentage int } /* @@ -108,104 +108,104 @@ type fractionalEvaluationDistribution struct { func FractionalEvaluation(values, data interface{}) interface{} { // 1. Check if the values object contains at least two elements: valuesArray, ok := values.([]interface{}) - if !ok { + if !ok { log.Error("fractional evaluation data is not an array") - return nil - } - if len(valuesArray) < 2 { + return nil + } + if len(valuesArray) < 2 { log.Error("fractional evaluation data has length under 2") - return nil - } + return nil + } // 2. Get the target property used for bucketing the values and retrieve its value from the evaluation context: bucketBy, ok := valuesArray[0].(string) - if !ok { + if !ok { log.Error("first element of fractional evaluation data isn't of type string") - return nil - } + return nil + } - dataMap, ok := data.(map[string]interface{}) - if !ok { + dataMap, ok := data.(map[string]interface{}) + if !ok { log.Error("data isn't of type map[string]interface{}") - return nil - } + return nil + } - v, ok := dataMap[bucketBy] - if !ok { + v, ok := dataMap[bucketBy] + if !ok { // if the target property is not part of the evaluation context, use an empty string for calculating the hash - v = "" - } + v = "" + } valueToDistribute, ok := v.(string) - if !ok { + if !ok { // the target property must have a string value log.Error("var: %s isn't of type string", bucketBy) - return nil - } + return nil + } // 3. Parse the fractionalEvaluation values distribution sumOfPercentages := 0 - var feDistributions []fractionalEvaluationDistribution + var feDistributions []fractionalEvaluationDistribution // start at index 1, as the first item of the values array is the target property - for i := 1; i < len(valuesArray); i++ { - distributionArray, ok := values[i].([]interface{}) - if !ok { + for i := 1; i < len(valuesArray); i++ { + distributionArray, ok := values[i].([]interface{}) + if !ok { log.Error("distribution elements aren't of type []interface{}") - return nil - } + return nil + } - if len(distributionArray) != 2 { + if len(distributionArray) != 2 { log.Error("distribution element isn't length 2") - return nil - } + return nil + } - variant, ok := distributionArray[0].(string) - if !ok { + variant, ok := distributionArray[0].(string) + if !ok { log.Error("first element of distribution element isn't a string") - return nil - } + return nil + } - percentage, ok := distributionArray[1].(float64) - if !ok { + percentage, ok := distributionArray[1].(float64) + if !ok { log.Error("second element of distribution element isn't float") - return nil - } + return nil + } - sumOfPercentages += int(percentage) + sumOfPercentages += int(percentage) - feDistributions = append(feDistributions, fractionalEvaluationDistribution{ - variant: variant, - percentage: int(percentage), - }) - } + feDistributions = append(feDistributions, fractionalEvaluationDistribution{ + variant: variant, + percentage: int(percentage), + }) + } // check if the sum of percentages adds up to 100, otherwise log an error - if sumOfPercentages != 100 { + if sumOfPercentages != 100 { log.Error("percentages must sum to 100, got: %d", sumOfPercentages) - return nil - } + return nil + } - // 4. Calculate the hash of the target property and map it to a number between [0, 99] + // 4. Calculate the hash of the target property and map it to a number between [0, 99] hashValue := murmur2.HashString(value) // divide the hash value by the largest possible value, integer 2^64 - hashRatio := float64(hashValue) / math.Pow(2, 64) + hashRatio := float64(hashValue) / math.Pow(2, 64) // integer in range [0, 99] - bucket := int(hashRatio * 100) + bucket := int(hashRatio * 100) // 5. Iterate through the variant and increment the threshold by the percentage of each variant. // return the first variant where the bucket is smaller than the threshold. - rangeEnd := 0 - for _, dist := range feDistribution { - rangeEnd += dist.percentage - if bucket < rangeEnd { + rangeEnd := 0 + for _, dist := range feDistribution { + rangeEnd += dist.percentage + if bucket < rangeEnd { // return the matching variant - return dist.variant - } - } + return dist.variant + } + } - return "" + return "" } -``` \ No newline at end of file +``` diff --git a/docs/other_resources/in-process-providers/evaluators/semver_evaluation.md b/docs/other_resources/in-process-providers/evaluators/semver_evaluation.md index 0d72ab79c..cc9960293 100644 --- a/docs/other_resources/in-process-providers/evaluators/semver_evaluation.md +++ b/docs/other_resources/in-process-providers/evaluators/semver_evaluation.md @@ -40,4 +40,4 @@ C -- No --> D; E -- Yes --> F{Is targetingRule at index 2 a semantic version string?}; E -- No --> D; F --> G[Compare the two versions using the operator and return a boolean value indicating if they match]; -``` \ No newline at end of file +``` diff --git a/docs/other_resources/in-process-providers/evaluators/string_comparison_evaluation.md b/docs/other_resources/in-process-providers/evaluators/string_comparison_evaluation.md index bb12dfb5c..5bee6e84f 100644 --- a/docs/other_resources/in-process-providers/evaluators/string_comparison_evaluation.md +++ b/docs/other_resources/in-process-providers/evaluators/string_comparison_evaluation.md @@ -36,4 +36,4 @@ B -- No --> D[Return nil]; C -- Yes --> E{Is targetingRule at index 1 a string?}; C -- No --> D; E --> F[Return a boolean value indicating if the first string starts/ends with the second string]; -``` \ No newline at end of file +``` diff --git a/docs/other_resources/in-process-providers/specification.md b/docs/other_resources/in-process-providers/specification.md index 517d0c149..548940a38 100755 --- a/docs/other_resources/in-process-providers/specification.md +++ b/docs/other_resources/in-process-providers/specification.md @@ -1,25 +1,23 @@ # Creating an in-process flagd provider -The in-process flagd provider is responsible for creating an abstraction between the (JsonLogic)[https://jsonlogic.com] based evaluation of flag configurations following the [flag configuration scheme](https://github.com/open-feature/schemas/blob/main/json/flagd-definitions.json) used by `flagd` and the OpenFeature SDK (for the [chosen technology](https://openfeature.dev/docs/reference/technologies/)). +The in-process flagd provider is responsible for creating an abstraction between the [JsonLogic](https://jsonlogic.com) based evaluation of flag configurations following the [flag configuration scheme](https://github.com/open-feature/schemas/blob/main/json/flagd-definitions.json) used by `flagd` and the OpenFeature SDK (for the [chosen technology](https://openfeature.dev/docs/reference/technologies/)). Prerequisites: - Understanding of [general provider concepts](https://openfeature.dev/docs/reference/concepts/provider/) - Proficiency in the chosen programming language (check the language isn't already covered by the [existing providers](../usage/flagd_providers.md)) -The Flag Configuration containing the feature flags and JsonLogic based targeting rules shall be retrieved by the in-process flagd provider via a gRPC client connection to a sync server, such as (flagd-proxy)[https://github.com/open-feature/flagd/flagd-proxy]. +The Flag Configuration containing the feature flags and JsonLogic based targeting rules shall be retrieved by the +in-process flagd provider via a gRPC client connection to a sync server, such as [flagd-proxy](https://github.com/open-feature/flagd/flagd-proxy). -# Sync sources +## Sync source An implementation of an in-process flagd-provider must accept the following environment variables which determine the sync source: -- `FLAGD_SOURCE_URI`: The URI identifying the sync source. Depending on the sync provider type, this can be the URI of a gRPC service providing the `sync` API required by the in-process flagd provider, or the name of a [core.openfeature.dev/v1alpha2.FeatureFlagConfiguration](https://github.com/open-feature/open-feature-operator/blob/main/docs/crds.md#featureflagconfiguration-1) Custom Resource containing the flag configuration. - +- `FLAGD_SOURCE_URI`: The URI identifying the sync source. Depending on the sync provider type, this can be the URI of a gRPC service providing the `sync` API required by the in-process flagd provider, or the name of a [core.openfeature.dev/v1alpha2.FeatureFlagConfiguration](https://github.com/open-feature/open-feature-operator/blob/main/docs/crds.md#featureflagconfiguration-1) Custom Resource containing the flag configuration. - `FLAGD_SOURCE_PROVIDER_TYPE`: The type of the provider. E.g. `grpc` or `kubernetes`. - - `FLAGD_SOURCE_SELECTOR`: Optional selector for the feature flag configuration of interest. This is used as a `selector` for the flagd-proxie's sync API to identify a flag configuration within a collection of feature flag configurations. - An implementation of an in-process flagd provider should provide a source for retrieving the flag configuration, namely a gRPC source. Other sources may be desired eventually, so separation of concerns should be mantained between the abstractions evaluating flags and those retreving confitation. @@ -65,7 +63,7 @@ An in-process flagd provider should provide the feature set offered by [JsonLogi In addition to the built-in evaluators provided by JsonLogic, the following custom targeting rules should be implemented by the provider: -- [Fractional evaluation](https://github.com/open-feature/flagd/blob/main/docs/configuration/fractional_evaluation.md): +- [Fractional evaluation](https://github.com/open-feature/flagd/blob/main/docs/configuration/fractional_evaluation.md): This evaluator allows to split the returned variants of a feature flag into different buckets, where each bucket can be assigned a percentage, representing how many requests will resolve to the corresponding variant. The sum of all weights must be 100, and the distribution must be performed by using the value of a referenced @@ -112,16 +110,14 @@ func NewProvider(options ...ProviderOption) *Provider { // create a store that is responsible for retrieving the flag configurations // from the sources that are given to the provider via the options s := store.NewFlags() - for _, provider := range config.SyncProviders { - s.FlagSources = append(s.FlagSources, provider.URI) - s.SourceMetadata[provider.URI] = store.SourceDetails{ - Source: provider.URI, - Selector: provider.Selector, - } - } + s.FlagSources = append(s.FlagSources, os.Getenv("FLAGD_SOURCE_URI")) + s.SourceMetadata[provider.URI] = store.SourceDetails{ + Source: os.Getenv("FLAGD_SOURCE_URI"), + Selector: os.Getenv("FLAGD_SOURCE_SELECTOR")), + } // derive evaluator - provider.evaluator := setupJSONEvaluator(logger, s) + provider.evaluator := setupJSONEvaluator(logger, s) return provider } @@ -237,4 +233,4 @@ The following steps will extend the reach of the newly created provider to other ### Open an issue to document the provider Create an issue in openfeature.dev [here](https://github.com/open-feature/openfeature.dev/issues/new?assignees=&labels=provider&template=document-provider.yaml&title=%5BProvider%5D%3A+). -This will ensure the provider is added to OpenFeature's website. \ No newline at end of file +This will ensure the provider is added to OpenFeature's website. From de147ca43c4cabf06e5161e713beee2550cb34e4 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Tue, 22 Aug 2023 07:35:44 +0200 Subject: [PATCH 03/18] Update docs/other_resources/in-process-providers/specification.md Co-authored-by: Michael Beemer Signed-off-by: Florian Bacher --- docs/other_resources/in-process-providers/specification.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/other_resources/in-process-providers/specification.md b/docs/other_resources/in-process-providers/specification.md index 548940a38..741421709 100755 --- a/docs/other_resources/in-process-providers/specification.md +++ b/docs/other_resources/in-process-providers/specification.md @@ -19,7 +19,7 @@ An implementation of an in-process flagd-provider must accept the following envi - `FLAGD_SOURCE_SELECTOR`: Optional selector for the feature flag configuration of interest. This is used as a `selector` for the flagd-proxie's sync API to identify a flag configuration within a collection of feature flag configurations. An implementation of an in-process flagd provider should provide a source for retrieving the flag configuration, namely a gRPC source. -Other sources may be desired eventually, so separation of concerns should be mantained between the abstractions evaluating flags and those retreving confitation. +Other sources may be desired eventually, so separation of concerns should be maintained between the abstractions evaluating flags and those retrieving confirmation. ## gRPC sources From e29df8aa6f2668a258b92c6fc2e914ffe5a39b0a Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Tue, 22 Aug 2023 07:36:25 +0200 Subject: [PATCH 04/18] Update docs/other_resources/in-process-providers/specification.md Co-authored-by: Michael Beemer Signed-off-by: Florian Bacher --- docs/other_resources/in-process-providers/specification.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/other_resources/in-process-providers/specification.md b/docs/other_resources/in-process-providers/specification.md index 741421709..05dafb16c 100755 --- a/docs/other_resources/in-process-providers/specification.md +++ b/docs/other_resources/in-process-providers/specification.md @@ -24,7 +24,8 @@ Other sources may be desired eventually, so separation of concerns should be mai ## gRPC sources gRPC sync sources are identified by the `provider` field set to `grpc`. -When such a sync source is specified, the in-process flagd provider should connect to the gRPC service located at the `uri` of the sync source, and use its `sync` API ([see here](https://github.com/open-feature/schemas)) to retrieve the feature flag configurations. If the `selector` field of the sync source is set, that selector should be passed through to the `Sync` and `FetchAllFlags` requests sent to the gRPC server. +When such a sync source is specified, the in-process flagd provider should connect to the gRPC service located at the `uri` of the sync source, and use its `sync` API ([see here](https://github.com/open-feature/schemas)) to retrieve the feature flag configurations. +If the `selector` field of the sync source is set, that selector should be passed through to the `Sync` and `FetchAllFlags` requests sent to the gRPC server. ### Protobuf From 2e53626671fab204fd7816ed4ecc507ee6e8f1a1 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Tue, 22 Aug 2023 07:36:46 +0200 Subject: [PATCH 05/18] Update docs/other_resources/in-process-providers/specification.md Co-authored-by: Michael Beemer Signed-off-by: Florian Bacher --- docs/other_resources/in-process-providers/specification.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/other_resources/in-process-providers/specification.md b/docs/other_resources/in-process-providers/specification.md index 05dafb16c..736610a1e 100755 --- a/docs/other_resources/in-process-providers/specification.md +++ b/docs/other_resources/in-process-providers/specification.md @@ -29,7 +29,8 @@ If the `selector` field of the sync source is set, that selector should be passe ### Protobuf -Protobuf schemas define the contract between a client (flagd or the in-process provider implementation) and server (`flagd-proxy`). `flagd-proxy`'s schemas are defined [here](https://github.com/open-feature/schemas/tree/main/protobuf/sync/v1). +Protobuf schemas define the contract between a client (flagd or the in-process provider implementation) and server (`flagd-proxy`). +`flagd-proxy`'s schemas are defined [here](https://github.com/open-feature/schemas/tree/main/protobuf/sync/v1). #### Code generation for gRPC sync From 96d541c24a9beef10eb589f9faa7f7b3c64caad4 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Tue, 22 Aug 2023 07:37:12 +0200 Subject: [PATCH 06/18] Update docs/other_resources/in-process-providers/specification.md Co-authored-by: Michael Beemer Signed-off-by: Florian Bacher --- docs/other_resources/in-process-providers/specification.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/other_resources/in-process-providers/specification.md b/docs/other_resources/in-process-providers/specification.md index 736610a1e..5d1e57df7 100755 --- a/docs/other_resources/in-process-providers/specification.md +++ b/docs/other_resources/in-process-providers/specification.md @@ -59,7 +59,8 @@ Note that for the in-process provider only the `sync` package will be relevant, ## JsonLogic evaluation -An in-process flagd provider should provide the feature set offered by [JsonLogic](https://jsonlogic.com) to evaluate flag resolution requests for a given context. If available, the JsonLogic library for the chosen technology should be used. +An in-process flagd provider should provide the feature set offered by [JsonLogic](https://jsonlogic.com) to evaluate flag resolution requests for a given context. +If available, the JsonLogic library for the chosen technology should be used. ### Custom JsonLogic evaluators From 584fab50795b956b99ed26369325c2f5785bcffe Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Tue, 22 Aug 2023 07:37:28 +0200 Subject: [PATCH 07/18] Update docs/other_resources/in-process-providers/evaluators/fractional_evaluation.md Co-authored-by: Michael Beemer Signed-off-by: Florian Bacher --- .../in-process-providers/evaluators/fractional_evaluation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/other_resources/in-process-providers/evaluators/fractional_evaluation.md b/docs/other_resources/in-process-providers/evaluators/fractional_evaluation.md index 9401ba154..de5010322 100644 --- a/docs/other_resources/in-process-providers/evaluators/fractional_evaluation.md +++ b/docs/other_resources/in-process-providers/evaluators/fractional_evaluation.md @@ -1,4 +1,4 @@ -# Fractional Evaluatior +# Fractional Evaluation This evaluator allows to split the returned variants of a feature flag into different buckets, where each bucket can be assigned a percentage, representing how many requests will resolve to the corresponding From 2dae7cb547ff0f6f692711f7153907ad16090d0b Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Tue, 22 Aug 2023 07:38:03 +0200 Subject: [PATCH 08/18] Update docs/other_resources/in-process-providers/specification.md Co-authored-by: Kavindu Dodanduwa Signed-off-by: Florian Bacher --- docs/other_resources/in-process-providers/specification.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/other_resources/in-process-providers/specification.md b/docs/other_resources/in-process-providers/specification.md index 5d1e57df7..786f55be6 100755 --- a/docs/other_resources/in-process-providers/specification.md +++ b/docs/other_resources/in-process-providers/specification.md @@ -8,7 +8,7 @@ Prerequisites: - Proficiency in the chosen programming language (check the language isn't already covered by the [existing providers](../usage/flagd_providers.md)) The Flag Configuration containing the feature flags and JsonLogic based targeting rules shall be retrieved by the -in-process flagd provider via a gRPC client connection to a sync server, such as [flagd-proxy](https://github.com/open-feature/flagd/flagd-proxy). +in-process flagd provider via a gRPC client connection to a sync server, such as [flagd-proxy](https://github.com/open-feature/flagd/tree/main/flagd-proxy). ## Sync source From 6520137227ab9082bda9ca83c1bad74a58dad2ab Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Tue, 22 Aug 2023 07:38:12 +0200 Subject: [PATCH 09/18] Update docs/other_resources/in-process-providers/specification.md Co-authored-by: Kavindu Dodanduwa Signed-off-by: Florian Bacher --- docs/other_resources/in-process-providers/specification.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/other_resources/in-process-providers/specification.md b/docs/other_resources/in-process-providers/specification.md index 786f55be6..713a3780d 100755 --- a/docs/other_resources/in-process-providers/specification.md +++ b/docs/other_resources/in-process-providers/specification.md @@ -162,7 +162,7 @@ func (p *Provider) BooleanEvaluation( ## Provider lifecycle, initialization and shutdown -With the release of the v0.x.0 spec, OpenFeature now outlines a lifecycle for in-process flagd provider initialization and shutdown. +With the release of the v0.6.0 spec, OpenFeature now outlines a lifecycle for in-process flagd provider initialization and shutdown. In-process flagd providers should do the following to make use of OpenFeature v0.6.0 features: From e7c32cb3e8bf7775196a5421843ae630e3800499 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Tue, 22 Aug 2023 07:42:17 +0200 Subject: [PATCH 10/18] switch to relative links Signed-off-by: Florian Bacher --- docs/other_resources/in-process-providers/specification.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/other_resources/in-process-providers/specification.md b/docs/other_resources/in-process-providers/specification.md index 713a3780d..b602eb1a3 100755 --- a/docs/other_resources/in-process-providers/specification.md +++ b/docs/other_resources/in-process-providers/specification.md @@ -66,7 +66,7 @@ If available, the JsonLogic library for the chosen technology should be used. In addition to the built-in evaluators provided by JsonLogic, the following custom targeting rules should be implemented by the provider: -- [Fractional evaluation](https://github.com/open-feature/flagd/blob/main/docs/configuration/fractional_evaluation.md): +- [Fractional evaluation](../../configuration/fractional_evaluation.md): This evaluator allows to split the returned variants of a feature flag into different buckets, where each bucket can be assigned a percentage, representing how many requests will resolve to the corresponding variant. The sum of all weights must be 100, and the distribution must be performed by using the value of a referenced @@ -78,11 +78,11 @@ the [MurmurHash3](https://github.com/aappleby/smhasher/blob/master/src/MurmurHas This is to ensure that flag resolution requests yield the same result, regardless of which implementation of the in-process flagd provider is being used. For more specific implementation guidelines, please refer to [this document](./evaluators/fractional_evaluation.md). -- [SemVer evaluation](https://github.com/open-feature/flagd/blob/main/docs/configuration/sem_ver_evaluation.md): +- [SemVer evaluation](../../configuration/sem_ver_evaluation.md): This evaluator checks if the given property within the evaluation context matches a semantic versioning condition. It returns 'true', if the value of the given property meets the condition, 'false' if not. For more specific implementation guidelines, please refer to [this document](./evaluators/semver_evaluation.md). -- [StartsWith/EndsWith evaluation](https://github.com/open-feature/flagd/blob/main/docs/configuration/string_comparison_evaluation.md): +- [StartsWith/EndsWith evaluation](../../configuration/string_comparison_evaluation.md): This evaluator selects a variant based on whether the specified property within the evaluation context starts/ends with a certain string. For more specific implementation guidelines, please refer to [this document](./evaluators/string_comparison_evaluation.md). From dadb25d54118d67059cf52a69d79fd89d989a6b2 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Tue, 22 Aug 2023 08:02:42 +0200 Subject: [PATCH 11/18] added paragraph explaining the difference between in process provider and flagd Signed-off-by: Florian Bacher --- .../other_resources/in-process-providers/specification.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/other_resources/in-process-providers/specification.md b/docs/other_resources/in-process-providers/specification.md index b602eb1a3..2e4ccda77 100755 --- a/docs/other_resources/in-process-providers/specification.md +++ b/docs/other_resources/in-process-providers/specification.md @@ -1,5 +1,13 @@ # Creating an in-process flagd provider +By default, **flagd** is a remote service that is accessed via **grpc** by a client application to retrieve feature flags. +Depending on the environment, flagd therefore is usually deployed as a standalone service, e.g. as a Kubernetes Deployment, +or injected as a sidecar container into the pod running the client application, +as it is done in the [OpenFeature Operator](https://github.com/open-feature/open-feature-operator). +An in-process flagd provider, on the other hand, is designed to be embedded into the application and therefore +no communication outside the process of the application is needed. This can be desired by some architectures, +especially if flag retrievals should not take longer than a certain amount of time. + The in-process flagd provider is responsible for creating an abstraction between the [JsonLogic](https://jsonlogic.com) based evaluation of flag configurations following the [flag configuration scheme](https://github.com/open-feature/schemas/blob/main/json/flagd-definitions.json) used by `flagd` and the OpenFeature SDK (for the [chosen technology](https://openfeature.dev/docs/reference/technologies/)). Prerequisites: From 1409f9f6ac149b3c3d2b246ad6c7f7b296545655 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Tue, 22 Aug 2023 08:38:28 +0200 Subject: [PATCH 12/18] fix markdown linting error Signed-off-by: Florian Bacher --- docs/other_resources/in-process-providers/specification.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/other_resources/in-process-providers/specification.md b/docs/other_resources/in-process-providers/specification.md index 2e4ccda77..928606796 100755 --- a/docs/other_resources/in-process-providers/specification.md +++ b/docs/other_resources/in-process-providers/specification.md @@ -97,7 +97,7 @@ For more specific implementation guidelines, please refer to [this document](./e ## Provider construction -(__using Go as an example__) +(**using Go as an example**) Create a provider struct/class/type (whichever is relevant to the chosen language) with an exported (public) constructor allowing configuration (e.g. `flagd` host). Give the provider an un-exported (private) client field, set this field as the client generated by the previous step. From e073ad7c6a203989e431fa958371194c13ea2b7b Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Tue, 22 Aug 2023 15:56:21 +0200 Subject: [PATCH 13/18] Update docs/other_resources/in-process-providers/evaluators/semver_evaluation.md Co-authored-by: Todd Baert Signed-off-by: Florian Bacher --- .../in-process-providers/evaluators/semver_evaluation.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/other_resources/in-process-providers/evaluators/semver_evaluation.md b/docs/other_resources/in-process-providers/evaluators/semver_evaluation.md index cc9960293..e0ee5ceea 100644 --- a/docs/other_resources/in-process-providers/evaluators/semver_evaluation.md +++ b/docs/other_resources/in-process-providers/evaluators/semver_evaluation.md @@ -39,5 +39,6 @@ C -- Yes --> E{Is targetingRule at index 1 a supported operator?}; C -- No --> D; E -- Yes --> F{Is targetingRule at index 2 a semantic version string?}; E -- No --> D; +F -- No --> D; F --> G[Compare the two versions using the operator and return a boolean value indicating if they match]; ``` From ee2e37ec7158a588c1110c882833a39208fe7c72 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Tue, 22 Aug 2023 15:56:34 +0200 Subject: [PATCH 14/18] Update docs/other_resources/in-process-providers/evaluators/string_comparison_evaluation.md Co-authored-by: Todd Baert Signed-off-by: Florian Bacher --- .../evaluators/string_comparison_evaluation.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/other_resources/in-process-providers/evaluators/string_comparison_evaluation.md b/docs/other_resources/in-process-providers/evaluators/string_comparison_evaluation.md index 5bee6e84f..bfa5b9fc0 100644 --- a/docs/other_resources/in-process-providers/evaluators/string_comparison_evaluation.md +++ b/docs/other_resources/in-process-providers/evaluators/string_comparison_evaluation.md @@ -35,5 +35,6 @@ B -- Yes --> C{Is targetingRule at index 0 a string?}; B -- No --> D[Return nil]; C -- Yes --> E{Is targetingRule at index 1 a string?}; C -- No --> D; +E -- No --> D; E --> F[Return a boolean value indicating if the first string starts/ends with the second string]; ``` From 0d08652b88a93c2d49c7f0992f3b456185f1d440 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Wed, 23 Aug 2023 11:33:40 +0200 Subject: [PATCH 15/18] adapted the fractional evaluation spec Signed-off-by: Florian Bacher --- .../evaluators/fractional_evaluation.md | 48 ++++++------------- .../evaluators/semver_evaluation.md | 3 +- 2 files changed, 17 insertions(+), 34 deletions(-) diff --git a/docs/other_resources/in-process-providers/evaluators/fractional_evaluation.md b/docs/other_resources/in-process-providers/evaluators/fractional_evaluation.md index de5010322..0b6366692 100644 --- a/docs/other_resources/in-process-providers/evaluators/fractional_evaluation.md +++ b/docs/other_resources/in-process-providers/evaluators/fractional_evaluation.md @@ -32,7 +32,7 @@ JSON object. Below is an example for a targetingRule containing a `fractionalEva "state": "ENABLED", "targeting": { "fractionalEvaluation": [ - "email", + {"var":"email"}, [ "red", 50 @@ -52,6 +52,10 @@ JSON object. Below is an example for a targetingRule containing a `fractionalEva } ``` +Please note that the implementation of this evaluator can assume that instead of `{"var": "email"}`, it will receive +the resolved value of that referenced property, as resolving the value will be taken care of by JsonLogic before +applying the evaluator. + The following flow chart depicts the logic of this evaluator: ```mermaid @@ -59,18 +63,15 @@ flowchart TD A[Parse targetingRule] --> B{Is an array containing at least two items?}; B -- Yes --> C{Is targetingRule at index 0 a string?}; B -- No --> D[Return nil]; -C -- Yes --> E[targetProperty := targetingRule at index 0]; +C -- Yes --> E[targetPropertyValue := targetingRule at index 0]; C -- No --> D; -E --> F[targetPropertyValue := evaluationContext at key 'targetProperty', empty string otherwise] -F --> G{Is targetPropertyValue a string?}; +E -- Yes --> F[Iterate through the remaining elements of the targetingRule array and parse the variants and their percentages]; +F --> G{Parsing successful?}; G -- No --> D; -G -- Yes --> H[Iterate through the remaining elements of the targetingRule array and parse the variants and their percentages]; -H --> I{Parsing successful?}; -I -- No --> D; -I -- Yes --> J{Does percentage of variants add up to 100?}; -J -- No --> D; -J -- Yes --> K[hash := murmur3Hash of targetPropertyValue divided by Int64.MaxValue] -K --> L[Iterate through the variant and increment the threshold by the percentage of each variant. Return the first variant where the bucket is smaller than the threshold.] +G -- Yes --> H{Does percentage of variants add up to 100?}; +H -- No --> D; +H -- Yes --> I[hash := murmur3Hash of targetPropertyValue divided by Int64.MaxValue] +I --> L[Iterate through the variant and increment the threshold by the percentage of each variant. Return the first variant where the bucket is smaller than the threshold.] ``` As a reference, below is a simplified version of the actual implementation of this evaluator in Go. @@ -85,7 +86,7 @@ type fractionalEvaluationDistribution struct { /* values: contains the targeting rule object; e.g.: [ - "email", + {"var":"email"}, [ "red", 50 @@ -117,32 +118,13 @@ func FractionalEvaluation(values, data interface{}) interface{} { return nil } - // 2. Get the target property used for bucketing the values and retrieve its value from the evaluation context: - bucketBy, ok := valuesArray[0].(string) + // 2. Get the target property value used for bucketing the values + valueToDistribute, ok := valuesArray[0].(string) if !ok { log.Error("first element of fractional evaluation data isn't of type string") return nil } - dataMap, ok := data.(map[string]interface{}) - if !ok { - log.Error("data isn't of type map[string]interface{}") - return nil - } - - v, ok := dataMap[bucketBy] - if !ok { - // if the target property is not part of the evaluation context, use an empty string for calculating the hash - v = "" - } - - valueToDistribute, ok := v.(string) - if !ok { - // the target property must have a string value - log.Error("var: %s isn't of type string", bucketBy) - return nil - } - // 3. Parse the fractionalEvaluation values distribution sumOfPercentages := 0 var feDistributions []fractionalEvaluationDistribution diff --git a/docs/other_resources/in-process-providers/evaluators/semver_evaluation.md b/docs/other_resources/in-process-providers/evaluators/semver_evaluation.md index e0ee5ceea..aa86b947c 100644 --- a/docs/other_resources/in-process-providers/evaluators/semver_evaluation.md +++ b/docs/other_resources/in-process-providers/evaluators/semver_evaluation.md @@ -9,7 +9,8 @@ The 'sem_ver' evaluation rule contains exactly three items: 1. Target property value: the resolved value of the target property referenced in the targeting rule 2. Operator: One of the following: `=`, `!=`, `>`, `<`, `>=`, `<=`, `~` (match minor version), `^` (match major version) -3. Target value: this needs to resolve to a semantic versioning string +3. Target value: this needs to resolve to a semantic versioning string. If this condition is not met, the evaluator should +log an appropriate error message and return `nil` The `sem_ver` evaluation returns a boolean, indicating whether the condition has been met. From 8fd72ccb83161344a082e549e95fef62766b7029 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Thu, 24 Aug 2023 08:47:06 +0200 Subject: [PATCH 16/18] Update docs/other_resources/in-process-providers/evaluators/fractional_evaluation.md Co-authored-by: Todd Baert Signed-off-by: Florian Bacher --- .../in-process-providers/evaluators/fractional_evaluation.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/other_resources/in-process-providers/evaluators/fractional_evaluation.md b/docs/other_resources/in-process-providers/evaluators/fractional_evaluation.md index 0b6366692..62c920f5a 100644 --- a/docs/other_resources/in-process-providers/evaluators/fractional_evaluation.md +++ b/docs/other_resources/in-process-providers/evaluators/fractional_evaluation.md @@ -12,9 +12,8 @@ regardless of which implementation of the in-process flagd provider is being use The implementation of this evaluator should accept the object containing the `fractionalEvaluation` evaluator configuration, and a `data` object containing the evaluation context. The evaluator configuration should be an -array containing at least two items, with the first item being a `string` -value representing the target property to base the distribution of values on, and the remaining items -being `arrays` with two values, with the first being `string` item representing the name of the variant, and the +array containing at least two items, with the first item being an optional [json logic variable declaration](https://jsonlogic.com/operations.html#var) +specifying the target property to base the distribution of values on. If not supplied, a concatination of the `flagKey` and `targetingKey` are used: `{"cat": [{"var":"$flagd.flag_key"}, {"var":"user.email"}]}`. The remaining items are `arrays`, each with two values, with the first being `string` item representing the name of the variant, and the second being a `float` item representing the percentage for that variant. The percentages of all items must add up to 100.0, otherwise unexpected behavior can occur during the evaluation. The `data` object can be an arbitrary JSON object. Below is an example for a targetingRule containing a `fractionalEvaluation`: From e317cda705a983ada089823135a4b75f06d29029 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Thu, 24 Aug 2023 08:47:38 +0200 Subject: [PATCH 17/18] Update docs/other_resources/in-process-providers/specification.md Co-authored-by: Kavindu Dodanduwa Signed-off-by: Florian Bacher --- docs/other_resources/in-process-providers/specification.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/other_resources/in-process-providers/specification.md b/docs/other_resources/in-process-providers/specification.md index 928606796..f9f62a0b7 100755 --- a/docs/other_resources/in-process-providers/specification.md +++ b/docs/other_resources/in-process-providers/specification.md @@ -5,7 +5,7 @@ Depending on the environment, flagd therefore is usually deployed as a standalon or injected as a sidecar container into the pod running the client application, as it is done in the [OpenFeature Operator](https://github.com/open-feature/open-feature-operator). An in-process flagd provider, on the other hand, is designed to be embedded into the application and therefore -no communication outside the process of the application is needed. This can be desired by some architectures, +no communication outside the process of the application for feature flag evaluation is needed. This can be desired by some architectures, especially if flag retrievals should not take longer than a certain amount of time. The in-process flagd provider is responsible for creating an abstraction between the [JsonLogic](https://jsonlogic.com) based evaluation of flag configurations following the [flag configuration scheme](https://github.com/open-feature/schemas/blob/main/json/flagd-definitions.json) used by `flagd` and the OpenFeature SDK (for the [chosen technology](https://openfeature.dev/docs/reference/technologies/)). From 2eeb575724b1dbe006dee09a98adae565df4a3af Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Thu, 24 Aug 2023 08:51:18 +0200 Subject: [PATCH 18/18] adapted to pr review comments Signed-off-by: Florian Bacher --- .../evaluators/fractional_evaluation.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/other_resources/in-process-providers/evaluators/fractional_evaluation.md b/docs/other_resources/in-process-providers/evaluators/fractional_evaluation.md index 62c920f5a..20cc1ce93 100644 --- a/docs/other_resources/in-process-providers/evaluators/fractional_evaluation.md +++ b/docs/other_resources/in-process-providers/evaluators/fractional_evaluation.md @@ -10,10 +10,10 @@ referenced evaluation context property, the [MurmurHash3](https://github.com/aap hash function should be used. This is to ensure that flag resolution requests yield the same result, regardless of which implementation of the in-process flagd provider is being used. -The implementation of this evaluator should accept the object containing the `fractionalEvaluation` evaluator -configuration, and a `data` object containing the evaluation context. The evaluator configuration should be an array containing at least two items, with the first item being an optional [json logic variable declaration](https://jsonlogic.com/operations.html#var) -specifying the target property to base the distribution of values on. If not supplied, a concatination of the `flagKey` and `targetingKey` are used: `{"cat": [{"var":"$flagd.flag_key"}, {"var":"user.email"}]}`. The remaining items are `arrays`, each with two values, with the first being `string` item representing the name of the variant, and the +specifying the target property to base the distribution of values on. If not supplied, a concatination of the +`flagKey` and `targetingKey` are used: `{"cat": [{"var":"$flagd.flag_key"}, {"var":"user.email"}]}`. +The remaining items are `arrays`, each with two values, with the first being `string` item representing the name of the variant, and the second being a `float` item representing the percentage for that variant. The percentages of all items must add up to 100.0, otherwise unexpected behavior can occur during the evaluation. The `data` object can be an arbitrary JSON object. Below is an example for a targetingRule containing a `fractionalEvaluation`: @@ -168,7 +168,7 @@ func FractionalEvaluation(values, data interface{}) interface{} { } // 4. Calculate the hash of the target property and map it to a number between [0, 99] - hashValue := murmur2.HashString(value) + hashValue := murmur3.HashString(value) // divide the hash value by the largest possible value, integer 2^64 hashRatio := float64(hashValue) / math.Pow(2, 64)