Packages

Elixir library for feature flags integration with Consul

Current section

Files

Jump to
feature_flipper lib feature_flipper config_loader.ex
Raw

lib/feature_flipper/config_loader.ex

defmodule FeatureFlipper.ConfigLoader do
@moduledoc """
Handles loading and caching of feature flipper configuration from Consul.
"""
require Logger
alias FeatureFlipper.ConsulClient
# Module state keys for persistent_term
@config_key :feature_flipper_config
## defined FF in code
@flippers_keys :defined_flipper_flippers
@consul_data_key :feature_flipper_consul_data
@cache_key :feature_flipper_cache
@doc """
Initialize the ConfigLoader with configuration.
"""
def init do
config = get_config() |> Enum.into(%{})
config =
if FeatureFlipper.Config.valid?(config) do
FeatureFlipper.Config.config(config)
else
raise "Invalid config"
end
if config.env == "test" do
:persistent_term.put(@config_key, config)
:persistent_term.put(@consul_data_key, %{})
:persistent_term.put(@cache_key, %{})
load_configuration_test()
else
# Store config in persistent_term
:persistent_term.put(@config_key, config)
:persistent_term.put(@consul_data_key, %{})
:persistent_term.put(@cache_key, %{})
# Load initial configuration
load_configuration()
end
end
@doc """
Register flippers from a definition module.
"""
def register_flippers(flippers) do
new_flippers =
flippers
|> Enum.into(%{}, fn {name, metadata} -> {name, metadata} end)
## Feature flippers that needs to fetched from consul
:persistent_term.put(@flippers_keys, new_flippers)
:ok
end
@doc """
Get the value of a feature flipper.
"""
def get_flipper_value(flipper_name, consul_key \\ nil, metadata \\ %{}) do
cache = :persistent_term.get(@cache_key, %{})
case Map.get(cache, flipper_name) do
nil ->
# Not in cache, determine value and cache it
value = determine_flipper_value(consul_key, metadata, %{}, config())
put_cache_value(flipper_name, value)
value
value ->
value
end
end
@doc """
Reload configuration from Consul.
"""
def reload_configuration do
case load_consul_data(config()) do
{:ok, consul_data} ->
Logger.info(
"Successfully loaded Consul configuration with #{map_size(consul_data)} entries"
)
:persistent_term.put(@consul_data_key, consul_data)
flippers = :persistent_term.get(@flippers_keys, %{})
populate_cache(flippers, consul_data, config())
:ok
{:error, reason} ->
Logger.error("Failed to reload Consul configuration: #{inspect(reason)}")
{:error, reason}
end
end
@doc """
Get all feature flags and their current values.
"""
def get_all_flags do
flippers = :persistent_term.get(@flippers_keys, %{})
cache = :persistent_term.get(@cache_key, %{})
flippers
|> Enum.into(%{}, fn {name, metadata} ->
consul_key = Map.get(metadata, :key, Atom.to_string(name))
value = get_cached_value(name, consul_key, metadata, cache)
{name, value}
end)
end
# Private functions
defp config do
:persistent_term.get(@config_key)
end
defp load_consul_data(config) do
ConsulClient.get_feature_flippers(config)
end
defp populate_cache(flippers, consul_data, config) do
cache_updates =
flippers
|> Enum.into(%{}, fn {name, metadata} ->
consul_key = Map.get(metadata, :key, Atom.to_string(name))
value = determine_flipper_value(consul_key, metadata, consul_data, config)
{name, value}
end)
current_cache = :persistent_term.get(@cache_key, %{})
new_cache = Map.merge(current_cache, cache_updates)
:persistent_term.put(@cache_key, new_cache)
end
defp get_cached_value(flipper_name, consul_key, metadata, cache) do
case Map.get(cache, flipper_name) do
nil ->
# Not in cache, determine value and cache it
value = determine_flipper_value(consul_key, metadata, %{}, config())
put_cache_value(flipper_name, value)
value
value ->
value
end
end
defp put_cache_value(flipper_name, value) do
current_cache = :persistent_term.get(@cache_key, %{})
new_cache = Map.put(current_cache, flipper_name, value)
:persistent_term.put(@cache_key, new_cache)
end
defp determine_flipper_value(consul_key, metadata, consul_data, config) do
if config.env in ["prod", "staging", "dev"] do
# Production environment - use Consul data
get_consul_value(consul_key, consul_data, config)
else
## test env
get_default_value(metadata, config)
end
end
defp get_consul_value(consul_key, consul_data, config) do
hostname_key = "#{consul_key}_#{config.hostname}"
cond do
# Check hostname-specific value first
Map.has_key?(consul_data, hostname_key) ->
consul_data[hostname_key]
# Check general value
Map.has_key?(consul_data, consul_key) ->
consul_data[consul_key]
# Default to false if not found in Consul
true ->
false
end
end
defp get_default_value(metadata, _config) do
case Map.get(metadata, :force_disable, false) do
true -> false
false -> true
end
end
defp get_config do
Application.get_all_env(:feature_flipper)
end
defp load_configuration do
case load_consul_data(config()) do
{:ok, consul_data} ->
Logger.info("Successfully loaded initial Consul configuration #{inspect(consul_data)}")
:persistent_term.put(@consul_data_key, consul_data)
defined_flippers = :persistent_term.get(@flippers_keys, %{})
populate_cache(defined_flippers, consul_data, config())
{:error, reason} ->
Logger.warning("Failed to load initial Consul configuration: #{inspect(reason)}")
{:error, reason}
end
end
defp load_configuration_test do
:persistent_term.put(@consul_data_key, %{})
:persistent_term.put(@cache_key, %{})
defined_flippers = :persistent_term.get(@flippers_keys)
new_cache =
defined_flippers
|> Enum.into(
%{},
fn
{name, %{force_disable: true}} ->
{name, false}
{name, _} ->
{name, true}
end
)
:persistent_term.put(@cache_key, new_cache)
end
end