├── .github ├── component_owners.yml └── workflows │ ├── ci.yaml │ ├── component-owners.yml │ ├── lint-pr.yaml │ └── release-please.yaml ├── .gitignore ├── .gitmodules ├── .golangci.yaml ├── .release-please-manifest.json ├── CODEOWNERS ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── hooks ├── README.md ├── open-telemetry │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── pkg │ │ ├── common_test.go │ │ ├── metrics.go │ │ ├── metrics_test.go │ │ ├── traces.go │ │ └── traces_test.go └── validator │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── pkg │ ├── regex │ ├── hex.go │ ├── hex_test.go │ └── regex.go │ └── validator │ └── validator.go ├── providers ├── README.md ├── configcat │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── pkg │ │ ├── provider.go │ │ └── provider_test.go ├── flagd-in-process │ └── README.md ├── flagd │ ├── CHANGELOG.md │ ├── LICENSE │ ├── Makefile │ ├── README.md │ ├── e2e │ │ ├── config_test.go │ │ ├── containers │ │ │ └── flagd.go │ │ ├── evaluation_test.go │ │ ├── json_evalutor_test.go │ │ └── utils.go │ ├── go.mod │ ├── go.sum │ ├── internal │ │ ├── cache │ │ │ ├── cache.go │ │ │ └── in_memory.go │ │ ├── logger │ │ │ └── logger.go │ │ └── mock │ │ │ └── service_mock.go │ └── pkg │ │ ├── configuration.go │ │ ├── configuration_test.go │ │ ├── iservice.go │ │ ├── provider.go │ │ ├── provider_test.go │ │ └── service │ │ ├── in_process │ │ ├── do_nothing_custom_sync_provider.go │ │ ├── service.go │ │ ├── service_custom_sync_provider_test.go │ │ ├── service_evaluation_test.go │ │ ├── service_file_test.go │ │ ├── service_grpc_test.go │ │ └── zap.go │ │ └── rpc │ │ ├── mock_client_test.go │ │ ├── retryCounter.go │ │ ├── service.go │ │ ├── service_evaluation_test.go │ │ └── service_eventing_test.go ├── flagsmith │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── pkg │ │ ├── provider.go │ │ └── provider_test.go ├── flipt │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── pkg │ │ ├── provider │ │ ├── doc.go │ │ ├── example_test.go │ │ ├── provider.go │ │ ├── provider_support.go │ │ └── provider_test.go │ │ └── service │ │ ├── client.go │ │ ├── doc.go │ │ ├── service_support.go │ │ └── transport │ │ ├── service.go │ │ └── service_test.go ├── from-env │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── pkg │ │ ├── env.go │ │ ├── eval.go │ │ ├── provider.go │ │ └── provider_test.go ├── gcp │ └── README.md ├── go-feature-flag-in-process │ ├── CHANGELOG.md │ ├── LICENCE │ ├── README.md │ ├── go.mod │ ├── go.sum │ ├── pkg │ │ ├── eval_request.go │ │ ├── generic_evaluation_detail.go │ │ ├── json_type.go │ │ ├── provider.go │ │ ├── provider_module_test.go │ │ └── provider_options.go │ └── testutils │ │ └── module │ │ └── flags.yaml ├── go-feature-flag │ ├── .gitignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── go.mod │ ├── go.sum │ ├── pkg │ │ ├── controller │ │ │ ├── cache.go │ │ │ ├── cache_test.go │ │ │ ├── configuration_change_status.go │ │ │ ├── data_collector_manager.go │ │ │ ├── data_collector_manager_test.go │ │ │ ├── goff_api.go │ │ │ ├── goff_api_test.go │ │ │ ├── http_client.go │ │ │ └── http_constants.go │ │ ├── goff_error │ │ │ └── invalid_option.go │ │ ├── hook │ │ │ ├── data_collector_hook.go │ │ │ └── evaluation_enrichment_hook.go │ │ ├── model │ │ │ ├── data_collector_request.go │ │ │ ├── feature_event.go │ │ │ └── feature_event_test.go │ │ ├── provider.go │ │ ├── provider_options.go │ │ ├── provider_test.go │ │ └── util │ │ │ └── targeting_key_validator.go │ └── testutils │ │ ├── mock_responses │ │ ├── bool_targeting_match.json │ │ ├── disabled_bool.json │ │ ├── disabled_float.json │ │ ├── disabled_int.json │ │ ├── disabled_string.json │ │ ├── double_key.json │ │ ├── flag_not_found.json │ │ ├── integer_key.json │ │ ├── invalid_json_body.json │ │ ├── list_key.json │ │ ├── object_key.json │ │ ├── string_key.json │ │ └── unknown_reason.json │ │ └── module │ │ └── flags.yaml ├── harness │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── pkg │ │ ├── provider.go │ │ ├── provider_config.go │ │ └── provider_test.go ├── launchdarkly │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── example_test.go │ ├── go.mod │ ├── go.sum │ └── pkg │ │ ├── logger.go │ │ ├── provider.go │ │ ├── provider_test.go │ │ └── testdata │ │ └── flags.json ├── multi-provider │ ├── CHANGELOG.md │ ├── Makefile │ ├── README.md │ ├── go.mod │ ├── go.sum │ ├── internal │ │ └── mocks │ │ │ └── provider_mock.go │ └── pkg │ │ ├── errors │ │ └── aggregate-errors.go │ │ ├── options.go │ │ ├── providers.go │ │ ├── providers_test.go │ │ └── strategies │ │ ├── comparison.go │ │ ├── comparison_test.go │ │ ├── first_match.go │ │ ├── first_match_test.go │ │ ├── first_success.go │ │ ├── first_success_test.go │ │ ├── strategies.go │ │ └── strategy_mock.go ├── ofrep │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── evaluate.go │ ├── go.mod │ ├── go.sum │ ├── internal │ │ ├── evaluate │ │ │ ├── flags.go │ │ │ ├── flags_test.go │ │ │ ├── resolver.go │ │ │ └── resolver_test.go │ │ └── outbound │ │ │ ├── http.go │ │ │ └── http_test.go │ ├── provider.go │ └── provider_test.go ├── prefab │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── go.mod │ ├── go.sum │ ├── internal │ │ └── util.go │ └── pkg │ │ ├── enabled.yaml │ │ ├── provider.go │ │ ├── provider_config.go │ │ └── provider_test.go ├── statsig │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── pkg │ │ ├── download_config_specs.json │ │ ├── provider.go │ │ ├── provider_config.go │ │ └── provider_test.go └── unleash │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── pkg │ ├── demo_app_toggles.json │ ├── provider.go │ ├── provider_config.go │ └── provider_test.go ├── release-please-config.json ├── renovate.json └── tests ├── README.md └── flagd ├── CHANGELOG.md ├── README.md ├── go.mod ├── go.sum └── pkg └── integration ├── config.go ├── evaluation.go ├── flagd_json_evaluator.go └── integration.go /.github/component_owners.yml: -------------------------------------------------------------------------------- 1 | # Keep all in alphabetical order 2 | components: 3 | hooks/open-telemetry: 4 | - Kavindu-Dodan 5 | hooks/validator: 6 | - Kavindu-Dodan 7 | - toddbaert 8 | providers/configcat: 9 | - rcrowe 10 | - z4kn4fein 11 | providers/flagd: 12 | - bacherfl 13 | - Kavindu-Dodan 14 | - toddbaert 15 | providers/flagsmith: 16 | - gagantrivedi 17 | - matthewelwell 18 | providers/flipt: 19 | - markphelps 20 | providers/from-env: 21 | - Kavindu-Dodan 22 | - toddbaert 23 | providers/gcp: 24 | - cupofcat 25 | - tangenti 26 | providers/go-feature-flag: 27 | - thomaspoignant 28 | providers/launchdarkly: 29 | - c4milo 30 | - kinyoklion 31 | providers/unleash: 32 | - liran2000 33 | - sighphyre 34 | providers/harness: 35 | - liran2000 36 | - davejohnston 37 | providers/statsig: 38 | - liran2000 39 | providers/prefab: 40 | - liran2000 41 | - semanticart 42 | 43 | ignored-authors: 44 | - renovate-bot 45 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - feature/workflows 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | env: 13 | GO_VERSION: '1.21' 14 | 15 | jobs: 16 | lint: 17 | runs-on: ubuntu-latest 18 | env: 19 | GOPATH: /home/runner/work/open-feature/go-sdk-contrib 20 | GOBIN: /home/runner/work/open-feature/go-sdk-contrib/bin 21 | steps: 22 | - name: Install Go 23 | uses: actions/setup-go@19bb51245e9c80abacb2e91cc42b33fa478b8639 # v4 24 | with: 25 | go-version: ${{ env.GO_VERSION }} 26 | - name: Checkout repository 27 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 28 | - name: Setup Environment 29 | run: | 30 | echo "GOPATH=$(go env GOPATH)" >> $GITHUB_ENV 31 | echo "$(go env GOPATH)/bin" >> $GITHUB_PATH 32 | - name: Module cache 33 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 34 | env: 35 | cache-name: go-mod-cache 36 | with: 37 | path: ~/go/pkg/mod 38 | key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/go.sum') }} 39 | - name: Run workspace init 40 | run: make workspace-init 41 | - name: Run linter 42 | run: make lint 43 | 44 | test: 45 | runs-on: ubuntu-latest 46 | steps: 47 | - name: Install Go 48 | uses: actions/setup-go@19bb51245e9c80abacb2e91cc42b33fa478b8639 # v4 49 | with: 50 | go-version: ${{ env.GO_VERSION }} 51 | - name: Checkout repository 52 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 53 | with: 54 | submodules: recursive 55 | - name: Setup Environment 56 | run: | 57 | echo "GOPATH=$(go env GOPATH)" >> $GITHUB_ENV 58 | echo "$(go env GOPATH)/bin" >> $GITHUB_PATH 59 | - name: Module cache 60 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 61 | env: 62 | cache-name: go-mod-cache 63 | with: 64 | path: ~/go/pkg/mod 65 | key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/go.sum') }} 66 | - name: Run workspace init 67 | run: make workspace-init 68 | - name: Run tests, including e2e 69 | run: make e2e 70 | -------------------------------------------------------------------------------- /.github/workflows/component-owners.yml: -------------------------------------------------------------------------------- 1 | name: 'Component Owners' 2 | on: 3 | pull_request_target: 4 | 5 | permissions: 6 | contents: read # to read changed files 7 | issues: write # to read/write issue assignees 8 | pull-requests: write # to read/write PR reviewers 9 | 10 | jobs: 11 | run_self: 12 | runs-on: ubuntu-latest 13 | name: Auto Assign Owners 14 | steps: 15 | - uses: dyladan/component-owners@58bd86e9814d23f1525d0a970682cead459fa783 16 | with: 17 | config-file: .github/component_owners.yml 18 | repo-token: ${{ secrets.GITHUB_TOKEN }} 19 | -------------------------------------------------------------------------------- /.github/workflows/lint-pr.yaml: -------------------------------------------------------------------------------- 1 | name: "Lint PR" 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | main: 12 | name: Validate PR title 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5 16 | id: lint_pr_title 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | 20 | - uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2 21 | # When the previous steps fails, the workflow would stop. By adding this 22 | # condition you can continue the execution with the populated error message. 23 | if: always() && (steps.lint_pr_title.outputs.error_message != null) 24 | with: 25 | header: pr-title-lint-error 26 | message: | 27 | Hey there and thank you for opening this pull request! 👋🏼 28 | 29 | We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted. 30 | 31 | Details: 32 | ``` 33 | ${{ steps.lint_pr_title.outputs.error_message }} 34 | ``` 35 | # Delete a previous comment when the issue has been resolved 36 | - if: ${{ steps.lint_pr_title.outputs.error_message == null }} 37 | uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2 38 | with: 39 | header: pr-title-lint-error 40 | delete: true -------------------------------------------------------------------------------- /.github/workflows/release-please.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | env: 6 | GO_VERSION: '1.21' 7 | name: Run Release Please 8 | jobs: 9 | release-please: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: googleapis/release-please-action@db8f2c60ee802b3748b512940dde88eabd7b7e01 # v3 13 | id: release 14 | with: 15 | command: manifest 16 | token: ${{secrets.GITHUB_TOKEN}} 17 | default-branch: main 18 | signoff: "OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com>" 19 | outputs: 20 | all: ${{ toJSON(steps.release.outputs) }} 21 | releases_created: ${{ steps.release.outputs.releases_created }} 22 | paths_released: ${{ steps.release.outputs.paths_released }} 23 | artifacts: 24 | needs: release-please 25 | runs-on: ubuntu-latest 26 | if: ${{ needs.release-please.outputs.releases_created }} 27 | strategy: 28 | matrix: 29 | release: ${{ fromJSON(needs.release-please.outputs.paths_released) }} 30 | env: 31 | TAG: ${{ fromJSON(needs.release-please.outputs.all)[format('{0}--tag_name', matrix.release)] }} 32 | steps: 33 | - name: Checkout repository 34 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 35 | - name: Set up Go 36 | uses: actions/setup-go@19bb51245e9c80abacb2e91cc42b33fa478b8639 # v4 37 | with: 38 | go-version: ${{ env.GO_VERSION }} 39 | # Create SBOM 40 | - name: Generate SBOM 41 | uses: CycloneDX/gh-gomod-generate-sbom@efc74245d6802c8cefd925620515442756c70d8f # v2 42 | with: 43 | version: v1 44 | args: mod -licenses -json -output bom.json ${{ matrix.release }} 45 | # Bundle extra assets to release 46 | - name: Bundle release assets 47 | uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1 48 | with: 49 | tag_name: ${{ env.TAG }} 50 | files: | 51 | bom.json 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | go.work 18 | go.work.sum 19 | 20 | .idea -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "providers/flagd/spec"] 2 | path = providers/flagd/spec 3 | url = https://github.com/open-feature/spec 4 | [submodule "providers/flagd/flagd-testbed"] 5 | path = providers/flagd/flagd-testbed 6 | url = https://github.com/open-feature/flagd-testbed.git 7 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 3m 3 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks/open-telemetry": "0.3.4", 3 | "hooks/validator": "0.1.6", 4 | "providers/configcat": "0.2.2", 5 | "providers/flagd": "0.2.6", 6 | "providers/flipt": "0.1.3", 7 | "providers/from-env": "0.1.5", 8 | "providers/go-feature-flag": "0.2.5", 9 | "providers/gcp": "0.0.1", 10 | "providers/flagsmith": "0.1.4", 11 | "providers/launchdarkly": "0.1.5", 12 | "providers/unleash": "0.1.0-alpha", 13 | "providers/harness": "0.0.4-alpha", 14 | "providers/statsig": "0.0.3", 15 | "providers/ofrep": "0.1.5", 16 | "providers/prefab": "0.0.2", 17 | "tests/flagd": "1.4.1", 18 | "providers/go-feature-flag-in-process": "0.1.0", 19 | "providers/multi-provider": "0.0.4" 20 | } 21 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in 2 | # the repo. Unless a later match takes precedence 3 | # 4 | # Managed by Peribolos: https://github.com/open-feature/community/blob/main/config/open-feature/sdk-golang/workgroup.yaml 5 | # 6 | * @open-feature/sdk-golang-maintainers @open-feature/maintainers 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## System Requirements 4 | 5 | go 1.19+ is recommended. 6 | 7 | ## Setup workspace 8 | 9 | Run the following command to set up the go workspace: 10 | 11 | ```shell 12 | make workspace-init 13 | ``` 14 | 15 | To sync `go.work` with the current modules, run: 16 | 17 | ```shell 18 | make workspace-update 19 | ``` 20 | 21 | ## Adding a module 22 | 23 | The project provides `makefile` targets for creating [hooks](https://openfeature.dev/docs/reference/concepts/hooks) and [providers](https://openfeature.dev/docs/reference/concepts/provider). 24 | To contribute a new hook or provider, fork this repository and create a new go module, it will then be discoverable by `make workspace-init` and `make workspace-update`. 25 | 26 | To automatically create and set up a new provider directory, use the following command (requires [jq](https://jqlang.github.io/jq/)): 27 | 28 | ```shell 29 | make MODULE_NAME=NAME new-provider 30 | ``` 31 | 32 | To automatically create and set up a new hook directory, use the following command (requires [jq](https://jqlang.github.io/jq/)): 33 | 34 | ```shell 35 | make MODULE_NAME=NAME new-hook 36 | ``` 37 | 38 | Note - [jq documentation](https://stedolan.github.io/jq/download/) 39 | 40 | ### Versioning 41 | 42 | The release version of the newly added module(hook/provider) is controlled by `.release-please-manifest.json`. 43 | You can control the versioning of your module by adding an entry with desired initial version(ex:`"provider/acme":"0.0.1"`). 44 | Otherwise, default versioning will start from `1.0.0`. 45 | 46 | ## Documentation 47 | 48 | Any published modules must have documentation in their root directory, explaining the basic purpose of the module as well as installation and usage instructions. 49 | Instructions for how to develop a module should also be included (required system dependencies, instructions for testing locally, etc). 50 | 51 | ## Testing 52 | 53 | Any published modules must have reasonable test coverage. 54 | 55 | To run tests in all existing go modules, use the command: 56 | 57 | ```shell 58 | make test 59 | ``` 60 | 61 | It is recommended to include end-to-end (e2e) tests in your provider when possible. 62 | If you have dependency services for your e2e tests, make sure to add them as service in the build pipeline. 63 | 64 | You can run all tests, including e2e tests using the command: 65 | 66 | ```shell 67 | make e2e 68 | ``` 69 | 70 | ## Releases 71 | 72 | This repo uses _Release Please_ to release packages. Release Please sets up a running PR that tracks all changes for the library components, and maintains the versions according to [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/), generated when [PRs are merged](https://github.com/amannn/action-semantic-pull-request). 73 | Merging the Release Please PR will create a GitHub release with updated library versions. 74 | 75 | ## Dependencies 76 | 77 | The [GO-SDK](https://github.com/open-feature/go-sdk) should be a _peer dependency_ of your module. 78 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ALL_GO_MOD_DIRS := $(shell find . -type f -name 'go.mod' -exec dirname {} \; | sort) 2 | MODULE_TYPE ?= providers 3 | FLAGD_TESTBED = flagd-testbed 4 | FLAGD_SYNC = sync-testbed 5 | 6 | workspace-init: 7 | go work init 8 | $(foreach module, $(ALL_GO_MOD_DIRS), go work use $(module);) 9 | 10 | workspace-update: 11 | $(foreach module, $(ALL_GO_MOD_DIRS), go work use $(module);) 12 | 13 | test: 14 | go list -f '{{.Dir}}/...' -m | xargs -I{} go test -v {} 15 | 16 | # call with TESTCONTAINERS_RYUK_DISABLED="true" to avoid problems with podman on Macs 17 | e2e: 18 | go clean -testcache && go list -f '{{.Dir}}/...' -m | xargs -I{} go test -tags=e2e {} 19 | 20 | lint: 21 | go install -v github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.5 22 | $(foreach module, $(ALL_GO_MOD_DIRS), ${GOPATH}/bin/golangci-lint run $(module)/...;) 23 | 24 | new-provider: 25 | mkdir ./providers/$(MODULE_NAME) 26 | cd ./providers/$(MODULE_NAME) && go mod init github.com/open-feature/go-sdk-contrib/providers/$(MODULE_NAME) && touch README.md 27 | $(MAKE) append-to-release-please MODULE_TYPE=providers MODULE_NAME=$(MODULE_NAME) 28 | 29 | new-hook: 30 | mkdir ./hooks/$(MODULE_NAME) 31 | cd ./hooks/$(MODULE_NAME) && go mod init github.com/open-feature/go-sdk-contrib/hooks/$(MODULE_NAME) && touch README.md 32 | $(MAKE) append-to-release-please MODULE_TYPE=hooks MODULE_NAME=$(MODULE_NAME) 33 | 34 | append-to-release-please: 35 | jq '.packages += {"${MODULE_TYPE}/${MODULE_NAME}": {"release-type":"go","package-name":"${MODULE_TYPE}/${MODULE_NAME}","bump-minor-pre-major":true,"bump-patch-for-minor-pre-major":true,"versioning":"default","extra-files": []}}' release-please-config.json > tmp.json 36 | mv tmp.json release-please-config.json 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenFeature Go Contributions 2 | 3 | This repository is intended for OpenFeature contributions which are not included in the [OpenFeature SDK](https://github.com/open-feature/go-sdk). 4 | 5 | The project includes: 6 | 7 | - [Providers](./providers) 8 | - [Hooks](./hooks) 9 | - [Tests](./tests) 10 | 11 | 12 | ## Contributing 13 | 14 | Interested in contributing? Great, we'd love your help! To get started, take a look at the [CONTRIBUTING](CONTRIBUTING.md) guide. 15 | 16 | ## Useful links 17 | 18 | * For more information on OpenFeature, visit [openfeature.dev](https://openfeature.dev) 19 | * For help or feedback on this project, join us on [Slack](https://cloud-native.slack.com/archives/C0344AANLA1) or create a [GitHub issue](https://github.com/open-feature/js-sdk-contrib/issues/new/choose). 20 | 21 | ## License 22 | 23 | Apache 2.0 - See [LICENSE](./LICENSE) for more information. 24 | -------------------------------------------------------------------------------- /hooks/README.md: -------------------------------------------------------------------------------- 1 | # OpenFeature Go Hooks 2 | 3 | Hooks are a mechanism whereby application developers can add arbitrary behavior to flag evaluation. They operate similarly to middleware in many web frameworks. Please see the [spec](https://github.com/open-feature/spec/blob/main/specification/sections/04-hooks.md) for more details. 4 | 5 | To contibute a new hook, fork this repository and create a new go module, it will then be discoverable by `make workspace-init` and `make workspace-update`. -------------------------------------------------------------------------------- /hooks/open-telemetry/README.md: -------------------------------------------------------------------------------- 1 | # OpenTelemetry Hook 2 | 3 | ## Requirements 4 | 5 | - open-feature/go-sdk >= v1.3.0 6 | 7 | ## Usage 8 | 9 | ## Metrics hook 10 | 11 | This hook performs metric collection by tapping into various hook stages. Given below are the metrics are extracted by this hook, 12 | 13 | - `feature_flag.evaluation_requests_total` 14 | - `feature_flag.evaluation_success_total` 15 | - `feature_flag.evaluation_error_total` 16 | - `feature_flag.evaluation_active_count` 17 | 18 | There are two ways to create hooks: 19 | 20 | ### Using Global MeterProvider 21 | 22 | Global provider should be set somewhere using `otel.SetMeterProvider` before calling this constructor. 23 | 24 | ```go 25 | // Derive metric hook from reader 26 | metricsHook, err := hooks.NewMetricsHook() 27 | if err != nil { 28 | return err 29 | } 30 | 31 | // Register OpenFeature API level hooks 32 | openfeature.AddHooks(metricsHook) 33 | ``` 34 | 35 | ### Passing MeterProvider to Constructor 36 | 37 | ```go 38 | // provider must be configured and provided to constructor based on application configurations 39 | var provider *metric.MeterProvider 40 | 41 | // Derive metric hook from reader 42 | metricsHook, err := hooks.NewMetricsHookForProvider(provider) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | // Register OpenFeature API level hooks 48 | openfeature.AddHooks(metricsHook) 49 | ``` 50 | 51 | ### Options 52 | 53 | #### WithMetricsAttributeSetter 54 | 55 | This constructor options allows to provide a custom callback to extract dimensions from `FlagMetadata`. 56 | These attributes are added at the `After` stage of the hook. 57 | 58 | ```go 59 | 60 | NewMetricsHookForProvider(provider, 61 | WithMetricsAttributeSetter( 62 | func(metadata openfeature.FlagMetadata) []attribute.KeyValue { 63 | // custom attribute extraction logic 64 | 65 | return attributes 66 | })) 67 | ``` 68 | 69 | #### WithFlagMetadataDimensions 70 | 71 | This constructor option allows to configure dimension descriptions to be extracted from `openfeature.FlagMetadata`. 72 | If present, these dimension will be added to the `feature_flag.evaluation_success_total` metric. 73 | Missing metadata keys will be ignored by the implementation. 74 | 75 | ```go 76 | NewMetricsHook(MeterProvider, 77 | WithFlagMetadataDimensions( 78 | DimensionDescription{ 79 | Key: "scope", 80 | Type: String, 81 | })) 82 | ``` 83 | 84 | ## Traces hook 85 | 86 | The traces hook taps into the after and error methods of the hook lifecycle to write `events` and `attributes`to an existing `span`. 87 | A `context.Context` containing a `span` must be passed to the client evaluation method, otherwise the hook will be no-op. 88 | 89 | ```go 90 | 91 | // Register traces hook 92 | openfeature.AddHooks(hooks.NewTracesHook()) 93 | client := openfeature.NewClient("methodA") 94 | 95 | // Initialize otel span 96 | spanCtx, span := tracer.Start(context.Background(), "myBoolFlag") 97 | client.BooleanValueDetails(spanCtx, "myBoolFlag", false, openfeature.EvaluationContext{}) 98 | 99 | ... 100 | 101 | span.End() 102 | ``` 103 | 104 | ### Options 105 | 106 | #### WithErrorStatusEnabled 107 | 108 | Enable setting span status to `Error` in case of an error. Default behavior is disabled, span status is unset for errors. 109 | 110 | #### WithTracesAttributeSetter 111 | 112 | This constructor options allows to provide a custom callback to extract dimensions from `FlagMetadata`. 113 | These attributes are added at the `After` stage of the hook. 114 | 115 | ```go 116 | 117 | NewTracesHook(WithTracesAttributeSetter( 118 | func(metadata openfeature.FlagMetadata) []attribute.KeyValue { 119 | // custom attribute extraction logic 120 | 121 | return attributes 122 | })) 123 | ``` 124 | -------------------------------------------------------------------------------- /hooks/open-telemetry/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/open-feature/go-sdk-contrib/hooks/open-telemetry 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/open-feature/go-sdk v1.11.0 7 | go.opentelemetry.io/otel v1.28.0 8 | go.opentelemetry.io/otel/metric v1.28.0 9 | go.opentelemetry.io/otel/sdk v1.28.0 10 | go.opentelemetry.io/otel/sdk/metric v1.28.0 11 | go.opentelemetry.io/otel/trace v1.28.0 12 | ) 13 | 14 | require ( 15 | github.com/go-logr/logr v1.4.2 // indirect 16 | github.com/go-logr/stdr v1.2.2 // indirect 17 | github.com/google/uuid v1.6.0 // indirect 18 | golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 // indirect 19 | golang.org/x/sys v0.21.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /hooks/open-telemetry/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 4 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 5 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 6 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 7 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 8 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 9 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 10 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 11 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 12 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 13 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 14 | github.com/open-feature/go-sdk v1.11.0 h1:4cp9rXl16ZvlMCef7O+I3vQSXae8DzAF0SfV9mvYInw= 15 | github.com/open-feature/go-sdk v1.11.0/go.mod h1:+rkJhLBtYsJ5PZNddAgFILhRAAxwrJ32aU7UEUm4zQI= 16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 18 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 19 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 20 | go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= 21 | go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= 22 | go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= 23 | go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= 24 | go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= 25 | go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= 26 | go.opentelemetry.io/otel/sdk/metric v1.28.0 h1:OkuaKgKrgAbYrrY0t92c+cC+2F6hsFNnCQArXCKlg08= 27 | go.opentelemetry.io/otel/sdk/metric v1.28.0/go.mod h1:cWPjykihLAPvXKi4iZc1dpER3Jdq2Z0YLse3moQUCpg= 28 | go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= 29 | go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= 30 | golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 h1:/RIbNt/Zr7rVhIkQhooTxCxFcdWLGIKnZA4IXNFSrvo= 31 | golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= 32 | golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= 33 | golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 34 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 35 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 36 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 37 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 38 | -------------------------------------------------------------------------------- /hooks/open-telemetry/pkg/common_test.go: -------------------------------------------------------------------------------- 1 | package otel 2 | 3 | import ( 4 | "github.com/open-feature/go-sdk/openfeature" 5 | "go.opentelemetry.io/otel/attribute" 6 | ) 7 | 8 | // test commons 9 | 10 | var scopeKey = "scope" 11 | var scopeValue = "7c34165e-fbef-11ed-be56-0242ac120002" 12 | var scopeDescription = DimensionDescription{ 13 | Key: scopeKey, 14 | Type: String, 15 | } 16 | 17 | var stageKey = "stage" 18 | var stageValue = 1 19 | var stageDescription = DimensionDescription{ 20 | Key: stageKey, 21 | Type: Int, 22 | } 23 | 24 | var scoreKey = "score" 25 | var scoreValue = 4.5 26 | var scoreDescription = DimensionDescription{ 27 | Key: scoreKey, 28 | Type: Float, 29 | } 30 | 31 | var cachedKey = "cached" 32 | var cacheValue = false 33 | var cachedDescription = DimensionDescription{ 34 | Key: cachedKey, 35 | Type: Bool, 36 | } 37 | 38 | var evalMetadata = map[string]interface{}{ 39 | scopeKey: scopeValue, 40 | stageKey: stageValue, 41 | scoreKey: scoreValue, 42 | cachedKey: cacheValue, 43 | } 44 | 45 | var extractionCallback = func(metadata openfeature.FlagMetadata) []attribute.KeyValue { 46 | attribs := []attribute.KeyValue{} 47 | 48 | scope, err := metadata.GetString(scopeKey) 49 | if err != nil { 50 | panic(err) 51 | } 52 | 53 | attribs = append(attribs, attribute.String(scopeKey, scope)) 54 | 55 | stage, err := metadata.GetInt(stageKey) 56 | if err != nil { 57 | panic(err) 58 | } 59 | 60 | attribs = append(attribs, attribute.Int64(stageKey, stage)) 61 | 62 | score, err := metadata.GetFloat(scoreKey) 63 | if err != nil { 64 | panic(err) 65 | } 66 | 67 | attribs = append(attribs, attribute.Float64(scoreKey, score)) 68 | 69 | cached, err := metadata.GetBool(cachedKey) 70 | if err != nil { 71 | panic(err) 72 | } 73 | 74 | attribs = append(attribs, attribute.Bool(cachedKey, cached)) 75 | return attribs 76 | } 77 | -------------------------------------------------------------------------------- /hooks/open-telemetry/pkg/traces.go: -------------------------------------------------------------------------------- 1 | package otel 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/open-feature/go-sdk/openfeature" 7 | "go.opentelemetry.io/otel/attribute" 8 | "go.opentelemetry.io/otel/codes" 9 | semconv "go.opentelemetry.io/otel/semconv/v1.18.0" 10 | "go.opentelemetry.io/otel/trace" 11 | ) 12 | 13 | const ( 14 | EventName = "feature_flag" 15 | EventPropertyFlagKey = "feature_flag.key" 16 | EventPropertyProviderName = "feature_flag.provider_name" 17 | EventPropertyVariant = "feature_flag.variant" 18 | ) 19 | 20 | // traceHook is the hook implementation for OTel traces 21 | type traceHook struct { 22 | setErrorStatus bool 23 | attributeMapperCallback func(openfeature.FlagMetadata) []attribute.KeyValue 24 | 25 | openfeature.UnimplementedHook 26 | } 27 | 28 | var _ openfeature.Hook = &traceHook{} 29 | 30 | // NewTracesHook return a reference to a new instance of the OpenTelemetry Hook 31 | func NewTracesHook(opts ...Options) *traceHook { 32 | h := &traceHook{} 33 | 34 | for _, opt := range opts { 35 | opt(h) 36 | } 37 | 38 | return h 39 | } 40 | 41 | // After sets the feature_flag event and associated attributes on the span stored in the context 42 | func (h *traceHook) After(ctx context.Context, hookContext openfeature.HookContext, flagEvaluationDetails openfeature.InterfaceEvaluationDetails, hookHints openfeature.HookHints) error { 43 | attribs := []attribute.KeyValue{ 44 | semconv.FeatureFlagKey(hookContext.FlagKey()), 45 | semconv.FeatureFlagProviderName(hookContext.ProviderMetadata().Name), 46 | } 47 | if flagEvaluationDetails.Variant != "" { 48 | attribs = append(attribs, semconv.FeatureFlagVariant(flagEvaluationDetails.Variant)) 49 | } 50 | 51 | if h.attributeMapperCallback != nil { 52 | attribs = append(attribs, h.attributeMapperCallback(flagEvaluationDetails.FlagMetadata)...) 53 | } 54 | 55 | span := trace.SpanFromContext(ctx) 56 | span.AddEvent(EventName, trace.WithAttributes(attribs...)) 57 | return nil 58 | } 59 | 60 | // Error records the given error against the span and sets the span to an error status 61 | func (h *traceHook) Error(ctx context.Context, hookContext openfeature.HookContext, err error, hookHints openfeature.HookHints) { 62 | span := trace.SpanFromContext(ctx) 63 | 64 | if h.setErrorStatus { 65 | span.SetStatus(codes.Error, 66 | fmt.Sprintf("error evaluating flag '%s' of type '%s'", hookContext.FlagKey(), hookContext.FlagType().String())) 67 | } 68 | 69 | span.RecordError(err, trace.WithAttributes( 70 | semconv.FeatureFlagKey(hookContext.FlagKey()), 71 | semconv.FeatureFlagProviderName(hookContext.ProviderMetadata().Name), 72 | )) 73 | } 74 | 75 | // Options of the hook 76 | 77 | type Options func(*traceHook) 78 | 79 | // WithErrorStatusEnabled enable setting span status to codes.Error in case of an error. Default behavior is disabled 80 | func WithErrorStatusEnabled() Options { 81 | return func(h *traceHook) { 82 | h.setErrorStatus = true 83 | } 84 | } 85 | 86 | // WithTracesAttributeSetter allows to set a extractionCallback which accept openfeature.FlagMetadata and returns 87 | // []attribute.KeyValue derived from those metadata. 88 | // If present, returned attributes will be added to successful evaluation traces 89 | func WithTracesAttributeSetter(callback func(openfeature.FlagMetadata) []attribute.KeyValue) Options { 90 | return func(tracesHook *traceHook) { 91 | tracesHook.attributeMapperCallback = callback 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /hooks/validator/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.1.6](https://github.com/open-feature/go-sdk-contrib/compare/hooks/validator/v0.1.5...hooks/validator/v0.1.6) (2024-02-07) 4 | 5 | 6 | ### 🐛 Bug Fixes 7 | 8 | * **deps:** update module github.com/open-feature/go-sdk to v1.6.0 ([#289](https://github.com/open-feature/go-sdk-contrib/issues/289)) ([13eeb48](https://github.com/open-feature/go-sdk-contrib/commit/13eeb482ee3d69c5fb8100563501c2250b6454f1)) 9 | * **deps:** update module github.com/open-feature/go-sdk to v1.7.0 ([#315](https://github.com/open-feature/go-sdk-contrib/issues/315)) ([3f049ad](https://github.com/open-feature/go-sdk-contrib/commit/3f049ad34e93c3b9b9d4cf5a2e56f3777eb858e6)) 10 | * **deps:** update module github.com/open-feature/go-sdk to v1.8.0 ([#329](https://github.com/open-feature/go-sdk-contrib/issues/329)) ([c99b527](https://github.com/open-feature/go-sdk-contrib/commit/c99b52728bad9dce52bfb78a08ae5f4eea83a397)) 11 | 12 | 13 | ### 🧹 Chore 14 | 15 | * update to go-sdk 1.9.0 ([#404](https://github.com/open-feature/go-sdk-contrib/issues/404)) ([11fa3ab](https://github.com/open-feature/go-sdk-contrib/commit/11fa3aba065a6dd81caca30e76efc16fb64a25e3)) 16 | 17 | ## [0.1.5](https://github.com/open-feature/go-sdk-contrib/compare/hooks/validator/v0.1.4...hooks/validator/v0.1.5) (2023-07-21) 18 | 19 | 20 | ### 🐛 Bug Fixes 21 | 22 | * **deps:** update module github.com/open-feature/go-sdk to v1.5.1 ([#263](https://github.com/open-feature/go-sdk-contrib/issues/263)) ([c75ffd6](https://github.com/open-feature/go-sdk-contrib/commit/c75ffd6017689a86860dec92c1a1564b6145f0c9)) 23 | 24 | ## [0.1.4](https://github.com/open-feature/go-sdk-contrib/compare/hooks/validator/v0.1.3...hooks/validator/v0.1.4) (2023-07-17) 25 | 26 | 27 | ### 🧹 Chore 28 | 29 | * update module github.com/open-feature/go-sdk to v1.4.0 ([#223](https://github.com/open-feature/go-sdk-contrib/issues/223)) ([7c8ea46](https://github.com/open-feature/go-sdk-contrib/commit/7c8ea46e3e094f746dbf6d80ba6a1b606314e8d7)) 30 | 31 | ## [0.1.3](https://github.com/open-feature/go-sdk-contrib/compare/hooks/validator/v0.1.2...hooks/validator/v0.1.3) (2023-03-02) 32 | 33 | 34 | ### Features 35 | 36 | * ⚠️ requires OpenFeature Go SDK v1.3.0 or above ⚠️ absorbed Hook API changes ([#130](https://github.com/open-feature/go-sdk-contrib/issues/130)) ([a65b009](https://github.com/open-feature/go-sdk-contrib/commit/a65b00957a425b89c261a979f81dcfdf2f5a2bcb)) 37 | 38 | ## [0.1.2](https://github.com/open-feature/go-sdk-contrib/compare/hooks/validator/v0.1.1...hooks/validator/v0.1.2) (2023-02-21) 39 | 40 | 41 | ### Bug Fixes 42 | 43 | * **deps:** update module github.com/open-feature/go-sdk to v1.2.0 ([#103](https://github.com/open-feature/go-sdk-contrib/issues/103)) ([eedb577](https://github.com/open-feature/go-sdk-contrib/commit/eedb577745fd98d5189132ebbaa8eb82bdf99dd8)) 44 | 45 | ## [0.1.1](https://github.com/open-feature/go-sdk-contrib/compare/hooks/validator-v0.1.0...hooks/validator/v0.1.1) (2023-01-26) 46 | 47 | 48 | ### Bug Fixes 49 | 50 | * tidy workspaces ([#97](https://github.com/open-feature/go-sdk-contrib/issues/97)) ([c71a5ec](https://github.com/open-feature/go-sdk-contrib/commit/c71a5ec7686ec0572bb47f17dbca7e0ec48252d7)) 51 | -------------------------------------------------------------------------------- /hooks/validator/README.md: -------------------------------------------------------------------------------- 1 | # Validator Hook 2 | 3 | The `validator hook` taps into the `After` lifecycle event to validate the result of flag evaluations. If the result is 4 | invalid, the default value and an error are returned. 5 | 6 | The hook defines a `validator` interface with function 7 | ```go 8 | IsValid(flagEvaluationDetails of.EvaluationDetails) error 9 | ``` 10 | to allow application authors to supply their own validators. 11 | 12 | There are, however, [ready to be used validators](#validators) that conform to the interface. 13 | 14 | ## Setup 15 | 16 | Import the [OpenFeature SDK](https://github.com/open-feature/go-sdk) and the validator. 17 | 18 | Create an instance of the validator hook struct (using the hex validator as an example): 19 | ```go 20 | package main 21 | 22 | import ( 23 | "fmt" 24 | "github.com/open-feature/go-sdk-contrib/hooks/validator/pkg/regex" 25 | "github.com/open-feature/go-sdk-contrib/hooks/validator/pkg/validator" 26 | "github.com/open-feature/go-sdk/openfeature" 27 | "log" 28 | ) 29 | 30 | func main() { 31 | hexValidator, err := regex.Hex() 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | v := validator.Hook{Validator: hexValidator} 36 | } 37 | ``` 38 | 39 | Use the validator hook (on invocation as an example): 40 | ```go 41 | client := openfeature.NewClient("foo") 42 | value, err := client. 43 | StringValueDetails("blue", "#0000FF", openfeature.EvaluationContext{}, openfeature.WithHooks(v)) 44 | if err != nil { 45 | fmt.Println("err:", err) 46 | } 47 | ``` 48 | 49 | ## Example 50 | 51 | Following [setup](#setup), use the `NoopProvider`, this simply returns the given default value on flag evaluation. 52 | Give a false hex color as the default value call and check that the flag evaluation returns an error. 53 | 54 | ```go 55 | package main 56 | 57 | import ( 58 | "fmt" 59 | "github.com/open-feature/go-sdk-contrib/hooks/validator/pkg/regex" 60 | "github.com/open-feature/go-sdk-contrib/hooks/validator/pkg/validator" 61 | "github.com/open-feature/go-sdk/openfeature" 62 | "log" 63 | ) 64 | 65 | func main() { 66 | openfeature.SetProvider(openfeature.NoopProvider{}) 67 | hexValidator, err := regex.Hex() 68 | if err != nil { 69 | log.Fatal(err) 70 | } 71 | v := validator.Hook{Validator: hexValidator} 72 | client := openfeature.NewClient("foo") 73 | 74 | result, err := client. 75 | StringValueDetails("blue", "invalidhex", openfeature.EvaluationContext{}, openfeature.WithHooks(v)) 76 | if err != nil { 77 | fmt.Println("err:", err) 78 | } 79 | 80 | fmt.Println("result:", result) 81 | } 82 | ``` 83 | 84 | ```shell 85 | go run main.go 86 | err: execute after hook: regex doesn't match on flag value 87 | result: {blue 1 {invalidhex }} 88 | ``` 89 | Note that despite getting an error we still get a result. 90 | 91 | ## Validators 92 | 93 | - [Regex](./pkg/regex/regex.go) validates the result matches the given regex 94 | - [Hex](./pkg/regex/hex.go) validates the result is a valid hex color (e.g. #FFFFFF) 95 | 96 | ## License 97 | 98 | Apache 2.0 - See [LICENSE](./../../LICENSE) for more information. 99 | -------------------------------------------------------------------------------- /hooks/validator/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/open-feature/go-sdk-contrib/hooks/validator 2 | 3 | go 1.21 4 | 5 | require github.com/open-feature/go-sdk v1.11.0 6 | 7 | require ( 8 | github.com/go-logr/logr v1.4.1 // indirect 9 | golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /hooks/validator/go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= 2 | github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 3 | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 4 | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 5 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 6 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 7 | github.com/open-feature/go-sdk v1.9.0 h1:1Nyj+XNHfL0rRGZgGCbZ29CHDD57PQJL7Q/2ZbW/E8c= 8 | github.com/open-feature/go-sdk v1.9.0/go.mod h1:n5BM4DfvIiKaWWquZnL/yVihcGM5aLsz7rNYE3BkXAM= 9 | github.com/open-feature/go-sdk v1.10.0 h1:druQtYOrN+gyz3rMsXp0F2jW1oBXJb0V26PVQnUGLbM= 10 | github.com/open-feature/go-sdk v1.10.0/go.mod h1:+rkJhLBtYsJ5PZNddAgFILhRAAxwrJ32aU7UEUm4zQI= 11 | github.com/open-feature/go-sdk v1.11.0 h1:4cp9rXl16ZvlMCef7O+I3vQSXae8DzAF0SfV9mvYInw= 12 | github.com/open-feature/go-sdk v1.11.0/go.mod h1:+rkJhLBtYsJ5PZNddAgFILhRAAxwrJ32aU7UEUm4zQI= 13 | golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb h1:mIKbk8weKhSeLH2GmUTrvx8CjkyJmnU1wFmg59CUjFA= 14 | golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= 15 | golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 h1:/RIbNt/Zr7rVhIkQhooTxCxFcdWLGIKnZA4IXNFSrvo= 16 | golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= 17 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 18 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 19 | -------------------------------------------------------------------------------- /hooks/validator/pkg/regex/hex.go: -------------------------------------------------------------------------------- 1 | package regex 2 | 3 | // Hex returns a Validator that validates that a flag result is a hex color 4 | func Hex() (Validator, error) { 5 | return NewValidator("^#(?:[0-9a-fA-F]{3}){1,2}$") 6 | } 7 | -------------------------------------------------------------------------------- /hooks/validator/pkg/regex/hex_test.go: -------------------------------------------------------------------------------- 1 | package regex_test 2 | 3 | import ( 4 | "github.com/open-feature/go-sdk-contrib/hooks/validator/pkg/regex" 5 | of "github.com/open-feature/go-sdk/openfeature" 6 | "testing" 7 | ) 8 | 9 | func TestValidator_Hex(t *testing.T) { 10 | tests := map[string]struct { 11 | flagEvaluationDetails of.InterfaceEvaluationDetails 12 | expectedErr bool 13 | }{ 14 | "#112233": { 15 | flagEvaluationDetails: of.InterfaceEvaluationDetails{ 16 | Value: "#112233", 17 | }, 18 | expectedErr: false, 19 | }, 20 | "#123": { 21 | flagEvaluationDetails: of.InterfaceEvaluationDetails{ 22 | Value: "#123", 23 | }, 24 | expectedErr: false, 25 | }, 26 | "#000233": { 27 | flagEvaluationDetails: of.InterfaceEvaluationDetails{ 28 | Value: "#000233", 29 | }, 30 | expectedErr: false, 31 | }, 32 | "#023": { 33 | flagEvaluationDetails: of.InterfaceEvaluationDetails{ 34 | Value: "#023", 35 | }, 36 | expectedErr: false, 37 | }, 38 | "invalid": { 39 | flagEvaluationDetails: of.InterfaceEvaluationDetails{ 40 | Value: "invalid", 41 | }, 42 | expectedErr: true, 43 | }, 44 | "#abcd": { 45 | flagEvaluationDetails: of.InterfaceEvaluationDetails{ 46 | Value: "#abcd", 47 | }, 48 | expectedErr: true, 49 | }, 50 | "#-12": { 51 | flagEvaluationDetails: of.InterfaceEvaluationDetails{ 52 | Value: "#-12", 53 | }, 54 | expectedErr: true, 55 | }, 56 | "non string": { 57 | flagEvaluationDetails: of.InterfaceEvaluationDetails{ 58 | Value: 3, 59 | }, 60 | expectedErr: true, 61 | }, 62 | } 63 | 64 | for name, tt := range tests { 65 | t.Run(name, func(t *testing.T) { 66 | validator, err := regex.Hex() 67 | if err != nil { 68 | t.Fatal(err) 69 | } 70 | 71 | err = validator.IsValid(tt.flagEvaluationDetails) 72 | if err != nil { 73 | if !tt.expectedErr { 74 | t.Errorf("didn't expect error, got: %v", err) 75 | } 76 | } else { 77 | if tt.expectedErr { 78 | t.Error("expected error, got nil") 79 | } 80 | } 81 | }) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /hooks/validator/pkg/regex/regex.go: -------------------------------------------------------------------------------- 1 | package regex 2 | 3 | import ( 4 | "errors" 5 | of "github.com/open-feature/go-sdk/openfeature" 6 | "regexp" 7 | ) 8 | 9 | // Validator implements the validator interface 10 | type Validator struct { 11 | RegularExpression *regexp.Regexp 12 | } 13 | 14 | // NewValidator compiles the given regex and returns a Validator 15 | func NewValidator(regularExpression string) (Validator, error) { 16 | r, err := regexp.Compile(regularExpression) 17 | if err != nil { 18 | return Validator{}, err 19 | } 20 | 21 | return Validator{RegularExpression: r}, nil 22 | } 23 | 24 | // IsValid returns an error if the flag evaluation details value isn't a hex color 25 | func (v Validator) IsValid(flagEvaluationDetails of.InterfaceEvaluationDetails) error { 26 | s, ok := flagEvaluationDetails.Value.(string) 27 | if !ok { 28 | return errors.New("flag value isn't of type string") 29 | } 30 | 31 | if !v.RegularExpression.MatchString(s) { 32 | return errors.New("regex doesn't match on flag value") 33 | } 34 | 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /hooks/validator/pkg/validator/validator.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "context" 5 | of "github.com/open-feature/go-sdk/openfeature" 6 | ) 7 | 8 | type validator interface { 9 | IsValid(flagEvaluationDetails of.InterfaceEvaluationDetails) error 10 | } 11 | 12 | // Hook validates the flag evaluation details After flag resolution 13 | type Hook struct { 14 | of.UnimplementedHook 15 | Validator validator 16 | } 17 | 18 | func (h Hook) After(ctx context.Context, hookContext of.HookContext, flagEvaluationDetails of.InterfaceEvaluationDetails, hookHints of.HookHints) error { 19 | err := h.Validator.IsValid(flagEvaluationDetails) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /providers/README.md: -------------------------------------------------------------------------------- 1 | # OpenFeature Go Providers 2 | 3 | Providers are responsible for performing flag evaluation. They provide an abstraction between the underlying flag management system and OpenFeature itself. This allows providers to be changed without requiring a major code refactor. Please see the [spec](https://github.com/open-feature/spec/blob/main/specification/sections/02-providers.md) for more details. 4 | 5 | To contibute a new provider, fork this repository and create a new module, it will then be discoverable by `make workspace-init` and `make workspace-update`. -------------------------------------------------------------------------------- /providers/configcat/README.md: -------------------------------------------------------------------------------- 1 | # ConfigCat OpenFeature provider for Go 2 | 3 | OpenFeature Go provider implementation for [ConfigCat](https://configcat.com) that uses the official [ConfigCat Go SDK](https://github.com/configcat/go-sdk). 4 | 5 | ## Installation 6 | 7 | ```shell 8 | # ConfigCat SDK 9 | go get github.com/configcat/go-sdk/v9 10 | 11 | # OpenFeature SDK 12 | go get github.com/open-feature/go-sdk/openfeature 13 | go get github.com/open-feature/go-sdk-contrib/providers/configcat 14 | ``` 15 | 16 | ## Usage 17 | 18 | Here's a basic example: 19 | 20 | ```go 21 | import ( 22 | "context" 23 | "fmt" 24 | 25 | sdk "github.com/configcat/go-sdk/v9" 26 | configcat "github.com/open-feature/go-sdk-contrib/providers/configcat/pkg" 27 | "github.com/open-feature/go-sdk/openfeature" 28 | ) 29 | 30 | func main() { 31 | provider := configcat.NewProvider(sdk.NewClient("...")) 32 | openfeature.SetProvider(provider) 33 | 34 | client := openfeature.NewClient("app") 35 | 36 | val, err := client.BooleanValue(context.Background(), "flag_name", false, openfeature.NewEvaluationContext("123", map[string]any{ 37 | configcat.EmailKey: "test@example.com", 38 | })) 39 | fmt.Printf("val: %+v - error: %v\n", val, err) 40 | } 41 | ``` 42 | -------------------------------------------------------------------------------- /providers/configcat/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/open-feature/go-sdk-contrib/providers/configcat 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/configcat/go-sdk/v9 v9.0.6 7 | github.com/open-feature/go-sdk v1.11.0 8 | github.com/stretchr/testify v1.9.0 9 | ) 10 | 11 | require ( 12 | github.com/blang/semver/v4 v4.0.0 // indirect 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/go-logr/logr v1.4.1 // indirect 15 | github.com/pmezard/go-difflib v1.0.0 // indirect 16 | golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 // indirect 17 | gopkg.in/yaml.v3 v3.0.1 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /providers/configcat/go.sum: -------------------------------------------------------------------------------- 1 | github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= 2 | github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= 3 | github.com/configcat/go-sdk/v9 v9.0.6 h1:wWoBqiOwA8q2TMHYfRLgF1Ub+qi1Wk3Opzag6mvz7ws= 4 | github.com/configcat/go-sdk/v9 v9.0.6/go.mod h1:LA9GtJxbY8tQAs/LO4VQMPBNmlFqWMdWg707/hvrpmg= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 8 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 9 | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 10 | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 11 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 12 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 13 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 14 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 15 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 16 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 17 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 18 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 19 | github.com/open-feature/go-sdk v1.11.0 h1:4cp9rXl16ZvlMCef7O+I3vQSXae8DzAF0SfV9mvYInw= 20 | github.com/open-feature/go-sdk v1.11.0/go.mod h1:+rkJhLBtYsJ5PZNddAgFILhRAAxwrJ32aU7UEUm4zQI= 21 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 22 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 23 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 24 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 25 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 26 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 27 | golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 h1:/RIbNt/Zr7rVhIkQhooTxCxFcdWLGIKnZA4IXNFSrvo= 28 | golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= 29 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 30 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 31 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 32 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 33 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 34 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 35 | -------------------------------------------------------------------------------- /providers/flagd-in-process/README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED - In-process Flagd Provider 2 | 3 | > [!IMPORTANT] 4 | > This provider is DEPRECATED and no longer maintained. 5 | > [flagd provider](../flagd/README.md) supports in-process flag evaluations and it will be maintained as a single package. -------------------------------------------------------------------------------- /providers/flagd/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: mockgen 2 | mockgen: 3 | mockgen -source=pkg/iservice.go -destination=internal/mock/service_mock.go -package=mock 4 | -------------------------------------------------------------------------------- /providers/flagd/e2e/config_test.go: -------------------------------------------------------------------------------- 1 | //go:build e2e 2 | 3 | package e2e 4 | 5 | import ( 6 | "flag" 7 | "github.com/open-feature/go-sdk-contrib/tests/flagd/pkg/integration" 8 | "os" 9 | "testing" 10 | 11 | "github.com/cucumber/godog" 12 | ) 13 | 14 | // usedEnvVars list of env vars that have been set 15 | var usedEnvVars []string 16 | 17 | func TestConfig(t *testing.T) { 18 | if testing.Short() { 19 | // skip e2e if testing -short 20 | t.Skip() 21 | } 22 | 23 | flag.Parse() 24 | 25 | name := "config.feature" 26 | 27 | testSuite := godog.TestSuite{ 28 | Name: name, 29 | TestSuiteInitializer: func(testSuiteContext *godog.TestSuiteContext) { 30 | integration.PrepareConfigTestSuite( 31 | func(envVar, envVarValue string) { 32 | t.Setenv(envVar, envVarValue) 33 | usedEnvVars = append(usedEnvVars, envVar) 34 | }, 35 | ) 36 | }, 37 | ScenarioInitializer: func(ctx *godog.ScenarioContext) { 38 | for _, envVar := range usedEnvVars { 39 | err := os.Unsetenv(envVar) 40 | 41 | if err != nil { 42 | t.Fatal("unsetting environment variable: non-zero status returned") 43 | } 44 | } 45 | usedEnvVars = nil 46 | integration.InitializeConfigScenario(ctx) 47 | }, 48 | Options: &godog.Options{ 49 | Format: "pretty", 50 | Paths: []string{"../flagd-testbed/gherkin/config.feature"}, 51 | TestingT: t, // Testing instance that will run subtests. 52 | Strict: true, 53 | }, 54 | } 55 | 56 | if testSuite.Run() != 0 { 57 | t.Fatal("non-zero status returned, failed to run evaluation tests") 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /providers/flagd/e2e/containers/flagd.go: -------------------------------------------------------------------------------- 1 | package containers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/testcontainers/testcontainers-go" 7 | "github.com/testcontainers/testcontainers-go/wait" 8 | "math/rand" 9 | "os" 10 | "strings" 11 | ) 12 | 13 | type ExposedPort string 14 | 15 | const ( 16 | Remote ExposedPort = "remote" 17 | InProcess ExposedPort = "in-process" 18 | Launchpad ExposedPort = "launchpad" 19 | ) 20 | 21 | const ( 22 | flagdPortRemote = "8013" 23 | flagdPortInProcess = "8015" 24 | flagdPortLaunchpad = "8016" 25 | ) 26 | 27 | type flagdConfig struct { 28 | version string 29 | } 30 | 31 | type FlagdContainer struct { 32 | testcontainers.Container 33 | portRemote int 34 | portInProcess int 35 | portLaunchpad int 36 | } 37 | 38 | func (fc *FlagdContainer) GetPort(port ExposedPort) int { 39 | switch port { 40 | case Remote: 41 | return fc.portRemote 42 | case InProcess: 43 | return fc.portInProcess 44 | case Launchpad: 45 | return fc.portLaunchpad 46 | } 47 | return fc.portRemote 48 | } 49 | 50 | func NewFlagd(ctx context.Context) (*FlagdContainer, error) { 51 | version, err := readTestbedVersion() 52 | 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | c := &flagdConfig{ 58 | version: fmt.Sprintf("v%v", version), 59 | } 60 | 61 | return setupContainer(ctx, c) 62 | } 63 | 64 | func setupContainer(ctx context.Context, cfg *flagdConfig) (*FlagdContainer, error) { 65 | registry := "ghcr.io/open-feature" 66 | imgName := "flagd-testbed" 67 | 68 | fullImgName := registry + "/" + imgName + ":" + cfg.version 69 | 70 | req := testcontainers.ContainerRequest{ 71 | Image: fullImgName, 72 | Name: fmt.Sprintf("%s-%d", imgName, rand.Int()), 73 | ExposedPorts: []string{ 74 | flagdPortRemote + "/tcp", 75 | flagdPortInProcess + "/tcp", 76 | flagdPortLaunchpad + "/tcp", 77 | }, 78 | WaitingFor: wait.ForExposedPort(), 79 | Privileged: false, 80 | } 81 | 82 | c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ 83 | ContainerRequest: req, 84 | Started: true, 85 | Reuse: true, 86 | }) 87 | 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | mappedPortRemote, errRemote := c.MappedPort(ctx, flagdPortRemote) 93 | mappedPortInProcess, errInProcess := c.MappedPort(ctx, flagdPortInProcess) 94 | mappedPortLaunchpad, errLaunchpad := c.MappedPort(ctx, flagdPortLaunchpad) 95 | if errRemote != nil || errInProcess != nil || errLaunchpad != nil { 96 | return nil, err 97 | } 98 | 99 | return &FlagdContainer{ 100 | Container: c, 101 | portRemote: mappedPortRemote.Int(), 102 | portInProcess: mappedPortInProcess.Int(), 103 | portLaunchpad: mappedPortLaunchpad.Int(), 104 | }, nil 105 | } 106 | 107 | func readTestbedVersion() (string, error) { 108 | wd, _ := os.Getwd() 109 | fileName := "../flagd-testbed/version.txt" 110 | 111 | content, err := os.ReadFile(fmt.Sprintf("%s/%s", wd, fileName)) 112 | if err != nil { 113 | fmt.Printf("Failed to read file: %s", fileName) 114 | return "", err 115 | } 116 | 117 | return strings.TrimSuffix(string(content), "\n"), nil 118 | } 119 | -------------------------------------------------------------------------------- /providers/flagd/e2e/evaluation_test.go: -------------------------------------------------------------------------------- 1 | //go:build e2e 2 | 3 | package e2e 4 | 5 | import ( 6 | "github.com/open-feature/go-sdk-contrib/providers/flagd/e2e/containers" 7 | flagd "github.com/open-feature/go-sdk-contrib/providers/flagd/pkg" 8 | "github.com/open-feature/go-sdk-contrib/tests/flagd/pkg/integration" 9 | "testing" 10 | ) 11 | 12 | func TestETestEvaluationFlagdInRPC(t *testing.T) { 13 | testJsonEvaluatorFlagd(t, containers.Remote) 14 | } 15 | 16 | func TestJsonEvaluatorFlagdInProcess(t *testing.T) { 17 | testJsonEvaluatorFlagd(t, containers.InProcess, flagd.WithInProcessResolver()) 18 | } 19 | 20 | func testJsonEvaluatorFlagd( 21 | t *testing.T, 22 | exposedPort containers.ExposedPort, 23 | providerOptions ...flagd.ProviderOption, 24 | ) { 25 | runGherkinTestWithFeatureProvider( 26 | gherkinTestRunConfig{ 27 | t: t, 28 | prepareTestSuite: integration.PrepareEvaluationTestSuite, 29 | scenarioInitializer: integration.InitializeEvaluationScenario, 30 | name: "evaluation.feature", 31 | gherkinFile: "../spec/specification/assets/gherkin/evaluation.feature", 32 | port: exposedPort, 33 | providerOptions: providerOptions, 34 | }, 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /providers/flagd/e2e/json_evalutor_test.go: -------------------------------------------------------------------------------- 1 | //go:build e2e 2 | 3 | package e2e 4 | 5 | import ( 6 | "github.com/open-feature/go-sdk-contrib/providers/flagd/e2e/containers" 7 | flagd "github.com/open-feature/go-sdk-contrib/providers/flagd/pkg" 8 | "github.com/open-feature/go-sdk-contrib/tests/flagd/pkg/integration" 9 | "testing" 10 | ) 11 | 12 | func TestJsonEvaluatorInRPC(t *testing.T) { 13 | testJsonEvaluator(t, containers.Remote) 14 | } 15 | 16 | func TestJsonEvaluatorInProcess(t *testing.T) { 17 | testJsonEvaluator(t, containers.InProcess, flagd.WithInProcessResolver()) 18 | } 19 | 20 | func testJsonEvaluator(t *testing.T, exposedPort containers.ExposedPort, providerOptions ...flagd.ProviderOption) { 21 | runGherkinTestWithFeatureProvider( 22 | gherkinTestRunConfig{ 23 | t: t, 24 | prepareTestSuite: integration.PrepareFlagdJsonTestSuite, 25 | scenarioInitializer: integration.InitializeFlagdJsonScenario, 26 | name: "flagd-json-evaluator.feature", 27 | gherkinFile: "../flagd-testbed/gherkin/flagd-json-evaluator.feature", 28 | port: exposedPort, 29 | providerOptions: providerOptions, 30 | }, 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /providers/flagd/e2e/utils.go: -------------------------------------------------------------------------------- 1 | //go:build e2e 2 | 3 | package e2e 4 | 5 | import ( 6 | "context" 7 | "flag" 8 | "github.com/cucumber/godog" 9 | "github.com/open-feature/go-sdk-contrib/providers/flagd/e2e/containers" 10 | flagd "github.com/open-feature/go-sdk-contrib/providers/flagd/pkg" 11 | "github.com/open-feature/go-sdk/openfeature" 12 | "testing" 13 | ) 14 | 15 | type gherkinTestRunConfig struct { 16 | t *testing.T 17 | prepareTestSuite func(func() openfeature.FeatureProvider) 18 | scenarioInitializer func(scenarioContext *godog.ScenarioContext) 19 | name string 20 | gherkinFile string 21 | port containers.ExposedPort 22 | providerOptions []flagd.ProviderOption 23 | } 24 | 25 | func runGherkinTestWithFeatureProvider(config gherkinTestRunConfig) { 26 | if testing.Short() { 27 | // skip e2e if testing -short 28 | config.t.Skip() 29 | } 30 | 31 | container, err := containers.NewFlagd(context.TODO()) 32 | if err != nil { 33 | config.t.Fatal(err) 34 | } 35 | flag.Parse() 36 | 37 | var opts []flagd.ProviderOption 38 | opts = append(opts, config.providerOptions...) 39 | opts = append(opts, flagd.WithPort(uint16(container.GetPort(config.port)))) 40 | 41 | testSuite := godog.TestSuite{ 42 | Name: config.name, 43 | TestSuiteInitializer: func(testSuiteContext *godog.TestSuiteContext) { 44 | config.prepareTestSuite(func() openfeature.FeatureProvider { 45 | provider, err := flagd.NewProvider(opts...) 46 | 47 | if err != nil { 48 | config.t.Fatal("Creating provider failed:", err) 49 | } 50 | 51 | return provider 52 | }) 53 | 54 | testSuiteContext.AfterSuite(func() { 55 | err = container.Terminate(context.Background()) 56 | 57 | if err != nil { 58 | config.t.Fatal(err) 59 | } 60 | }) 61 | }, 62 | ScenarioInitializer: config.scenarioInitializer, 63 | Options: &godog.Options{ 64 | Format: "pretty", 65 | Paths: []string{config.gherkinFile}, 66 | TestingT: config.t, // Testing instance that will run subtests. 67 | Strict: true, 68 | }, 69 | } 70 | 71 | if testSuite.Run() != 0 { 72 | config.t.Fatal("non-zero status returned, failed to run evaluation tests") 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /providers/flagd/internal/cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "github.com/go-logr/logr" 5 | lru "github.com/hashicorp/golang-lru/v2" 6 | ) 7 | 8 | type Type string 9 | 10 | const ( 11 | LRUValue Type = "lru" 12 | InMemValue Type = "mem" 13 | DisabledValue Type = "disabled" 14 | ) 15 | 16 | // Cache is the contract of the cache implementation 17 | type Cache[K comparable, V any] interface { 18 | Add(K, V) (evicted bool) 19 | Purge() 20 | Get(K) (value V, ok bool) 21 | Remove(K) (present bool) 22 | } 23 | 24 | type Service struct { 25 | cacheEnabled bool 26 | cache Cache[string, interface{}] 27 | } 28 | 29 | func NewCacheService(cacheType Type, maxCacheSize int, log logr.Logger) *Service { 30 | var c Cache[string, interface{}] 31 | var err error 32 | var cacheEnabled bool 33 | 34 | // setup cache 35 | switch cacheType { 36 | case LRUValue: 37 | c, err = lru.New[string, interface{}](maxCacheSize) 38 | if err != nil { 39 | log.Error(err, "init lru cache") 40 | } else { 41 | cacheEnabled = true 42 | } 43 | case InMemValue: 44 | c = NewInMemory[string, interface{}]() 45 | cacheEnabled = true 46 | case DisabledValue: 47 | default: 48 | cacheEnabled = false 49 | c = nil 50 | } 51 | 52 | return &Service{ 53 | cacheEnabled: cacheEnabled, 54 | cache: c, 55 | } 56 | } 57 | 58 | func (s *Service) GetCache() Cache[string, interface{}] { 59 | return s.cache 60 | } 61 | 62 | func (s *Service) IsEnabled() bool { 63 | return s.cacheEnabled 64 | } 65 | 66 | func (s *Service) Disable() { 67 | if s.IsEnabled() { 68 | s.cacheEnabled = false 69 | s.cache.Purge() 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /providers/flagd/internal/cache/in_memory.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type InMemory[K comparable, V any] struct { 8 | values map[K]V 9 | rwMux *sync.RWMutex 10 | } 11 | 12 | func NewInMemory[K comparable, V any]() *InMemory[K, V] { 13 | return &InMemory[K, V]{ 14 | values: make(map[K]V), 15 | rwMux: &sync.RWMutex{}, 16 | } 17 | } 18 | 19 | func (m *InMemory[K, V]) Add(flagKey K, value V) (evicted bool) { 20 | m.rwMux.Lock() 21 | defer m.rwMux.Unlock() 22 | m.values[flagKey] = value 23 | 24 | return false 25 | } 26 | 27 | func (m *InMemory[K, V]) Get(flagKey K) (value V, ok bool) { 28 | m.rwMux.RLock() 29 | defer m.rwMux.RUnlock() 30 | 31 | val, ok := m.values[flagKey] 32 | 33 | return val, ok 34 | } 35 | 36 | func (m *InMemory[K, V]) Remove(flagKey K) (present bool) { 37 | m.rwMux.Lock() 38 | defer m.rwMux.Unlock() 39 | 40 | _, ok := m.values[flagKey] 41 | delete(m.values, flagKey) 42 | return ok 43 | } 44 | 45 | func (m *InMemory[K, V]) Purge() { 46 | m.rwMux.Lock() 47 | defer m.rwMux.Unlock() 48 | 49 | m.values = make(map[K]V) 50 | } 51 | -------------------------------------------------------------------------------- /providers/flagd/internal/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/go-logr/logr" 7 | ) 8 | 9 | const ( 10 | Warn = 0 11 | Info = 1 12 | Debug = 2 13 | ) 14 | 15 | // Logger is the provider's default logger 16 | // logs using the standard log package on error, all other logs are no-ops 17 | type Logger struct{} 18 | 19 | func (l Logger) Init(info logr.RuntimeInfo) {} 20 | 21 | func (l Logger) Enabled(level int) bool { return true } 22 | 23 | func (l Logger) Info(level int, msg string, keysAndValues ...interface{}) {} 24 | 25 | func (l Logger) Error(err error, msg string, keysAndValues ...interface{}) { 26 | log.Println("flagd-provider:", err) 27 | } 28 | 29 | func (l Logger) WithValues(keysAndValues ...interface{}) logr.LogSink { return l } 30 | 31 | func (l Logger) WithName(name string) logr.LogSink { return l } 32 | -------------------------------------------------------------------------------- /providers/flagd/pkg/configuration_test.go: -------------------------------------------------------------------------------- 1 | package flagd 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestConfigureProviderConfigurationInProcessWithOfflineFile(t *testing.T) { 8 | // given 9 | providerConfiguration := &ProviderConfiguration{ 10 | Resolver: inProcess, 11 | OfflineFlagSourcePath: "somePath", 12 | } 13 | 14 | // when 15 | configureProviderConfiguration(providerConfiguration) 16 | 17 | // then 18 | if providerConfiguration.Resolver != file { 19 | t.Errorf("incorrect Resolver, expected %v, got %v", file, providerConfiguration.Resolver) 20 | } 21 | } 22 | 23 | func TestConfigureProviderConfigurationRpcWithoutPort(t *testing.T) { 24 | // given 25 | providerConfiguration := &ProviderConfiguration{ 26 | Resolver: rpc, 27 | } 28 | 29 | // when 30 | configureProviderConfiguration(providerConfiguration) 31 | 32 | // then 33 | if providerConfiguration.Port != defaultRpcPort { 34 | t.Errorf("incorrect Port, expected %v, got %v", defaultRpcPort, providerConfiguration.Port) 35 | } 36 | } 37 | 38 | func TestConfigureProviderConfigurationInProcessWithoutPort(t *testing.T) { 39 | // given 40 | providerConfiguration := &ProviderConfiguration{ 41 | Resolver: inProcess, 42 | } 43 | 44 | // when 45 | configureProviderConfiguration(providerConfiguration) 46 | 47 | // then 48 | if providerConfiguration.Port != defaultInProcessPort { 49 | t.Errorf("incorrect Port, expected %v, got %v", defaultInProcessPort, providerConfiguration.Port) 50 | } 51 | } 52 | 53 | func TestValidateProviderConfigurationFileMissingData(t *testing.T) { 54 | // given 55 | providerConfiguration := &ProviderConfiguration{ 56 | Resolver: file, 57 | } 58 | 59 | // when 60 | err := validateProviderConfiguration(providerConfiguration) 61 | 62 | // then 63 | if err == nil { 64 | t.Errorf("Error expected but check succeeded") 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /providers/flagd/pkg/iservice.go: -------------------------------------------------------------------------------- 1 | package flagd 2 | 3 | import ( 4 | "context" 5 | 6 | of "github.com/open-feature/go-sdk/openfeature" 7 | ) 8 | 9 | // IService abstract the service implementation for flagd provider 10 | type IService interface { 11 | Init() error 12 | Shutdown() 13 | ResolveBoolean(ctx context.Context, key string, defaultValue bool, 14 | evalCtx map[string]interface{}) of.BoolResolutionDetail 15 | ResolveString(ctx context.Context, key string, defaultValue string, 16 | evalCtx map[string]interface{}) of.StringResolutionDetail 17 | ResolveFloat(ctx context.Context, key string, defaultValue float64, 18 | evalCtx map[string]interface{}) of.FloatResolutionDetail 19 | ResolveInt(ctx context.Context, key string, defaultValue int64, 20 | evalCtx map[string]interface{}) of.IntResolutionDetail 21 | ResolveObject(ctx context.Context, key string, defaultValue interface{}, 22 | evalCtx map[string]interface{}) of.InterfaceResolutionDetail 23 | EventChannel() <-chan of.Event 24 | } 25 | -------------------------------------------------------------------------------- /providers/flagd/pkg/service/in_process/do_nothing_custom_sync_provider.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | context "context" 5 | 6 | "github.com/open-feature/flagd/core/pkg/sync" 7 | ) 8 | 9 | // Fake implementation of sync.ISync. Does not conform to the contract because it does not send any events to the DataSync. 10 | // Only used for unit tests. 11 | type DoNothingCustomSyncProvider struct { 12 | } 13 | 14 | func (fps DoNothingCustomSyncProvider) Init(ctx context.Context) error { 15 | return nil 16 | } 17 | 18 | func (fps DoNothingCustomSyncProvider) IsReady() bool { 19 | return true 20 | } 21 | 22 | func (fps DoNothingCustomSyncProvider) Sync(ctx context.Context, dataSync chan<- sync.DataSync) error { 23 | return nil 24 | } 25 | 26 | func (fps DoNothingCustomSyncProvider) ReSync(ctx context.Context, dataSync chan<- sync.DataSync) error { 27 | return nil 28 | } 29 | 30 | // Returns an implementation of sync.ISync interface that does nothing at all. 31 | // The returned implementation does not conform to the sync.DataSync contract. 32 | // This is useful only for unit tests. 33 | func NewDoNothingCustomSyncProvider() DoNothingCustomSyncProvider { 34 | return DoNothingCustomSyncProvider{} 35 | } 36 | 37 | var _ sync.ISync = &DoNothingCustomSyncProvider{} 38 | -------------------------------------------------------------------------------- /providers/flagd/pkg/service/in_process/service_custom_sync_provider_test.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestInProcessWithCustomSyncProvider(t *testing.T) { 8 | customSyncProvider := NewDoNothingCustomSyncProvider() 9 | service := NewInProcessService(Configuration{CustomSyncProvider: customSyncProvider, CustomSyncProviderUri: "not tested here"}) 10 | 11 | // If custom sync provider is supplied the in-process service should use it. 12 | if service.sync != customSyncProvider { 13 | t.Fatalf("Expected service.sync to be the mockCustomSyncProvider, but got %s", service.sync) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /providers/flagd/pkg/service/in_process/service_file_test.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "context" 5 | of "github.com/open-feature/go-sdk/openfeature" 6 | "os" 7 | "path/filepath" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestInProcessOfflineMode(t *testing.T) { 13 | // given 14 | flagFile := "config.json" 15 | offlinePath := filepath.Join(t.TempDir(), flagFile) 16 | 17 | err := os.WriteFile(offlinePath, []byte(flagRsp), 0644) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | // when 23 | service := NewInProcessService(Configuration{OfflineFlagSource: offlinePath}) 24 | 25 | err = service.Init() 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | 30 | // then 31 | channel := service.EventChannel() 32 | 33 | select { 34 | case event := <-channel: 35 | if event.EventType != of.ProviderReady { 36 | t.Fatalf("Provider initialization failed. Got event type %s with message %s", event.EventType, event.Message) 37 | } 38 | case <-time.After(2 * time.Second): 39 | t.Fatal("Provider initialization did not complete within acceptable timeframe ") 40 | } 41 | 42 | // provider must evaluate flag from the grpc source data 43 | detail := service.ResolveBoolean(context.Background(), "myBoolFlag", false, make(map[string]interface{})) 44 | 45 | if !detail.Value { 46 | t.Fatal("Expected true, but got false") 47 | } 48 | 49 | // check for metadata - scope from grpc sync 50 | if len(detail.FlagMetadata) == 0 && detail.FlagMetadata["scope"] == "" { 51 | t.Fatal("Expected scope to be present, but got none") 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /providers/flagd/pkg/service/in_process/zap.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | "go.uber.org/zap/zapcore" 6 | "os" 7 | "time" 8 | ) 9 | 10 | // EncoderConfigOption is a function that can modify a `zapcore.EncoderConfig`. 11 | type EncoderConfigOption func(*zapcore.EncoderConfig) 12 | 13 | func newJSONEncoder(opts ...EncoderConfigOption) zapcore.Encoder { 14 | encoderConfig := zap.NewProductionEncoderConfig() 15 | for _, opt := range opts { 16 | opt(&encoderConfig) 17 | } 18 | return zapcore.NewJSONEncoder(encoderConfig) 19 | } 20 | 21 | // NewRaw returns a new zap.Logger configured with the passed Opts 22 | // or their defaults. It uses KubeAwareEncoder which adds Type 23 | // information and Namespace/Name to the log. 24 | func NewRaw() *zap.Logger { 25 | level := zap.NewAtomicLevelAt(zap.InfoLevel) 26 | 27 | var zapOpts []zap.Option 28 | if level.Enabled(zapcore.Level(-2)) { 29 | zapOpts = append(zapOpts, 30 | zap.WrapCore(func(core zapcore.Core) zapcore.Core { 31 | return zapcore.NewSamplerWithOptions(core, time.Second, 100, 100) 32 | })) 33 | } 34 | zapOpts = append(zapOpts, zap.AddStacktrace(zap.NewAtomicLevelAt(zap.ErrorLevel))) 35 | 36 | f := func(ecfg *zapcore.EncoderConfig) { 37 | ecfg.EncodeTime = zapcore.RFC3339TimeEncoder 38 | } 39 | encoder := newJSONEncoder(f) 40 | 41 | // this basically mimics NewConfig, but with a custom sink 42 | sink := zapcore.AddSync(os.Stderr) 43 | zapOpts = append(zapOpts, zap.ErrorOutput(sink)) 44 | log := zap.New(zapcore.NewCore(encoder, sink, level)) 45 | log = log.WithOptions(zapOpts...) 46 | return log 47 | } 48 | -------------------------------------------------------------------------------- /providers/flagd/pkg/service/rpc/mock_client_test.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | v1 "buf.build/gen/go/open-feature/flagd/protocolbuffers/go/flagd/evaluation/v1" 5 | "connectrpc.com/connect" 6 | "context" 7 | ) 8 | 9 | // MockClient is a test mock for service client 10 | type MockClient struct { 11 | booleanResponse v1.ResolveBooleanResponse 12 | stringResponse v1.ResolveStringResponse 13 | floatResponse v1.ResolveFloatResponse 14 | intResponse v1.ResolveIntResponse 15 | objResponse v1.ResolveObjectResponse 16 | 17 | error error 18 | } 19 | 20 | func (m *MockClient) ResolveBoolean(context.Context, *connect.Request[v1.ResolveBooleanRequest]) (*connect.Response[v1.ResolveBooleanResponse], error) { 21 | return &connect.Response[v1.ResolveBooleanResponse]{ 22 | Msg: &m.booleanResponse, 23 | }, m.error 24 | } 25 | 26 | func (m *MockClient) ResolveString(context.Context, *connect.Request[v1.ResolveStringRequest]) (*connect.Response[v1.ResolveStringResponse], error) { 27 | return &connect.Response[v1.ResolveStringResponse]{ 28 | Msg: &m.stringResponse, 29 | }, m.error 30 | } 31 | 32 | func (m *MockClient) ResolveFloat(context.Context, *connect.Request[v1.ResolveFloatRequest]) (*connect.Response[v1.ResolveFloatResponse], error) { 33 | return &connect.Response[v1.ResolveFloatResponse]{ 34 | Msg: &m.floatResponse, 35 | }, m.error 36 | } 37 | 38 | func (m *MockClient) ResolveInt(context.Context, *connect.Request[v1.ResolveIntRequest]) (*connect.Response[v1.ResolveIntResponse], error) { 39 | return &connect.Response[v1.ResolveIntResponse]{ 40 | Msg: &m.intResponse, 41 | }, m.error 42 | } 43 | 44 | func (m *MockClient) ResolveObject(context.Context, *connect.Request[v1.ResolveObjectRequest]) (*connect.Response[v1.ResolveObjectResponse], error) { 45 | return &connect.Response[v1.ResolveObjectResponse]{ 46 | Msg: &m.objResponse, 47 | }, m.error 48 | } 49 | 50 | func (m *MockClient) EventStream(context.Context, *connect.Request[v1.EventStreamRequest]) (*connect.ServerStreamForClient[v1.EventStreamResponse], error) { 51 | // note - mocking this is impossible 52 | return &connect.ServerStreamForClient[v1.EventStreamResponse]{}, m.error 53 | } 54 | 55 | func (m *MockClient) ResolveAll(context.Context, *connect.Request[v1.ResolveAllRequest]) (*connect.Response[v1.ResolveAllResponse], error) { 56 | return &connect.Response[v1.ResolveAllResponse]{ 57 | Msg: &v1.ResolveAllResponse{}, 58 | }, m.error 59 | } 60 | -------------------------------------------------------------------------------- /providers/flagd/pkg/service/rpc/retryCounter.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import "time" 4 | 5 | const ( 6 | defaultDelay = time.Second 7 | factor = 2 8 | ) 9 | 10 | type retryCounter struct { 11 | baseRetryDelay time.Duration 12 | maxRetries int 13 | 14 | currentDelay time.Duration 15 | currentRetries int 16 | } 17 | 18 | func newRetryCounter(maxRetries int) retryCounter { 19 | return retryCounter{ 20 | baseRetryDelay: defaultDelay, 21 | maxRetries: maxRetries, 22 | } 23 | } 24 | 25 | // reset the retry counter and sleep delay 26 | func (c *retryCounter) reset() { 27 | c.currentDelay = defaultDelay 28 | c.currentRetries = 0 29 | } 30 | 31 | // retry increments current retry attempts, check and return a boolean stating retry is allowed 32 | func (c *retryCounter) retry() bool { 33 | c.currentRetries++ 34 | return c.currentRetries <= c.maxRetries 35 | } 36 | 37 | // sleep returns the current sleep delay and increment the next sleep value 38 | func (c *retryCounter) sleep() time.Duration { 39 | var value = c.currentDelay 40 | c.currentDelay = factor * c.currentDelay 41 | 42 | return value 43 | } 44 | -------------------------------------------------------------------------------- /providers/flagd/pkg/service/rpc/service_eventing_test.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | schemaV1 "buf.build/gen/go/open-feature/flagd/protocolbuffers/go/flagd/evaluation/v1" 5 | "context" 6 | "errors" 7 | "github.com/open-feature/go-sdk-contrib/providers/flagd/internal/cache" 8 | of "github.com/open-feature/go-sdk/openfeature" 9 | "google.golang.org/protobuf/types/known/structpb" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func TestRetries(t *testing.T) { 15 | // given - stream is always errored 16 | client := MockClient{ 17 | error: errors.New("streaming error"), 18 | } 19 | 20 | service := Service{ 21 | retryCounter: retryCounter{ 22 | baseRetryDelay: 100 * time.Millisecond, 23 | maxRetries: 1, 24 | }, 25 | client: &client, 26 | cache: cache.NewCacheService(cache.DisabledValue, 0, log), 27 | events: make(chan of.Event), 28 | } 29 | 30 | // when - start event stream, knowing it will result in error 31 | go func() { 32 | service.startEventStream(context.Background()) 33 | }() 34 | 35 | // then - expect an error event after retries 36 | var event of.Event 37 | select { 38 | case event = <-service.EventChannel(): 39 | break 40 | case <-time.After(1 * time.Second): 41 | t.Fatal("timed out waiting for event") 42 | 43 | } 44 | 45 | if event.EventType != of.ProviderError { 46 | t.Errorf("expected event of %s, got %s", of.ProviderError, event.EventType) 47 | } 48 | } 49 | 50 | func TestConfigChange(t *testing.T) { 51 | data := map[string]interface{}{ 52 | "flags": map[string]interface{}{ 53 | "a": "", 54 | "b": "", 55 | }, 56 | } 57 | 58 | stData, err := structpb.NewStruct(data) 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | 63 | t.Run("no cache - do nothing", func(t *testing.T) { 64 | // given 65 | service := Service{ 66 | cache: cache.NewCacheService(cache.DisabledValue, 0, log), 67 | events: make(chan of.Event), 68 | } 69 | 70 | // when 71 | go func() { 72 | service.handleConfigurationChangeEvent(&schemaV1.EventStreamResponse{ 73 | Data: stData, 74 | }) 75 | }() 76 | 77 | // then - expect no event 78 | select { 79 | case event := <-service.EventChannel(): 80 | t.Fatalf("expected no event, but got with type: %s", event.EventType) 81 | case <-time.After(100 * time.Millisecond): 82 | // no events mean pass 83 | break 84 | } 85 | }) 86 | 87 | t.Run("with cache - validate config change event", func(t *testing.T) { 88 | // given 89 | service := Service{ 90 | cache: cache.NewCacheService(cache.InMemValue, 1, log), 91 | events: make(chan of.Event), 92 | } 93 | 94 | // when 95 | go func() { 96 | service.handleConfigurationChangeEvent(&schemaV1.EventStreamResponse{ 97 | Data: stData, 98 | }) 99 | }() 100 | 101 | // then - expect no event 102 | select { 103 | case event := <-service.EventChannel(): 104 | if event.EventType != of.ProviderConfigChange { 105 | t.Fatalf("expected event %s, got %s", of.ProviderConfigChange, event.EventType) 106 | } 107 | case <-time.After(100 * time.Millisecond): 108 | t.Fatalf("timed out waiting for event") 109 | } 110 | }) 111 | 112 | } 113 | -------------------------------------------------------------------------------- /providers/flagsmith/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.1.4](https://github.com/open-feature/go-sdk-contrib/compare/providers/flagsmith/v0.1.3...providers/flagsmith/v0.1.4) (2023-08-02) 4 | 5 | 6 | ### 🧹 Chore 7 | 8 | * **flagsmith:** bump provider version ([#281](https://github.com/open-feature/go-sdk-contrib/issues/281)) ([f003ac8](https://github.com/open-feature/go-sdk-contrib/commit/f003ac8f309db7d0bae233994041fe0e416aa82d)) 9 | 10 | ## [0.1.3](https://github.com/open-feature/go-sdk-contrib/compare/providers/flagsmith/v0.1.2...providers/flagsmith/v0.1.3) (2023-07-21) 11 | 12 | 13 | ### 🐛 Bug Fixes 14 | 15 | * **deps:** update module github.com/flagsmith/flagsmith-go-client/v2 to v3 ([#246](https://github.com/open-feature/go-sdk-contrib/issues/246)) ([7246837](https://github.com/open-feature/go-sdk-contrib/commit/72468374bcdad9f173773f4ad35b345952bcd3f1)) 16 | * **deps:** update module github.com/open-feature/go-sdk to v1.5.1 ([#263](https://github.com/open-feature/go-sdk-contrib/issues/263)) ([c75ffd6](https://github.com/open-feature/go-sdk-contrib/commit/c75ffd6017689a86860dec92c1a1564b6145f0c9)) 17 | * **deps:** update module github.com/stretchr/testify to v1.8.3 ([#213](https://github.com/open-feature/go-sdk-contrib/issues/213)) ([4282bbe](https://github.com/open-feature/go-sdk-contrib/commit/4282bbe2bcccda3c4f59f6fe7cfd272df90e675e)) 18 | * **deps:** update module github.com/stretchr/testify to v1.8.4 ([#229](https://github.com/open-feature/go-sdk-contrib/issues/229)) ([d75b066](https://github.com/open-feature/go-sdk-contrib/commit/d75b0666417a0b0e46cbe4f157e34765fe9bc7d9)) 19 | 20 | 21 | ### 🧹 Chore 22 | 23 | * update module github.com/open-feature/go-sdk to v1.4.0 ([#223](https://github.com/open-feature/go-sdk-contrib/issues/223)) ([7c8ea46](https://github.com/open-feature/go-sdk-contrib/commit/7c8ea46e3e094f746dbf6d80ba6a1b606314e8d7)) 24 | 25 | ## [0.1.2](https://github.com/open-feature/go-sdk-contrib/compare/providers/flagsmith/v0.1.1...providers/flagsmith/v0.1.2) (2023-04-11) 26 | 27 | 28 | ### 🐛 Bug Fixes 29 | 30 | * **deps:** update module github.com/flagsmith/flagsmith-go-client/v2 to v2.3.1 ([#153](https://github.com/open-feature/go-sdk-contrib/issues/153)) ([fca8383](https://github.com/open-feature/go-sdk-contrib/commit/fca838357c1198ccb66cbcf68d52e73e37e9a51b)) 31 | 32 | ## [0.1.1](https://github.com/open-feature/go-sdk-contrib/compare/providers/flagsmith-v0.1.0...providers/flagsmith/v0.1.1) (2023-03-15) 33 | 34 | 35 | ### ✨ New Features 36 | 37 | * **flagsmith-provider:** Add flagsmith provider ([#128](https://github.com/open-feature/go-sdk-contrib/issues/128)) ([185b721](https://github.com/open-feature/go-sdk-contrib/commit/185b721566c0f17ea2065005f20fe1d76624d805)) 38 | -------------------------------------------------------------------------------- /providers/flagsmith/README.md: -------------------------------------------------------------------------------- 1 | # Flagsmith OpenFeature GO Provider 2 | 3 | [Flagsmith](https://flagsmith.com/) provides an all-in-one platform for developing, implementing, and managing your feature flags. 4 | 5 | # Installation 6 | 7 | To use the Flagsmith provider, you'll need to install [flagsmith Go client](https://github.com/Flagsmith/flagsmith-go-client) and flagsmith provider. You can install the packages using the following command 8 | 9 | ```shell 10 | go get github.com/Flagsmith/flagsmith-go-client/v3 11 | go get github.com/open-feature/go-sdk-contrib/providers/flagsmith 12 | ``` 13 | 14 | ## Usage 15 | Here's an example of how you can use the Flagsmith provider: 16 | 17 | ```go 18 | import ( 19 | flagsmithClient "github.com/Flagsmith/flagsmith-go-client/v3" 20 | of "github.com/open-feature/go-sdk/openfeature" 21 | flagsmith "github.com/open-feature/go-sdk-contrib/providers/flagsmith/pkg" 22 | ) 23 | ... 24 | // Initialize the flagsmith client 25 | client := flagsmithClient.NewClient(os.Getenv("FLAGSMITH_ENVIRONMENT_KEY")) 26 | 27 | // Initialize the flagsmith provider 28 | provider := flagsmith.NewProvider(client, flagsmith.WithUsingBooleanConfigValue()) 29 | 30 | of.SetProvider(provider) 31 | 32 | // Create open feature client 33 | ofClient := of.NewClient("my-app") 34 | 35 | // Start interacting with the client 36 | Value, err := ofClient.BooleanValue(context.Background(), "bool_feature", defaultboolValue, evalCtx) 37 | .... 38 | 39 | // With traits 40 | traitKey := "some_key" 41 | traitValue := "some_value" 42 | 43 | evalCtx := of.NewEvaluationContext( 44 | "openfeature_user", 45 | map[string]interface{}{ 46 | traitKey:traitValue 47 | }, 48 | ) 49 | valueForIdentity, err := ofClient.BooleanValue(context.Background(), "bool_feature", defaultboolValue, evalCtx) 50 | ... 51 | 52 | ``` 53 | You can find the flagsmith client document [here](https://docs.flagsmith.com/clients/server-side) 54 | 55 | ### Options 56 | - `WithUsingBooleanConfigValue`: Determines whether to resolve a feature value as a boolean or use the isFeatureEnabled as the flag itself. 57 | i.e: if the flag is enabled, the value will be true, otherwise it will be false 58 | -------------------------------------------------------------------------------- /providers/flagsmith/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/open-feature/go-sdk-contrib/providers/flagsmith 2 | 3 | go 1.22 4 | 5 | toolchain go1.24.3 6 | 7 | require ( 8 | github.com/Flagsmith/flagsmith-go-client/v3 v3.7.0 9 | github.com/open-feature/go-sdk v1.11.0 10 | github.com/stretchr/testify v1.9.0 11 | ) 12 | 13 | require ( 14 | github.com/blang/semver/v4 v4.0.0 // indirect 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/go-logr/logr v1.4.1 // indirect 17 | github.com/go-resty/resty/v2 v2.14.0 // indirect 18 | github.com/itlightning/dateparse v0.2.0 // indirect 19 | github.com/kr/pretty v0.3.1 // indirect 20 | github.com/pmezard/go-difflib v1.0.0 // indirect 21 | golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 // indirect 22 | golang.org/x/net v0.27.0 // indirect 23 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 24 | gopkg.in/yaml.v3 v3.0.1 // indirect 25 | ) 26 | -------------------------------------------------------------------------------- /providers/flipt/README.md: -------------------------------------------------------------------------------- 1 | # Flipt OpenFeature Provider (Go) 2 | 3 | This repository and package provides a [Flipt](https://github.com/flipt-io/flipt) [OpenFeature Provider](https://docs.openfeature.dev/docs/specification/sections/providers) for interacting with the Flipt service backend using the [OpenFeature Go SDK](https://github.com/open-feature/go-sdk). 4 | 5 | From the [OpenFeature Specification](https://docs.openfeature.dev/docs/specification/sections/providers): 6 | 7 | > Providers are the "translator" between the flag evaluation calls made in application code, and the flag management system that stores flags and in some cases evaluates flags. 8 | 9 | ## Requirements 10 | 11 | - Go 1.20+ 12 | - A running instance of [Flipt](https://www.flipt.io/docs/installation) 13 | 14 | ## Usage 15 | 16 | ### Installation 17 | 18 | ```bash 19 | go get github.com/open-feature/go-sdk-contrib/providers/flipt 20 | ``` 21 | 22 | ### Example 23 | 24 | ```go 25 | package main 26 | 27 | import ( 28 | "context" 29 | 30 | flipt "github.com/open-feature/go-sdk-contrib/providers/flipt/pkg/provider" 31 | "github.com/open-feature/go-sdk/openfeature" 32 | ) 33 | 34 | 35 | func main() { 36 | // http://localhost:8080 is the default Flipt address 37 | openfeature.SetProvider(flipt.NewProvider()) 38 | 39 | client := openfeature.NewClient("my-app") 40 | value, err := client.BooleanValue(context.Background(), "v2_enabled", false, openfeature.NewEvaluationContext( 41 | "tim@apple.com", 42 | map[string]interface{}{ 43 | "favorite_color": "blue", 44 | }, 45 | )) 46 | 47 | if err != nil { 48 | panic(err) 49 | } 50 | 51 | if value { 52 | // do something 53 | } else { 54 | // do something else 55 | } 56 | } 57 | ``` 58 | 59 | ## Configuration 60 | 61 | The Flipt provider allows you to change the [namespace](https://docs.flipt.io/concepts#namespaces) that the evaluation is performed against. If not provided, it defaults to the `Default` namespace: 62 | 63 | ### Target Namespace 64 | 65 | ```go 66 | provider := flipt.NewProvider(flipt.ForNamespace("your-namespace")) 67 | ``` 68 | 69 | ### Protocol 70 | 71 | The Flipt provider allows you to communicate with Flipt over either HTTP(S) or GRPC, depending on the address provided. 72 | 73 | #### HTTP(S) 74 | 75 | ```go 76 | provider := flipt.NewProvider(flipt.WithAddress("https://localhost:443")) 77 | ``` 78 | 79 | ##### Unix Socket 80 | 81 | ```go 82 | provider := flipt.NewProvider(flipt.WithAddress("unix:///path/to/socket")) 83 | ``` 84 | 85 | #### GRPC 86 | 87 | ##### HTTP/2 88 | 89 | ```go 90 | type Token string 91 | 92 | func (t Token) ClientToken() (string, error) { 93 | return t, nil 94 | } 95 | 96 | provider := flipt.NewProvider( 97 | flipt.WithAddress("localhost:9000"), 98 | flipt.WithCertificatePath("/path/to/cert.pem"), // optional 99 | flipt.WithClientProvider(Token("a-client-token")), // optional 100 | ) 101 | ``` 102 | 103 | ##### Unix Socket 104 | 105 | ```go 106 | provider := flipt.NewProvider( 107 | flipt.WithAddress("unix:///path/to/socket"), 108 | ) 109 | ``` 110 | -------------------------------------------------------------------------------- /providers/flipt/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/open-feature/go-sdk-contrib/providers/flipt 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/open-feature/go-sdk v1.13.1 7 | github.com/stretchr/testify v1.9.0 8 | go.flipt.io/flipt/rpc/flipt v1.45.0 9 | go.flipt.io/flipt/sdk/go v0.12.0 10 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0 11 | google.golang.org/grpc v1.67.1 12 | google.golang.org/protobuf v1.35.1 13 | ) 14 | 15 | require ( 16 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 17 | github.com/go-logr/logr v1.4.2 // indirect 18 | github.com/go-logr/stdr v1.2.2 // indirect 19 | github.com/golang/protobuf v1.5.4 // indirect 20 | github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect 21 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect 22 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 23 | github.com/stretchr/objx v0.5.2 // indirect 24 | go.flipt.io/flipt/errors v1.45.0 // indirect 25 | go.opentelemetry.io/otel v1.31.0 // indirect 26 | go.opentelemetry.io/otel/metric v1.31.0 // indirect 27 | go.opentelemetry.io/otel/trace v1.31.0 // indirect 28 | go.uber.org/multierr v1.11.0 // indirect 29 | go.uber.org/zap v1.27.0 // indirect 30 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect 31 | golang.org/x/net v0.30.0 // indirect 32 | golang.org/x/sys v0.26.0 // indirect 33 | golang.org/x/text v0.19.0 // indirect 34 | google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda // indirect 35 | google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect 36 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect 37 | gopkg.in/yaml.v3 v3.0.1 // indirect 38 | ) 39 | -------------------------------------------------------------------------------- /providers/flipt/pkg/provider/doc.go: -------------------------------------------------------------------------------- 1 | // This package provides a [Flipt] [OpenFeature Provider] for interacting with the Flipt service backend using the [OpenFeature Go SDK]. 2 | // 3 | // From the [OpenFeature Specification]: 4 | // Providers are the "translator" between the flag evaluation calls made in application code, and the flag management system that stores flags and in some cases evaluates flags. 5 | // 6 | // You can configure the provider to connect to Flipt using any of the provided "[Option]"s. 7 | // This configuration allows you to specify the "[ServiceType]" (protocol), and to configure the host, port and other properties to connect to the Flipt service. 8 | // 9 | // [Flipt]: https://github.com/flipt-io/flipt 10 | // [OpenFeature Provider]: https://docs.openfeature.dev/docs/specification/sections/providers 11 | // [OpenFeature Go SDK]: https://github.com/open-feature/go-sdk 12 | // [OpenFeature Specification]: https://docs.openfeature.dev/docs/specification/sections/providers 13 | package flipt 14 | -------------------------------------------------------------------------------- /providers/flipt/pkg/provider/example_test.go: -------------------------------------------------------------------------------- 1 | package flipt_test 2 | 3 | import ( 4 | "context" 5 | 6 | flipt "github.com/open-feature/go-sdk-contrib/providers/flipt/pkg/provider" 7 | "github.com/open-feature/go-sdk/openfeature" 8 | ) 9 | 10 | func Example() { 11 | openfeature.SetProvider(flipt.NewProvider( 12 | flipt.WithAddress("localhost:9000"), 13 | )) 14 | 15 | client := openfeature.NewClient("my-app") 16 | value, err := client.BooleanValue( 17 | context.Background(), "v2_enabled", false, openfeature.NewEvaluationContext("tim@apple.com", map[string]interface{}{ 18 | "favorite_color": "blue", 19 | }), 20 | ) 21 | 22 | if err != nil { 23 | panic(err) 24 | } 25 | 26 | if value { 27 | // do something 28 | } else { 29 | // do something else 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /providers/flipt/pkg/service/client.go: -------------------------------------------------------------------------------- 1 | package flipt 2 | 3 | import ( 4 | "context" 5 | 6 | flipt "go.flipt.io/flipt/rpc/flipt" 7 | "go.flipt.io/flipt/rpc/flipt/evaluation" 8 | ) 9 | 10 | //go:generate mockery --name=Client --case=underscore --inpackage --filename=service_support.go --testonly --with-expecter --disable-version-string 11 | type Client interface { 12 | GetFlag(ctx context.Context, c *flipt.GetFlagRequest) (*flipt.Flag, error) 13 | Variant(ctx context.Context, v *evaluation.EvaluationRequest) (*evaluation.VariantEvaluationResponse, error) 14 | Boolean(ctx context.Context, v *evaluation.EvaluationRequest) (*evaluation.BooleanEvaluationResponse, error) 15 | } 16 | -------------------------------------------------------------------------------- /providers/flipt/pkg/service/doc.go: -------------------------------------------------------------------------------- 1 | // This package contains the lower level Flipt service client implementation. 2 | // Under normal circumstances, you should not need to use this package directly. Instead, you should use the `Provider` to configure and interact with the Flipt service. 3 | package flipt 4 | -------------------------------------------------------------------------------- /providers/from-env/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.1.5](https://github.com/open-feature/go-sdk-contrib/compare/providers/from-env/v0.1.4...providers/from-env/v0.1.5) (2024-06-14) 4 | 5 | 6 | ### 🐛 Bug Fixes 7 | 8 | * **deps:** update module github.com/open-feature/go-sdk to v1.10.0 ([#469](https://github.com/open-feature/go-sdk-contrib/issues/469)) ([21810af](https://github.com/open-feature/go-sdk-contrib/commit/21810afc33fce9a3940ec9dc59e65f140fcbaa57)) 9 | * **deps:** update module github.com/open-feature/go-sdk to v1.11.0 ([#501](https://github.com/open-feature/go-sdk-contrib/issues/501)) ([3f0eaa5](https://github.com/open-feature/go-sdk-contrib/commit/3f0eaa575500baa663dc24dbfc6cf8214565471f)) 10 | 11 | 12 | ### ✨ New Features 13 | 14 | * **from_env:** option for mapping flag to env name ([#528](https://github.com/open-feature/go-sdk-contrib/issues/528)) ([cede073](https://github.com/open-feature/go-sdk-contrib/commit/cede073056c39346c660b3289459f38464786cea)) 15 | 16 | ## [0.1.4](https://github.com/open-feature/go-sdk-contrib/compare/providers/from-env/v0.1.3...providers/from-env/v0.1.4) (2024-03-22) 17 | 18 | 19 | ### 🐛 Bug Fixes 20 | 21 | * **deps:** update module github.com/open-feature/go-sdk to v1.6.0 ([#289](https://github.com/open-feature/go-sdk-contrib/issues/289)) ([13eeb48](https://github.com/open-feature/go-sdk-contrib/commit/13eeb482ee3d69c5fb8100563501c2250b6454f1)) 22 | * **deps:** update module github.com/open-feature/go-sdk to v1.7.0 ([#315](https://github.com/open-feature/go-sdk-contrib/issues/315)) ([3f049ad](https://github.com/open-feature/go-sdk-contrib/commit/3f049ad34e93c3b9b9d4cf5a2e56f3777eb858e6)) 23 | * **deps:** update module github.com/open-feature/go-sdk to v1.8.0 ([#329](https://github.com/open-feature/go-sdk-contrib/issues/329)) ([c99b527](https://github.com/open-feature/go-sdk-contrib/commit/c99b52728bad9dce52bfb78a08ae5f4eea83a397)) 24 | 25 | 26 | ### 🧹 Chore 27 | 28 | * bump Go to version 1.21 ([#452](https://github.com/open-feature/go-sdk-contrib/issues/452)) ([7ec90ce](https://github.com/open-feature/go-sdk-contrib/commit/7ec90ce4f9b06670187561afd9e342eed4228be1)) 29 | * update to go-sdk 1.9.0 ([#404](https://github.com/open-feature/go-sdk-contrib/issues/404)) ([11fa3ab](https://github.com/open-feature/go-sdk-contrib/commit/11fa3aba065a6dd81caca30e76efc16fb64a25e3)) 30 | 31 | ## [0.1.3](https://github.com/open-feature/go-sdk-contrib/compare/providers/from-env/v0.1.2...providers/from-env/v0.1.3) (2023-07-21) 32 | 33 | 34 | ### 🐛 Bug Fixes 35 | 36 | * **deps:** update module github.com/open-feature/go-sdk to v1.5.1 ([#263](https://github.com/open-feature/go-sdk-contrib/issues/263)) ([c75ffd6](https://github.com/open-feature/go-sdk-contrib/commit/c75ffd6017689a86860dec92c1a1564b6145f0c9)) 37 | 38 | 39 | ### 🧹 Chore 40 | 41 | * update module github.com/open-feature/go-sdk to v1.4.0 ([#223](https://github.com/open-feature/go-sdk-contrib/issues/223)) ([7c8ea46](https://github.com/open-feature/go-sdk-contrib/commit/7c8ea46e3e094f746dbf6d80ba6a1b606314e8d7)) 42 | 43 | ## [0.1.2](https://github.com/open-feature/go-sdk-contrib/compare/providers/from-env/v0.1.1...providers/from-env/v0.1.2) (2023-02-21) 44 | 45 | 46 | ### Bug Fixes 47 | 48 | * **deps:** update module github.com/open-feature/go-sdk to v1.2.0 ([#103](https://github.com/open-feature/go-sdk-contrib/issues/103)) ([eedb577](https://github.com/open-feature/go-sdk-contrib/commit/eedb577745fd98d5189132ebbaa8eb82bdf99dd8)) 49 | 50 | ## [0.1.1](https://github.com/open-feature/go-sdk-contrib/compare/providers/from-env-v0.1.0...providers/from-env/v0.1.1) (2023-01-26) 51 | 52 | 53 | ### Bug Fixes 54 | 55 | * tidy workspaces ([#97](https://github.com/open-feature/go-sdk-contrib/issues/97)) ([c71a5ec](https://github.com/open-feature/go-sdk-contrib/commit/c71a5ec7686ec0572bb47f17dbca7e0ec48252d7)) 56 | -------------------------------------------------------------------------------- /providers/from-env/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/open-feature/go-sdk-contrib/providers/from-env 2 | 3 | go 1.21 4 | 5 | require github.com/open-feature/go-sdk v1.11.0 6 | 7 | require ( 8 | github.com/go-logr/logr v1.4.1 // indirect 9 | golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /providers/from-env/go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= 2 | github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 3 | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 4 | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 5 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 6 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 7 | github.com/open-feature/go-sdk v1.9.0 h1:1Nyj+XNHfL0rRGZgGCbZ29CHDD57PQJL7Q/2ZbW/E8c= 8 | github.com/open-feature/go-sdk v1.9.0/go.mod h1:n5BM4DfvIiKaWWquZnL/yVihcGM5aLsz7rNYE3BkXAM= 9 | github.com/open-feature/go-sdk v1.10.0 h1:druQtYOrN+gyz3rMsXp0F2jW1oBXJb0V26PVQnUGLbM= 10 | github.com/open-feature/go-sdk v1.10.0/go.mod h1:+rkJhLBtYsJ5PZNddAgFILhRAAxwrJ32aU7UEUm4zQI= 11 | github.com/open-feature/go-sdk v1.11.0 h1:4cp9rXl16ZvlMCef7O+I3vQSXae8DzAF0SfV9mvYInw= 12 | github.com/open-feature/go-sdk v1.11.0/go.mod h1:+rkJhLBtYsJ5PZNddAgFILhRAAxwrJ32aU7UEUm4zQI= 13 | golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb h1:mIKbk8weKhSeLH2GmUTrvx8CjkyJmnU1wFmg59CUjFA= 14 | golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= 15 | golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 h1:/RIbNt/Zr7rVhIkQhooTxCxFcdWLGIKnZA4IXNFSrvo= 16 | golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= 17 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 18 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 19 | -------------------------------------------------------------------------------- /providers/from-env/pkg/env.go: -------------------------------------------------------------------------------- 1 | package from_env 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/open-feature/go-sdk/openfeature" 9 | ) 10 | 11 | type envFetch struct { 12 | mapper FlagToEnvMapper 13 | } 14 | 15 | func (ef *envFetch) fetchStoredFlag(key string) (StoredFlag, error) { 16 | v := StoredFlag{} 17 | mappedKey := key 18 | 19 | if ef.mapper != nil { 20 | mappedKey = ef.mapper(key) 21 | } 22 | 23 | if val := os.Getenv(mappedKey); val != "" { 24 | if err := json.Unmarshal([]byte(val), &v); err != nil { 25 | return v, openfeature.NewParseErrorResolutionError(err.Error()) 26 | } 27 | return v, nil 28 | } 29 | 30 | msg := fmt.Sprintf("key %s not found in environment variables", mappedKey) 31 | 32 | return v, openfeature.NewFlagNotFoundResolutionError(msg) 33 | } 34 | -------------------------------------------------------------------------------- /providers/from-env/pkg/eval.go: -------------------------------------------------------------------------------- 1 | package from_env 2 | 3 | import ( 4 | "github.com/open-feature/go-sdk/openfeature" 5 | ) 6 | 7 | type StoredFlag struct { 8 | DefaultVariant string `json:"defaultVariant"` 9 | Variants []Variant `json:"variants"` 10 | } 11 | 12 | type Variant struct { 13 | Criteria []Criteria `json:"criteria"` 14 | TargetingKey string `json:"targetingKey"` 15 | Value interface{} `json:"value"` 16 | Name string `json:"name"` 17 | } 18 | 19 | type Criteria struct { 20 | Key string `json:"key"` 21 | Value interface{} `json:"value"` 22 | } 23 | 24 | func (f *StoredFlag) evaluate(evalCtx map[string]interface{}) (string, openfeature.Reason, interface{}, error) { 25 | var defaultVariant *Variant 26 | for _, variant := range f.Variants { 27 | if variant.Name == f.DefaultVariant { 28 | v := variant 29 | defaultVariant = &v 30 | } 31 | if variant.TargetingKey != "" && variant.TargetingKey != evalCtx["targetingKey"] { 32 | continue 33 | } 34 | match := true 35 | for _, criteria := range variant.Criteria { 36 | val, ok := evalCtx[criteria.Key] 37 | if !ok || val != criteria.Value { 38 | match = false 39 | break 40 | } 41 | } 42 | if match { 43 | return variant.Name, openfeature.TargetingMatchReason, variant.Value, nil 44 | } 45 | } 46 | if defaultVariant == nil { 47 | return "", openfeature.ErrorReason, nil, openfeature.NewParseErrorResolutionError("") 48 | } 49 | return defaultVariant.Name, openfeature.DefaultReason, defaultVariant.Value, nil 50 | } 51 | -------------------------------------------------------------------------------- /providers/gcp/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-feature/go-sdk-contrib/b435fb47b3c31d5518079e4f8ed2edf4c8ff23ff/providers/gcp/README.md -------------------------------------------------------------------------------- /providers/go-feature-flag-in-process/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.1.0 (2024-08-14) 4 | 5 | 6 | ### ✨ New Features 7 | 8 | * **go-feature-flag:** GO Feature Flag in process using GO module ([#546](https://github.com/open-feature/go-sdk-contrib/issues/546)) ([ee5b36c](https://github.com/open-feature/go-sdk-contrib/commit/ee5b36c2d5ed3367dfe4e3f98b4aefd66f889580)) 9 | -------------------------------------------------------------------------------- /providers/go-feature-flag-in-process/README.md: -------------------------------------------------------------------------------- 1 | # GO Feature Flag In Process Provider 2 | 3 | > [!WARNING] 4 | > This provider is in process and has as dependency to GO Feature Flag completely; it means that it will include a lot of dependencies in your project. 5 | > 6 | > This provider is recommended if you want an OpenFeature facade in front of the GO Feature Flag go module. 7 | > If you aim to use the relay proxy, please check the [GO Feature Flag provider](../go-feature-flag/README.md). 8 | 9 | ## Install dependencies 10 | 11 | The first things we will do are to install the **Open Feature SDK** and the **GO Feature Flag In Process provider**. 12 | 13 | ```shell 14 | go get github.com/open-feature/go-sdk-contrib/providers/go-feature-flag-in-process 15 | ``` 16 | 17 | ## Initialize your Open Feature provider 18 | 19 | You can check the [GO Feature Flag documentation website](https://docs.gofeatureflag.org) to look how to configure the 20 | GO module. 21 | 22 | #### Example 23 | ```go 24 | options := gofeatureflaginprocess.ProviderOptions{ 25 | GOFeatureFlagConfig: &ffclient.Config{ 26 | PollingInterval: 10 * time.Second, 27 | Context: context.Background(), 28 | Retriever: &fileretriever.Retriever{ 29 | Path: "../testutils/module/flags.yaml", 30 | }, 31 | }, 32 | } 33 | provider, _ := gofeatureflaginprocess.NewProviderWithContext(ctx, options) 34 | ``` 35 | 36 | ## Initialize your Open Feature client 37 | 38 | To evaluate a flag, you need to have an OpenFeature configured in your app. 39 | This code block shows you how you can create a client that you can use in your application. 40 | 41 | ```go 42 | import ( 43 | // ... 44 | gofeatureflaginprocess "github.com/open-feature/go-sdk-contrib/providers/go-feature-flag/pkg" 45 | of "github.com/open-feature/go-sdk/openfeature" 46 | ) 47 | 48 | // ... 49 | 50 | options := gofeatureflaginprocess.ProviderOptions{ 51 | GOFeatureFlagConfig: &ffclient.Config{ 52 | PollingInterval: 10 * time.Second, 53 | Context: context.Background(), 54 | Retriever: &fileretriever.Retriever{ 55 | Path: "../testutils/module/flags.yaml", 56 | }, 57 | }, 58 | } 59 | provider, err := gofeatureflaginprocess.NewProviderWithContext(ctx, options) 60 | of.SetProvider(provider) 61 | client := of.NewClient("my-app") 62 | ``` 63 | 64 | ## Evaluate your flag 65 | 66 | This code block explain how you can create an `EvaluationContext` and use it to evaluate your flag. 67 | 68 | 69 | > In this example we are evaluating a `boolean` flag, but other types are available. 70 | > 71 | > **Refer to the [Open Feature documentation](https://openfeature.dev/docs/reference/concepts/evaluation-api#basic-evaluation) to know more about it.** 72 | 73 | ```go 74 | evaluationCtx := of.NewEvaluationContext( 75 | "1d1b9238-2591-4a47-94cf-d2bc080892f1", 76 | map[string]interface{}{ 77 | "firstname", "john", 78 | "lastname", "doe", 79 | "email", "john.doe@gofeatureflag.org", 80 | "admin", true, 81 | "anonymous", false, 82 | }) 83 | adminFlag, _ := client.BoolValue(context.TODO(), "flag-only-for-admin", false, evaluationCtx) 84 | if adminFlag { 85 | // flag "flag-only-for-admin" is true for the user 86 | } else { 87 | // flag "flag-only-for-admin" is false for the user 88 | } 89 | ``` -------------------------------------------------------------------------------- /providers/go-feature-flag-in-process/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/open-feature/go-sdk-contrib/providers/go-feature-flag-in-process 2 | 3 | go 1.22.5 4 | 5 | require ( 6 | github.com/open-feature/go-sdk v1.12.0 7 | github.com/stretchr/testify v1.9.0 8 | github.com/thomaspoignant/go-feature-flag v1.32.0 9 | ) 10 | 11 | require ( 12 | github.com/BurntSushi/toml v1.4.0 // indirect 13 | github.com/antlr4-go/antlr/v4 v4.13.0 // indirect 14 | github.com/blang/semver v3.5.1+incompatible // indirect 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/go-logr/logr v1.4.2 // indirect 17 | github.com/google/go-cmp v0.6.0 // indirect 18 | github.com/nikunjy/rules v1.5.0 // indirect 19 | github.com/pmezard/go-difflib v1.0.0 // indirect 20 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect 21 | gopkg.in/yaml.v3 v3.0.1 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /providers/go-feature-flag-in-process/pkg/eval_request.go: -------------------------------------------------------------------------------- 1 | package gofeatureflaginprocess 2 | 3 | import ( 4 | of "github.com/open-feature/go-sdk/openfeature" 5 | ) 6 | 7 | const targetingKey = "targetingKey" 8 | 9 | func NewEvalFlagRequest[T JsonType](flatCtx of.FlattenedContext, defaultValue T) (EvalFlagRequest, *of.ResolutionError) { 10 | if _, ok := flatCtx[targetingKey]; !ok { 11 | err := of.NewTargetingKeyMissingResolutionError("no targetingKey provided in the evaluation context") 12 | return EvalFlagRequest{}, &err 13 | } 14 | targetingKey, ok := flatCtx[targetingKey].(string) 15 | if !ok { 16 | err := of.NewTargetingKeyMissingResolutionError("targetingKey field MUST be a string") 17 | return EvalFlagRequest{}, &err 18 | } 19 | 20 | anonymous := true 21 | if val, ok := flatCtx["anonymous"].(bool); ok { 22 | anonymous = val 23 | } 24 | 25 | return EvalFlagRequest{ 26 | // We keep user to be compatible with old version of GO Feature Flag proxy. 27 | User: &UserRequest{ 28 | Key: targetingKey, 29 | Anonymous: anonymous, 30 | Custom: flatCtx, 31 | }, 32 | EvaluationContext: &EvaluationContextRequest{ 33 | Key: targetingKey, 34 | Custom: flatCtx, 35 | }, 36 | DefaultValue: defaultValue, 37 | }, nil 38 | } 39 | 40 | type EvalFlagRequest struct { 41 | // User The representation of a user for your feature flag system. 42 | // Deprecated: User please use EvaluationContext instead 43 | User *UserRequest `json:"user" xml:"user" form:"user" query:"user"` 44 | // EvaluationContext the context to evaluate the flag. 45 | EvaluationContext *EvaluationContextRequest `json:"evaluationContext,omitempty" xml:"evaluationContext,omitempty" form:"evaluationContext,omitempty" query:"evaluationContext,omitempty"` 46 | // The value will we use if we are not able to get the variation of the flag. 47 | DefaultValue interface{} `json:"defaultValue" xml:"defaultValue" form:"defaultValue" query:"defaultValue"` 48 | } 49 | 50 | // UserRequest The representation of a user for your feature flag system. 51 | type UserRequest struct { 52 | // Key is the identifier of the UserRequest. 53 | Key string `json:"key" xml:"key" form:"key" query:"key" example:"08b5ffb7-7109-42f4-a6f2-b85560fbd20f"` 54 | 55 | // Anonymous set if this is a logged-in user or not. 56 | Anonymous bool `json:"anonymous" xml:"anonymous" form:"anonymous" query:"anonymous" example:"false"` 57 | 58 | // Custom is a map containing all extra information for this user. 59 | Custom map[string]interface{} `json:"custom" xml:"custom" form:"custom" query:"custom" swaggertype:"object,string" example:"email:contact@gofeatureflag.org,firstname:John,lastname:Doe,company:GO Feature Flag"` // nolint: lll 60 | } 61 | 62 | // EvaluationContextRequest The representation of the evaluation context. 63 | type EvaluationContextRequest struct { 64 | // Key is the identifier of the UserRequest. 65 | Key string `json:"key" xml:"key" form:"key" query:"key" example:"08b5ffb7-7109-42f4-a6f2-b85560fbd20f"` 66 | 67 | // Custom is a map containing all extra information for this user. 68 | Custom map[string]interface{} `json:"custom" xml:"custom" form:"custom" query:"custom" swaggertype:"object,string" example:"email:contact@gofeatureflag.org,firstname:John,lastname:Doe,company:GO Feature Flag"` // nolint: lll 69 | } 70 | -------------------------------------------------------------------------------- /providers/go-feature-flag-in-process/pkg/generic_evaluation_detail.go: -------------------------------------------------------------------------------- 1 | package gofeatureflaginprocess 2 | 3 | import of "github.com/open-feature/go-sdk/openfeature" 4 | 5 | type GenericResolutionDetail[T JsonType] struct { 6 | Value T 7 | of.ProviderResolutionDetail 8 | } 9 | -------------------------------------------------------------------------------- /providers/go-feature-flag-in-process/pkg/json_type.go: -------------------------------------------------------------------------------- 1 | package gofeatureflaginprocess 2 | 3 | type JsonType interface { 4 | float64 | int64 | string | bool | interface{} 5 | } 6 | -------------------------------------------------------------------------------- /providers/go-feature-flag-in-process/pkg/provider_options.go: -------------------------------------------------------------------------------- 1 | package gofeatureflaginprocess 2 | 3 | import ( 4 | ffclient "github.com/thomaspoignant/go-feature-flag" 5 | ) 6 | 7 | // ProviderOptions is the struct containing the provider options you can 8 | // use while initializing GO Feature Flag. 9 | // To have a valid configuration you need to have an Endpoint or GOFeatureFlagConfig set. 10 | type ProviderOptions struct { 11 | // GOFeatureFlagConfig is the configuration struct for the GO Feature Flag module. 12 | // If not nil we will launch the provider using the GO Feature Flag module. 13 | GOFeatureFlagConfig *ffclient.Config 14 | } 15 | -------------------------------------------------------------------------------- /providers/go-feature-flag-in-process/testutils/module/flags.yaml: -------------------------------------------------------------------------------- 1 | bool_targeting_match: 2 | variations: 3 | Default: false 4 | "False": false 5 | "True": true 6 | targeting: 7 | - query: email eq "john.doe@gofeatureflag.org" 8 | variation: "True" 9 | defaultRule: 10 | percentage: 11 | "False": 0 12 | "True": 100 13 | disabled_bool: 14 | variations: 15 | Default: false 16 | "False": false 17 | "True": true 18 | defaultRule: 19 | percentage: 20 | "False": 0 21 | "True": 100 22 | disable: true 23 | disabled_float: 24 | variations: 25 | Default: 103.25 26 | "False": 101.25 27 | "True": 100.25 28 | defaultRule: 29 | percentage: 30 | "False": 0 31 | "True": 100 32 | disable: true 33 | disabled_int: 34 | variations: 35 | Default: 103 36 | "False": 101 37 | "True": 100 38 | defaultRule: 39 | percentage: 40 | "False": 0 41 | "True": 100 42 | disable: true 43 | disabled_interface: 44 | variations: 45 | Default: 46 | test: default 47 | "False": 48 | test: "false" 49 | "True": 50 | test: test1 51 | test2: false 52 | test3: 123.3 53 | test4: 1 54 | defaultRule: 55 | percentage: 56 | "False": 0 57 | "True": 100 58 | disable: true 59 | disabled_string: 60 | variations: 61 | Default: CC0002 62 | "False": CC0001 63 | "True": CC0000 64 | defaultRule: 65 | percentage: 66 | "False": 0 67 | "True": 100 68 | disable: true 69 | double_key: 70 | variations: 71 | Default: 103.25 72 | "False": 101.25 73 | "True": 100.25 74 | targeting: 75 | - query: email eq "john.doe@gofeatureflag.org" 76 | variation: "True" 77 | defaultRule: 78 | percentage: 79 | "False": 0 80 | "True": 100 81 | integer_key: 82 | variations: 83 | Default: 103 84 | "False": 101 85 | "True": 100 86 | targeting: 87 | - query: email eq "john.doe@gofeatureflag.org" 88 | variation: "True" 89 | defaultRule: 90 | percentage: 91 | "False": 0 92 | "True": 100 93 | object_key: 94 | variations: 95 | Default: 96 | test: default 97 | "False": 98 | test: "false" 99 | "True": 100 | test: test1 101 | test2: false 102 | test3: 123.3 103 | test4: 1 104 | targeting: 105 | - query: email eq "john.doe@gofeatureflag.org" 106 | variation: "True" 107 | defaultRule: 108 | percentage: 109 | "False": 0 110 | "True": 100 111 | string_key: 112 | variations: 113 | Default: CC0002 114 | "False": CC0001 115 | "True": CC0000 116 | targeting: 117 | - query: email eq "john.doe@gofeatureflag.org" 118 | variation: "True" 119 | defaultRule: 120 | percentage: 121 | "False": 0 122 | "True": 100 123 | string_key_with_version: 124 | variations: 125 | Default: CC0002 126 | "False": CC0001 127 | "True": CC0000 128 | targeting: 129 | - query: email eq "john.doe@gofeatureflag.org" 130 | variation: "True" 131 | defaultRule: 132 | percentage: 133 | "False": 0 134 | "True": 100 135 | -------------------------------------------------------------------------------- /providers/go-feature-flag/.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | -------------------------------------------------------------------------------- /providers/go-feature-flag/README.md: -------------------------------------------------------------------------------- 1 | # GO Feature Flag GO Provider 2 | 3 | GO Feature Flag provider allows you to connect to your GO Feature Flag instance. 4 | 5 | [GO Feature Flag](https://gofeatureflag.org) believes in simplicity and offers a simple and lightweight solution to use feature flags. 6 | Our focus is to avoid any complex infrastructure work to use GO Feature Flag. 7 | 8 | This is a complete feature flagging solution with the possibility to target only a group of users, use any types of flags, store your configuration in various location and advanced rollout functionality. You can also collect usage data of your flags and be notified of configuration changes. 9 | 10 | 11 | # GO SDK usage 12 | 13 | ## Install dependencies 14 | 15 | The first things we will do are to install the **Open Feature SDK** and the **GO Feature Flag provider**. 16 | 17 | ```shell 18 | go get github.com/open-feature/go-sdk-contrib/providers/go-feature-flag 19 | ``` 20 | 21 | ## Initialize your Open Feature provider 22 | 23 | ### Connecting to the relay proxy 24 | 25 | This provider has to connect with the **relay proxy**, to do that you should set the field `Endpoint` in the options. 26 | By default it will use a default `HTTPClient` with a **timeout** configured at **10000** milliseconds. You can change 27 | this configuration by providing your own configuration of the `HTTPClient`. 28 | 29 | #### Example 30 | ```go 31 | options := gofeatureflag.ProviderOptions{ 32 | Endpoint: "http://localhost:1031", 33 | HTTPClient: &http.Client{ 34 | Timeout: 1 * time.Second, 35 | }, 36 | } 37 | provider, _ := gofeatureflag.NewProviderWithContext(ctx, options) 38 | ``` 39 | 40 | ## Initialize your Open Feature client 41 | 42 | To evaluate the flag you need to have an Open Feature configured in you app. 43 | This code block shows you how you can create a client that you can use in your application. 44 | 45 | ```go 46 | import ( 47 | // ... 48 | gofeatureflag "github.com/open-feature/go-sdk-contrib/providers/go-feature-flag/pkg" 49 | of "github.com/open-feature/go-sdk/openfeature" 50 | ) 51 | 52 | // ... 53 | 54 | options := gofeatureflag.ProviderOptions{ 55 | Endpoint: "http://localhost:1031", 56 | } 57 | provider, err := gofeatureflag.NewProviderWithContext(ctx, options) 58 | of.SetProvider(provider) 59 | client := of.NewClient("my-app") 60 | ``` 61 | 62 | ## Evaluate your flag 63 | 64 | This code block explain how you can create an `EvaluationContext` and use it to evaluate your flag. 65 | 66 | 67 | > In this example we are evaluating a `boolean` flag, but other types are available. 68 | > 69 | > **Refer to the [Open Feature documentation](https://openfeature.dev/docs/reference/concepts/evaluation-api#basic-evaluation) to know more about it.** 70 | 71 | ```go 72 | evaluationCtx := of.NewEvaluationContext( 73 | "1d1b9238-2591-4a47-94cf-d2bc080892f1", 74 | map[string]interface{}{ 75 | "firstname": "john", 76 | "lastname": "doe", 77 | "email": "john.doe@gofeatureflag.org", 78 | "admin": true, 79 | "anonymous": false, 80 | }) 81 | adminFlag, _ := client.BooleanValue(context.TODO(), "flag-only-for-admin", false, evaluationCtx) 82 | if adminFlag { 83 | // flag "flag-only-for-admin" is true for the user 84 | } else { 85 | // flag "flag-only-for-admin" is false for the user 86 | } 87 | ``` 88 | -------------------------------------------------------------------------------- /providers/go-feature-flag/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/open-feature/go-sdk-contrib/providers/go-feature-flag 2 | 3 | go 1.21.0 4 | 5 | toolchain go1.24.3 6 | 7 | require ( 8 | github.com/bluele/gcache v0.0.2 9 | github.com/open-feature/go-sdk v1.11.0 10 | github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.5 11 | github.com/stretchr/testify v1.9.0 12 | ) 13 | 14 | require ( 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/go-logr/logr v1.4.2 // indirect 17 | github.com/kr/pretty v0.3.1 // indirect 18 | github.com/pmezard/go-difflib v1.0.0 // indirect 19 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect 20 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 21 | gopkg.in/yaml.v3 v3.0.1 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /providers/go-feature-flag/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw= 2 | github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0= 3 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 7 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 8 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 9 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 10 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 11 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 12 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 13 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 14 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 15 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 16 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 17 | github.com/open-feature/go-sdk v1.11.0 h1:4cp9rXl16ZvlMCef7O+I3vQSXae8DzAF0SfV9mvYInw= 18 | github.com/open-feature/go-sdk v1.11.0/go.mod h1:+rkJhLBtYsJ5PZNddAgFILhRAAxwrJ32aU7UEUm4zQI= 19 | github.com/open-feature/go-sdk v1.14.1 h1:jcxjCIG5Up3XkgYwWN5Y/WWfc6XobOhqrIwjyDBsoQo= 20 | github.com/open-feature/go-sdk v1.14.1/go.mod h1:t337k0VB/t/YxJ9S0prT30ISUHwYmUd/jhUZgFcOvGg= 21 | github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.5 h1:ZdqlGnNwhWf3luhBQlIpbglvcCzjkcuEgOEhYhr5Emc= 22 | github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.5/go.mod h1:jrD4UG3ZCzuwImKHlyuIN2iWeYjlOX5+zJ/sX45efuE= 23 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 24 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 25 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 26 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 27 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 28 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 29 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 30 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= 31 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= 32 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 33 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 34 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 35 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 36 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 37 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 38 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 39 | -------------------------------------------------------------------------------- /providers/go-feature-flag/pkg/controller/configuration_change_status.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | type ConfigurationChangeStatus = string 4 | 5 | const ( 6 | FlagConfigurationInitialized ConfigurationChangeStatus = "FLAG_CONFIGURATION_INITIALIZED" 7 | FlagConfigurationUpdated ConfigurationChangeStatus = "FLAG_CONFIGURATION_UPDATED" 8 | FlagConfigurationNotChanged ConfigurationChangeStatus = "FLAG_CONFIGURATION_NOT_CHANGED" 9 | ErrorConfigurationChange ConfigurationChangeStatus = "ERROR_CONFIGURATION_CHANGE" 10 | ) 11 | -------------------------------------------------------------------------------- /providers/go-feature-flag/pkg/controller/data_collector_manager.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "github.com/open-feature/go-sdk-contrib/providers/go-feature-flag/pkg/model" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | // DataCollectorManager is a manager for the GO Feature Flag data collector 11 | type DataCollectorManager struct { 12 | mutex *sync.Mutex 13 | goffAPI GoFeatureFlagAPI 14 | events []model.FeatureEvent 15 | dataCollectorMaxEventStored int64 16 | 17 | ticker *time.Ticker 18 | collectChannel chan bool 19 | } 20 | 21 | // NewDataCollectorManager creates a new data collector manager 22 | func NewDataCollectorManager( 23 | goffAPI GoFeatureFlagAPI, 24 | dataCollectorMaxEventStored int64, 25 | collectInterval time.Duration) DataCollectorManager { 26 | if dataCollectorMaxEventStored <= 0 { 27 | dataCollectorMaxEventStored = 100000 28 | } 29 | if collectInterval <= 0 { 30 | collectInterval = 1 * time.Minute 31 | } 32 | return DataCollectorManager{ 33 | mutex: &sync.Mutex{}, 34 | goffAPI: goffAPI, 35 | events: make([]model.FeatureEvent, 0), 36 | dataCollectorMaxEventStored: dataCollectorMaxEventStored, 37 | ticker: time.NewTicker(collectInterval), 38 | collectChannel: make(chan bool), 39 | } 40 | } 41 | 42 | func (d *DataCollectorManager) Start() { 43 | go func() { 44 | for { 45 | select { 46 | case <-d.collectChannel: 47 | return 48 | case <-d.ticker.C: 49 | _ = d.SendData() 50 | } 51 | } 52 | }() 53 | } 54 | 55 | func (d *DataCollectorManager) Stop() { 56 | d.collectChannel <- true 57 | d.ticker.Stop() 58 | } 59 | 60 | // SendData sends the data to the data collector 61 | func (d *DataCollectorManager) SendData() error { 62 | d.mutex.Lock() 63 | defer d.mutex.Unlock() 64 | 65 | if len(d.events) <= 0 { 66 | return nil 67 | } 68 | 69 | copySend := make([]model.FeatureEvent, len(d.events)) 70 | copy(copySend, d.events) 71 | err := d.goffAPI.CollectData(copySend) 72 | if err != nil { 73 | return err 74 | } 75 | d.events = make([]model.FeatureEvent, 0) 76 | return nil 77 | } 78 | 79 | // AddEvent adds an event to the data collector manager 80 | // If the number of events in the queue is greater than the maxItem, the event will be skipped 81 | func (d *DataCollectorManager) AddEvent(event model.FeatureEvent) error { 82 | d.mutex.Lock() 83 | defer d.mutex.Unlock() 84 | 85 | if nbItem := int64(len(d.events)); nbItem >= d.dataCollectorMaxEventStored { 86 | return fmt.Errorf("too many events in the queue, this event will be skipped: %d", nbItem) 87 | } 88 | 89 | d.events = append(d.events, event) 90 | return nil 91 | } 92 | -------------------------------------------------------------------------------- /providers/go-feature-flag/pkg/controller/http_client.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | ) 7 | 8 | func DefaultHTTPClient() *http.Client { 9 | netTransport := &http.Transport{ 10 | TLSHandshakeTimeout: 10000 * time.Millisecond, 11 | IdleConnTimeout: 90 * time.Second, 12 | } 13 | 14 | return &http.Client{ 15 | Timeout: 10000 * time.Millisecond, 16 | Transport: netTransport, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /providers/go-feature-flag/pkg/controller/http_constants.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | const ContentTypeHeader = "Content-Type" 4 | const IfNoneMatchHeader = "If-None-Match" 5 | const AuthorizationHeader = "Authorization" 6 | 7 | const ApplicationJson = "application/json" 8 | const BearerPrefix = "Bearer " 9 | -------------------------------------------------------------------------------- /providers/go-feature-flag/pkg/goff_error/invalid_option.go: -------------------------------------------------------------------------------- 1 | package goff_error 2 | 3 | type InvalidOption struct { 4 | Message string 5 | } 6 | 7 | func (i InvalidOption) Error() string { 8 | return i.Message 9 | } 10 | 11 | func NewInvalidOption(message string) InvalidOption { 12 | return InvalidOption{Message: message} 13 | } 14 | -------------------------------------------------------------------------------- /providers/go-feature-flag/pkg/hook/data_collector_hook.go: -------------------------------------------------------------------------------- 1 | package hook 2 | 3 | import ( 4 | "context" 5 | "github.com/open-feature/go-sdk-contrib/providers/go-feature-flag/pkg/controller" 6 | "github.com/open-feature/go-sdk-contrib/providers/go-feature-flag/pkg/model" 7 | "github.com/open-feature/go-sdk/openfeature" 8 | "time" 9 | ) 10 | 11 | func NewDataCollectorHook(dataCollectorManager *controller.DataCollectorManager) openfeature.Hook { 12 | return &dataCollectorHook{dataCollectorManager: dataCollectorManager} 13 | } 14 | 15 | type dataCollectorHook struct { 16 | openfeature.UnimplementedHook 17 | dataCollectorManager *controller.DataCollectorManager 18 | } 19 | 20 | func (d *dataCollectorHook) After(_ context.Context, hookCtx openfeature.HookContext, 21 | evalDetails openfeature.InterfaceEvaluationDetails, hint openfeature.HookHints) error { 22 | if evalDetails.Reason != openfeature.CachedReason { 23 | // we send it only when cached because the evaluation will be collected directly in the relay-proxy 24 | return nil 25 | } 26 | event := model.FeatureEvent{ 27 | Kind: "feature", 28 | ContextKind: "user", 29 | UserKey: hookCtx.EvaluationContext().TargetingKey(), 30 | CreationDate: time.Now().Unix(), 31 | Key: hookCtx.FlagKey(), 32 | Variation: evalDetails.Variant, 33 | Value: evalDetails.Value, 34 | Default: false, 35 | Source: "PROVIDER_CACHE", 36 | } 37 | _ = d.dataCollectorManager.AddEvent(event) 38 | return nil 39 | } 40 | 41 | func (d *dataCollectorHook) Error(_ context.Context, hookCtx openfeature.HookContext, 42 | err error, hint openfeature.HookHints) { 43 | event := model.FeatureEvent{ 44 | Kind: "feature", 45 | ContextKind: "user", 46 | UserKey: hookCtx.EvaluationContext().TargetingKey(), 47 | CreationDate: time.Now().Unix(), 48 | Key: hookCtx.FlagKey(), 49 | Variation: "SdkDefault", 50 | Value: hookCtx.DefaultValue(), 51 | Default: true, 52 | Source: "PROVIDER_CACHE", 53 | } 54 | _ = d.dataCollectorManager.AddEvent(event) 55 | } 56 | -------------------------------------------------------------------------------- /providers/go-feature-flag/pkg/hook/evaluation_enrichment_hook.go: -------------------------------------------------------------------------------- 1 | package hook 2 | 3 | import ( 4 | "context" 5 | "github.com/open-feature/go-sdk/openfeature" 6 | ) 7 | 8 | func NewEvaluationEnrichmentHook(exporterMetadata map[string]interface{}) openfeature.Hook { 9 | return &evaluationEnrichmentHook{exporterMetadata: exporterMetadata} 10 | } 11 | 12 | type evaluationEnrichmentHook struct { 13 | openfeature.UnimplementedHook 14 | exporterMetadata map[string]interface{} 15 | } 16 | 17 | func (d *evaluationEnrichmentHook) Before(_ context.Context, hookCtx openfeature.HookContext, _ openfeature.HookHints) (*openfeature.EvaluationContext, error) { 18 | attributes := hookCtx.EvaluationContext().Attributes() 19 | if goffSpecific, ok := attributes["gofeatureflag"]; ok { 20 | switch typed := goffSpecific.(type) { 21 | case map[string]interface{}: 22 | typed["exporterMetadata"] = d.exporterMetadata 23 | default: 24 | attributes["gofeatureflag"] = map[string]interface{}{"exporterMetadata": d.exporterMetadata} 25 | } 26 | } else { 27 | attributes["gofeatureflag"] = map[string]interface{}{"exporterMetadata": d.exporterMetadata} 28 | } 29 | newCtx := openfeature.NewEvaluationContext(hookCtx.EvaluationContext().TargetingKey(), attributes) 30 | return &newCtx, nil 31 | } 32 | -------------------------------------------------------------------------------- /providers/go-feature-flag/pkg/model/data_collector_request.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type DataCollectorRequest struct { 4 | Events []FeatureEvent `json:"events"` 5 | Meta map[string]interface{} `json:"meta"` 6 | } 7 | -------------------------------------------------------------------------------- /providers/go-feature-flag/pkg/model/feature_event.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "encoding/json" 5 | of "github.com/open-feature/go-sdk/openfeature" 6 | "time" 7 | ) 8 | 9 | func NewFeatureEvent( 10 | evalCtx of.EvaluationContext, 11 | flagKey string, 12 | value interface{}, 13 | variation string, 14 | failed bool, 15 | version string, 16 | source string, 17 | ) FeatureEvent { 18 | contextKind := "user" 19 | if evalCtx.Attribute("anonymous") == true { 20 | contextKind = "anonymousUser" 21 | } 22 | 23 | return FeatureEvent{ 24 | Kind: "feature", 25 | ContextKind: contextKind, 26 | UserKey: evalCtx.TargetingKey(), 27 | CreationDate: time.Now().Unix(), 28 | Key: flagKey, 29 | Variation: variation, 30 | Value: value, 31 | Default: failed, 32 | Version: version, 33 | Source: source, 34 | } 35 | } 36 | 37 | // FeatureEvent represent an event that we store in the data storage 38 | // nolint:lll 39 | type FeatureEvent struct { 40 | // Kind for a feature event is feature. 41 | // A feature event will only be generated if the trackEvents attribute of the flag is set to true. 42 | Kind string `json:"kind" example:"feature" parquet:"name=kind, type=BYTE_ARRAY, convertedtype=UTF8"` 43 | 44 | // ContextKind is the kind of context which generated an event. This will only be "anonymousUser" for events generated 45 | // on behalf of an anonymous user or the reserved word "user" for events generated on behalf of a non-anonymous user 46 | ContextKind string `json:"contextKind,omitempty" example:"user" parquet:"name=contextKind, type=BYTE_ARRAY, convertedtype=UTF8"` 47 | 48 | // UserKey The key of the user object used in a feature flag evaluation. Details for the user object used in a feature 49 | // flag evaluation as reported by the "feature" event are transmitted periodically with a separate index event. 50 | UserKey string `json:"userKey" example:"94a25909-20d8-40cc-8500-fee99b569345" parquet:"name=userKey, type=BYTE_ARRAY, convertedtype=UTF8"` 51 | 52 | // CreationDate When the feature flag was requested at Unix epoch time in milliseconds. 53 | CreationDate int64 `json:"creationDate" example:"1680246000011" parquet:"name=creationDate, type=INT64"` 54 | 55 | // Key of the feature flag requested. 56 | Key string `json:"key" example:"my-feature-flag" parquet:"name=key, type=BYTE_ARRAY, convertedtype=UTF8"` 57 | 58 | // Variation of the flag requested. Flag variation values can be "True", "False", "Default" or "SdkDefault" 59 | // depending on which value was taken during flag evaluation. "SdkDefault" is used when an error is detected and the 60 | // default value passed during the call to your variation is used. 61 | Variation string `json:"variation" example:"admin-variation" parquet:"name=variation, type=BYTE_ARRAY, convertedtype=UTF8"` 62 | 63 | // Value of the feature flag returned by feature flag evaluation. 64 | Value interface{} `json:"value" parquet:"name=value, type=BYTE_ARRAY, convertedtype=UTF8"` 65 | 66 | // Default value is set to true if feature flag evaluation failed, in which case the value returned was the default 67 | // value passed to variation. If the default field is omitted, it is assumed to be false. 68 | Default bool `json:"default" example:"false" parquet:"name=default, type=BOOLEAN"` 69 | 70 | // Version contains the version of the flag. If the field is omitted for the flag in the configuration file 71 | // the default version will be 0. 72 | Version string `json:"version" example:"v1.0.0" parquet:"name=version, type=BYTE_ARRAY, convertedtype=UTF8"` 73 | 74 | // Source indicates where the event was generated. 75 | // This is set to SERVER when the event was evaluated in the relay-proxy and PROVIDER_CACHE when it is evaluated from the cache. 76 | Source string `json:"source" example:"SERVER" parquet:"name=source, type=BYTE_ARRAY, convertedtype=UTF8"` 77 | } 78 | 79 | // MarshalInterface marshals all interface type fields in FeatureEvent into JSON-encoded string. 80 | func (f *FeatureEvent) MarshalInterface() error { 81 | if f == nil { 82 | return nil 83 | } 84 | b, err := json.Marshal(f.Value) 85 | if err != nil { 86 | return err 87 | } 88 | f.Value = string(b) 89 | return nil 90 | } 91 | -------------------------------------------------------------------------------- /providers/go-feature-flag/pkg/model/feature_event_test.go: -------------------------------------------------------------------------------- 1 | package model_test 2 | 3 | import ( 4 | "github.com/open-feature/go-sdk-contrib/providers/go-feature-flag/pkg/model" 5 | of "github.com/open-feature/go-sdk/openfeature" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestNewFeatureEvent(t *testing.T) { 13 | type args struct { 14 | user of.EvaluationContext 15 | flagKey string 16 | value interface{} 17 | variation string 18 | failed bool 19 | version string 20 | source string 21 | } 22 | tests := []struct { 23 | name string 24 | args args 25 | want model.FeatureEvent 26 | }{ 27 | { 28 | name: "anonymous user", 29 | args: args{ 30 | user: of.NewEvaluationContext("ABCD", map[string]interface{}{"anonymous": true}), 31 | flagKey: "random-key", 32 | value: "YO", 33 | variation: "Default", 34 | failed: false, 35 | version: "", 36 | source: "SERVER", 37 | }, 38 | want: model.FeatureEvent{ 39 | Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: time.Now().Unix(), Key: "random-key", 40 | Variation: "Default", Value: "YO", Default: false, Source: "SERVER", 41 | }, 42 | }, 43 | } 44 | for _, tt := range tests { 45 | t.Run(tt.name, func(t *testing.T) { 46 | assert.Equalf(t, tt.want, model.NewFeatureEvent(tt.args.user, tt.args.flagKey, tt.args.value, tt.args.variation, tt.args.failed, tt.args.version, tt.args.source), "NewFeatureEvent(%v, %v, %v, %v, %v, %v, %V)", tt.args.user, tt.args.flagKey, tt.args.value, tt.args.variation, tt.args.failed, tt.args.version, tt.args.source) 47 | }) 48 | } 49 | } 50 | 51 | func TestFeatureEvent_MarshalInterface(t *testing.T) { 52 | tests := []struct { 53 | name string 54 | featureEvent *model.FeatureEvent 55 | want *model.FeatureEvent 56 | wantErr bool 57 | }{ 58 | { 59 | name: "happy path", 60 | featureEvent: &model.FeatureEvent{ 61 | Kind: "feature", 62 | ContextKind: "anonymousUser", 63 | UserKey: "ABCD", 64 | CreationDate: 1617970547, 65 | Key: "random-key", 66 | Variation: "Default", 67 | Value: map[string]interface{}{ 68 | "string": "string", 69 | "bool": true, 70 | "float": 1.23, 71 | "int": 1, 72 | }, 73 | Default: false, 74 | }, 75 | want: &model.FeatureEvent{ 76 | Kind: "feature", 77 | ContextKind: "anonymousUser", 78 | UserKey: "ABCD", 79 | CreationDate: 1617970547, 80 | Key: "random-key", 81 | Variation: "Default", 82 | Value: `{"bool":true,"float":1.23,"int":1,"string":"string"}`, 83 | Default: false, 84 | }, 85 | }, 86 | { 87 | name: "marshal failed", 88 | featureEvent: &model.FeatureEvent{ 89 | Kind: "feature", 90 | ContextKind: "anonymousUser", 91 | UserKey: "ABCD", 92 | CreationDate: 1617970547, 93 | Key: "random-key", 94 | Variation: "Default", 95 | Value: make(chan int), 96 | Default: false, 97 | }, 98 | wantErr: true, 99 | }, 100 | { 101 | name: "nil featureEvent", 102 | featureEvent: nil, 103 | }, 104 | } 105 | for _, tt := range tests { 106 | t.Run(tt.name, func(t *testing.T) { 107 | if err := tt.featureEvent.MarshalInterface(); (err != nil) != tt.wantErr { 108 | t.Errorf("FeatureEvent.MarshalInterface() error = %v, wantErr %v", err, tt.wantErr) 109 | return 110 | } 111 | if tt.want != nil { 112 | assert.Equal(t, tt.want, tt.featureEvent) 113 | } 114 | }) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /providers/go-feature-flag/pkg/provider_options.go: -------------------------------------------------------------------------------- 1 | package gofeatureflag 2 | 3 | import ( 4 | "fmt" 5 | "github.com/open-feature/go-sdk-contrib/providers/go-feature-flag/pkg/goff_error" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | // ProviderOptions is the struct containing the provider options you can 11 | // use while initializing GO Feature Flag. 12 | // To have a valid configuration you need to have an Endpoint or GOFeatureFlagConfig set. 13 | type ProviderOptions struct { 14 | // Endpoint contains the DNS of your GO Feature Flag relay proxy (ex: http://localhost:1031) 15 | Endpoint string 16 | 17 | // HTTPClient (optional) is the HTTP Client we will use to contact GO Feature Flag. 18 | // By default, we are using a custom HTTPClient with a timeout configure to 10000 milliseconds. 19 | HTTPClient *http.Client 20 | 21 | // APIKey (optional) If the relay proxy is configured to authenticate the requests, you should provide 22 | // an API Key to the provider. Please ask the administrator of the relay proxy to provide an API Key. 23 | // (This feature is available only if you are using GO Feature Flag relay proxy v1.7.0 or above) 24 | // Default: null 25 | APIKey string 26 | 27 | // DisableCache (optional) set to true if you would like that every flag evaluation goes to the GO Feature Flag directly. 28 | DisableCache bool 29 | 30 | // FlagCacheSize (optional) is the maximum number of flag events we keep in memory to cache your flags. 31 | // default: 10000 32 | FlagCacheSize int 33 | 34 | // FlagCacheTTL (optional) is the time we keep the evaluation in the cache before we consider it as obsolete. 35 | // If you want to keep the value forever you can set the FlagCacheTTL field to -1 36 | // default: 1 minute 37 | FlagCacheTTL time.Duration 38 | 39 | // DataFlushInterval (optional) interval time we use to call the relay proxy to collect data. 40 | // The parameter is used only if the cache is enabled, otherwise the collection of the data is done directly 41 | // when calling the evaluation API. 42 | // default: 1 minute 43 | DataFlushInterval time.Duration 44 | 45 | // DataMaxEventInMemory (optional) maximum number of item we keep in memory before calling the API. 46 | // If this number is reached before the DataFlushInterval we will call the API. 47 | // The parameter is used only if the cache is enabled, otherwise the collection of the data is done directly 48 | // when calling the evaluation API. 49 | // default: 500 50 | DataMaxEventInMemory int64 51 | 52 | // DataCollectorMaxEventStored (optional) maximum number of event we keep in memory, if we reach this number it means 53 | // that we will start to drop the new events. This is a security to avoid a memory leak. 54 | // default: 100000 55 | DataCollectorMaxEventStored int64 56 | 57 | // DisableDataCollector (optional) set to true if you would like to disable the data collector. 58 | DisableDataCollector bool 59 | 60 | // FlagChangePollingInterval (optional) interval time we poll the proxy to check if the configuration has changed. 61 | // If the cache is enabled, we will poll the relay-proxy every X milliseconds to check if the configuration has changed. 62 | // Use -1 if you want to deactivate polling. 63 | // default: 120000ms 64 | FlagChangePollingInterval time.Duration 65 | 66 | // ExporterMetadata (optional) is the metadata we send to the GO Feature Flag relay proxy when we report the 67 | // evaluation data usage. 68 | // 69 | // ‼️Important: If you are using a GO Feature Flag relay proxy before version v1.41.0, the information of this 70 | // field will not be added to your feature events. 71 | ExporterMetadata map[string]interface{} 72 | } 73 | 74 | func (o *ProviderOptions) Validation() error { 75 | if o.Endpoint == "" { 76 | return goff_error.NewInvalidOption(fmt.Sprintf("invalid option: %s", o.Endpoint)) 77 | } 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /providers/go-feature-flag/pkg/util/targeting_key_validator.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "github.com/open-feature/go-sdk/openfeature" 4 | 5 | const targetingKey = "targetingKey" 6 | 7 | func ValidateTargetingKey(evalCtx openfeature.FlattenedContext) *openfeature.ResolutionError { 8 | if _, ok := evalCtx[targetingKey]; !ok { 9 | err := openfeature.NewTargetingKeyMissingResolutionError("no targetingKey provided in the evaluation context") 10 | return &err 11 | } 12 | 13 | if _, ok := evalCtx[targetingKey].(string); !ok { 14 | err := openfeature.NewTargetingKeyMissingResolutionError("targetingKey field MUST be a string") 15 | return &err 16 | } 17 | 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /providers/go-feature-flag/testutils/mock_responses/bool_targeting_match.json: -------------------------------------------------------------------------------- 1 | { 2 | "value": true, 3 | "key": "bool_targeting_match", 4 | "reason": "TARGETING_MATCH", 5 | "variant": "True", 6 | "metadata": { 7 | "gofeatureflag_cacheable": true 8 | } 9 | } -------------------------------------------------------------------------------- /providers/go-feature-flag/testutils/mock_responses/disabled_bool.json: -------------------------------------------------------------------------------- 1 | { 2 | "value": false, 3 | "key": "disabled_bool", 4 | "reason": "DISABLED", 5 | "variant": "SdkDefault", 6 | "metadata": {} 7 | } -------------------------------------------------------------------------------- /providers/go-feature-flag/testutils/mock_responses/disabled_float.json: -------------------------------------------------------------------------------- 1 | { 2 | "value": 123.45, 3 | "key": "disabled_float", 4 | "reason": "DISABLED", 5 | "variant": "SdkDefault", 6 | "metadata": {} 7 | } -------------------------------------------------------------------------------- /providers/go-feature-flag/testutils/mock_responses/disabled_int.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "disabled_int", 3 | "reason": "DISABLED", 4 | "variant": "SdkDefault", 5 | "metadata": {} 6 | } -------------------------------------------------------------------------------- /providers/go-feature-flag/testutils/mock_responses/disabled_string.json: -------------------------------------------------------------------------------- 1 | { 2 | "value": "default", 3 | "key": "disabled_string", 4 | "reason": "DISABLED", 5 | "variant": "SdkDefault", 6 | "metadata": {} 7 | } -------------------------------------------------------------------------------- /providers/go-feature-flag/testutils/mock_responses/double_key.json: -------------------------------------------------------------------------------- 1 | { 2 | "value": 100.25, 3 | "key": "double_key", 4 | "reason": "TARGETING_MATCH", 5 | "variant": "True", 6 | "metadata": {} 7 | } 8 | -------------------------------------------------------------------------------- /providers/go-feature-flag/testutils/mock_responses/flag_not_found.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "flag_not_found", 3 | "reason": "ERROR", 4 | "errorCode": "FLAG_NOT_FOUND", 5 | "metadata": {} 6 | } -------------------------------------------------------------------------------- /providers/go-feature-flag/testutils/mock_responses/integer_key.json: -------------------------------------------------------------------------------- 1 | { 2 | "value": 100, 3 | "key": "integer_key", 4 | "reason": "TARGETING_MATCH", 5 | "variant": "True", 6 | "metadata": {} 7 | } -------------------------------------------------------------------------------- /providers/go-feature-flag/testutils/mock_responses/invalid_json_body.json: -------------------------------------------------------------------------------- 1 | { 2 | "value": 100, 3 | "key": "integer_key", 4 | "reason": "TARGETING_MATCH", 5 | "variant": "True" 6 | -------------------------------------------------------------------------------- /providers/go-feature-flag/testutils/mock_responses/list_key.json: -------------------------------------------------------------------------------- 1 | { 2 | "value": [ 3 | "test", 4 | "test1", 5 | "test2", 6 | "false", 7 | "test3" 8 | ], 9 | "key": "list_key", 10 | "reason": "TARGETING_MATCH", 11 | "variant": "True", 12 | "metadata": {} 13 | } -------------------------------------------------------------------------------- /providers/go-feature-flag/testutils/mock_responses/object_key.json: -------------------------------------------------------------------------------- 1 | { 2 | "value": { 3 | "test": "test1", 4 | "test2": false, 5 | "test3": 123.3, 6 | "test4": 1, 7 | "test5": null 8 | }, 9 | "key": "object_key", 10 | "reason": "TARGETING_MATCH", 11 | "variant": "True", 12 | "metadata": {} 13 | } -------------------------------------------------------------------------------- /providers/go-feature-flag/testutils/mock_responses/string_key.json: -------------------------------------------------------------------------------- 1 | { 2 | "value": "CC0000", 3 | "key": "string_key", 4 | "reason": "TARGETING_MATCH", 5 | "variant": "True", 6 | "metadata": {} 7 | } -------------------------------------------------------------------------------- /providers/go-feature-flag/testutils/mock_responses/unknown_reason.json: -------------------------------------------------------------------------------- 1 | { 2 | "value": true, 3 | "key": "unknown_reason", 4 | "reason": "CUSTOM_REASON", 5 | "variant": "True", 6 | "metadata": {} 7 | } -------------------------------------------------------------------------------- /providers/go-feature-flag/testutils/module/flags.yaml: -------------------------------------------------------------------------------- 1 | bool_targeting_match: 2 | variations: 3 | Default: false 4 | "False": false 5 | "True": true 6 | targeting: 7 | - query: email eq "john.doe@gofeatureflag.org" 8 | variation: "True" 9 | defaultRule: 10 | percentage: 11 | "False": 0 12 | "True": 100 13 | disabled_bool: 14 | variations: 15 | Default: false 16 | "False": false 17 | "True": true 18 | defaultRule: 19 | percentage: 20 | "False": 0 21 | "True": 100 22 | disable: true 23 | disabled_float: 24 | variations: 25 | Default: 103.25 26 | "False": 101.25 27 | "True": 100.25 28 | defaultRule: 29 | percentage: 30 | "False": 0 31 | "True": 100 32 | disable: true 33 | disabled_int: 34 | variations: 35 | Default: 103 36 | "False": 101 37 | "True": 100 38 | defaultRule: 39 | percentage: 40 | "False": 0 41 | "True": 100 42 | disable: true 43 | disabled_interface: 44 | variations: 45 | Default: 46 | test: default 47 | "False": 48 | test: "false" 49 | "True": 50 | test: test1 51 | test2: false 52 | test3: 123.3 53 | test4: 1 54 | defaultRule: 55 | percentage: 56 | "False": 0 57 | "True": 100 58 | disable: true 59 | disabled_string: 60 | variations: 61 | Default: CC0002 62 | "False": CC0001 63 | "True": CC0000 64 | defaultRule: 65 | percentage: 66 | "False": 0 67 | "True": 100 68 | disable: true 69 | double_key: 70 | variations: 71 | Default: 103.25 72 | "False": 101.25 73 | "True": 100.25 74 | targeting: 75 | - query: email eq "john.doe@gofeatureflag.org" 76 | variation: "True" 77 | defaultRule: 78 | percentage: 79 | "False": 0 80 | "True": 100 81 | integer_key: 82 | variations: 83 | Default: 103 84 | "False": 101 85 | "True": 100 86 | targeting: 87 | - query: email eq "john.doe@gofeatureflag.org" 88 | variation: "True" 89 | defaultRule: 90 | percentage: 91 | "False": 0 92 | "True": 100 93 | object_key: 94 | variations: 95 | Default: 96 | test: default 97 | "False": 98 | test: "false" 99 | "True": 100 | test: test1 101 | test2: false 102 | test3: 123.3 103 | test4: 1 104 | targeting: 105 | - query: email eq "john.doe@gofeatureflag.org" 106 | variation: "True" 107 | defaultRule: 108 | percentage: 109 | "False": 0 110 | "True": 100 111 | string_key: 112 | variations: 113 | Default: CC0002 114 | "False": CC0001 115 | "True": CC0000 116 | targeting: 117 | - query: email eq "john.doe@gofeatureflag.org" 118 | variation: "True" 119 | defaultRule: 120 | percentage: 121 | "False": 0 122 | "True": 100 123 | string_key_with_version: 124 | variations: 125 | Default: CC0002 126 | "False": CC0001 127 | "True": CC0000 128 | targeting: 129 | - query: email eq "john.doe@gofeatureflag.org" 130 | variation: "True" 131 | defaultRule: 132 | percentage: 133 | "False": 0 134 | "True": 100 135 | -------------------------------------------------------------------------------- /providers/harness/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.0.4-alpha](https://github.com/open-feature/go-sdk-contrib/compare/providers/harness/v0.0.3-alpha...providers/harness/v0.0.4-alpha) (2024-01-05) 4 | 5 | 6 | ### 🐛 Bug Fixes 7 | 8 | * **deps:** update module github.com/harness/ff-golang-server-sdk to v0.1.15 ([#358](https://github.com/open-feature/go-sdk-contrib/issues/358)) ([afd3051](https://github.com/open-feature/go-sdk-contrib/commit/afd30515e98ef29adab4b895e7b58cd4ec2f1bba)) 9 | * **deps:** update module github.com/jarcoal/httpmock to v1.3.1 ([#362](https://github.com/open-feature/go-sdk-contrib/issues/362)) ([103de24](https://github.com/open-feature/go-sdk-contrib/commit/103de246316d242a70b56b07e0df13fb71777d7d)) 10 | * **deps:** update module github.com/open-feature/go-sdk to v1.8.0 ([#365](https://github.com/open-feature/go-sdk-contrib/issues/365)) ([dafbcb5](https://github.com/open-feature/go-sdk-contrib/commit/dafbcb5d88ebbd824bbe1fe6b667ba28d5d08b2e)) 11 | 12 | 13 | ### 🧹 Chore 14 | 15 | * update to go-sdk 1.9.0 ([#404](https://github.com/open-feature/go-sdk-contrib/issues/404)) ([11fa3ab](https://github.com/open-feature/go-sdk-contrib/commit/11fa3aba065a6dd81caca30e76efc16fb64a25e3)) 16 | 17 | ## [0.0.3-alpha](https://github.com/open-feature/go-sdk-contrib/compare/providers/harness/v0.0.2-alpha...providers/harness/v0.0.3-alpha) (2023-11-01) 18 | 19 | 20 | ### 🐛 Bug Fixes 21 | 22 | * go mod/sum mismatches ([#359](https://github.com/open-feature/go-sdk-contrib/issues/359)) ([98ae316](https://github.com/open-feature/go-sdk-contrib/commit/98ae316c9d97de62cf1b742ac5592d15db6bbbe2)) 23 | 24 | ## [0.0.2-alpha](https://github.com/open-feature/go-sdk-contrib/compare/providers/harness-v0.0.1-alpha...providers/harness/v0.0.2-alpha) (2023-10-31) 25 | 26 | 27 | ### ✨ New Features 28 | 29 | * Add Harness provider ([#348](https://github.com/open-feature/go-sdk-contrib/issues/348)) ([a6940bc](https://github.com/open-feature/go-sdk-contrib/commit/a6940bc495820f10e317434a89ac580ee925264c)) 30 | -------------------------------------------------------------------------------- /providers/harness/README.md: -------------------------------------------------------------------------------- 1 | # Unofficial Harness OpenFeature GO Provider 2 | 3 | [Harness](https://developer.harness.io/docs/feature-flags) OpenFeature Provider can provide usage for Harness via OpenFeature GO SDK. 4 | 5 | # Installation 6 | 7 | To use the Harness provider, you'll need to install [Harness Go client](github.com/harness/ff-golang-server-sdk) and Harness provider. You can install the packages using the following command 8 | 9 | ```shell 10 | go get github.com/harness/ff-golang-server-sdk 11 | go get github.com/open-feature/go-sdk-contrib/providers/harness 12 | ``` 13 | 14 | ## Concepts 15 | * Provider Object evaluation gets Harness JSON evaluation. 16 | * Other provider types evaluation gets Harness matching type evaluation. 17 | 18 | ## Usage 19 | Harness OpenFeature Provider is using Harness GO SDK. 20 | 21 | ### Evaluation Context 22 | Evaluation Context is mapped to Harness [target](https://developer.harness.io/docs/feature-flags/ff-sdks/server-sdks/feature-flag-sdks-go-application/#add-a-target). 23 | OpenFeature targetingKey is mapped to _Identifier_, _Name_ is mapped to _Name_ and other fields are mapped to Attributes 24 | fields. 25 | 26 | ### Usage Example 27 | 28 | ```go 29 | import ( 30 | harness "github.com/harness/ff-golang-server-sdk/client" 31 | harnessProvider "github.com/open-feature/go-sdk-contrib/providers/harness/pkg" 32 | ) 33 | 34 | providerConfig := harnessProvider.ProviderConfig{ 35 | Options: []harness.ConfigOption{ 36 | harness.WithWaitForInitialized(true), 37 | harness.WithURL(URL), 38 | harness.WithStreamEnabled(false), 39 | harness.WithHTTPClient(http.DefaultClient), 40 | harness.WithStoreEnabled(false), 41 | }, 42 | SdkKey: ValidSDKKey, 43 | } 44 | 45 | provider, err := harnessProvider.NewProvider(providerConfig) 46 | if err != nil { 47 | t.Fail() 48 | } 49 | err = provider.Init(of.EvaluationContext{}) 50 | if err != nil { 51 | t.Fail() 52 | } 53 | 54 | ctx := context.Background() 55 | 56 | of.SetProvider(provider) 57 | ofClient := of.NewClient("my-app") 58 | 59 | evalCtx := of.NewEvaluationContext( 60 | "john", 61 | map[string]interface{}{ 62 | "Firstname": "John", 63 | "Lastname": "Doe", 64 | "Email": "john@doe.com", 65 | }, 66 | ) 67 | enabled, err := ofClient.BooleanValue(context.Background(), "TestTrueOn", false, evalCtx) 68 | if enabled == false { 69 | t.Fatalf("Expected feature to be enabled") 70 | } 71 | 72 | ``` 73 | See [provider_test.go](./pkg/provider_test.go) for more information. 74 | 75 | -------------------------------------------------------------------------------- /providers/harness/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/open-feature/go-sdk-contrib/providers/harness 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/harness/ff-golang-server-sdk v0.1.25 7 | github.com/jarcoal/httpmock v1.4.0 8 | github.com/open-feature/go-sdk v1.11.0 9 | github.com/stretchr/testify v1.9.0 10 | ) 11 | 12 | require ( 13 | github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect 14 | github.com/cenkalti/backoff/v4 v4.2.1 // indirect 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/deepmap/oapi-codegen v1.11.0 // indirect 17 | github.com/deepmap/oapi-codegen/v2 v2.1.0 // indirect 18 | github.com/getkin/kin-openapi v0.124.0 // indirect 19 | github.com/ghodss/yaml v1.0.0 // indirect 20 | github.com/go-logr/logr v1.4.1 // indirect 21 | github.com/go-openapi/jsonpointer v0.20.2 // indirect 22 | github.com/go-openapi/swag v0.22.8 // indirect 23 | github.com/golang-jwt/jwt v3.2.2+incompatible // indirect 24 | github.com/google/uuid v1.5.0 // indirect 25 | github.com/harness-community/sse/v3 v3.1.0 // indirect 26 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 27 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect 28 | github.com/hashicorp/golang-lru v0.5.4 // indirect 29 | github.com/invopop/yaml v0.2.0 // indirect 30 | github.com/josharian/intern v1.0.0 // indirect 31 | github.com/json-iterator/go v1.1.12 // indirect 32 | github.com/kr/pretty v0.3.1 // indirect 33 | github.com/mailru/easyjson v0.7.7 // indirect 34 | github.com/mitchellh/go-homedir v1.1.0 // indirect 35 | github.com/mitchellh/mapstructure v1.3.3 // indirect 36 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 37 | github.com/modern-go/reflect2 v1.0.2 // indirect 38 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 39 | github.com/oapi-codegen/runtime v1.1.1 // indirect 40 | github.com/perimeterx/marshmallow v1.1.5 // indirect 41 | github.com/pmezard/go-difflib v1.0.0 // indirect 42 | github.com/spaolacci/murmur3 v1.1.0 // indirect 43 | go.uber.org/atomic v1.7.0 // indirect 44 | go.uber.org/multierr v1.6.0 // indirect 45 | go.uber.org/zap v1.16.0 // indirect 46 | golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 // indirect 47 | golang.org/x/net v0.25.0 // indirect 48 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect 49 | gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect 50 | gopkg.in/yaml.v2 v2.4.0 // indirect 51 | gopkg.in/yaml.v3 v3.0.1 // indirect 52 | ) 53 | -------------------------------------------------------------------------------- /providers/harness/pkg/provider_config.go: -------------------------------------------------------------------------------- 1 | package harness 2 | 3 | import ( 4 | harness "github.com/harness/ff-golang-server-sdk/client" 5 | ) 6 | 7 | type ProviderConfig struct { 8 | Options []harness.ConfigOption 9 | SdkKey string 10 | } 11 | -------------------------------------------------------------------------------- /providers/launchdarkly/README.md: -------------------------------------------------------------------------------- 1 | # Unofficial LaunchDarkly OpenFeature Provider for Go 2 | 3 | This provider is maintained by the Open Feature community. 4 | 5 | ## Installation 6 | 7 | ``` 8 | # LaunchDarkly SDK 9 | go get github.com/launchdarkly/go-sdk-common/v3/... 10 | go get github.com/launchdarkly/go-server-sdk/v7/... 11 | 12 | # Open Feature SDK 13 | go get github.com/open-feature/go-sdk/openfeature 14 | go get github.com/open-feature/go-sdk-contrib/providers/launchdarkly/pkg 15 | ``` 16 | 17 | ## Usage 18 | See [example_test.go](./example_test.go) 19 | 20 | ## Representing LaunchDarkly (multi) contexts 21 | 22 | The LaunchDarkly provider expects contexts to be either single- or 23 | multi-context, matching [LaunchDarkly's concept of Contexts](https://docs.launchdarkly.com/guides/flags/intro-contexts). 24 | The representation of LaunchDarkly context(s) within the OpenFeature 25 | context needs to be well-formed. 26 | 27 | ### Single context 28 | 29 | ```javascript 30 | { 31 | // The "kind" of the context. Required. 32 | // Cannot be missing, empty, "multi", or "kind". 33 | // Must match `[a-zA-Z0-9._-]*` 34 | // (The default LaunchDarkly kind is "user".) 35 | kind: string, 36 | 37 | // The targeting key. One of the following is required to be 38 | // present and non-empty. If both are present, `targetingKey` 39 | // takes precedence. 40 | key: string, 41 | targetingKey: string, 42 | 43 | // Private attribute annotations. Optional. 44 | // See https://docs.launchdarkly.com/sdk/features/private-attributes 45 | // for the formatting specifications. 46 | privateAttributes: [string], 47 | 48 | // Anonymous annotation. Optional. 49 | // See https://docs.launchdarkly.com/sdk/features/anonymous 50 | anonymous: bool, 51 | 52 | // Name. Optional. 53 | // If present, used by LaunchDarkly as the display name of the context. 54 | name: string|null, 55 | 56 | // Further attributes, in the normal OpenFeature format. 57 | // Attribute names can be any non-empty string except "_meta". 58 | // 59 | // Repeated `string: any` 60 | } 61 | ``` 62 | 63 | ### Multi context 64 | 65 | ```javascript 66 | { 67 | // The "kind" of the context. Required. 68 | // Must be "multi". 69 | kind: "multi", 70 | 71 | // Sub-contexts. Each further key is taken to be a "kind" (and 72 | // thus must match `[a-zA-Z0-9._-]`). 73 | // The value is should be an object, and is processed using the 74 | // rules described in [Single context](#single-context) above, 75 | // except that the "kind" attribute is ignored if present. 76 | // 77 | // Repeated `string: object` 78 | } 79 | ``` 80 | 81 | ### References 82 | * https://docs.openfeature.dev/blog/creating-a-provider-for-the-go-sdk/ 83 | -------------------------------------------------------------------------------- /providers/launchdarkly/example_test.go: -------------------------------------------------------------------------------- 1 | package launchdarkly_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/launchdarkly/go-sdk-common/v3/ldlog" 9 | "github.com/launchdarkly/go-server-sdk/v7/ldcomponents" 10 | "github.com/open-feature/go-sdk/openfeature" 11 | 12 | ld "github.com/launchdarkly/go-server-sdk/v7" 13 | ofld "github.com/open-feature/go-sdk-contrib/providers/launchdarkly/pkg" 14 | ) 15 | 16 | var emptyEvalCtx = openfeature.EvaluationContext{} 17 | 18 | func Example() { 19 | var config ld.Config 20 | config.Logging = ldcomponents.Logging().MinLevel(ldlog.Debug) 21 | ldClient, err := ld.MakeCustomClient("my api key", config, 5*time.Second) 22 | if err != nil { 23 | panic(err) 24 | } 25 | 26 | // Flushes all pending analytics events. 27 | defer ldClient.Close() 28 | 29 | // Set Launchdarkly as OpenFeature provider 30 | err = openfeature.SetProvider(ofld.NewProvider(ldClient)) 31 | if err != nil { 32 | // handle error for provider initialization 33 | } 34 | 35 | // Set a multi-context evaluation context as example 36 | evalCtx := openfeature.NewEvaluationContext("redpanda-12342", map[string]any{ 37 | "kind": "multi", 38 | "organization": map[string]any{ 39 | "key": "blah1234", 40 | "name": "Redpanda", 41 | "customer_tier": "GOLD", 42 | }, 43 | "redpanda-id": map[string]any{ 44 | "key": "redpanda-12342", 45 | "cloud-provider": "aws", 46 | }, 47 | }) 48 | 49 | // Get an openfeature client and set the evaluation context to it as example. 50 | // For more information about OpenFeature evaluation contexts please refer to 51 | // https://openfeature.dev/docs/reference/concepts/evaluation-context/ 52 | client := openfeature.NewClient("hello-world") 53 | client.SetEvaluationContext(evalCtx) 54 | 55 | if err := doSomething(context.Background(), client); err != nil { 56 | panic(err) 57 | } 58 | } 59 | 60 | func doSomething(ctx context.Context, ofclient *openfeature.Client) error { 61 | mtlsEnabled, err := ofclient.BooleanValue(ctx, "mtls_enabled", false, emptyEvalCtx) 62 | if err != nil { 63 | return fmt.Errorf("doing something: %w", err) 64 | } 65 | 66 | if mtlsEnabled { 67 | println("configuring mTLS...") 68 | } 69 | 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /providers/launchdarkly/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/open-feature/go-sdk-contrib/providers/launchdarkly 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.3 6 | 7 | require ( 8 | github.com/hooklift/assert v0.1.0 9 | github.com/launchdarkly/go-sdk-common/v3 v3.1.0 10 | github.com/launchdarkly/go-server-sdk/v7 v7.6.2 11 | github.com/open-feature/go-sdk v1.12.0 12 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 13 | ) 14 | 15 | require ( 16 | github.com/go-logr/logr v1.4.2 // indirect 17 | github.com/google/uuid v1.6.0 // indirect 18 | github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect 19 | github.com/josharian/intern v1.0.0 // indirect 20 | github.com/launchdarkly/ccache v1.1.0 // indirect 21 | github.com/launchdarkly/eventsource v1.7.1 // indirect 22 | github.com/launchdarkly/go-jsonstream/v3 v3.1.0 // indirect 23 | github.com/launchdarkly/go-sdk-events/v3 v3.4.0 // indirect 24 | github.com/launchdarkly/go-semver v1.0.3 // indirect 25 | github.com/launchdarkly/go-server-sdk-evaluation/v3 v3.0.1 // indirect 26 | github.com/mailru/easyjson v0.7.7 // indirect 27 | github.com/patrickmn/go-cache v2.1.0+incompatible // indirect 28 | golang.org/x/sync v0.13.0 // indirect 29 | gopkg.in/ghodss/yaml.v1 v1.0.0 // indirect 30 | gopkg.in/yaml.v2 v2.4.0 // indirect 31 | gopkg.in/yaml.v3 v3.0.1 // indirect 32 | ) 33 | -------------------------------------------------------------------------------- /providers/launchdarkly/pkg/logger.go: -------------------------------------------------------------------------------- 1 | package launchdarkly 2 | 3 | // Logger defines a minimal interface for the provider's logger. 4 | type Logger interface { 5 | Debug(msg string, args ...any) 6 | Error(msg string, args ...any) 7 | Warn(msg string, args ...any) 8 | } 9 | 10 | var _ Logger = (*NoOpLogger)(nil) 11 | 12 | type NoOpLogger struct{} 13 | 14 | func (l *NoOpLogger) Debug(msg string, args ...any) {} 15 | func (l *NoOpLogger) Info(msg string, args ...any) {} 16 | func (l *NoOpLogger) Error(msg string, args ...any) {} 17 | func (l *NoOpLogger) Warn(msg string, args ...any) {} 18 | -------------------------------------------------------------------------------- /providers/launchdarkly/pkg/testdata/flags.json: -------------------------------------------------------------------------------- 1 | { 2 | "flags": { 3 | "mtls_enabled": { 4 | "key": "mtls_enabled", 5 | "on": true, 6 | "variations": [ 7 | true, 8 | false 9 | ], 10 | "targets": [ 11 | { 12 | "contextKind": "redpanda-id", 13 | "values": [ 14 | "redpanda-blah12342" 15 | ], 16 | "variation": 0 17 | } 18 | ] 19 | }, 20 | "dataplane_generation": { 21 | "key": "dataplane_generation", 22 | "on": true, 23 | "variations": [ 24 | "k8s.v1", 25 | "metal.v1", 26 | "vm.v1" 27 | ], 28 | "targets": [ 29 | { 30 | "contextKind": "organization-id", 31 | "values": [ 32 | "blah1234" 33 | ], 34 | "variation": 1 35 | } 36 | ] 37 | }, 38 | "global_discount_pct": { 39 | "key": "global_discount_pct", 40 | "on": true, 41 | "variations": [ 42 | 1.5, 43 | 5.5, 44 | 10 45 | ], 46 | "targets": [ 47 | { 48 | "contextKind": "organization-id", 49 | "values": [ 50 | "blah1234" 51 | ], 52 | "variation": 1 53 | } 54 | ] 55 | }, 56 | "abuse_risk_weight": { 57 | "key": "abuse_risk_weight", 58 | "on": true, 59 | "variations": [ 60 | 10, 61 | 20, 62 | 50 63 | ], 64 | "targets": [ 65 | { 66 | "contextKind": "organization-id", 67 | "values": [ 68 | "blah1234" 69 | ], 70 | "variation": 2 71 | } 72 | ] 73 | }, 74 | "rate_limit_config": { 75 | "key": "rate_limit_config", 76 | "on": true, 77 | "variations": [ 78 | { 79 | "target_quota_byte_rate": 2147483648, 80 | "target_fetch_quota_byte_rate": 1073741824, 81 | "kafka_connection_rate_limit": 100 82 | } 83 | ], 84 | "targets": [ 85 | { 86 | "contextKind": "redpanda-id", 87 | "values": [ 88 | "redpanda-blah12342" 89 | ], 90 | "variation": 0 91 | } 92 | ] 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /providers/multi-provider/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.0.4](https://github.com/open-feature/go-sdk-contrib/compare/providers/multi-provider-v0.0.3...providers/multi-provider/v0.0.4) (2025-04-25) 4 | 5 | 6 | ### ✨ New Features 7 | 8 | * implement multiprovider ([#669](https://github.com/open-feature/go-sdk-contrib/issues/669)) ([d35f4d6](https://github.com/open-feature/go-sdk-contrib/commit/d35f4d6bd7a1eab2801700476be06da944307924)) 9 | -------------------------------------------------------------------------------- /providers/multi-provider/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: generate test 2 | GOPATH_LOC = ${GOPATH} 3 | 4 | generate: 5 | go generate ./... 6 | go mod download 7 | mockgen -source=${GOPATH}/pkg/mod/github.com/open-feature/go-sdk@v1.13.1/openfeature/provider.go -package=mocks -destination=./internal/mocks/provider_mock.go 8 | 9 | test: 10 | go test ./... -------------------------------------------------------------------------------- /providers/multi-provider/README.md: -------------------------------------------------------------------------------- 1 | OpenFeature Multi-Provider 2 | ------------ 3 | 4 | The Multi-Provider allows you to use multiple underlying providers as sources of flag data for the OpenFeature server SDK. 5 | When a flag is being evaluated, the Multi-Provider will consult each underlying provider it is managing in order to 6 | determine the final result. Different evaluation strategies can be defined to control which providers get evaluated and 7 | which result is used. 8 | 9 | The Multi-Provider is a powerful tool for performing migrations between flag providers, or combining multiple providers 10 | into a single feature flagging interface. For example: 11 | 12 | - **Migration**: When migrating between two providers, you can run both in parallel under a unified flagging interface. 13 | As flags are added to the new provider, the Multi-Provider will automatically find and return them, falling back to the old provider 14 | if the new provider does not have 15 | - **Multiple Data Sources**: The Multi-Provider allows you to seamlessly combine many sources of flagging data, such as 16 | environment variables, local files, database values and SaaS hosted feature management systems. 17 | 18 | # Installation 19 | 20 | ```sh 21 | go get github.com/open-feature/go-sdk-contrib/providers/multi-provider 22 | go get github.com/open-feature/go-sdk 23 | ``` 24 | 25 | # Usage 26 | 27 | ```go 28 | import ( 29 | "github.com/open-feature/go-sdk/openfeature" 30 | mp "github.com/open-feature/go-sdk-contrib/providers/multi-provider" 31 | ) 32 | 33 | providers := make(mp.ProviderMap) 34 | providers["providerA"] = providerA 35 | providers["providerB"] = providerB 36 | provider, err := mp.NewMultiProvider(providers, mp.StrategyFirstMatch, WithLogger(myLogger)) 37 | openfeature.SetProvider(provider) 38 | ``` 39 | 40 | # Options 41 | 42 | - `WithTimeout` - the duration is used for the total timeout across parallel operations. If none is set it will default 43 | to 5 seconds. This is not supported for `FirstMatch` yet, which executes sequentially 44 | - `WithFallbackProvider` - Used for setting a fallback provider for the `Comparison` strategy 45 | - `WithLogger` - Provides slog support 46 | 47 | # Strategies 48 | 49 | There are multiple strategies that can be used to determine the result returned to the caller. A strategy must be set at 50 | initialization time. 51 | 52 | There are 3 strategies available currently: 53 | 54 | - _First Match_ 55 | - _First Success_ 56 | - _Comparison_ 57 | 58 | ## First Match Strategy 59 | 60 | The first match strategy works by **sequentially** calling each provider in the order that they are provided to the mutli-provider. 61 | The first provider that returns a result. It will try calling the next provider whenever it encounters a `FLAG_NOT_FOUND` 62 | error. However, if a provider returns an error other than `FLAG_NOT_FOUND` the provider will stop and return the default 63 | value along with setting the error details if a detailed request is issued. (allow changing this behavior?) 64 | 65 | ## First Success Strategy 66 | 67 | The First Success strategy works by calling each provider in **parallel**. The first provider that returns a response 68 | with no errors is returned and all other calls are cancelled. If no provider provides a successful result the default 69 | value will be returned to the caller. 70 | 71 | ## Comparison 72 | 73 | The Comparison strategy works by calling each provider in **parallel**. All results are collected from each provider and 74 | then the resolved results are compared to each other. If they all agree then that value is returned. If not and a fallback 75 | provider is specified then the fallback will be executed. If no fallback is configured then the default value will be 76 | returned. If a provider returns `FLAG_NOT_FOUND` that is not included in the comparison. If all providers 77 | return not found then the default value is returned. Finally, if any provider returns an error other than `FLAG_NOT_FOUND` 78 | the evaluation immediately stops and that error result is returned. This strategy does NOT support `ObjectEvaluation` 79 | 80 | # Not Yet Implemented 81 | 82 | - Hooks support 83 | - Event support 84 | - Full slog support -------------------------------------------------------------------------------- /providers/multi-provider/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/open-feature/go-sdk-contrib/providers/multi-provider 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/open-feature/go-sdk v1.13.1 7 | github.com/stretchr/testify v1.9.0 8 | go.uber.org/mock v0.5.2 9 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 10 | golang.org/x/sync v0.13.0 11 | ) 12 | 13 | require ( 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/go-logr/logr v1.4.2 // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | gopkg.in/yaml.v3 v3.0.1 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /providers/multi-provider/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 4 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 5 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 6 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 7 | github.com/open-feature/go-sdk v1.13.1 h1:RJbS70eyi7Jd3Zm5bFnaahNKNDXn+RAVnctpGu+uPis= 8 | github.com/open-feature/go-sdk v1.13.1/go.mod h1:O8r4mhgeRIsjJ0ZBXlnE0BtbT/79W44gQceR7K8KYgo= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 12 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 13 | go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs= 14 | go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= 15 | go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= 16 | go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= 17 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= 18 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= 19 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= 20 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= 21 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 22 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 23 | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= 24 | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 25 | golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= 26 | golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 27 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 28 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 29 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 30 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 31 | -------------------------------------------------------------------------------- /providers/multi-provider/pkg/errors/aggregate-errors.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "fmt" 5 | "golang.org/x/exp/maps" 6 | "strings" 7 | ) 8 | 9 | type ( 10 | // ProviderError is how the error in the Init stage of a provider is reported. 11 | ProviderError struct { 12 | Err error 13 | ProviderName string 14 | } 15 | 16 | // AggregateError map that contains up to one error per provider within the multi-provider 17 | AggregateError map[string]ProviderError 18 | ) 19 | 20 | var ( 21 | _ error = (*ProviderError)(nil) 22 | _ error = (AggregateError)(nil) 23 | ) 24 | 25 | func (e *ProviderError) Error() string { 26 | return fmt.Sprintf("Provider %s: %s", e.ProviderName, e.Err.Error()) 27 | } 28 | 29 | // NewAggregateError Creates a new AggregateError 30 | func NewAggregateError(providerErrors []ProviderError) *AggregateError { 31 | err := make(AggregateError) 32 | for _, se := range providerErrors { 33 | err[se.ProviderName] = se 34 | } 35 | return &err 36 | } 37 | 38 | func (ae AggregateError) Error() string { 39 | size := len(ae) 40 | switch size { 41 | case 0: 42 | return "" 43 | case 1: 44 | for _, err := range ae { 45 | return err.Error() 46 | } 47 | default: 48 | errs := make([]string, 0, size) 49 | for _, err := range maps.Values(ae) { 50 | errs = append(errs, err.Error()) 51 | } 52 | return strings.Join(errs, ", ") 53 | } 54 | 55 | return "" // This will never occur, switch is exhaustive 56 | } 57 | -------------------------------------------------------------------------------- /providers/multi-provider/pkg/options.go: -------------------------------------------------------------------------------- 1 | package multiprovider 2 | 3 | import ( 4 | "github.com/open-feature/go-sdk-contrib/providers/multi-provider/pkg/strategies" 5 | of "github.com/open-feature/go-sdk/openfeature" 6 | "log/slog" 7 | "time" 8 | ) 9 | 10 | // WithLogger Sets a logger to be used with slog for internal logging 11 | func WithLogger(l *slog.Logger) Option { 12 | return func(conf *Configuration) { 13 | conf.logger = l 14 | } 15 | } 16 | 17 | // WithTimeout Set a timeout for the total runtime for evaluation of parallel strategies 18 | func WithTimeout(d time.Duration) Option { 19 | return func(conf *Configuration) { 20 | conf.timeout = d 21 | } 22 | } 23 | 24 | // WithFallbackProvider Sets a fallback provider when using the StrategyComparison 25 | func WithFallbackProvider(p of.FeatureProvider) Option { 26 | return func(conf *Configuration) { 27 | conf.fallbackProvider = p 28 | conf.useFallback = true 29 | } 30 | } 31 | 32 | // WithCustomStrategy sets a custom strategy. This must be used in conjunction with StrategyCustom 33 | func WithCustomStrategy(s strategies.Strategy) Option { 34 | return func(conf *Configuration) { 35 | conf.customStrategy = s 36 | } 37 | } 38 | 39 | // WithEventPublishing Enables event publishing (Not Yet Implemented) 40 | func WithEventPublishing() Option { 41 | return func(conf *Configuration) { 42 | conf.publishEvents = true 43 | } 44 | } 45 | 46 | // WithoutEventPublishing Disables event publishing (this is the default, but included for explicit usage) 47 | func WithoutEventPublishing() Option { 48 | return func(conf *Configuration) { 49 | conf.publishEvents = false 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /providers/ofrep/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.1.5](https://github.com/open-feature/go-sdk-contrib/compare/providers/ofrep/v0.1.4...providers/ofrep/v0.1.5) (2024-08-27) 4 | 5 | 6 | ### 🐛 Bug Fixes 7 | 8 | * **ofrep:** close http response body after using it ([#576](https://github.com/open-feature/go-sdk-contrib/issues/576)) ([02a3b5d](https://github.com/open-feature/go-sdk-contrib/commit/02a3b5d17e402573384f65efcc0b32e7863dd8e6)) 9 | 10 | 11 | ### 🧹 Chore 12 | 13 | * add license to module ([#554](https://github.com/open-feature/go-sdk-contrib/issues/554)) ([abb7657](https://github.com/open-feature/go-sdk-contrib/commit/abb76571c373582f36837587400104eb754c01b9)) 14 | 15 | ## [0.1.4](https://github.com/open-feature/go-sdk-contrib/compare/providers/ofrep/v0.1.3...providers/ofrep/v0.1.4) (2024-07-30) 16 | 17 | 18 | ### 🐛 Bug Fixes 19 | 20 | * **ofrep:** Manage disabled flag ([#543](https://github.com/open-feature/go-sdk-contrib/issues/543)) ([b4dd97d](https://github.com/open-feature/go-sdk-contrib/commit/b4dd97d06fad07afdfb4cb725194f558e0a685b1)) 21 | 22 | ## [0.1.3](https://github.com/open-feature/go-sdk-contrib/compare/providers/ofrep/v0.1.2...providers/ofrep/v0.1.3) (2024-07-26) 23 | 24 | 25 | ### 🐛 Bug Fixes 26 | 27 | * Default GO parsing is float64 for int ([#539](https://github.com/open-feature/go-sdk-contrib/issues/539)) ([2f6a40e](https://github.com/open-feature/go-sdk-contrib/commit/2f6a40e6a6ffa75ac583aaaee6a937d8ab10ca19)) 28 | * Don't panic if metadata not present in the response ([#537](https://github.com/open-feature/go-sdk-contrib/issues/537)) ([f0b5547](https://github.com/open-feature/go-sdk-contrib/commit/f0b554746934b496902563c2fdf7cb68bf8e2f1d)) 29 | * Should return a parse error if error while parsing ([#538](https://github.com/open-feature/go-sdk-contrib/issues/538)) ([1e2a7f4](https://github.com/open-feature/go-sdk-contrib/commit/1e2a7f4abb2fb48ca5047b7e4aa16cfc50a199a8)) 30 | 31 | ## [0.1.2](https://github.com/open-feature/go-sdk-contrib/compare/providers/ofrep/v0.1.1...providers/ofrep/v0.1.2) (2024-07-26) 32 | 33 | 34 | ### 🐛 Bug Fixes 35 | 36 | * **deps:** update module github.com/open-feature/go-sdk to v1.11.0 ([#501](https://github.com/open-feature/go-sdk-contrib/issues/501)) ([3f0eaa5](https://github.com/open-feature/go-sdk-contrib/commit/3f0eaa575500baa663dc24dbfc6cf8214565471f)) 37 | * Make OFREP option public ([#536](https://github.com/open-feature/go-sdk-contrib/issues/536)) ([b005dce](https://github.com/open-feature/go-sdk-contrib/commit/b005dce8126476fb893914f0b631305015dee91f)) 38 | 39 | ## 0.1.1 (2024-04-05) 40 | 41 | 42 | ### ✨ New Features 43 | 44 | * introduce OFREP provider ([#477](https://github.com/open-feature/go-sdk-contrib/issues/477)) ([a1cb699](https://github.com/open-feature/go-sdk-contrib/commit/a1cb699d4903502797a1184b79372b45ac1ef0b2)) 45 | -------------------------------------------------------------------------------- /providers/ofrep/README.md: -------------------------------------------------------------------------------- 1 | # OpenFeature Remote Evaluation Protocol Provider 2 | 3 | This is the Go implementation of the OFREP provider. 4 | The provider works by evaluating flags against OFREP single flag evaluation endpoint. 5 | 6 | ## Installation 7 | 8 | Use OFREP provider with the latest OpenFeature Go SDK 9 | 10 | ```sh 11 | go get github.com/open-feature/go-sdk-contrib/providers/ofrep 12 | go get github.com/open-feature/go-sdk 13 | ``` 14 | 15 | ## Usage 16 | 17 | Initialize the provider with the URL of the OFREP implementing service, 18 | 19 | ```go 20 | ofrepProvider := ofrep.NewProvider("http://localhost:8016") 21 | ``` 22 | 23 | Then, register the provider with the OpenFeature Go SDK and use derived clients for flag evaluations, 24 | 25 | ```go 26 | openfeature.SetProvider(ofrepProvider) 27 | ``` 28 | 29 | ## Configuration 30 | 31 | You can configure the provider using following configuration options, 32 | 33 | | Configuration option | Details | 34 | |----------------------|-------------------------------------------------------------------------------------------------------------------------| 35 | | WithApiKeyAuth | Set the token to be used with "X-API-Key" header | 36 | | WithBearerToken | Set the token to be used with "Bearer" HTTP Authorization schema | 37 | | WithClient | Provider a custom, pre-configured http.Client for OFREP service communication | 38 | | WithHeaderProvider | Register a custom header provider for OFREP calls. You may utilize this for custom authentication/authorization headers | 39 | 40 | 41 | For example, consider below example which set bearer token and provider a customized http client, 42 | 43 | ```go 44 | provider := ofrep.NewProvider( 45 | "http://localhost:8016", 46 | ofrep.WithBearerToken("TOKEN"), 47 | ofrep.WithClient(&http.Client{ 48 | Timeout: 1 * time.Second, 49 | })) 50 | ``` -------------------------------------------------------------------------------- /providers/ofrep/evaluate.go: -------------------------------------------------------------------------------- 1 | package ofrep 2 | 3 | import ( 4 | "context" 5 | 6 | of "github.com/open-feature/go-sdk/openfeature" 7 | ) 8 | 9 | // Evaluator contract for flag evaluation 10 | type Evaluator interface { 11 | ResolveBoolean(ctx context.Context, key string, defaultValue bool, 12 | evalCtx map[string]interface{}) of.BoolResolutionDetail 13 | ResolveString(ctx context.Context, key string, defaultValue string, 14 | evalCtx map[string]interface{}) of.StringResolutionDetail 15 | ResolveFloat(ctx context.Context, key string, defaultValue float64, 16 | evalCtx map[string]interface{}) of.FloatResolutionDetail 17 | ResolveInt(ctx context.Context, key string, defaultValue int64, 18 | evalCtx map[string]interface{}) of.IntResolutionDetail 19 | ResolveObject(ctx context.Context, key string, defaultValue interface{}, 20 | evalCtx map[string]interface{}) of.InterfaceResolutionDetail 21 | } 22 | -------------------------------------------------------------------------------- /providers/ofrep/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/open-feature/go-sdk-contrib/providers/ofrep 2 | 3 | go 1.21.0 4 | 5 | require github.com/open-feature/go-sdk v1.11.0 6 | 7 | require ( 8 | github.com/go-logr/logr v1.4.1 // indirect 9 | golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /providers/ofrep/go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 2 | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 3 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 4 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 5 | github.com/open-feature/go-sdk v1.10.0 h1:druQtYOrN+gyz3rMsXp0F2jW1oBXJb0V26PVQnUGLbM= 6 | github.com/open-feature/go-sdk v1.10.0/go.mod h1:+rkJhLBtYsJ5PZNddAgFILhRAAxwrJ32aU7UEUm4zQI= 7 | github.com/open-feature/go-sdk v1.11.0 h1:4cp9rXl16ZvlMCef7O+I3vQSXae8DzAF0SfV9mvYInw= 8 | github.com/open-feature/go-sdk v1.11.0/go.mod h1:+rkJhLBtYsJ5PZNddAgFILhRAAxwrJ32aU7UEUm4zQI= 9 | golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 h1:/RIbNt/Zr7rVhIkQhooTxCxFcdWLGIKnZA4IXNFSrvo= 10 | golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= 11 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 12 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 13 | -------------------------------------------------------------------------------- /providers/ofrep/internal/outbound/http.go: -------------------------------------------------------------------------------- 1 | package outbound 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "net/url" 10 | "time" 11 | 12 | of "github.com/open-feature/go-sdk/openfeature" 13 | ) 14 | 15 | const ofrepV1 = "/ofrep/v1/evaluate/flags/" 16 | 17 | // HeaderCallback is a callback returning header name and header value 18 | type HeaderCallback func() (name string, value string) 19 | 20 | type Configuration struct { 21 | BaseURI string 22 | Callbacks []HeaderCallback 23 | Client *http.Client 24 | } 25 | 26 | type Resolution struct { 27 | Data []byte 28 | Status int 29 | Headers http.Header 30 | } 31 | 32 | // Outbound client for http communication 33 | type Outbound struct { 34 | baseURI string 35 | client *http.Client 36 | headerProvider []HeaderCallback 37 | } 38 | 39 | func NewHttp(cfg Configuration) *Outbound { 40 | if cfg.Client == nil { 41 | cfg.Client = &http.Client{ 42 | Timeout: 10 * time.Second, 43 | } 44 | } 45 | 46 | return &Outbound{ 47 | headerProvider: cfg.Callbacks, 48 | baseURI: cfg.BaseURI, 49 | client: cfg.Client, 50 | } 51 | } 52 | 53 | func (h *Outbound) Single(ctx context.Context, key string, payload []byte) (*Resolution, error) { 54 | path, err := url.JoinPath(h.baseURI, ofrepV1, key) 55 | if err != nil { 56 | return nil, fmt.Errorf("error building request path: %w", err) 57 | } 58 | 59 | req, err := http.NewRequestWithContext(ctx, http.MethodPost, path, bytes.NewReader(payload)) 60 | if err != nil { 61 | resErr := of.NewGeneralResolutionError(fmt.Sprintf("request building error: %v", err)) 62 | return nil, &resErr 63 | } 64 | 65 | for _, callback := range h.headerProvider { 66 | req.Header.Set(callback()) 67 | } 68 | 69 | rsp, err := h.client.Do(req) 70 | if err != nil { 71 | return nil, err 72 | } 73 | defer rsp.Body.Close() 74 | 75 | b, err := io.ReadAll(rsp.Body) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | return &Resolution{ 81 | Data: b, 82 | Status: rsp.StatusCode, 83 | Headers: rsp.Header, 84 | }, nil 85 | } 86 | -------------------------------------------------------------------------------- /providers/ofrep/internal/outbound/http_test.go: -------------------------------------------------------------------------------- 1 | package outbound 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | ) 10 | 11 | func TestHttpOutbound(t *testing.T) { 12 | // given 13 | key := "flag" 14 | server := httptest.NewServer(mockHandler{t: t, key: key}) 15 | t.Cleanup(server.Close) 16 | 17 | outbound := NewHttp(Configuration{ 18 | Callbacks: []HeaderCallback{ 19 | func() (string, string) { 20 | return "Authorization", "Token" 21 | }, 22 | }, 23 | BaseURI: server.URL, 24 | }) 25 | 26 | // when 27 | response, err := outbound.Single(context.Background(), key, []byte{}) 28 | if err != nil { 29 | t.Fatalf("error from request: %v", err) 30 | return 31 | } 32 | 33 | // then - expect an ok response 34 | if response.Status != http.StatusOK { 35 | t.Errorf("expected 200, but got %d", response.Status) 36 | } 37 | } 38 | 39 | type mockHandler struct { 40 | key string 41 | t *testing.T 42 | } 43 | 44 | func (r mockHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { 45 | if req.Method != http.MethodPost { 46 | r.t.Logf("invalid request method, expected %s, got %s. test will fail", http.MethodPost, req.Method) 47 | resp.WriteHeader(http.StatusBadRequest) 48 | return 49 | } 50 | 51 | path := fmt.Sprintf("%s%s", ofrepV1, r.key) 52 | if req.RequestURI != fmt.Sprintf("%s%s", ofrepV1, r.key) { 53 | r.t.Logf("invalid request path, expected %s, got %s. test will fail", path, req.RequestURI) 54 | resp.WriteHeader(http.StatusBadRequest) 55 | return 56 | } 57 | 58 | if req.Header.Get("Authorization") == "" { 59 | r.t.Log("expected non-empty Authorization header, but got empty. test will fail") 60 | resp.WriteHeader(http.StatusBadRequest) 61 | return 62 | } 63 | 64 | resp.WriteHeader(http.StatusOK) 65 | } 66 | -------------------------------------------------------------------------------- /providers/ofrep/provider.go: -------------------------------------------------------------------------------- 1 | package ofrep 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/open-feature/go-sdk-contrib/providers/ofrep/internal/evaluate" 9 | "github.com/open-feature/go-sdk-contrib/providers/ofrep/internal/outbound" 10 | "github.com/open-feature/go-sdk/openfeature" 11 | ) 12 | 13 | // Provider implementation for OFREP 14 | type Provider struct { 15 | evaluator Evaluator 16 | } 17 | 18 | type Option func(*outbound.Configuration) 19 | 20 | // NewProvider returns an OFREP provider configured with provided configuration. 21 | // The only mandatory configuration is the baseUri, which is the base path of the OFREP service implementation. 22 | func NewProvider(baseUri string, options ...Option) *Provider { 23 | cfg := outbound.Configuration{ 24 | BaseURI: baseUri, 25 | } 26 | 27 | for _, option := range options { 28 | option(&cfg) 29 | } 30 | 31 | provider := &Provider{ 32 | evaluator: evaluate.NewFlagsEvaluator(cfg), 33 | } 34 | 35 | return provider 36 | } 37 | 38 | func (p Provider) Metadata() openfeature.Metadata { 39 | return openfeature.Metadata{ 40 | Name: "OpenFeature Remote Evaluation Protocol Provider", 41 | } 42 | } 43 | 44 | func (p Provider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx openfeature.FlattenedContext) openfeature.BoolResolutionDetail { 45 | return p.evaluator.ResolveBoolean(ctx, flag, defaultValue, evalCtx) 46 | } 47 | 48 | func (p Provider) StringEvaluation(ctx context.Context, flag string, defaultValue string, evalCtx openfeature.FlattenedContext) openfeature.StringResolutionDetail { 49 | return p.evaluator.ResolveString(ctx, flag, defaultValue, evalCtx) 50 | } 51 | 52 | func (p Provider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx openfeature.FlattenedContext) openfeature.FloatResolutionDetail { 53 | return p.evaluator.ResolveFloat(ctx, flag, defaultValue, evalCtx) 54 | } 55 | 56 | func (p Provider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx openfeature.FlattenedContext) openfeature.IntResolutionDetail { 57 | return p.evaluator.ResolveInt(ctx, flag, defaultValue, evalCtx) 58 | } 59 | 60 | func (p Provider) ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, evalCtx openfeature.FlattenedContext) openfeature.InterfaceResolutionDetail { 61 | return p.evaluator.ResolveObject(ctx, flag, defaultValue, evalCtx) 62 | } 63 | 64 | func (p Provider) Hooks() []openfeature.Hook { 65 | return []openfeature.Hook{} 66 | } 67 | 68 | // options of the OFREP provider 69 | 70 | // WithHeaderProvider allows to configure a custom header callback to set a custom authorization header 71 | func WithHeaderProvider(callback outbound.HeaderCallback) func(*outbound.Configuration) { 72 | return func(c *outbound.Configuration) { 73 | c.Callbacks = append(c.Callbacks, callback) 74 | } 75 | } 76 | 77 | // WithBearerToken allows to set token to be used for bearer token authorization 78 | func WithBearerToken(token string) func(*outbound.Configuration) { 79 | return func(c *outbound.Configuration) { 80 | c.Callbacks = append(c.Callbacks, func() (string, string) { 81 | return "Authorization", fmt.Sprintf("Bearer %s", token) 82 | }) 83 | } 84 | } 85 | 86 | // WithApiKeyAuth allows to set token to be used for api key authorization 87 | func WithApiKeyAuth(token string) func(*outbound.Configuration) { 88 | return func(c *outbound.Configuration) { 89 | c.Callbacks = append(c.Callbacks, func() (string, string) { 90 | return "X-API-Key", token 91 | }) 92 | } 93 | } 94 | 95 | // WithClient allows to provide a pre-configured http.Client for the communication with the OFREP service 96 | func WithClient(client *http.Client) func(configuration *outbound.Configuration) { 97 | return func(configuration *outbound.Configuration) { 98 | configuration.Client = client 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /providers/ofrep/provider_test.go: -------------------------------------------------------------------------------- 1 | package ofrep 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | "time" 9 | 10 | "github.com/open-feature/go-sdk/openfeature" 11 | 12 | "github.com/open-feature/go-sdk-contrib/providers/ofrep/internal/outbound" 13 | ) 14 | 15 | func TestConfigurations(t *testing.T) { 16 | t.Run("validate header provider", func(t *testing.T) { 17 | c := outbound.Configuration{} 18 | 19 | WithHeaderProvider(func() (key string, value string) { 20 | return "HEADER", "VALUE" 21 | })(&c) 22 | 23 | h, v := c.Callbacks[0]() 24 | 25 | if h != "HEADER" { 26 | t.Errorf("expected header %s, but got %s", "HEADER", h) 27 | } 28 | 29 | if v != "VALUE" { 30 | t.Errorf("expected value %s, but got %s", "VALUE", v) 31 | } 32 | }) 33 | 34 | t.Run("validate bearer token", func(t *testing.T) { 35 | c := outbound.Configuration{} 36 | 37 | WithBearerToken("TOKEN")(&c) 38 | 39 | h, v := c.Callbacks[0]() 40 | 41 | if h != "Authorization" { 42 | t.Errorf("expected header %s, but got %s", "Authorization", h) 43 | } 44 | 45 | if v != "Bearer TOKEN" { 46 | t.Errorf("expected value %s, but got %s", "Bearer TOKEN", v) 47 | } 48 | }) 49 | 50 | t.Run("validate api auth key", func(t *testing.T) { 51 | c := outbound.Configuration{} 52 | 53 | WithApiKeyAuth("TOKEN")(&c) 54 | 55 | h, v := c.Callbacks[0]() 56 | 57 | if h != "X-API-Key" { 58 | t.Errorf("expected header %s, but got %s", "X-API-Key", h) 59 | } 60 | 61 | if v != "TOKEN" { 62 | t.Errorf("expected value %s, but got %s", "TOKEN", v) 63 | } 64 | }) 65 | } 66 | 67 | func TestWiringE2E(t *testing.T) { 68 | // mock server with mocked response 69 | server := httptest.NewServer( 70 | mockHandler{ 71 | response: "{\"value\":true,\"key\":\"my-flag\",\"reason\":\"STATIC\",\"variant\":\"true\",\"metadata\":{}}", 72 | t: t, 73 | }, 74 | ) 75 | t.Cleanup(server.Close) 76 | 77 | // custom client with reduced timeout 78 | customClient := &http.Client{ 79 | Timeout: 1 * time.Second, 80 | } 81 | 82 | provider := NewProvider(server.URL, WithClient(customClient)) 83 | booleanEvaluation := provider.BooleanEvaluation(context.Background(), "flag", false, nil) 84 | 85 | if booleanEvaluation.Value != true { 86 | t.Errorf("expected %v, but got %v", true, booleanEvaluation.Value) 87 | } 88 | 89 | if booleanEvaluation.Variant != "true" { 90 | t.Errorf("expected %v, but got %v", "true", booleanEvaluation.Variant) 91 | } 92 | 93 | if booleanEvaluation.Reason != openfeature.StaticReason { 94 | t.Errorf("expected %v, but got %v", openfeature.StaticReason, booleanEvaluation.Reason) 95 | } 96 | 97 | if booleanEvaluation.Error() != nil { 98 | t.Errorf("expected no errors, but got %v", booleanEvaluation.Error()) 99 | } 100 | } 101 | 102 | type mockHandler struct { 103 | response string 104 | t *testing.T 105 | } 106 | 107 | func (r mockHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { 108 | resp.WriteHeader(http.StatusOK) 109 | _, err := resp.Write([]byte(r.response)) 110 | if err != nil { 111 | r.t.Logf("error wriging bytes: %v", err) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /providers/prefab/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.0.2](https://github.com/open-feature/go-sdk-contrib/compare/providers/prefab-v0.0.1...providers/prefab/v0.0.2) (2024-09-18) 4 | 5 | 6 | ### ✨ New Features 7 | 8 | * add Prefab provider ([#575](https://github.com/open-feature/go-sdk-contrib/issues/575)) ([cbafc90](https://github.com/open-feature/go-sdk-contrib/commit/cbafc906ed2ed1ce8f93854100be6d29acf13509)) 9 | 10 | ## Changelog 11 | -------------------------------------------------------------------------------- /providers/prefab/README.md: -------------------------------------------------------------------------------- 1 | # Unofficial Prefab OpenFeature Provider for GO 2 | 3 | [Prefab](https://www.prefab.cloud/) OpenFeature Provider can provide usage for Prefab via OpenFeature GO SDK. 4 | 5 | ## Installation 6 | 7 | To use the provider, you'll need to install [Prefab Go client](https://github.com/prefab-cloud/prefab-cloud-go) and Prefab provider. You can install the packages using the following command 8 | 9 | ```shell 10 | go get github.com/prefab-cloud/prefab-cloud-go 11 | go get github.com/open-feature/go-sdk-contrib/providers/prefab 12 | ``` 13 | 14 | ## Usage 15 | Prefab OpenFeature Provider is using Prefab GO SDK. 16 | 17 | ### Usage Example 18 | 19 | ```go 20 | import ( 21 | prefabProvider "github.com/open-feature/go-sdk-contrib/providers/prefab/pkg" 22 | of "github.com/open-feature/go-sdk/openfeature" 23 | prefab "github.com/prefab-cloud/prefab-cloud-go/pkg" 24 | ) 25 | 26 | var provider *prefabProvider.Provider 27 | var ofClient *of.Client 28 | 29 | providerConfig := prefabProvider.ProviderConfig{ 30 | APIKey: "YOUR_API_KEY", 31 | } 32 | 33 | var err error 34 | provider, err = prefabProvider.NewProvider(providerConfig) 35 | if err != nil { 36 | fmt.Printf("Error during new provider: %v\n", err) 37 | os.Exit(1) 38 | } 39 | err = provider.Init(of.EvaluationContext{}) 40 | if err != nil { 41 | fmt.Printf("Error during provider init: %v\n", err) 42 | os.Exit(1) 43 | } 44 | 45 | of.SetProvider(provider) 46 | ofClient = of.NewClient("my-app") 47 | 48 | evalCtx := of.NewEvaluationContext( 49 | "", 50 | map[string]interface{}{ 51 | "user.key": "key1", 52 | "team.domain": "prefab.cloud", 53 | "team.description": "team1", 54 | }, 55 | ) 56 | enabled, _ := ofClient.BooleanValue(context.Background(), "always_on_gate", false, evalCtx) 57 | fmt.Printf("enabled: %v\n", enabled) 58 | value, _ := ofClient.StringValue(context.Background(), "string", "fallback", evalCtx) 59 | fmt.Printf("value: %v\n", value) 60 | slice, _ := ofClient.ObjectValueDetails(context.Background(), "sample_list", []string{"a2", "b2"}, evalCtx) 61 | fmt.Printf("slice: %v\n", slice) 62 | 63 | of.Shutdown() 64 | 65 | ``` 66 | See [provider_test.go](./pkg/provider_test.go) for more information. 67 | 68 | ## Notes 69 | 70 | Some Prefab custom operations are supported from the Prefab client via PrefabClient. 71 | 72 | ## Prefab Provider Tests Strategies 73 | 74 | Unit test based on Prefab yaml config file. 75 | Can be enhanced pending [JSON dump data source](https://github.com/prefab-cloud/prefab-cloud-go/blob/0e3d5a4ba7171bbc4484cc99ccaad4c0c32d7e81/README.md?plain=1#L58) 76 | JSON evaluation not tested properly until then. 77 | See [provider_test.go](./pkg/provider_test.go) for more information. 78 | -------------------------------------------------------------------------------- /providers/prefab/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/open-feature/go-sdk-contrib/providers/prefab 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/open-feature/go-sdk v1.12.0 7 | github.com/prefab-cloud/prefab-cloud-go v0.0.5 8 | github.com/stretchr/testify v1.9.0 9 | ) 10 | 11 | require ( 12 | github.com/davecgh/go-spew v1.1.1 // indirect 13 | github.com/go-logr/logr v1.4.2 // indirect 14 | github.com/kr/pretty v0.3.1 // indirect 15 | github.com/pmezard/go-difflib v1.0.0 // indirect 16 | github.com/r3labs/sse/v2 v2.10.0 // indirect 17 | github.com/rogpeppe/go-internal v1.12.0 // indirect 18 | github.com/sosodev/duration v1.3.1 // indirect 19 | github.com/spaolacci/murmur3 v1.1.0 // indirect 20 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect 21 | golang.org/x/net v0.26.0 // indirect 22 | google.golang.org/protobuf v1.34.2 // indirect 23 | gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect 24 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 25 | gopkg.in/yaml.v3 v3.0.1 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /providers/prefab/internal/util.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | of "github.com/open-feature/go-sdk/openfeature" 8 | prefab "github.com/prefab-cloud/prefab-cloud-go/pkg" 9 | ) 10 | 11 | func ToPrefabContext(evalCtx of.FlattenedContext) (prefab.ContextSet, error) { 12 | if len(evalCtx) == 0 { 13 | return prefab.ContextSet{}, nil 14 | } 15 | prefabContext := prefab.NewContextSet() 16 | for k, v := range evalCtx { 17 | // val, ok := toStr(v) 18 | parts := strings.SplitN(k, ".", 2) 19 | if len(parts) < 2 { 20 | return *prefabContext, fmt.Errorf("context key structure should be in the form of x.y: %s", k) 21 | } 22 | key, subkey := parts[0], parts[1] 23 | if _, exists := prefabContext.Data[key]; !exists { 24 | prefabContext.WithNamedContextValues(key, map[string]interface{}{ 25 | subkey: v, 26 | }) 27 | } else { 28 | prefabContext.Data[key].Data[subkey] = v 29 | } 30 | } 31 | return *prefabContext, nil 32 | } 33 | 34 | func toStr(val interface{}) (string, bool) { 35 | switch v := val.(type) { 36 | case string: 37 | return v, true 38 | case int, int8, int16, int32, int64: 39 | return fmt.Sprintf("%d", v), true 40 | case float32, float64: 41 | return fmt.Sprintf("%.6f", v), true 42 | case bool: 43 | return fmt.Sprintf("%t", v), true 44 | default: 45 | return "", false 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /providers/prefab/pkg/enabled.yaml: -------------------------------------------------------------------------------- 1 | sample_int: 123 2 | sample_double: 12.12 3 | sample_bool: true 4 | false_value: false 5 | zero_value: 0 6 | sample_to_override: Foo 7 | prefab.log_level: debug 8 | sample: test sample value 9 | enabled_flag: true 10 | disabled_flag: false 11 | flag_with_a_value: { "feature_flag": "true", value: "all-features" } 12 | in_lookup_key: { "feature_flag": "true", value: true, criteria: { operator: LOOKUP_KEY_IN, values: [ "abc123", "xyz987" ] } } 13 | test1: { "feature_flag": "true", value: "new-version", criteria: { operator: PROP_IS_ONE_OF, property: "domain", values: [ "prefab.cloud", "example.com" ] } } 14 | nested: 15 | values: 16 | _: top level 17 | string: nested value 18 | 19 | 20 | nested2: 21 | _: the value 22 | 23 | log-level: 24 | _: warn 25 | cloud.prefab.client: warn 26 | tests: 27 | _: debug 28 | capitalized: INFO 29 | uncapitalized: info 30 | nested: 31 | _: warn 32 | deeply: error 33 | 34 | example: 35 | nested: 36 | path: hello 37 | 38 | example2.nested.path: hello2 39 | 40 | sample_list: 41 | - a 42 | - b 43 | sample_json: { "feature_flag": "true","value": "all-features" } 44 | -------------------------------------------------------------------------------- /providers/prefab/pkg/provider_config.go: -------------------------------------------------------------------------------- 1 | package prefab 2 | 3 | type ProviderConfig struct { 4 | Configs map[string]interface{} 5 | APIKey string 6 | APIURLs []string 7 | Sources []string 8 | // EnvironmentNames []string 9 | // ProjectEnvID int64 10 | // InitializationTimeoutSeconds float64 11 | // OnInitializationFailure OnInitializationFailure 12 | } 13 | -------------------------------------------------------------------------------- /providers/statsig/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.0.3](https://github.com/open-feature/go-sdk-contrib/compare/providers/statsig/v0.0.2...providers/statsig/v0.0.3) (2024-08-13) 4 | 5 | 6 | ### 🐛 Bug Fixes 7 | 8 | * **deps:** update module github.com/open-feature/go-sdk to v1.10.0 ([#469](https://github.com/open-feature/go-sdk-contrib/issues/469)) ([21810af](https://github.com/open-feature/go-sdk-contrib/commit/21810afc33fce9a3940ec9dc59e65f140fcbaa57)) 9 | * **deps:** update module github.com/open-feature/go-sdk to v1.11.0 ([#501](https://github.com/open-feature/go-sdk-contrib/issues/501)) ([3f0eaa5](https://github.com/open-feature/go-sdk-contrib/commit/3f0eaa575500baa663dc24dbfc6cf8214565471f)) 10 | 11 | 12 | ### ✨ New Features 13 | 14 | * improve evaluation context to Statsig user conversion ([#520](https://github.com/open-feature/go-sdk-contrib/issues/520)) ([b90eb4d](https://github.com/open-feature/go-sdk-contrib/commit/b90eb4de72975b4b60addefdab3f2cf20a82ff72)) 15 | 16 | ## [0.0.2](https://github.com/open-feature/go-sdk-contrib/compare/providers/statsig-v0.0.1...providers/statsig/v0.0.2) (2024-03-15) 17 | 18 | 19 | ### ✨ New Features 20 | 21 | * Add Statsig provider ([#445](https://github.com/open-feature/go-sdk-contrib/issues/445)) ([409a06f](https://github.com/open-feature/go-sdk-contrib/commit/409a06fcf0157469495cf759692f333ae9d808f6)) 22 | 23 | ## Changelog 24 | -------------------------------------------------------------------------------- /providers/statsig/README.md: -------------------------------------------------------------------------------- 1 | # Unofficial Statsig OpenFeature GO Provider 2 | 3 | [Statsig](https://statsig.com/) OpenFeature Provider can provide usage for Statsig via OpenFeature GO SDK. 4 | 5 | # Installation 6 | 7 | To use the provider, you'll need to install [Statsig Go client](https://github.com/statsig-io/go-sdk) and Statsig provider. You can install the packages using the following command 8 | 9 | ```shell 10 | go get github.com/statsig-io/go-sdk 11 | go get github.com/open-feature/go-sdk-contrib/providers/statsig 12 | ``` 13 | 14 | ## Concepts 15 | * String/Integer/Float evaluations evaluation gets [Dynamic config](https://docs.statsig.com/server/golangSDK#reading-a-dynamic-config) or [Layer](https://docs.statsig.com/server/golangSDK#getting-an-layerexperiment) evaluation. 16 | As the key represents an inner attribute, feature config is required as a parameter with data needed for evaluation. 17 | For an example of dynamic config of product alias, need to differentiate between dynamic config or layer, and the dynamic config name. 18 | * Boolean evaluation gets [gate](https://docs.statsig.com/server/golangSDK#checking-a-gate) status when feature config is not passed. 19 | When feature config exists, it evaluates to the config/layer attribute, similar to String/Integer/Float evaluations. 20 | 21 | * Object evaluation gets a structure representing the dynamic config or layer. 22 | * [Private Attributes](https://docs.statsig.com/server/golangSDK#private-attributes) are supported as 'privateAttributes' context key. 23 | 24 | 25 | ## Usage 26 | Statsig OpenFeature Provider is using Statsig GO SDK. 27 | 28 | ## Usage Example 29 | 30 | ```go 31 | import ( 32 | statsigProvider "github.com/open-feature/go-sdk-contrib/providers/statsig/pkg" 33 | of "github.com/open-feature/go-sdk/openfeature" 34 | statsig "github.com/statsig-io/go-sdk" 35 | ) 36 | 37 | of.SetProvider(provider) 38 | ofClient := of.NewClient("my-app") 39 | 40 | evalCtx := of.NewEvaluationContext( 41 | "", 42 | map[string]interface{}{ 43 | "UserID": "123", 44 | }, 45 | ) 46 | enabled, _ := ofClient.BooleanValue(context.Background(), "always_on_gate", false, evalCtx) 47 | 48 | featureConfig := statsigProvider.FeatureConfig{ 49 | FeatureConfigType: statsigProvider.FeatureConfigType("CONFIG"), 50 | Name: "test_config", 51 | } 52 | 53 | evalCtx := of.NewEvaluationContext( 54 | "", 55 | map[string]interface{}{ 56 | "UserID": "123", // can use "UserID" or of.TargetingKey ("targetingKey") 57 | "Email": "testuser1@statsig.com", 58 | "feature_config": featureConfig, 59 | }, 60 | ) 61 | expected := "statsig" 62 | value, _ := ofClient.StringValue(context.Background(), "string", "fallback", evalCtx) 63 | 64 | of.Shutdown() 65 | 66 | ``` 67 | See [provider_test.go](./pkg/provider_test.go) for more information. 68 | 69 | 70 | ## Notes 71 | Some Statsig custom operations are supported from the Statsig client via statsig. 72 | 73 | ## Statsig Provider Tests Strategies 74 | 75 | Unit test based on Statsig [BootstrapValues](https://docs.statsig.com/server/golangSDK#statsig-options) config file. 76 | As it is limited, evaluation context based tests are limited. 77 | See [provider_test.go](./pkg/provider_test.go) for more information. 78 | 79 | ## Known issues 80 | - Gate BooleanEvaluation with default value true cannot fallback to true. 81 | https://github.com/statsig-io/go-sdk/issues/32 82 | -------------------------------------------------------------------------------- /providers/statsig/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/open-feature/go-sdk-contrib/providers/statsig 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/open-feature/go-sdk v1.11.0 7 | github.com/statsig-io/go-sdk v1.32.1 8 | github.com/stretchr/testify v1.9.0 9 | ) 10 | 11 | require ( 12 | github.com/davecgh/go-spew v1.1.1 // indirect 13 | github.com/go-logr/logr v1.4.1 // indirect 14 | github.com/google/uuid v1.3.0 // indirect 15 | github.com/kr/pretty v0.3.1 // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | github.com/statsig-io/ip3country-go v0.2.0 // indirect 18 | github.com/ua-parser/uap-go v0.0.0-20211112212520-00c877edfe0f // indirect 19 | golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 // indirect 20 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 21 | gopkg.in/yaml.v2 v2.4.0 // indirect 22 | gopkg.in/yaml.v3 v3.0.1 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /providers/statsig/go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 5 | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 6 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 7 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 8 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 9 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 10 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 11 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 12 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 13 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 14 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 15 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 16 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 17 | github.com/open-feature/go-sdk v1.11.0 h1:4cp9rXl16ZvlMCef7O+I3vQSXae8DzAF0SfV9mvYInw= 18 | github.com/open-feature/go-sdk v1.11.0/go.mod h1:+rkJhLBtYsJ5PZNddAgFILhRAAxwrJ32aU7UEUm4zQI= 19 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 20 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 21 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 22 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 23 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 24 | github.com/statsig-io/go-sdk v1.32.1 h1:z0VjBSRuNzguNb+PK/4+8YsfcLClOxs3aZr2hUj4xm4= 25 | github.com/statsig-io/go-sdk v1.32.1/go.mod h1:Pej0D6R75gTHj7FdS6pbXQ7ayF0HL1cwOgiz5zDNdyc= 26 | github.com/statsig-io/ip3country-go v0.2.0 h1:4z4ovVCx7GnQAKJC753bjcOgxLQJFsrDdcCKda4I2U8= 27 | github.com/statsig-io/ip3country-go v0.2.0/go.mod h1:PKuA/VSpe4puBXw3BNGAHyP8IOZOiXAh/xIz+iYYoMQ= 28 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 29 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 30 | github.com/ua-parser/uap-go v0.0.0-20211112212520-00c877edfe0f h1:A+MmlgpvrHLeUP8dkBVn4Pnf5Bp5Yk2OALm7SEJLLE8= 31 | github.com/ua-parser/uap-go v0.0.0-20211112212520-00c877edfe0f/go.mod h1:OBcG9bn7sHtXgarhUEb3OfCnNsgtGnkVf41ilSZ3K3E= 32 | golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 h1:/RIbNt/Zr7rVhIkQhooTxCxFcdWLGIKnZA4IXNFSrvo= 33 | golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= 34 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 35 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 36 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 37 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 38 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 39 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 40 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 41 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 42 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 43 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 44 | -------------------------------------------------------------------------------- /providers/statsig/pkg/provider_config.go: -------------------------------------------------------------------------------- 1 | package statsig 2 | 3 | import ( 4 | statsig "github.com/statsig-io/go-sdk" 5 | ) 6 | 7 | // ProviderConfig is the struct containing the provider options. 8 | type ProviderConfig struct { 9 | Options statsig.Options 10 | SdkKey string 11 | } 12 | -------------------------------------------------------------------------------- /providers/unleash/README.md: -------------------------------------------------------------------------------- 1 | # Unofficial Unleash OpenFeature GO Provider 2 | 3 | [Unleash](https://getunleash.io) OpenFeature Provider can provide usage for Unleash via OpenFeature GO SDK. 4 | 5 | # Installation 6 | 7 | To use the Unleash provider, you'll need to install [unleash Go client](https://github.com/Unleash/unleash-client-go/tree/v4) and unleash provider. You can install the packages using the following command 8 | 9 | ```shell 10 | go get github.com/Unleash/unleash-client-go/v4 11 | go get github.com/open-feature/go-sdk-contrib/providers/unleash 12 | ``` 13 | 14 | ## Concepts 15 | * Boolean evaluation gets feature enabled status. 16 | * String evaluation gets feature variant value. 17 | 18 | ## Usage 19 | Unleash OpenFeature Provider is using Unleash GO SDK. 20 | 21 | ## Usage Example 22 | 23 | ```go 24 | import ( 25 | "github.com/Unleash/unleash-client-go/v4" 26 | unleashProvider "github.com/open-feature/go-sdk-contrib/providers/unleash/pkg" 27 | ) 28 | 29 | providerConfig := unleashProvider.ProviderConfig{ 30 | Options: []unleash.ConfigOption{ 31 | unleash.WithListener(&unleash.DebugListener{}), 32 | unleash.WithAppName("my-application"), 33 | unleash.WithRefreshInterval(5 * time.Second), 34 | unleash.WithMetricsInterval(5 * time.Second), 35 | unleash.WithStorage(&unleash.BootstrapStorage{Reader: demoReader}), 36 | unleash.WithUrl("https://localhost:4242"), 37 | }, 38 | } 39 | 40 | provider, err := unleashProvider.NewProvider(providerConfig) 41 | err = provider.Init(of.EvaluationContext{}) 42 | 43 | ctx := context.Background() 44 | 45 | of.SetProvider(provider) 46 | ofClient := of.NewClient("my-app") 47 | 48 | evalCtx := of.NewEvaluationContext( 49 | "", 50 | map[string]interface{}{ 51 | "UserId": "111", 52 | }, 53 | ) 54 | enabled, err := ofClient.BooleanValue(context.Background(), "users-flag", false, evalCtx) 55 | 56 | evalCtx := of.NewEvaluationContext( 57 | "", 58 | map[string]interface{}{}, 59 | ) 60 | value, err := ofClient.StringValue(context.Background(), "variant-flag", "", evalCtx) 61 | 62 | of.Shutdown() 63 | 64 | ``` 65 | See [provider_test.go](./pkg/provider_test.go) for more information. 66 | 67 | 68 | ### Additional Usage Details 69 | 70 | * When default value is used and returned, default variant is not used and variant name is not set. 71 | * json/csv payloads are evaluated via object evaluation as what returned from Unleash - string, wrapped with Value. 72 | * Additional evaluation data can be received via flag metadata, such as: 73 | * *enabled* - boolean 74 | -------------------------------------------------------------------------------- /providers/unleash/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/open-feature/go-sdk-contrib/providers/unleash 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/Unleash/unleash-client-go/v4 v4.1.4 7 | github.com/open-feature/go-sdk v1.11.0 8 | github.com/stretchr/testify v1.9.0 9 | ) 10 | 11 | require ( 12 | github.com/Masterminds/semver/v3 v3.1.1 // indirect 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/go-logr/logr v1.4.1 // indirect 15 | github.com/kr/pretty v0.3.1 // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | github.com/stretchr/objx v0.5.2 // indirect 18 | github.com/twmb/murmur3 v1.1.8 // indirect 19 | golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect 20 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 21 | gopkg.in/yaml.v3 v3.0.1 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /providers/unleash/pkg/provider_config.go: -------------------------------------------------------------------------------- 1 | package unleash 2 | 3 | import ( 4 | "github.com/Unleash/unleash-client-go/v4" 5 | ) 6 | 7 | // ProviderConfig is the struct containing the provider options. 8 | type ProviderConfig struct { 9 | Options []unleash.ConfigOption 10 | } 11 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["github>open-feature/community-tooling"], 4 | "customManagers": [ 5 | { 6 | "customType": "regex", 7 | "fileMatch": [ 8 | "^Makefile$" 9 | ], 10 | "matchStrings": [ 11 | "ghcr\\.io\\/open-feature\\/flagd-testbed:(?.*?)\\n", 12 | "ghcr\\.io\\/open-feature\\/sync-testbed:(?.*?)\\n" 13 | ], 14 | "depNameTemplate": "open-feature/test-harness", 15 | "datasourceTemplate": "github-releases" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # OpenFeature testing packages 2 | 3 | Testing packages provide shared testing functionality across OpenFeature components, avoiding duplication. 4 | -------------------------------------------------------------------------------- /tests/flagd/README.md: -------------------------------------------------------------------------------- 1 | # flagd integration testing 2 | 3 | This module implements a set of [gherkin integration tests](https://github.com/open-feature/test-harness/blob/main/features). Designed to be imported and tested against by components concerned with breaking changes in the flow. 4 | ```mermaid 5 | flowchart LR 6 | go-sdk --> flagd-provider --> flagd 7 | ``` 8 | -------------------------------------------------------------------------------- /tests/flagd/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/open-feature/go-sdk-contrib/tests/flagd 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/cucumber/godog v0.13.0 7 | github.com/open-feature/go-sdk v1.9.0 8 | ) 9 | 10 | require ( 11 | github.com/cucumber/gherkin/go/v26 v26.2.0 // indirect 12 | github.com/cucumber/messages/go/v21 v21.0.1 // indirect 13 | github.com/go-logr/logr v1.3.0 // indirect 14 | github.com/gofrs/uuid v4.3.1+incompatible // indirect 15 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 16 | github.com/hashicorp/go-memdb v1.3.4 // indirect 17 | github.com/hashicorp/golang-lru v0.5.4 // indirect 18 | github.com/spf13/pflag v1.0.5 // indirect 19 | golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /tests/flagd/pkg/integration/integration.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "time" 7 | 8 | "github.com/open-feature/go-sdk/openfeature" 9 | ) 10 | 11 | var test_provider_supplier func() openfeature.FeatureProvider 12 | 13 | // ctxStorageKey is the key used to pass test data across context.Context 14 | type ctxStorageKey struct{} 15 | 16 | // ctxClientKey is the key used to pass the openfeature client across context.Context 17 | type ctxClientKey struct{} 18 | var domain = "flagd-e2e-tests" 19 | 20 | func aFlagdProviderIsSet(ctx context.Context) (context.Context, error) { 21 | readyChan := make(chan struct{}) 22 | 23 | err := openfeature.SetNamedProvider(domain, test_provider_supplier()) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | callBack := func(details openfeature.EventDetails) { 29 | // emit readiness 30 | close(readyChan) 31 | } 32 | 33 | client := openfeature.NewClient(domain) 34 | client.AddHandler(openfeature.ProviderReady, &callBack) 35 | 36 | select { 37 | case <-readyChan: 38 | case <-time.After(500 * time.Millisecond): 39 | return ctx, errors.New("provider not ready after 500 milliseconds") 40 | } 41 | 42 | return context.WithValue(ctx, ctxClientKey{}, client), nil 43 | } 44 | --------------------------------------------------------------------------------