Skip to main content

README.md

# Botica

Environment diagnostics and health checks for Elixir. Define checks and run diagnostics with structured results.

## Installation

```elixir
def deps do
  [
    {:botica, "~> 1.0"}
  ]
end
```

## Dependencies

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

```elixir
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

```elixir
# 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

```elixir
# Run fixes for all failed checks
Botica.Doctor.fix(config)
```

### Quick health check

```elixir
# 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")
end
```

## Common Check Examples

### PostgreSQL

```elixir
%{
  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

```elixir
%{
  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

```elixir
%{
  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:

```elixir
%{
  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:

```elixir
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
end
```

## API

- `Botica.Doctor.run/1` - Run all checks, returns `{:ok, [result]}`
- `Botica.Doctor.fix/1` - Run fixes for failed checks, returns `:ok`
- `Botica.Doctor.summary/1` - Get a summary map with counts
- `Botica.Doctor.health_check/1` - Convenience wrapper returning just pass/fail

## Check Definition

Each check in the config must have:
- `id` - Unique atom identifier
- `name` - Human-readable name
- `description` - What this check verifies
- `priority` - 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