tutorial

How to Monitor Solid with Vigilmon

Monitor Elixir applications using Solid (Liquid templates) — track template rendering health, sandbox isolation, and user-provided template jobs with Vigilmon uptime and heartbeat monitoring.

How to Monitor Solid with Vigilmon

Solid is a Liquid template engine for Elixir, fully compatible with Shopify's Liquid template language. It is designed for CMS platforms, email template systems, and any use case where users or operators provide templates that must be rendered in a sandboxed environment — isolated from server-side code execution.

Solid handles rendering user-provided content safely. It does not tell you whether your app is reachable, whether a batch email render job finished, or whether a malformed user template is silently causing render failures. That is the role of external monitoring.

This tutorial covers:

  • A health check endpoint relevant to Solid-powered applications
  • HTTP uptime monitoring with Vigilmon
  • Heartbeat monitoring for batch template render jobs
  • Alerts when sandboxed rendering fails silently

Why Monitor Solid?

| Failure mode | Symptom without monitoring | |---|---| | App unreachable | Template rendering irrelevant; users see nothing | | Batch email render job crashes | Emails not sent; no external alert | | User-supplied template causes parsing errors | Silent per-record failures, no aggregate alert | | DB slow — context fetch delays render | High latency; users see slow email delivery | | Memory growth from large template contexts | Eventual OOM; no early warning |


Step 1: Add Solid and a health check endpoint

Install Solid:

# mix.exs
{:solid, "~> 0.15"},

A typical Solid render (rendering a user-provided email template with a context):

defmodule MyApp.TemplateRenderer do
  @doc """
  Renders a Liquid template string with the provided context map.
  Returns {:ok, rendered_string} or {:error, reason}.
  """
  def render(template_string, context) when is_binary(template_string) do
    with {:ok, parsed} <- Solid.parse(template_string),
         {:ok, rendered, _warnings} <- Solid.render(parsed, context) do
      {:ok, IO.iodata_to_binary(rendered)}
    else
      {:error, %Solid.TemplateError{} = err} ->
        {:error, {:template_parse_error, err.message}}
      {:error, reason} ->
        {:error, reason}
    end
  end
end

Add a health check endpoint:

# lib/my_app_web/plugs/health_check.ex
defmodule MyAppWeb.Plugs.HealthCheck do
  import Plug.Conn

  def init(opts), do: opts

  def call(%Plug.Conn{request_path: "/health"} = conn, _opts) do
    checks = %{
      database:        check_database(),
      template_engine: check_solid(),
      memory:          check_memory()
    }

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

    conn
    |> put_resp_content_type("application/json")
    |> send_resp(status, Jason.encode!(%{
      status: if(status == 200, do: "ok", else: "degraded"),
      checks: checks
    }))
    |> halt()
  end

  def call(conn, _opts), do: conn

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

  defp check_solid do
    # Verify Solid can parse and render a trivial template
    case Solid.parse("Hello {{ name }}!") do
      {:ok, template} ->
        case Solid.render(template, %{"name" => "world"}) do
          {:ok, _result, _warnings} -> :ok
          _ -> :error
        end
      _ ->
        :error
    end
  end

  defp check_memory do
    total_mb = :erlang.memory()[:total] / 1_048_576
    if total_mb < 1_500, do: :ok, else: :error
  end
end

Register before the router:

# lib/my_app_web/endpoint.ex
plug MyAppWeb.Plugs.HealthCheck
plug MyAppWeb.Router

Test:

mix phx.server
curl http://localhost:4000/health | jq .
# {
#   "status": "ok",
#   "checks": {"database": "ok", "template_engine": "ok", "memory": "ok"}
# }

Step 2: Set up HTTP monitoring in Vigilmon

  1. Sign up at vigilmon.online.
  2. Click New Monitor → HTTP.
  3. Enter https://your-app.example.com/health.
  4. Set check interval to 60 seconds.
  5. Add a keyword check for "ok" to catch degraded responses.
  6. Set alert threshold to 2 consecutive failures.

The template_engine check in your health response gives Vigilmon an early signal if Solid itself becomes unavailable — which can happen after a dependency upgrade changes the Solid module interface.


Step 3: Heartbeat monitoring for batch template render jobs

The most common production use of Solid is in batch email systems: a background worker fetches records, renders each one against a user-supplied template, and sends the result. If this job crashes or stalls, emails are never sent — and nothing raises an alert.

# lib/my_app/workers/email_campaign_worker.ex
defmodule MyApp.Workers.EmailCampaignWorker do
  use Oban.Worker, queue: :campaigns, max_attempts: 3

  require Logger

  @impl Oban.Worker
  def perform(%Oban.Job{args: %{"campaign_id" => campaign_id}}) do
    with {:ok, campaign}   <- MyApp.Campaigns.get_campaign(campaign_id),
         {:ok, recipients} <- MyApp.Campaigns.get_recipients(campaign),
         :ok               <- render_and_send_all(campaign, recipients) do
      ping_heartbeat(campaign_id)
      :ok
    else
      {:error, reason} ->
        Logger.error("EmailCampaignWorker failed for #{campaign_id}: #{inspect(reason)}")
        {:error, reason}
    end
  end

  defp render_and_send_all(campaign, recipients) do
    results =
      Enum.map(recipients, fn recipient ->
        context = build_context(recipient, campaign)
        case MyApp.TemplateRenderer.render(campaign.template_body, context) do
          {:ok, html}      -> MyApp.Mailer.send_campaign_email(recipient, html)
          {:error, reason} ->
            Logger.warning("Template render failed for #{recipient.email}: #{inspect(reason)}")
            {:error, reason}
        end
      end)

    if Enum.any?(results, &match?({:error, _}, &1)) do
      {:error, :partial_failure}
    else
      :ok
    end
  end

  defp build_context(recipient, campaign) do
    %{
      "first_name"    => recipient.first_name,
      "campaign_name" => campaign.name,
      "unsubscribe_url" => MyApp.Router.Helpers.unsubscribe_url(
        MyAppWeb.Endpoint,
        :show,
        recipient.unsubscribe_token
      )
    }
  end

  defp ping_heartbeat(campaign_id) do
    url = System.get_env("VIGILMON_CAMPAIGN_HEARTBEAT_URL")
    if url do
      Logger.info("Campaign #{campaign_id} complete — pinging heartbeat")
      Task.start(fn ->
        :httpc.request(:get, {String.to_charlist(url), []}, [], [])
      end)
    end
  end
end

In Vigilmon, create a Heartbeat Monitor per critical campaign type:

  1. Click New Monitor → Heartbeat.
  2. Set grace period to match your campaign schedule (e.g. 2 hours for an hourly campaign).
  3. Copy the ping URL into VIGILMON_CAMPAIGN_HEARTBEAT_URL.

For high-value campaigns (transactional emails, subscription renewals), create a separate heartbeat monitor so failures are isolated and clearly labelled in your alert feed.


Step 4: Key metrics to alert on

| Metric | Alert threshold | Why | |---|---|---| | /health HTTP status | Non-200 for 2 checks | App or DB down | | template_engine check | error in body | Solid unavailable post-deploy | | Heartbeat silence | Grace period exceeded | Batch render job stalled | | Response latency p95 | > 5 000 ms | Context fetch slow; DB query issues | | Memory (health response) | > 1 500 MB | Large template contexts bloating heap |

Use Vigilmon's keyword check with body match on "degraded" to alert on template engine failures without needing a full 503.


Step 5: Status page

  1. In Vigilmon, go to Status Pages → New.
  2. Add your HTTP health monitor and campaign heartbeat monitors.
  3. Label them "App health", "Email campaign renderer", and any other batch jobs.
  4. Share the URL with your customer success team so they can report template system outages without needing engineering access.

For SaaS products where customers upload their own Liquid templates, a public status page reduces support ticket volume during incidents — customers can self-serve before opening a ticket.


Conclusion

Solid enables safe, user-provided Liquid templating in Elixir. Vigilmon ensures the infrastructure running those templates stays healthy and that batch render jobs complete reliably. Together:

  • Uptime checks detect when your app is unreachable
  • Template engine checks surface Solid failures immediately after deploys
  • Heartbeat monitors alert when email render jobs stall before a customer notices

Start with the /health endpoint and one heartbeat per critical batch job, then expand to per-campaign monitors as your usage grows.

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 →