tutorial

How to Monitor Sourceror with Vigilmon

Sourceror is an Elixir library for parsing and transforming Elixir source code at the AST level. Learn how to monitor code analysis pipelines, track transformation jobs, and alert on static analysis failures using Vigilmon.

Sourceror is an Elixir library for reading, analyzing, and transforming Elixir source code at the quoted expression (AST) level. It preserves source location metadata, handles comments, and provides a zipper-based traversal API that makes surgical code transformations safe and composable. Libraries like Igniter use Sourceror under the hood to apply precise patches to config files and module definitions. In production, Sourceror shows up wherever static analysis, automated refactoring, or code quality enforcement runs at scale: CI pipelines that audit module structure, migration generators that patch existing files, or build-time tools that enforce architectural constraints. When these jobs fail silently, bad code accumulates undetected. Vigilmon gives you heartbeat and HTTP monitoring so Sourceror-powered pipelines surface failures immediately.

What You'll Set Up

  • HTTP health endpoint for services that run Sourceror analysis at request time
  • Heartbeat monitor for CI jobs and background workers that run Sourceror pipelines
  • Slack alerts on analysis pipeline failure
  • SSL monitoring for code analysis API services

Prerequisites

  • Elixir 1.14+ with sourceror in mix.exs
  • A pipeline that runs Sourceror analysis or transformation (CI job, Mix task, GenServer worker)
  • A free Vigilmon account

Step 1: Identify What to Monitor in Your Sourceror Pipeline

Sourceror usage falls into a few categories, each with a different monitoring approach:

| Usage pattern | Monitoring approach | |---------------|-------------------| | One-off local refactoring | Nothing — failure is immediate | | CI code quality gate | Heartbeat per scheduled CI run | | Background analysis service | HTTP health + heartbeat | | Build-time code mod (Mix task) | Heartbeat in deploy pipeline |

This tutorial covers the two runtime patterns: background analysis services and scheduled CI pipelines.


Step 2: Add a Health Endpoint for Analysis Services

If you run a service that accepts source code for analysis (e.g. an internal API that checks module structure or enforces naming conventions), add a health endpoint that exercises Sourceror end-to-end:

# lib/my_analysis_web/controllers/health_controller.ex
defmodule MyAnalysisWeb.HealthController do
  use MyAnalysisWeb, :controller

  @sample_source """
  defmodule Sample do
    def hello, do: :world
  end
  """

  def index(conn, _params) do
    checks = %{
      sourceror_parse: check_sourceror_parse(),
      sourceror_traverse: check_sourceror_traverse(),
      database: check_database()
    }

    status = if Enum.all?(checks, fn {_, v} -> v == :ok end), do: 200, else: 503

    conn
    |> put_status(status)
    |> json(%{status: if(status == 200, do: "ok", else: "degraded"), checks: checks})
  end

  defp check_sourceror_parse do
    case Sourceror.parse_string(@sample_source) do
      {:ok, _quoted} -> :ok
      {:error, _} -> :error
    end
  end

  defp check_sourceror_traverse do
    # Verify the zipper traversal works end-to-end
    case Sourceror.parse_string(@sample_source) do
      {:ok, quoted} ->
        result =
          Sourceror.Zipper.zip(quoted)
          |> Sourceror.Zipper.traverse(fn zipper ->
            zipper
          end)

        if match?(%Sourceror.Zipper{}, result), do: :ok, else: :error

      {:error, _} ->
        :error
    end
  end

  defp check_database do
    case Ecto.Adapters.SQL.query(MyAnalysis.Repo, "SELECT 1", []) do
      {:ok, _} -> :ok
      _ -> :error
    end
  end
end

Wire it to a route:

# lib/my_analysis_web/router.ex
scope "/", MyAnalysisWeb do
  get "/health", HealthController, :index
end

Step 3: Monitor Background Sourceror Analysis Workers

For long-running GenServers or Oban workers that run Sourceror pipelines on a schedule (e.g. nightly module structure audits), add a heartbeat:

# lib/my_app/workers/code_analysis_worker.ex
defmodule MyApp.Workers.CodeAnalysisWorker do
  use Oban.Worker, queue: :analysis, max_attempts: 3

  require Logger

  @impl Oban.Worker
  def perform(%Oban.Job{args: %{"repo_path" => repo_path}}) do
    with {:ok, files} <- discover_elixir_files(repo_path),
         {:ok, results} <- analyze_files(files),
         :ok <- persist_results(results) do
      ping_heartbeat()
      :ok
    else
      {:error, reason} ->
        Logger.error("Code analysis failed: #{inspect(reason)}")
        {:error, reason}
    end
  end

  defp discover_elixir_files(repo_path) do
    files =
      Path.wildcard(Path.join(repo_path, "**/*.ex"))
      |> Enum.reject(&String.contains?(&1, "_build"))

    {:ok, files}
  end

  defp analyze_files(files) do
    results =
      files
      |> Task.async_stream(
        fn file ->
          source = File.read!(file)
          analyze_source(file, source)
        end,
        max_concurrency: System.schedulers_online(),
        timeout: 30_000
      )
      |> Enum.reduce([], fn
        {:ok, result}, acc -> [result | acc]
        {:error, reason}, acc ->
          Logger.warning("Analysis failed for a file: #{inspect(reason)}")
          acc
      end)

    {:ok, results}
  end

  defp analyze_source(file, source) do
    case Sourceror.parse_string(source) do
      {:ok, quoted} ->
        # Example: collect all public function definitions
        functions =
          Sourceror.Zipper.zip(quoted)
          |> Sourceror.Zipper.traverse_while([], fn zipper, acc ->
            node = Sourceror.Zipper.node(zipper)

            case node do
              {:def, _meta, [{name, _, _args} | _]} ->
                {:cont, zipper, [{file, name} | acc]}

              _ ->
                {:cont, zipper, acc}
            end
          end)
          |> elem(1)

        %{file: file, functions: functions, status: :ok}

      {:error, reason} ->
        %{file: file, error: reason, status: :error}
    end
  end

  defp persist_results(results) do
    # Store results in your database or cache
    Logger.info("Analysis complete: #{length(results)} files processed")
    :ok
  end

  defp ping_heartbeat do
    url = Application.get_env(:my_app, :vigilmon)[:analysis_heartbeat_url]

    if url do
      case Req.get(url, receive_timeout: 5_000) do
        {:ok, _} -> :ok
        {:error, reason} ->
          Logger.warning("Vigilmon heartbeat ping failed: #{inspect(reason)}")
      end
    end
  end
end

Configure the heartbeat URL:

# config/runtime.exs
config :my_app, :vigilmon,
  analysis_heartbeat_url: System.get_env("VIGILMON_ANALYSIS_HEARTBEAT_URL")

Step 4: Wrap Mix Task Pipelines with Heartbeat Pings

For Sourceror-powered Mix tasks in your deploy pipeline (e.g. mix code.audit that uses Sourceror to verify module structure before promotion), add a heartbeat ping at the end:

#!/bin/bash
# scripts/run_code_audit.sh
set -euo pipefail

echo "Running Sourceror code audit..."
mix code.audit --strict

EXIT_CODE=$?

if [ $EXIT_CODE -eq 0 ]; then
  echo "Audit passed — pinging Vigilmon heartbeat..."
  curl -fsS "$VIGILMON_AUDIT_HEARTBEAT_URL" > /dev/null
  echo "Heartbeat sent."
else
  echo "Audit failed with exit code $EXIT_CODE"
  exit $EXIT_CODE
fi

Set the expected heartbeat interval to match your deploy cadence. If you deploy daily, set a 26-hour window with a 2-hour tolerance.


Step 5: Set Up Monitoring in Vigilmon

HTTP health monitor (for analysis API services):

  1. Sign in at vigilmon.online
  2. New Monitor → HTTP
  3. URL: https://your-analysis-service.com/health
  4. Expected status: 200
  5. Interval: 1 minute

Heartbeat monitor (for background analysis workers):

  1. New Monitor → Heartbeat
  2. Name: sourceror-code-analysis
  3. Expected interval: match your schedule (e.g. 24 hours for nightly audits)
  4. Tolerance: 1 hour
  5. Copy the ping URL and set it as VIGILMON_ANALYSIS_HEARTBEAT_URL

Heartbeat monitor (for deploy-time Mix task):

  1. New Monitor → Heartbeat
  2. Name: sourceror-audit-pipeline
  3. Expected interval: match your deploy frequency
  4. Tolerance: 2 hours

Step 6: Configure Slack Alerts

  1. In Vigilmon: Notifications → New Channel → Slack
  2. Paste your Slack incoming webhook URL
  3. Enable on all monitors

When the nightly analysis job misses its window, you'll see:

🔴 MISSED: sourceror-code-analysis
Last ping: 25 hours ago (expected: 24 hours)

When it recovers:

✅ RECOVERED: sourceror-code-analysis
Gap: 1 hour 4 minutes

Step 7: SSL Monitoring for Code Analysis APIs

If your Sourceror-based service is exposed over HTTPS:

  1. New Monitor → SSL Certificate
  2. URL: https://your-analysis-service.com
  3. Alert: 14 days before expiry

An expired certificate blocks external callers from reaching your analysis API, and the service itself continues running — the error is only visible to callers, not to internal health checks.


What You've Built

| What | How | |------|-----| | Active health check | /health exercising Sourceror parse and zipper traversal | | Background analysis worker | Heartbeat monitor with 24-hour window | | Deploy-time audit pipeline | Heartbeat in CI/CD bash script | | Slack alerts | Downtime and missed-heartbeat notifications | | SSL monitoring | Certificate expiry alert |

Sourceror gives you precise control over your Elixir codebase. Vigilmon makes sure the pipelines running Sourceror don't fail quietly in the background.


Next Steps

  • Add a separate heartbeat per analysis dimension (module naming, public API surface, dependency graph) to isolate which check is failing
  • Use Vigilmon's response time history to track audit job duration — growth indicates accumulating tech debt or a growing codebase
  • Add a monitor per repository if you run cross-repo analysis
  • If you use Sourceror inside a Phoenix LiveDashboard custom page, add a monitor for the LiveDashboard endpoint

Get started free at vigilmon.online.

Monitor your app with Vigilmon

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

Start free →