RuntimeCheck

A GenServer to run a set of system checks on application start up.

The process is normally run after the rest of the children in the supervisor, so processes like Ecto, Oban, and FunWithFlags are available. Additionally, it is not actually kept in the supervision tree as init/1 returns :ignore when the checks succeed.

Usage

Define a module that uses RuntimeCheck:

defmodule MyApp.RuntimeChecks do
  use RuntimeCheck

  @impl true
  def run? do
    # Decide if checks should run. Maybe skip them during tests or if an env var is set.
    true
  end

  @impl true
  def checks do
    # Return a list of checks. See `RuntimeCheck.DSL`.
    [
      check(:foo, fn ->
        # Run function that should return :ok, {:ok, something}, :ignore or {:error, reason}
        :ok
      end),
      check(:nested, [
        check(:bar, fn -> :ignore end),
        check(:baz, fn ->
          if everything_ok() do
            :ok
          else
            {:error, "not everything is ok!"}
          end
        end)
      ]),
      # If FunWithFlags is installed. Run nested checks only if the flag is enabled.
      feature_check(:some_flag, [
        app_var(:quzz_url :my_app, [:quzz, :url]),
        env_var("QUZZ_API_KEY")
      ])
    ]
  end
end

Then in MyApp.application add the worker

children = [
  # ...
  MyApp.Repo,
  MyAppWeb.Endpoint,
  # ...
  MyApp.RuntimeChecks
]

Then when running the app, something like this will be logged:

[info] [RuntimeCheck] starting...
[info] [RuntimeCheck] foo: passed
[info] [RuntimeCheck] nested:
[warning] [RuntimeCheck] > bar: ignored
[info] [RuntimeCheck] > baz: passed
[info] [RuntimeCheck] nested: passed
[info] [RuntimeCheck] some_flag:
[info] [RuntimeCheck] > quzz_url: passed
[info] [RuntimeCheck] > QUZZ_API_KEY: passed
[info] [RuntimeCheck] some_flag: passed
[info] [RuntimeCheck] done

Or if some checks fail:

[info] [RuntimeCheck] starting...
[info] [RuntimeCheck] foo: passed
[info] [RuntimeCheck] nested:
[warning] [RuntimeCheck] > bar: ignored
[error] [RuntimeCheck] > baz: failed. Reason: "not everything is ok!"
[error] [RuntimeCheck] nested: failed
[info] [RuntimeCheck] some_flag:
[info] [RuntimeCheck] > quzz_url: passed
[info] [RuntimeCheck] > QUZZ_API_KEY: passed
[info] [RuntimeCheck] some_flag: passed
[error] [RuntimeCheck] some checks failed!
** (Mix) Could not start application my_app: MyApp.Application.start(:normal, []) returned an error: shutdown: failed to start child: MyApp.RuntimeChecks
    ** (EXIT) :runtime_check_failed

Checks

Each check is a RuntimeCheck.Check but normally, they are constructed using the functions in RuntimeCheck.DSL.

Feature flag checks

If fun_with_flags is installed, a feature_check function will be available in the DSL. It allows running checks only if a feature flag is enabled.

If you add fun_with_flags after adding runtime_check make sure to recompile with mix deps.compile --force runtime_check.

Testing

To test the checks you can run MyApp.RuntimeChecks.run(), which will run the checks in the current process instead of starting a new one. You can pass log: false to disable logging.

assert MyApp.RuntimeChecks.run(log: false) == {:ok, %{}}

See RuntimeCheck.run/2 for details on the return value.

Installation

Add runtime_check to your list of dependencies in mix.exs:

def deps do
  [
    {:runtime_check, "~> 0.1.0"}
  ]
end