Configuration
Configuration is an essential aspect of any cloud-native application. Effect simplifies the process of managing configuration by offering a convenient interface for configuration providers.
The configuration front-end in Effect enables ecosystem libraries and applications to specify their configuration requirements in a declarative manner. It offloads the complex tasks to a ConfigProvider
, which can be supplied by third-party libraries.
Effect comes bundled with a straightforward default ConfigProvider
that retrieves configuration data from environment variables. This default provider can be used during development or as a starting point before transitioning to more advanced configuration providers.
To make our application configurable, we need to understand three essential elements:
-
Config Description: We describe the configuration data using an instance of
Config<A>
. If the configuration data is simple, such as astring
,number
, orboolean
, we can use the built-in functions provided by theConfig
module. For more complex data types like HostPort, we can combine primitive configs to create a custom configuration description. -
Config Frontend: We utilize the instance of
Config<A>
to load the configuration data described by the instance (aConfig
is, in itself, an effect). This process leverages the currentConfigProvider
to retrieve the configuration. -
Config Backend: The
ConfigProvider
serves as the underlying engine that manages the configuration loading process. Effect comes with a default config provider as part of its default services. This default provider reads the configuration data from environment variables. If we want to use a custom config provider, we can utilize theLayer.setConfigProvider
layer to configure the Effect runtime accordingly.
Effect provides several built-in types for configuration values, which you can use right out of the box:
Type | Description |
---|---|
string | Constructs a config for a string value. |
number | Constructs a config for a float value. |
boolean | Constructs a config for a boolean value. |
integer | Constructs a config for an integer value. |
date | Constructs a config for a date value. |
literal | Constructs a config for a literal (*) value. |
logLevel | Constructs a config for a LogLevel value. |
duration | Constructs a config for a duration value. |
redacted | Constructs a config for a secret value. |
url | Constructs a config for an URL value. |
(*) string | number | boolean | null | bigint
Example (Using Primitives)
Here’s an example of loading a basic configuration using environment variables for HOST
and PORT
:
If you run this without setting the required environment variables:
you’ll see an error indicating the missing configuration:
To run the program successfully, set the environment variables as shown below:
Output:
Sometimes, you may encounter situations where an environment variable is missing, leading to an incomplete configuration. To address this, Effect provides the Config.withDefault
function, which allows you to specify a default value. This fallback ensures that your application continues to function even if a required environment variable is not set.
Example (Using Default Values)
Running this program with only the HOST
environment variable set:
produces the following output:
In this case, even though the PORT
environment variable is not set, the program continues to run, using the default value of 8080
for the port. This ensures that the application remains functional without requiring every configuration to be explicitly provided.
Effect provides several built-in constructors that allow you to define and manipulate configurations. These constructors take a Config
as input and produce another Config
, enabling more complex configuration structures.
Here are some of the key constructors:
Constructor | Description |
---|---|
array | Constructs a configuration for an array of values. |
chunk | Constructs a configuration for a sequence of values. |
option | Returns an optional configuration. If the data is missing, the result will be None ; otherwise, it will be Some . |
repeat | Describes a sequence of values, each following the structure of the given config. |
hashSet | Constructs a configuration for a set of values. |
hashMap | Constructs a configuration for a key-value map. |
Additionally, there are three special constructors for specific use cases:
Constructor | Description |
---|---|
succeed | Constructs a config that contains a predefined value. |
fail | Constructs a config that fails with the specified error message. |
all | Combines multiple configurations into a tuple, struct, or argument list. |
Example (Using array
Constructor)
The following example demonstrates how to load an environment variable as an array of strings using the Config.array
constructor.
If we run this program with the following environment variable:
The output will be:
This shows how the array
constructor converts a comma-separated string from an environment variable into an array of values, making configuration handling more flexible.
Effect provides several built-in operators to work with configurations, allowing you to manipulate and transform them according to your needs.
These operators enable you to modify configurations or validate their values:
Operator | Description |
---|---|
validate | Ensures that a configuration meets certain criteria, returning a validation error if it does not. |
map | Transforms the values of a configuration using a provided function. |
mapAttempt | Similar to map , but catches any errors thrown by the function and converts them into validation errors. |
mapOrFail | Like map , but the function can fail. If it does, the result is a validation error. |
Example (Using validate
Operator)
If we run this program with an invalid NAME
value:
The output will be:
Fallback operators are useful when you want to provide alternative configurations in case of errors or missing data. These operators ensure that your program can still run even if some configuration values are unavailable.
Operator | Description |
---|---|
orElse | Attempts to use the primary config first. If it fails or is missing, it falls back to another config. |
orElseIf | Similar to orElse , but it switches to the fallback config only if the error matches a condition. |
Example (Using orElse
for Fallback)
In this example, the program requires two configuration values: A
and B
. We set up two configuration providers, each containing only one of the required values. Using the orElse
operator, we combine these providers so the program can retrieve both A
and B
.
If we run this program:
The output will be:
Effect allows you to define configurations for custom types by combining primitive configs using Config
operators (such as zip
, orElse
, map
) and constructors (like array
, hashSet
).
For example, let’s create a HostPort
class, which has two fields: host
and port
.
To define a configuration for this custom type, we can combine primitive configs for string
and number
:
In this example, Config.all(configs)
combines two primitive configurations, Config<string>
and Config<number>
, into a Config<[string, number]>
. The Config.map
operator is then used to transform these values into an instance of the HostPort
class.
Example (Using Custom Configuration)
When you run this program, it will try to retrieve the values for HOST
and PORT
from your environment variables:
If successful, it will print:
We’ve seen how to define configurations at the top level, whether for primitive or custom types. In some cases, though, you might want to structure your configurations in a more nested way, organizing them under common namespaces for clarity and manageability.
For instance, consider the following ServiceConfig
type:
If you were to use this configuration in your application, it would expect the HOST
, PORT
, and TIMEOUT
environment variables at the top level. But in many cases, you may want to organize configurations under a shared namespace—for example, grouping HOST
and PORT
under a SERVER
namespace, while keeping TIMEOUT
at the root.
To do this, you can use the Config.nested
operator, which allows you to nest configuration values under a specific namespace. Let’s update the previous example to reflect this:
Now, if you run your application with this configuration setup, it will look for the following environment variables:
SERVER_HOST
for the host valueSERVER_PORT
for the port valueTIMEOUT
for the timeout value
This structured approach keeps your configuration more organized, especially when dealing with multiple services or complex applications.
When testing services, there are times when you need to provide specific configurations for your tests. To simulate this, it’s useful to mock the configuration backend that reads these values.
You can achieve this using the ConfigProvider.fromMap
constructor. This method allows you to create a configuration provider from a Map<string, string>
, where the map represents the configuration data. You can then use this mock provider in place of the default one by calling Layer.setConfigProvider
. This function returns a Layer
that can override the default configuration for your tests.
Example (Mocking a Config Provider for Testing)
This approach helps you create isolated tests that don’t rely on external environment variables, ensuring your tests run consistently with mock configurations.
The Config.redacted
function is used to handle sensitive information safely. It parses the configuration value and wraps it in a Redacted<string>
, a specialized data type designed to protect secrets.
When you log a Redacted
value using console.log
, the actual content remains hidden, providing an extra layer of security. To access the real value, you must explicitly use Redacted.value
.
Example (Handling Redacted Values)
When this program is executed:
The output will look like this:
As shown, when logging the Redacted
value using console.log
, the output is <redacted>
, ensuring that sensitive data remains concealed. However, by using Redacted.value
, the true value ("my-api-key"
) can be accessed and displayed, providing controlled access to the secret.
Deprecated since version 3.3.0: Please use Config.redacted for handling sensitive information going forward.
The Config.secret
function was previously used to secure sensitive information in a similar way to Config.redacted
. It wraps configuration values in a Secret
type, which also conceals details when logged but allows access via Secret.value
.
Example (Using Deprecated Config.secret
)
When this program is executed:
The output will look like this: