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.
Intro
Requirements
Quickstart
Further Reading
Build Instructions
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.
-
For Erlang applications, install:
- Erlang/OTP 24 or later
- Rebar3 3.20.0 or later
- Important, since version 2.0.1 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
To follow along with our test code sample, make sure you have:
- Created a Feature Flag on the Harness Platform
called
harnessappdemodarkmode
- Created a server SDK key and made a copy of it
To install the SDK for Erlang based applications:
- Add the SDK as a dependency to your
rebar.config
file:
{deps, [{cfclient, "2.0.1", {pkg, harness_ff_erlang_server_sdk}}]}.
- Add the dependency to your project's
app.src
.
{applications,
[kernel, stdlib, cfclient]
},
- Add the
rebar_mix
plugin to yourrebar.config
file:
{project_plugins, [rebar_mix]}.
Imporatant: for this plugin to work ensure you have Elixir 1.13.4 or later installed onto your build system
To install the SDK for Elixir based applications:
-
Add the SDK as a dependency to
mix.exs
deps()
:defp deps do [ {:cfclient, "~> 2.0.1", hex: :harness_ff_erlang_server_sdk} ]
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"},
]}
].
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"
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},
]},
]}]
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,
# Set the log level of the SDK to debug
log_level: :debug,
verbose_evaluation_logs: true,
[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, error},
{verbose_evaluation_logs, true},
{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},
]},
]}]
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.
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]}}.
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]).
-
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 upcfclient_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 ofbool_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
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().
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()
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
.
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'}
},
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}
}
For further examples and config options, see the Erlang SDK Further Reading.
For more information about Feature Flags, see our Feature Flags documentation.
In order to run the tests, pull the submodules:
git submodule update --init