- - - Page not found -
- -Sorry, but the page you were trying to get to, does not exist. You -may want to try searching this site using the sidebar - - or using our API Reference page - -to find what you were looking for.
- -diff --git a/CHANGELOG.md b/CHANGELOG.md index 85ffbf6..4e9d570 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [2.0.0] - 2023-06-30 +## ** Breaking for Erlang applications (not affecting Elixir applications) ** + +- Due to a new dependency on a murmur3 hashing library implemented in Elixir, the following is now required to use the SDK in Erlang applications: + - Elixir is now required to be installed on your build system when compiling your application. Version 1.13.4 and above is required. + - Rebar3 `rebar_mix` plugin installed in your Rebar3 plugins + - For full details, see the [Erlang SDK reference](https://developer.harness.io/docs/feature-flags/ff-sdks/server-sdks/erlang-sdk-reference/#for-erlang-applications) + +### Enhancements +- Implemented retry logic for authentication, polling, and metrics services for resilience and fault tolerance. +- Changes supervisor +### Fixes +- Swaps out murmur3 nif library which was giving unpredictable runtime behaviour in favour of pure Elixir implementation + + ## [1.2.1] - 2023-06-29 ### Fixes The optional configuration option introduced in 1.2.0 would only work if the application level was set to `info` - this change now sets the `cfclient_evaluation` module to `info` level if `verbose_evaluation_logs` is enabled. diff --git a/README.md b/README.md index dff2a97..0c8fce8 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,18 @@ For a sample FF Erlang SDK Project for Elixir, see our ![FeatureFlags](https://github.com/harness/ff-erlang-server-sdk/raw/main/docs/images/ff-gui.png) -## Requirements +* **For Erlang** applications, install: + + * Erlang/OTP 24 or later + * Rebar3 3.20.0 or later + * Important, since version 2.0.0 the SDK depends on an Elixir hashing library, so the following is also required for Erlang applications: + * Elixir 1.13.4 or later available on your build system + * Rebar3 `rebar_mix` plugin installed in your Rebar3 plugins + +* **For Elixir** applications, install: + * Elixir version 1.13.4 or later + * OTP 24 or later -Erlang OTP 22 or newer. ## Quickstart @@ -52,17 +61,26 @@ To install the SDK for Erlang based applications: 1. Add the SDK as a dependency to your `rebar.config` file: ``` - {deps, [{cfclient, "1.2.0", {pkg, harness_ff_erlang_server_sdk}}]}. + {deps, [{cfclient, "2.0.0", {pkg, harness_ff_erlang_server_sdk}}]}. ``` 2. Add the dependency to your project's `app.src`. ```erlang {applications, - [kernel, stdlib, cfclient] - }, +[kernel, stdlib, cfclient] +}, ``` +2. Add the `rebar_mix` plugin to your `rebar.config` file: + + ```erlang + {project_plugins, [rebar_mix]}. + ``` + +Imporatant: for this plugin to work ensure you have Elixir 1.13.4 or later installed onto your build system + + ### For Elixir applications To install the SDK for Elixir based applications: @@ -72,7 +90,7 @@ To install the SDK for Elixir based applications: ``` defp deps do [ - {:cfclient, "~> 1.1.0", hex: :harness_ff_erlang_server_sdk} + {:cfclient, "~> 2.0.0", hex: :harness_ff_erlang_server_sdk} ] ``` @@ -84,9 +102,9 @@ Provide your API key in `sys.config` using an environment variable: ```erlang [ - {cfclient, [ - {api_key, {environment_variable, "YOUR_API_KEY_ENV_VARIABLE"}, - ]} +{cfclient, [ +{api_key, {environment_variable, "YOUR_API_KEY_ENV_VARIABLE"}, +]} ]. ``` @@ -94,9 +112,9 @@ Or you may provide the API key directly if required: ```erlang [ - {cfclient, [ - {api_key, "YOUR_API_KEY"}, - ]} +{cfclient, [ +{api_key, "YOUR_API_KEY"}, +]} ]. ``` @@ -140,21 +158,21 @@ config :cfclient, ```erlang [{cfclient, [ - %% Set the log level of the SDK to debug - {log_level, debug}, - {api_key, {envrionment_variable, "YOUR_API_KEY_ENV_VARIABLE"}, - {config, [ - {config_url, "https://config.ff.harness.io/api/1.0"}, - {events_url, "https://config.ff.harness.io/api/1.0"}, - {poll_interval, 60}, - {analytics_enabled, true}, - ]}, - ]}] +%% Set the log level of the SDK to debug +{log_level, debug}, +{api_key, {envrionment_variable, "YOUR_API_KEY_ENV_VARIABLE"}, +{config, [ +{config_url, "https://config.ff.harness.io/api/1.0"}, +{events_url, "https://config.ff.harness.io/api/1.0"}, +{poll_interval, 60}, +{analytics_enabled, true}, +]}, +]}] ``` ### Enable Verbose Evaluation Logs -Evaluation logs are `debug` level by default. If required, they can be changed to `info` level. This is useful if production environments do not use `debug` level, but there is a requirement to check low level evaluation logs. +Evaluation logs are `debug` level by default. If required, they can be changed to `info` level. This is useful if production environments do not use `debug` level, but there is a requirement to check low level evaluation logs. Note that this will only affect evaluation log statements. #### Elixir @@ -174,7 +192,7 @@ config :cfclient, ]] ``` -#### Erlang +#### Erlang ```erlang [{cfclient, [ @@ -194,11 +212,11 @@ config :cfclient, ## Run multiple instances of the SDK The SDK by default starts up a single instance called `default` which is configured with your project API key. -If different parts of your application need to use specific [projects](https://developer.harness.io/docs/feature-flags/ff-using-flags/ff-creating-flag/create-a-project/), you can start up additional client instances using by defining additional configuration for each unique project. +If different parts of your application need to use specific [projects](https://developer.harness.io/docs/feature-flags/ff-using-flags/ff-creating-flag/create-a-project/), you can start up additional client instances using by defining additional configuration for each unique project. -### Erlang Project Config +### Erlang Project Config -The additional project config is defined in `sys.config` +The additional project config is defined in `sys.config` The following `sys.config` snippet starts up two additional instances as well along with the default instance: @@ -259,7 +277,7 @@ If you don't require the default instance to be started up, you can do: ``` In your application supervisor, e.g. `src/myapp_sup.erl`, start up a `cfclient_instance` -for each additional project. As the default instance is booted when your application starts, you cannot (and don't need to) start it here. +for each additional project. As the default instance is booted when your application starts, you cannot (and don't need to) start it here. ```erlang init(Args) -> @@ -334,8 +352,8 @@ multi_instance_evaluations() -> ] ``` -2. In your application supervisor, e.g. `lib/myapp/supervisor.ex`, start up `cfclient_instance` -for each of the additional project configurations you provided above. As the default instance is booted when your application starts, you cannot (and don't need to) start it here: +2. In your application supervisor, e.g. `lib/myapp/supervisor.ex`, start up `cfclient_instance` + for each of the additional project configurations you provided above. As the default instance is booted when your application starts, you cannot (and don't need to) start it here: ```elixir def init(_opts) do @@ -533,4 +551,4 @@ In order to run the tests, pull the submodules: ```command git submodule update --init -``` +``` \ No newline at end of file diff --git a/doc/.build b/doc/.build deleted file mode 100644 index 56e9298..0000000 --- a/doc/.build +++ /dev/null @@ -1,43 +0,0 @@ -404.html -api-reference.html -cfclient.html -cfclient_app.html -cfclient_cache.html -cfclient_config.html -cfclient_ets.html -cfclient_evaluator.html -cfclient_instance.html -cfclient_metrics.html -cfclient_retrieve.html -cfclient_sup.html -dist/handlebars.runtime-NWIB6V2M.js -dist/handlebars.templates-IV5W3OL2.js -dist/html-XN2TSG4M.js -dist/html-erlang-6FXMBT73.css -dist/inconsolata-latin-400-normal-RGKDDNDD.woff2 -dist/inconsolata-latin-700-normal-DTS2D7TO.woff2 -dist/inconsolata-latin-ext-400-normal-K7HVGTP7.woff2 -dist/inconsolata-latin-ext-700-normal-4MPBLFZC.woff2 -dist/inconsolata-vietnamese-400-normal-IGQPHHJH.woff2 -dist/inconsolata-vietnamese-700-normal-LHEGSN35.woff2 -dist/lato-latin-300-normal-YUMVEFOL.woff2 -dist/lato-latin-700-normal-2XVSBPG4.woff2 -dist/lato-latin-ext-300-normal-VPGGJKJL.woff2 -dist/lato-latin-ext-700-normal-Q2L5DVMW.woff2 -dist/merriweather-cyrillic-300-italic-M6KMXZSZ.woff2 -dist/merriweather-cyrillic-300-normal-7PAAHU3N.woff2 -dist/merriweather-cyrillic-ext-300-italic-JP3ZEV2P.woff2 -dist/merriweather-cyrillic-ext-300-normal-5LF5LCEK.woff2 -dist/merriweather-latin-300-italic-353COS6Q.woff2 -dist/merriweather-latin-300-normal-RWDJH4FN.woff2 -dist/merriweather-latin-ext-300-italic-MWCA36KE.woff2 -dist/merriweather-latin-ext-300-normal-K6L27CZ5.woff2 -dist/merriweather-vietnamese-300-italic-EHHNZPUO.woff2 -dist/merriweather-vietnamese-300-normal-U376L4Z4.woff2 -dist/remixicon-NKANDIL5.woff2 -dist/search_items-9462DBD2.js -dist/sidebar_items-32BCB229.js -index.html -license.html -readme.html -search.html diff --git a/doc/404.html b/doc/404.html deleted file mode 100644 index 14567d8..0000000 --- a/doc/404.html +++ /dev/null @@ -1,144 +0,0 @@ - - -
- - - - - - -Sorry, but the page you were trying to get to, does not exist. You -may want to try searching this site using the sidebar - - or using our API Reference page - -to find what you were looking for.
- -modules
- - Modules -Feature flags client instance.
Top level supervisor for cfclient.
-type config() :: map().
-
- -type target() ::
- #{identifier := binary(),
- name := binary(),
- anonymous => boolean(),
- attributes := #{atom() := binary() | atom() | list()} | null}.
-
- -spec bool_variation(binary() | string(), target(), boolean()) -> boolean().- -
-spec json_variation(binary() | string(), target(), map()) -> map().- -
-spec number_variation(binary() | list(), target(), number()) -> number().- -
-spec string_variation(binary() | string(), target(), binary()) -> binary().- -
-type config() :: cfclient:config().- -
-type flag() :: cfclient_evaluator:flag().- -
-type segment() :: cfapi_segment:cfapi_segment().- -
-spec cache_flag(flag()) -> ok | {error, outdated}.- -
-spec cache_segment(segment()) -> ok | {error, outdated}.- -
-spec set_pid(pid()) -> ok.
-
- -type config() :: map().
-
- -spec authenticate(binary() | string() | undefined | nil, map()) ->
- {ok, Config :: map()} | {error, Response :: term()}.
-
- -spec create_tables(config()) -> ok.- -
-spec defaults() -> map().
-
- -spec delete_tables(list()) -> ok.
-
- -spec get_config() -> config().- -
-spec get_config(atom()) -> config().- -
-spec get_value(atom() | binary() | string()) -> term().
-
- -spec get_value(atom(), map()) -> term().
-
- -spec init(proplists:proplist()) -> ok.- -
-spec normalize(proplists:proplist()) -> map().- -
-spec parse_jwt(binary()) -> {ok, map()} | {error, Reason :: term()}.
-
- -spec set_config(config()) -> ok.- -
-spec set_config(atom(), config()) -> ok.- -
-spec get(atom(), binary()) -> term().
-
- -spec lookup(atom(), term()) -> list().
-
- -type config() :: map().
-
- -type flag() :: - #{createdAt => integer(), - defaultServe := cfapi_serve:cfapi_serve(), - environment := binary(), - excluded => list(), - feature := binary(), - identifier => binary(), - included => list(), - kind := binary(), - modifiedAt => integer(), - name => binary(), - offVariation := binary(), - prerequisites => list(), - project := binary(), - rules => [map()], - state := binary() | map(), - tags => list(), - variationToTargetMap => [variation_map()] | null, - variations := list(), - version => integer()}.- -
-type rule() :: - #{priority := non_neg_integer(), - clauses := [rule_clause()], - serve => rule_serve(), - op => binary(), - values => [binary()], - excluded => [map()] | null, - included => [map()] | null}.- -
-type rule_clause() :: #{op := binary(), values := [binary()]}.
-
- -type rule_serve() :: #{variation := binary(), distribution => boolean()}.
-
- -type segment() :: cfapi_segment:cfapi_segment().- -
-type target() :: cfclient:target().- -
-type variation_map() :: - #{variation := binary(), targets := [cfapi_variation_map:cfapi_variation_map()]}.- -
-spec custom_attribute_to_binary(binary() | atom() | number() | string()) -> binary() | [binary()].
-
- -spec is_rule_included_or_excluded([rule_clause()], target()) -> included | excluded | false.- -
Feature flags client instance.
It creates the ETS tables used to cache flag data from the server and flag usage metrics. It runs periodic tasks to pull data from the server and send metrics to it.
An default instance is started by the cfclient application. Additional instances can be started if multiple Harness projects need to be used. project. --spec start_link(proplists:proplist()) -> {ok, pid()} | ignore | {error, term()}.- -
-spec stop(map()) -> ok | {error, not_found, term()}.
-
- -type config() :: map().
-
- -spec process_metrics(config()) -> ok | {error, api}.- -
-spec record(binary(), cfclient:target(), binary(), binary(), config()) -> atom().- -
-type config() :: map().
-
- -type flag() :: cfapi_feature_config:cfapi_feature_config().- -
-type segment() :: cfapi_segment:cfapi_segment().- -
Top level supervisor for cfclient.
Called by application, starting up the default client instance. --spec start_link(proplists:proplist()) -> supervisor:startlink_ret().- -
'+((e=n.lambda(l,l))!=null?e:"")+`
-`},9:function(n,l,a,c,s){var e,o=n.lookupProperty||function(u,r){if(Object.prototype.hasOwnProperty.call(u,r))return u[r]};return((e=(o(a,"isArray")||l&&o(l,"isArray")||n.hooks.helperMissing).call(l??(n.nullContext||{}),l!=null?o(l,"results"):l,{name:"isArray",hash:{},fn:n.program(10,s,0),inverse:n.program(12,s,0),data:s,loc:{start:{line:28,column:2},end:{line:34,column:14}}}))!=null?e:"")+` -The search functionality is full-text based. Here are some tips:
- -foo bar
) are searched as OR
*
anywhere (such as fo*
) as wildcard+
before a word (such as +foo
) to make its presence required-
before a word (such as -foo
) to make its absence required:
to search on a particular field (such as field:word
). The available fields are title
and doc
WORD^NUMBER
(such as foo^2
) to boost the given wordWORD~NUMBER
(such as foo~2
) to do a search with edit distance on wordTo quickly go to a module, type, or function, use the autocompletion feature in the sidebar search.
-`},10:function(n,l,a,c,s){var e,o=n.lookupProperty||function(u,r){if(Object.prototype.hasOwnProperty.call(u,r))return u[r]};return"Sorry, we couldn't find anything for "+n.escapeExpression((e=(e=o(a,"value")||(l!=null?o(l,"value"):l))!=null?e:n.hooks.helperMissing,typeof e=="function"?e.call(l??(n.nullContext||{}),{name:"value",hash:{},data:s,loc:{start:{line:29,column:48},end:{line:29,column:57}}}):e))+`.
-`},12:function(n,l,a,c,s){var e,o=n.lookupProperty||function(u,r){if(Object.prototype.hasOwnProperty.call(u,r))return u[r]};return(e=o(a,"if").call(l??(n.nullContext||{}),l!=null?o(l,"value"):l,{name:"if",hash:{},fn:n.program(13,s,0),inverse:n.program(15,s,0),data:s,loc:{start:{line:30,column:2},end:{line:34,column:2}}}))!=null?e:""},13:function(n,l,a,c,s){var e,o=n.lookupProperty||function(u,r){if(Object.prototype.hasOwnProperty.call(u,r))return u[r]};return"Invalid search: "+n.escapeExpression((e=(e=o(a,"errorMessage")||(l!=null?o(l,"errorMessage"):l))!=null?e:n.hooks.helperMissing,typeof e=="function"?e.call(l??(n.nullContext||{}),{name:"errorMessage",hash:{},data:s,loc:{start:{line:31,column:23},end:{line:31,column:39}}}):e))+`.
-`},15:function(n,l,a,c,s){return`Please type something into the search bar to perform a search.
- `},compiler:[8,">= 4.3.0"],main:function(n,l,a,c,s){var e,o=l??(n.nullContext||{}),u=n.lookupProperty||function(r,i){if(Object.prototype.hasOwnProperty.call(r,i))return r[i]};return`- Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License.-
Harness is a feature management platform that helps -teams to build better software and to test features quicker.
This repository contains our Feature Flags SDK for Erlang and other BEAM -languages such as Elixir.
table-of-contents
- - Table of Contents -Intro<br> -Requirements<br> -Quickstart<br> -Further Reading<br> -Build Instructions<br>
intro
- - Intro -This sample doesn’t include configuration -options. For in depth steps and configuring the SDK, e.g. disabling -streaming or using our Relay Proxy, see the -Erlang SDK Reference
For a sample FF Erlang SDK project, see our -test Erlang project.
For a sample FF Erlang SDK Project for Elixir, see our -test Elixir Project.
requirements
- - Requirements -Erlang OTP 22 or newer.
quickstart
- - Quickstart -To follow along with our test code sample, make sure you have:
harnessappdemodarkmode
install-the-sdk
- - Install the SDK -for-erlang-applications
- - For Erlang applications -To install the SDK for Erlang based applications:
rebar.config
file: {deps, [{cfclient, "1.2.0", {pkg, harness_ff_erlang_server_sdk}}]}.
app.src
. {applications,
- [kernel, stdlib, cfclient]
- },
for-elixir-applications
- - For Elixir applications -To install the SDK for Elixir based applications:
Add the SDK as a dependency to mix.exs
deps()
:
defp deps do
- [
- {:cfclient, "~> 1.1.0", hex: :harness_ff_erlang_server_sdk}
- ]
configuration
- - Configuration -erlang
- - Erlang -Provide your API key in sys.config
using an environment variable:
[
- {cfclient, [
- {api_key, {environment_variable, "YOUR_API_KEY_ENV_VARIABLE"},
- ]}
-].
Or you may provide the API key directly if required:
[
- {cfclient, [
- {api_key, "YOUR_API_KEY"},
- ]}
-].
elixir
- - Elixir -Provide your API key in config/prod.exs
using an environment variable: :
config :cfclient,
- api_key: System.get_env("YOUR_API_KEY_ENVIRONMENT_VARIABLE")
Or you may provide the API key directly if required:
config :cfclient,
- api_key: "YOUR_API_KEY"
set-logging-level
- - Set logging level -Optionally you may set the required log level of the SDK. If not provided, the SDK will default to warning
.
config :cfclient,
- # Set the log level of the SDK to debug
- log_level: :debug
- [api_key: System.get_env("FF_API_KEY_0"),
- # For additional config you can pass in, see Erlang SDK docs: https://github.com/harness/ff-erlang-server-sdk/blob/main/docs/further_reading.md#further-reading
- # we are just using the main config url here as an example.
- config: [
- config_url: "https://config.ff.harness.io/api/1.0",
- events_url: "https://events.ff.harness.io/api/1.0",
- poll_interval: 60000,
- analytics_enabled: true
- ]]
[{cfclient, [
- %% Set the log level of the SDK to debug
- {log_level, debug},
- {api_key, {envrionment_variable, "YOUR_API_KEY_ENV_VARIABLE"},
- {config, [
- {config_url, "https://config.ff.harness.io/api/1.0"},
- {events_url, "https://config.ff.harness.io/api/1.0"},
- {poll_interval, 60},
- {analytics_enabled, true},
- ]},
- ]}]
enable-verbose-evaluation-logs
- - Enable Verbose Evaluation Logs -Evaluation logs are debug
level by default. If required, they can be changed to info
level. This is useful if production environments do not use debug
level, but there is a requirement to check low level evaluation logs.
-Note that this will only affect evaluation log statements.
config :cfclient,
- log_level: :error
- [api_key: System.get_env("FF_API_KEY_0"),
- config: [
- verbose_evaluation_logs: true
- ]]
[{cfclient, [
- {log_level, error},
- {api_key, {envrionment_variable, "YOUR_API_KEY_ENV_VARIABLE"},
- {config, [
- {verbose_evaluation_logs, true},
- ]},
- ]}]
run-multiple-instances-of-the-sdk
- - Run multiple instances of the SDK -The SDK by default starts up a single instance called default
which is configured with your project API key.
-If different parts of your application need to use specific projects, you can start up additional client instances using by defining additional configuration for each unique project.
erlang-project-config
- - Erlang Project Config -The additional project config is defined in sys.config
The following sys.config
snippet starts up two additional instances as well along with the default instance:
[
- %% Project config name: This is an arbitrary identifier, but it must be unique per project config you define.
- {harness_project_1_config, [
- {cfclient, [
- {config, [
- %% Instance name: This must be unique across all of the project configs. E.g. it cannot be the same as an instance name
- %% in another project config.
- %% It will be the name you use when calling SDK API functions like `bool_variation/4`,
- {name, instance_name_1}
- ]},
- %% The API key for the Harness project you want to use with this SDK instance.
- {api_key, {environment_variable, "PROJECT_1_API_KEY"}}]
- }
- ]},
-
- {harness_project_2_config, [
- {cfclient, [
- {config, [
- {name, instance_name_2}
- ]},
- {api_key, {environment_variable, "PROJECT_2_API_KEY"}}]
- }
- ]},
-
- {cfclient, [
- {api_key, {environment_variable, "FF_API_KEY"}},
- {config, [
- {config_url, "https://config.ff.harness.io/api/1.0"},
- {events_url, "https://config.ff.harness.io/api/1.0"}
- ]},
- {analytics_push_interval, 60000}
- ]
-}].
Note: if the default instance fails to start, for example due to an authentication error with the API key, then the SDK -will fail to boot and the additional instances won't start.
If you don't require the default instance to be started up, you can do:
% ... additional project config
-
- {cfclient, [
- {start_default_instance, false},
- %% The remaining tuples will be ignored, so you can choose to include or omit them.
- {api_key, {environment_variable, "FF_API_KEY"}},
- {config, [
- {config_url, "https://config.ff.harness.io/api/1.0"},
- {events_url, "https://config.ff.harness.io/api/1.0"}
- ]},
- {analytics_push_interval, 60000}
- ]
-},
In your application supervisor, e.g. src/myapp_sup.erl
, start up a cfclient_instance
-for each additional project. As the default instance is booted when your application starts, you cannot (and don't need to) start it here.
init(Args) ->
- HarnessProject1Args = application:get_env(harness_project_1_config, cfclient, []),
- HarnessProject2Args = application:get_env(harness_project_2_config, cfclient, []),
-
- ChildSpec1 = #{id => project1_cfclient_instance, start => {cfclient_instance, start_link, [HarnessProject1Args]}},
- ChildSpec2 = #{id => project2_cfclient_instance, start => {cfclient_instance, start_link, [HarnessProject2Args]}},
-
- MaxRestarts = 1000,
- MaxSecondsBetweenRestarts = 3600,
- SupFlags = #{strategy => one_for_one,
- intensity => MaxRestarts,
- period => MaxSecondsBetweenRestarts},
-
- {ok, {SupFlags, [ChildSpec1, ChildSpec2]}}.
using-a-specific-instance-of-the-sdk
- - Using a specific instance of the SDK -To use a specific SDK instance, you provide the instance name to the public function you are calling. For example bool_variation/4
.
The following is an example of referencing the instances we have created above:
-module(multi_instance_example).
-
--export([multi_instance_evaluations/0]).
-
-multi_instance_evaluations() ->
- Target = #{
- identifier => "Harness_Target_1",
- name => "HT_1",
- attributes => #{email => <<"demo@harness.io">>}
- },
-
- %% Instance 1
- Project1Flag = <<"harnessappdemodarkmodeproject1">>,
- Project1Result = cfclient:bool_variation(instance_name_1, Project1Flag, Target, false),
- logger:info("Instance Name 1 : Variation for Flag ~p with Target ~p is: ~p~n",
- [Project1Flag, maps:get(identifier, Target), Project1Result]),
-
- %% Instance 2
- Project2Flag = <<"harnessappdemodarkmodeproject2">>,
- Project2Result = cfclient:bool_variation(instance_name_2, Project2Flag, Target, false),
- logger:info("Instance name 2 Variation for Flag ~p with Target ~p is: ~p~n",
- [Project2Flag, maps:get(identifier, Target), Project2Result]).
-
- %% Default instance
- DefaultProjectFlag = <<"harnessappdemodarkmodeprojectdefault">>,
- DefaultProjectResult = cfclient:bool_variation(Project2Flag, Target, false),
- logger:info("Default instance Variation for Flag ~p with Target ~p is: ~p~n",
- [DefaultProjectFlag, maps:get(identifier, Target), DefaultProjectResult]).
elixir-1
- - Elixir -Create project configurations for each new instance you would like to start in your config/config.exs
file:
# Config for "project 1"
- config :elixirsample, project1:
- [
- api_key: System.get_env("FF_API_KEY_1"),
- config: [name: :project1]
- ]
-
- # Config for "project 2"
- config :elixirsample, project2:
- [
- api_key: System.get_env("FF_API_KEY_2"),
- config: [name: :project2]
- ]
In your application supervisor, e.g. lib/myapp/supervisor.ex
, start up cfclient_instance
-for each of the additional project configurations you provided above. As the default instance is booted when your application starts, you cannot (and don't need to) start it here:
def init(_opts) do
- project_1_config = Application.get_env(:elixirsample, :project1, [])
- project_2_config = Application.get_env(:elixirsample, :project2, [])
- children = [
- %{
- id: :project1_cfclient_instance,
- start: {:cfclient_instance, :start_link, [project_1_config]},
- type: :supervisor
- },
- %{
- id: :project2_cfclient_instance,
- start: {:cfclient_instance, :start_link, [project_2_config]},
- type: :supervisor
- },
- ]
- Supervisor.init(children, strategy: :one_for_one)
- end
To use a specific SDK instance, you provide the instance name to the public function you are calling. For example use bool_variation/4
instead of bool_variation/3
- see the following code sample:
defmodule ElixirSample.EvaluationSample do
- require Logger
-
- def getFlagLoop() do
- target = %{
- identifier: "harness",
- name: "Harness",
- anonymous: false,
- attributes: %{}
- }
-
- # Default instance
- flag = "projectflag"
- result = :cfclient.bool_variation(flag, target, false)
-
- Logger.info(
- "SVariation for Flag #{flag} with Target #{inspect(target)} is: #{result}"
- )
-
- # Instance 1
- project_1_flag = "project1flag"
- project_1_result = :cfclient.number_variation(:project1, project_1_flag, target, 3)
-
- Logger.info(
- "SDK instance 1: Variation for Flag #{project_1_flag} with Target #{inspect(target)} is: #{project_1_result}"
- )
-
- # Instance 2
- project_2_flag = "project2flag"
- project_2_result = :cfclient.bool_variation(:project2, project_2_flag, target, false)
-
- Logger.info(
- "SDK instance 2: Variation for Flag #{project_2_flag} with Target #{inspect(target)} is: #{project_2_result}"
- )
-
- Process.sleep(10000)
- getFlagLoop()
-
- # Default instance
- default_project_flag = "defaultflag"
- default_project_result = :cfclient.bool_variation(default_project_flag, target, false)
-
- Logger.info(
- "Default instance: Variation for Flag #{default_project_flag} with Target #{inspect(target)} is: #{default_project_result}"
- )
-
- Process.sleep(10000)
- getFlagLoop()
- end
- end
code-sample
- - Code Sample -erlang-1
- - Erlang -Call the API to get the value of the harnessappdemodarkmode
flag you created
-via https://www.harness.io/.
get_flag_loop() ->
- Target = #{identifier => "Harness_Target_1",
- name => "HT_1",
- %% Attribute keys must be atoms.
- %% Values must be either bitstrings, atoms, or a list of bitstrings/atoms - see Targets with custom attributes section below.
- attributes => #{email => <<"demo@harness.io">>}
- },
- FlagIdentifier = "harnessappdemodarkmode",
- Result = cfclient:bool_variation(FlagIdentifier, Target, false),
- logger:info("Variation for Flag ~p witih Target ~p is: ~p~n", [FlagIdentifier, maps:get(identifier, Target), Result]),
- timer:sleep(10000),
- get_flag_loop().
elixir-2
- - Elixir -Call the API to get the value of the harnessappdemodarkmode
flag you created
-via https://www.harness.io/.
def getFlagLoop() do
- target = %{
- identifier: "Harness_Target_1",
- name: "HT_1"
-
- # Attribute keys must be atoms.
- # Values must be either binaries, atoms, or a list of binaries/atoms.
- # See "targets with custom attributes" below.
- attributes: %{email: "demo@harness.io"}
- }
-
- flag_identifier = "harnessappdemodarkmode"
-
- result = :cfclient.bool_variation(flag_identifier, target, false)
- Logger.info("Variation for Flag #{flag_identifier} with Target #{inspect(target)} is: #{result)")
- Process.sleep(10000)
- getFlagLoop()
-
targets-with-custom-attributes
- - Targets with custom attributes -You can use the attributes
map to provide custom attributes. If the target
-isn't anonymous, the attributes will shortly appear in the Harness UI after an
-evaluation using the target.
You can create Group Rules -based on these attributes.
Note: attribute
keys must be atoms
and the values must either be binaries
-or atoms
or a list of binaries
or atoms
.
erlang-2
- - Erlang: - TargetBetaGroup = #{'identifier' => <<"my_target">>,
- name => <<"my_target_name">>,
- anonymous => <<"">>,
- attributes => #{beta => <<"beta_group_1">>}
- },
- TargetBetaGroups = #{'identifier' => <<"my_other_target">>,
- name => <<"my_other_target_name">>,
- anonymous => <<"">>,
- attributes => #{beta => [<<"beta_group_1">>, 'beta_group_2']}}
- },
- TargetAlphaGroup = #{'identifier' => <<"my_alpha_target">>,
- name => <<"my_alpha_target_name">>,
- anonymous => <<"">>,
- attributes => #{alpha => 'alpha_group_1'}
- },
elixir-3
- - Elixir -target_beta_group = %{
- identifier: "my_target",
- name: "my_target_name",
- anonymous: "",
- attributes: %{beta: "beta_group_1"}
-}
-
-target_beta_groups = %{
- identifier: "my_other_target",
- name: "my_other_target_name",
- anonymous: "",
- attributes: %{
- beta: ["beta_group_1", :beta_group_2]
- }
-}
-
-target_alpha_group = %{
- identifier: "my_alpha_target",
- name: "my_alpha_target_name",
- anonymous: "",
- attributes: %{alpha: :alpha_group_1}
-}
additional-reading
- - Additional Reading -For further examples and config options, see the Erlang SDK Further -Reading.
For more information about Feature Flags, see our Feature Flags -documentation.
contributing
- - Contributing -In order to run the tests, pull the submodules:
git submodule update --init
-