tutorial

How to Monitor Owl with Vigilmon

Owl is an Elixir library for rich terminal output with styled text, tables, spinners, and live progress bars. Learn how to monitor CLI applications built with Owl using Vigilmon heartbeats and health checks.

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 owl in your mix.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:

  1. In Vigilmon, click Add MonitorCron Heartbeat.
  2. Set Expected ping interval to slightly longer than your script's expected runtime (e.g. 120 minutes for a 2-hour batch job, or 5 minutes for a recurring hourly task).
  3. 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:

  1. Click Add MonitorHTTP / HTTPS.
  2. Enter the upstream API URL.
  3. Enable Monitor SSL certificate.
  4. Set Alert when certificate expires in less than 21 days.
  5. 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

  1. Go to Alert Channels in Vigilmon and add Slack, email, or PagerDuty.
  2. Set Consecutive failures before alert to 1 for heartbeat monitors — a single missed heartbeat from a batch job is meaningful.
  3. 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.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →