Owl is an Elixir library for producing rich terminal output — styled text with ANSI colours, aligned tables, spinners, progress bars, and multi-line live updates — all built on ANSI escape codes and Elixir processes. CLI tools and long-running scripts built with Owl often orchestrate batch jobs, data migrations, and multi-step pipelines where silent failures are costly: the terminal looks busy but the underlying work has stalled. Vigilmon gives you the heartbeat monitoring and alerting to catch those stalls and ensure your background Elixir processes keep making progress.
What You'll Set Up
- Cron heartbeat to confirm long-running Owl scripts are completing each cycle
- HTTP health endpoint for daemon-mode Owl applications
- Progress milestone alerts so you know when batch jobs hit expected checkpoints
- SSL certificate monitoring for any HTTPS endpoints the script interacts with
Prerequisites
- Elixir 1.14+ with
owlin yourmix.exs - A free Vigilmon account
Step 1: Heartbeat Monitoring for Long-Running Owl Scripts
The most common Owl use case is a CLI script or Mix task that renders progress bars while processing a large dataset, running a migration, or syncing records. If the script crashes halfway through — or hangs forever waiting on a slow external call — the terminal shows a frozen spinner and nothing else.
Add a Vigilmon cron heartbeat to detect both crashes and hangs:
- In Vigilmon, click Add Monitor → Cron Heartbeat.
- Set Expected ping interval to slightly longer than your script's expected runtime (e.g.
120minutes for a 2-hour batch job, or5minutes for a recurring hourly task). - Copy the heartbeat URL (e.g.
https://vigilmon.online/heartbeat/abc123).
Add the heartbeat ping at the end of your script's main function:
# lib/mix/tasks/data_sync.ex
defmodule Mix.Tasks.DataSync do
use Mix.Task
require Logger
@shortdoc "Sync records from upstream API with progress display"
def run(_args) do
{:ok, _} = Application.ensure_all_started(:my_app)
records = fetch_records()
Owl.ProgressBar.start(
id: :sync,
label: "Syncing records",
total: length(records)
)
records
|> Enum.each(fn record ->
sync_record(record)
Owl.ProgressBar.inc(id: :sync)
end)
Owl.LiveScreen.await_render()
case ping_vigilmon() do
:ok -> Owl.IO.puts([Owl.Data.tag("✓ Sync complete, heartbeat sent", :green)])
:error -> Logger.warning("Failed to ping Vigilmon heartbeat")
end
end
defp sync_record(record) do
# Your sync logic here
Process.sleep(10)
record
end
defp fetch_records do
# Your data fetch logic here
Enum.to_list(1..100)
end
defp ping_vigilmon do
heartbeat_url = System.get_env("VIGILMON_HEARTBEAT_URL")
if heartbeat_url do
case :httpc.request(:get, {String.to_charlist(heartbeat_url), []}, [{:timeout, 5000}], []) do
{:ok, _} -> :ok
{:error, _} -> :error
end
else
:ok
end
end
end
Now if the script hangs on a slow external call or crashes before completing, Vigilmon sends an alert after the expected interval — you don't have to watch the terminal to know something went wrong.
Step 2: Milestone Heartbeats for Multi-Phase Pipelines
For pipelines with distinct phases — fetch, transform, load — use separate heartbeats per phase so you know exactly where a failure occurred:
defmodule MyApp.Pipeline do
require Logger
def run do
Owl.IO.puts([Owl.Data.tag("Starting pipeline...", :cyan)])
with {:ok, raw_data} <- phase_fetch(),
:ok <- ping_heartbeat("VIGILMON_FETCH_HEARTBEAT_URL"),
{:ok, transformed} <- phase_transform(raw_data),
:ok <- ping_heartbeat("VIGILMON_TRANSFORM_HEARTBEAT_URL"),
{:ok, _} <- phase_load(transformed),
:ok <- ping_heartbeat("VIGILMON_LOAD_HEARTBEAT_URL") do
Owl.IO.puts([Owl.Data.tag("Pipeline complete", :green)])
:ok
else
{:error, reason} ->
Owl.IO.puts([Owl.Data.tag("Pipeline failed: #{inspect(reason)}", :red)])
{:error, reason}
end
end
defp phase_fetch do
Owl.IO.puts("Fetching data...")
# fetch logic
{:ok, []}
end
defp phase_transform(data) do
Owl.IO.puts("Transforming #{length(data)} records...")
# transform logic
{:ok, data}
end
defp phase_load(data) do
Owl.IO.puts("Loading #{length(data)} records...")
# load logic
{:ok, data}
end
defp ping_heartbeat(env_key) do
url = System.get_env(env_key)
if url do
case :httpc.request(:get, {String.to_charlist(url), []}, [{:timeout, 5000}], []) do
{:ok, _} -> :ok
{:error, reason} ->
Logger.warning("Heartbeat ping failed for #{env_key}: #{inspect(reason)}")
:ok
end
else
:ok
end
end
end
Create three separate Cron Heartbeat monitors in Vigilmon — one per phase — so alerts tell you immediately which phase stalled, not just that "the pipeline broke."
Step 3: Health Endpoint for Daemon-Mode Owl Applications
Some Owl apps run as daemons — continuously updating a live terminal display (using Owl.LiveScreen) while polling services in the background. For these, add an HTTP health endpoint:
# lib/my_app/health_server.ex
defmodule MyApp.HealthServer do
use GenServer
def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
@impl true
def init(state) do
{:ok, _} = :inets.start(:httpd, [
port: 4001,
server_name: 'health',
server_root: '/tmp',
document_root: '/tmp',
modules: [:mod_esi],
erl_script_alias: {'/health', [{:erl, MyApp.HealthHandler}]}
])
{:ok, state}
end
end
defmodule MyApp.HealthHandler do
def health(session_id, _env, _input) do
checks = %{
live_screen: check_live_screen(),
data_poller: check_data_poller()
}
all_ok = Enum.all?(checks, fn {_, v} -> v == :ok end)
status = if all_ok, do: "ok", else: "degraded"
code = if all_ok, do: "200 OK", else: "503 Service Unavailable"
body = Jason.encode!(%{status: status, checks: checks})
:mod_esi.deliver(session_id, [
"Content-Type: application/json\r\n",
"Content-Length: #{byte_size(body)}\r\n",
"\r\n",
body
])
end
defp check_live_screen do
case Process.whereis(Owl.LiveScreen) do
nil -> :error
pid -> if Process.alive?(pid), do: :ok, else: :error
end
end
defp check_data_poller do
case Process.whereis(MyApp.DataPoller) do
nil -> :error
pid -> if Process.alive?(pid), do: :ok, else: :error
end
end
end
Then add the monitor in Vigilmon pointing at http://your-server:4001/health.
Step 4: Monitor Upstream API SSL Certificates
If your Owl application polls external HTTPS APIs to populate its terminal display, add SSL certificate monitoring for each endpoint:
- Click Add Monitor → HTTP / HTTPS.
- Enter the upstream API URL.
- Enable Monitor SSL certificate.
- Set Alert when certificate expires in less than
21 days. - Click Save.
An expired upstream certificate causes HTTPS requests to fail with TLS errors, which often manifests in an Owl application as a frozen spinner or missing data rows — hard to diagnose without an alert.
Step 5: Configure Alert Channels
- Go to Alert Channels in Vigilmon and add Slack, email, or PagerDuty.
- Set Consecutive failures before alert to
1for heartbeat monitors — a single missed heartbeat from a batch job is meaningful. - Route heartbeat alerts to a higher-priority channel than HTTP health alerts, since a missed heartbeat from a batch pipeline usually means data is not being processed.
Use Maintenance windows when you intentionally skip a scheduled pipeline run:
curl -X POST https://vigilmon.online/api/maintenance \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{"monitor_id": "fetch-heartbeat-id", "duration_minutes": 60}'
Summary
| Monitor | Target | What It Catches |
|---|---|---|
| Cron heartbeat (pipeline) | Heartbeat URL after run | Script crash or hang before completion |
| Cron heartbeat (per phase) | Phase-specific heartbeat URL | Which pipeline phase stalled |
| HTTP health check | /health endpoint | Daemon-mode process crash |
| SSL certificate | Upstream HTTPS endpoint | TLS expiry causing fetch errors |
Owl makes terminal output expressive and interactive — Vigilmon makes sure the scripts and daemons producing that output are reliably completing their work. With heartbeats at each pipeline milestone and a health endpoint for daemon-mode apps, you'll catch stalls and crashes before they become invisible data gaps.