Current section
Files
Jump to
Current section
Files
lib/ex_health.ex
defmodule ExHealth do
@moduledoc """
ExHealth is a simple extensible health check utility that monitors your
applications.
"""
use Application
@function_prefix "hc__"
@doc """
Defines a healthcheck function.
Takes the following arguments:
* `:name` - a string for the name of the health check
* `:block` - block that returns `:ok | true | {:error, "Reason"}`
## Examples
defmodule MyApp.HealthChecks do
health_check("Database") do
MyDB.ping() # This should return :ok | true | {:error, "Reason"}
end
end
"""
defmacro health_check(name, do: block) do
function_name = String.to_atom("#{@function_prefix}" <> name)
quote do
def unquote(function_name)() do
try do
unquote(block)
rescue
_ -> {:error, "Error in HealthCheck"}
end
end
end
end
@doc """
Defines a healthcheck function for a given process.
Returns `:ok` if the process has one of the following statuses:
* `:running`
* `:waiting`
See [Process.info/1](https://hexdocs.pm/elixir/Process.html#info/1) for more
information about process status.
## Examples
defmodule MyApp.HealthChecks do
process_check(MyApp.SomeImportantService)
end
"""
defmacro process_check({_, _, module_list} = _module) do
{module, _} = Code.eval_string(Enum.join(module_list, "."))
function_name = String.to_atom("#{@function_prefix}" <> Enum.join(module_list, "_"))
quote do
def unquote(function_name)() do
with pid when not is_nil(pid) <- Process.whereis(unquote(module)),
info <- Process.info(pid),
{:ok, status} <- Keyword.fetch(info, :status),
status == :running || :waiting do
:ok
else
nil -> {:error, "no proc"}
_ -> {:error, "process not running/waiting"}
end
end
end
end
defimpl Jason.Encoder, for: [Tuple] do
def encode(tuple, opts) do
Jason.Encode.list(Tuple.to_list(tuple), opts)
end
end
@doc """
Starts the application with empty state.
"""
def start() do
start(:normal, state: %ExHealth.Status{})
end
def start(_type, args) do
import Supervisor.Spec
configure(args)
initial_state = load_config()
children = [
supervisor(ExHealth.HealthServer, [initial_state])
]
opts = [strategy: :one_for_one, name: ExHealth.Supervisor]
Supervisor.start_link(children, opts)
end
@doc """
Synchronously fetches the latest status from `ExHealth.HealthServer`.
## Examples
iex> ExHealth.status()
%ExHealth.Status{
checks: [
%ExHealth.Check{
mfa: {ExHealth.SelfCheck, :hc__ExHealth_HealthServer, []},
name: "ExHealth_HealthServer"
}
],
interval_ms: 15000,
last_check: nil,
result: %{check_results: [], msg: :pending}
}
"""
@spec status() :: ExHealth.Status.t()
def status() do
GenServer.call(ExHealth.HealthServer, :status)
end
@doc """
Stops the Application.
"""
@spec stop() :: :ok
def stop() do
Supervisor.stop(ExHealth.Supervisor, :normal)
end
defp configure([]), do: nil
defp configure([{k, v} | remainder]) do
Application.put_env(:ex_health, k, v)
configure(remainder)
end
defp extract_health_checks(module) do
hc_regex = ~r/#{@function_prefix}.*/
module.__info__(:functions)
|> Enum.filter(fn {func, _arr} ->
Atom.to_string(func) =~ hc_regex
end)
end
@spec extract_and_transform(module()) :: list(ExHealth.Check.t())
defp extract_and_transform(module) do
function_list = extract_health_checks(module)
for {func, _arr} <- function_list do
%ExHealth.Check{name: remove_function_prefix(func), mfa: {module, func, []}}
end
end
@spec load_config() :: ExHealth.Status.t()
defp load_config() do
:ok =
case Application.load(:ex_health) do
:ok ->
:ok
{:error, {:already_loaded, :ex_health}} ->
:ok
end
module = Application.get_env(:ex_health, :module)
interval_ms = Application.get_env(:ex_health, :interval_ms, 15_000)
mfas =
case module do
nil -> extract_and_transform(ExHealth.SelfCheck)
mod -> extract_and_transform(mod)
end
%ExHealth.Status{
checks: mfas,
interval_ms: interval_ms
}
end
defp remove_function_prefix(function) do
name_with_prefix = Atom.to_string(function)
prefix = String.length(@function_prefix)
String.slice(name_with_prefix, prefix, String.length(name_with_prefix))
end
end