[![Build Status](https://drone.harton.dev/api/badges/james/lamina/status.svg)](https://drone.harton.dev/james/lamina)
[![Hex.pm](https://img.shields.io/hexpm/v/lamina.svg)](https://hex.pm/packages/lamina)
[![Hippocratic License HL3-FULL](https://img.shields.io/static/v1?label=Hippocratic%20License&message=HL3-FULL&labelColor=5e2751&color=bc8c3d)](https://firstdonoharm.dev/version/3/0/full.html)
# Lamina
Dynamic, runtime configuration for your Elixir app.
Lamina allows you to define a run-time configuration pipeline that can merge
configuration from several sources. This allows the system to be reactive to
changes in its environment.
## Example
The following example defines a configuration for an imaginary HTTP server
application which takes it's configuration from a combination of default values,
the OTP application environment and system environment variables:
```elixir
defmodule MyHttpServer.Config do
use Lamina
provider(Lamina.Provider.Default, listen_port: 4000, listen_address: "0.0.0.0")
provider(Lamina.Provider.ApplicationEnv, otp_app: :my_http_server, key: MyHttpServer.Endpoint)
provider(Lamina.Provider.Env, prefix: "HTTP")
config :listen_port do
cast(&Lamina.Cast.to_integer/1)
validate(fn
port when is_integer(port) and (port in [80, 443] or port >= 1000) -> true
_ -> false
end)
end
config :listen_address do
validate(fn
address when is_binary(address) ->
address
|> String.to_charlist()
|> :inet.parse_address()
|> case do
{:ok, _} -> true
_ -> false
end
_ ->
false
end)
end
end
```
Provider order is preserved, such that providers added later (via the
`provider/1` or `provider/2` macro) have more priority than their predecessors.
This has the effect that when more than one provider can provide a value for a
given configuration item, the most preferred value will be returned.
Each configuration item is defined using the `config/1` or `config/2` macro. If
the configuration item does not need casting to another type, nor validation
then just defining it with `config/1` is sufficient. In some cases it is
necessary to provide additional casting or validating functions. They can be
provided by passing a block containing the `cast/1` or `validate/1` macros.
Make sure that you add your configuration module to your application's
supervisor tree **before** any processes that rely on it's information. Lamina
will fail to start or shutdown on any errors it encounters.
## Lifetimes
All configuration items in Lamina are explicitly marked with a lifetime, which
must be specified by the configuration provider when returning values. The
semantics are as follows:
- `:volatile` - a configuration that could potentially be different every time
it is read. Volatile configuration items are returned by the `ApplicationEnv`
and `Env` providers.
- `:static` - a configuration value that is not going to change until the
provider changes it. Static configuration items are returned by the `Default`
provider, but could also be used for a configuration provider that notifies
the system of configuration changes in some way.
- `{non_neg_integer(), System.time_unit()}` - a value that has a specific expiry
time. This may be used for a configuration source that has explicit leases on
values (ala [Vault](https://www.vaultproject.io/) or a value for which
querying is expensive, and providing an expiry would effectively cache it.
## Querying
When asked to retrieve a configuration value, Lamina queries it's ETS table
using the following query plan; values for which there is no expiry, or which
have not yet expired, ordered by provider weight, descending. It only ever
returns a single row.
If the returned row is marked as `:volatile` then the configuration provider is
immediately queried for a new value, meaning that these requests will pay the
cost of a `GenServer.call/3` to ensure freshness. If this is an issue then you
should consider changing the provider lifetime to use an expiry. The
`ApplicationEnv` and `Env` providers have a configuration option to do this. If
you are the developer of a volatile provider, it is strongly suggested that you
provide for this use case.
## Server configuration
The following options can be passed to the `use Lamina` macro, although it's
probably advisable to leave them as their defaults.
- `gc_timeout: pos_integer()` - how long the server should be idle before
removing expired configuration from the ETS table in milliseconds. Defaults
to 3000.
- `ttl_refresh_fraction: float` - when presented with a configuration value
which has an expiry, the server queues a refresh at some point prior to the
value expiring, in order to avoid having missing configuration. Setting this
to a value between `0` and `1` specifies the proportion of the expiry time to
wait before attempting to refresh the value. Defaults to `0.95`.
## Configuration subscriptions
Lamina defines a `subscribe/1` and `unsubscribe/1` function on each
configuration module, which uses a `Registry` to handle pub-sub for
configuration changes.
This allows your processes to subscribe to configuration changes and update or
restart any services they provide.
### Example
For example, a simple HTTP server which changes it's listen port in response to
a configuration change:
```elixir
defmodule MyHttpServer.Cowboy do
use GenServer
alias Plug.Cowboy
alias MyHttpServer.{Config, Plug}
def init(_) do
with {:ok, port} <- Config.listen_port(),
{:ok, srv} <- Cowboy.http(Plug, [], port: port),
:ok <- Config.subscribe(:listen_port) do
{:ok, %{srv: srv, port: port}}
end
end
def handle_info({:config_change, Config, :listen_port, _old_port, new_port}, %{
port: current_port
})
when new_port != current_port do
with :ok <- Cowboy.shutdown(Plug.HTTP),
{:ok, srv} <- Cowboy.http(Plug, [], port: new_port) do
{:noreply, %{port: new_port, srv: srv}}
else
{:error, reason} -> {:stop, reason, nil}
end
end
def handle_info({:config_change, _, _, _}, state), do: {:noreply, state}
end
```
## Installation
Lamina is [available in Hex](https://hex.pm/packages/lamina), the package can be installed
by adding `lamina` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:lamina, "~> 0.4.2"}
]
end
```
Documentation for the latest release can be found on
[HexDocs](https://hexdocs.pm/lamina) and for the `main` branch on
[docs.harton.nz](https://docs.harton.nz/james/lamina).
## Github Mirror
This repository is mirrored [on Github](https://github.com/jimsynz/lamina)
from it's primary location [on my Forgejo instance](https://harton.dev/james/lamina).
Feel free to raise issues and open PRs on Github.
## License
This software is licensed under the terms of the
[HL3-FULL](https://firstdonoharm.dev), see the `LICENSE.md` file included with
this package for the terms.
This license actively proscribes this software being used by and for some
industries, countries and activities. If your usage of this software doesn't
comply with the terms of this license, then [contact me](mailto:james@harton.nz)
with the details of your use-case to organise the purchase of a license - the
cost of which may include a donation to a suitable charity or NGO.