Environment diagnostics and health checks for Elixir. Define checks and run diagnostics with structured results.
Installation
def deps do
[
{:botica, "~> 1.0"}
]
endDependencies
Botica requires:
:apero- System utilities (included automatically via path in dev):arrea- Parallel execution (included automatically via path in dev)
Usage
Define a health check configuration
config = %{
app_name: "myapp",
checks: [
%{
id: :postgresql,
name: "PostgreSQL",
description: "Database server is running",
priority: 1,
check: fn ->
case System.cmd("pg_isready", [], stderr_to_stdout: true) do
{_, 0} -> {:ok, "PostgreSQL is ready"}
{output, _} -> {:error, "PostgreSQL not ready: #{output}"}
end
end,
fix: fn -> {:ok, "sudo systemctl start postgresql"} end,
fix_command: "sudo systemctl start postgresql"
},
%{
id: :elixir_version,
name: "Elixir Version",
description: "Check Elixir version is recent enough",
priority: 2,
check: fn ->
version = System.version()
if Version.match?(version, ">= 1.15.0") do
{:ok, "Elixir #{version}"}
else
{:warning, "Elixir #{version} is old"}
end
end,
fix: fn -> :skipped end,
fix_command: nil
}
]
}Run diagnostics
# Run all checks in parallel
{:ok, results} = Botica.Doctor.run(config)
# Check results
Enum.each(results, fn result ->
IO.puts("#{result.name}: #{result.status} - #{result.message}")
end)
# Get summary
summary = Botica.Doctor.summary(results)
IO.puts("Passed: #{summary.ok}, Failed: #{summary.error}")Run automatic fixes
# Run fixes for all failed checks
Botica.Doctor.fix(config)Quick health check
# Returns a simple status map
result = Botica.Doctor.health_check(config)
# => %{status: :ok, summary: %{ok: 3, warning: 1, error: 0, total: 4, passed?: true}}
case result.status do
:ok -> IO.puts("All systems healthy")
:degraded -> IO.puts("Some checks warned")
:fail -> IO.puts("Critical failures detected")
endCommon Check Examples
PostgreSQL
%{
id: :postgresql,
name: "PostgreSQL",
description: "Database server is running",
priority: 1,
check: fn ->
case System.cmd("pg_isready", [], stderr_to_stdout: true) do
{_, 0} -> {:ok, "PostgreSQL is ready"}
{output, _} -> {:error, "PostgreSQL not ready: #{output}"}
end
end,
fix: fn -> {:ok, "sudo systemctl start postgresql"} end,
fix_command: "sudo systemctl start postgresql"
}Redis
%{
id: :redis,
name: "Redis",
description: "Cache server is running",
priority: 2,
check: fn ->
case System.cmd("redis-cli", ["ping"], stderr_to_stdout: true) do
{"PONG\n", 0} -> {:ok, "Redis is responding"}
{output, _} -> {:error, "Redis not responding: #{output}"}
end
end,
fix: fn -> {:ok, "sudo systemctl start redis"} end,
fix_command: "sudo systemctl start redis"
}Directory Permissions
%{
id: :data_dir,
name: "Data Directory",
description: "Application data directory is writable",
priority: 3,
check: fn ->
path = "/var/data/myapp"
if File.exists?(path) && File.stat!(path).access == :write do
{:ok, "Data directory is writable"}
else
{:error, "Data directory not writable: #{path}"}
end
end,
fix: fn -> {:ok, "sudo chown -R myapp:myapp /var/data/myapp"} end,
fix_command: "sudo chown -R myapp:myapp /var/data/myapp"
}Result Structure
Each check result is a map with:
%{
id: :postgresql, # Check identifier
name: "PostgreSQL", # Human-readable name
status: :ok | :warning | :error, # Check status
message: "PostgreSQL is ready", # Status message
fix_command: "sudo systemctl start postgresql" # Hint for fix
}Supervisor Integration
Integrate with an Elixir supervisor for application startup health checks:
defmodule MyApp.Application do
use Supervisor
def start_link(arg) do
Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
end
@impl true
def init(_arg) do
config = Botica.config() # Your health check configuration
children = [
# ... other children
{Botica.HealthCheckWorker, config}
]
Supervisor.init(children, strategy: :one_for_all)
end
end
defmodule MyApp.HealthCheckWorker do
use GenServer
def start_link(config) do
GenServer.start_link(__MODULE__, config)
end
@impl true
def init(config) do
result = Botica.Doctor.health_check(config)
if result.status == :fail do
{:stop, {:shutdown, :health_check_failed}}
else
{:ok, config}
end
end
endAPI
Botica.Doctor.run/1- Run all checks, returns{:ok, [result]}Botica.Doctor.fix/1- Run fixes for failed checks, returns:okBotica.Doctor.summary/1- Get a summary map with countsBotica.Doctor.health_check/1- Convenience wrapper returning just pass/fail
Check Definition
Each check in the config must have:
id- Unique atom identifiername- Human-readable namedescription- What this check verifiespriority- Order to run checks (lower = first)check- Zero-arity function returning{:ok, msg},{:warning, msg}, or{:error, msg}fix- Zero-arity function to repair (returns{:ok, msg},{:error, msg}, or:skipped)fix_command- Optional shell command hint for the user
License
MIT