Mahaul - Supercharge the environment variables usage in your Elixir app

Hey all :wave:

I just published mahaul package for streamlining the environment variables usage in your Elixir apps. Parse and validate your environment variables easily in Elixir with the following benefits.

  • Compile time access guarantees
  • Parsed values with accurate elixir data types
  • Validation of required values before app boot
  • mix environment specific defaults and fallbacks

Read more for understanding why to use this package and its benefits. The complete documentation for mahaul is available online at HexDocs.

We have been using this in production and it has already saved us from bad releases where we deployed to production and forgot to set the needed environment variables after an update. Though it seems to work nicely for a variety of use cases, I’m relatively new to Elixir (only a few months) so I’d love your inputs and reviews on this.

Thanks to this wonderful Elixir community, I’ve never come across anything like this from a community standpoint, so hats off, I’m enjoying every moment of it since the day I started learning Elixir :heavy_heart_exclamation:

10 Likes

Not bad!

The only issue I have with this library is the fact that you have to define the variables in separate modules. I think it would be nice to be able to define the variables directly in dev.exs, runtime.exs, and most probably it can be achieved with some metaprogramming magic.

I really like the idea of compile time guarantees for env variables at first glance, but I guess it’s false confidence b/c the env can always change on you after compilation leading to runtime errors. Still seems like a nice useful library. Thanks for sharing.

@D4no0 I’m not sure if I understand the issue, can you please provide some example or reference?

You don’t have to create separate modules, rather only a single module for initializing your environment variables configurations. mahaul follows the similar patterns used by other libraries from the elixir community like ecto, phoenix and many more.

If I have to draw parallels with say ecto, you initialize your MyApp.Repo once using the ecto provided Ecto.Repo module, passing configurations. And ecto, at compile time, generates the functions and needed interfaces on your MyApp.Repo which you can use later on in your app. mahaul does exactly the same, you initialize your MyApp.Env once using the Mahaul module, passing configurations. mahaul will then generate the methods needed to access your environment variables on your MyApp.Env which you can use later on from any part of your elixir app.

defmodule MyApp.Env do
  use Mahaul,
    PORT: [type: :port, default_dev: "4000"],
    DATABASE_URL: [type: :uri],
    DATABASE_POOL_SIZE: [type: :int, default: "10"]
end

Now you can use the following generated methods to access your environment variables from anywhere in your elixir app code.

MyApp.Env.port()
MyApp.Env.database_url()
MyApp.Env.database_pool_size()

@stevensonmt I believe you misunderstood the compile time guarantees provided by the package. mahaul does not check your environment variables at compile time, that would be rarely of much use. What it instead does is, it generates functions on you module at compile time. And the confidence and guarantees you get are from using the generated functions from your code instead of using System.get_env with strings where there is no compile time checks if you used the correct environment variables names from various places in your code. Furthermore, the generated functions returns parsed values as per your provided configuration. So you can confidently use those elixir type values in your code instead of always manipulating strings that you get traditionally.

Regarding runtime guarantees, mahaul also generates validate/0 and validate!/0 on your module and it is recommended to add those in config/runtime.exs for :dev and :prod mix environments respectively. This will ensure that in dev environment you’d get a warning if some environment variables are missing or invalid, and an exception raised in prod environment which will fail to boot your app unless all needed environment variables are set correctly.

defmodule MyApp.Env do
  use Mahaul,
    PORT: [type: :port, default_dev: "4000"],
    DATABASE_URL: [type: :uri],
    DATABASE_POOL_SIZE: [type: :int, default: "10"]
end

Now you can use the following generated methods to access your environment variables from anywhere in your elixir app code.

MyApp.Env.port()
MyApp.Env.database_url()
MyApp.Env.database_pool_size()

Also add the following in config/runtime.exs to ensure the runtime guarantees.

import Config

if config_env() == :dev do
  MyApp.Env.validate()

  # set configs
  config :my_app, MyApp.Endpoint,
    http: [port: MyApp.Env.port()]
end

if config_env() == :prod do
  MyApp.Env.validate!()

  # set configs
  config :my_app, MyApp.Repo,
    url: MyApp.Env.database_url()
    pool_size: Atrium.Env.database_pool_size()
end
1 Like

I really like the approach taken on this one, as it doesn’t try / need to overtake how configuration works. I just sits between pulling the variables out of the system env and putting them into the configuration flow as it exists.

2 Likes

Oh I see, this is a limitation of elixir configs. It would be nice to also have a macro or sigil to define a typed/checked environment variable directly into config, without generating the function at compile-time of course, as I hate having to look into 10 different files to find the actual config.

@D4no0 IMHO I don’t think elixir configs are limiting in any way, if anything I’ve found it to be very well thought and structured with perfect balance of standardization vs flexibility. I’ve rarely come across anything so flexible and feature complete that’s baked in as part of the core language feature, as elixir configs.

I feel it’s important to understand there’s a difference between app configuration and environment variables and they are not a replacement to each other rather environment variables compliment your app’s configurability. You can choose to configure your app with static values or environment variables, the choice is totally up to you. The fact that elixir configs lets you achieve this in a feature complete way, in itself is wonderful to me.

The usage of environment variables is not in question here, the thing that I try to point out is that I would like to use this library, however I would like the environment variable specification defined in the config.exs directly, not with a separate module.

In my opinion there is no reason to create your own wrappers over the config way of storing those values, however a check of the environment value type is a great thing.

To make my point cleaner, I will illustrate an dev.exs example:

import Config
# Import or use your module here
import MagicMacro

config :my_app, MyApp.Endpoint,
    http: [port: safe_env(PORT: [type: :port, default: "4000"])]

You can only do that with Config.Provider implementations (e.g. runtime.exs), but that won’t be able to provide compile time configuration. The chicken/egg situation means config first then applications, because you need to be able to configure said applications. So applications cannot be available or be used in config/config.exs.

Ah true, quite sad but it makes sense. Well at least for runtime.exs it would be nice to have the check.

I certainly use environment variables in my code beyond just config files. So even though the idea of macros in runtime.exs is certainly nice, I still want to be able to access those environment variables values independently in my code as needed. This is why a separately initialized module where I can see/configure all the environment variables used by my app in one glance, makes more sense to me from flexibility and ease of use standpoint.

Well, the point of configuration is to use them in your code later, just as libraries use them and you can do that easily by calling get_env/3 or fetch_env/2, however taking in consideration the limitation of the configuration/compilation it seems that the only way to enforce something at compile-time is to make a custom module.

It would be great if such a functionality were to be introduced somewhere in the elixir internals so it could be used in all configurations.

1 Like

Thanks for clarifying as I had indeed misunderstood.

I’m still not sure I follow here though. This seems like with pattern matching you could hard code the strings for the env variables of interest and have the same sort of confidence. I can see that mahaul provides a single interface with the system env where you handle checking for the variables and providing defaults or handling errors rather than needing to do so everywhere you use the env variable in the application. Mahaul also seems to make it a lot easier to use different variables for different environments and to update them easily when the names of variables change between versions or something.

This sounds like a true win for me.

Can you give some brief example of how such a dynamic pattern matching can work at compile time? Form what I can think of, the following will give you runtime errors without any compile time warnings.

defmodule Env do
  def get_env("VAR_1"), do: "..."
  def get_env("VAR_2"), do: "..."
end

defmodule SomeModule do
  def something() do
    Env.get_env("VAR_1") # works
    Env.get_env("VAR_2") # works
    Env.get_env("VAR_3") # runtime error with no compile time warnings
  end
end

vs

defmodule Env do
  use Mahaul,
    VAR_1: [type: :str],
    VAR_2: [type: :num]
end

defmodule SomeModule do
  def something() do
    Env.var_1() # works
    Env.var_2() # works
    Env.var_3() # compile time warnings for non-existing method call
  end
end

:heavy_heart_exclamation: :heavy_heart_exclamation: :heavy_heart_exclamation:

2 Likes

LIGHTBULB
I get it now. Thanks!

1 Like