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
- Sign up at vigilmon.online.
- Click New Monitor → HTTP.
- Enter
https://your-app.example.com/health. - Set check interval to 60 seconds.
- Add a keyword check for
"ok"to catch degraded responses. - 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:
- Click New Monitor → Heartbeat.
- Set grace period to match your campaign schedule (e.g. 2 hours for an hourly campaign).
- 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
- In Vigilmon, go to Status Pages → New.
- Add your HTTP health monitor and campaign heartbeat monitors.
- Label them "App health", "Email campaign renderer", and any other batch jobs.
- 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.