How to Monitor Vex with Vigilmon
Vex is a data validation library for Elixir that works independently of Ecto, supporting struct and map validation with built-in validators (presence, format, length, inclusion) and custom validator functions. It's common in Elixir applications that process external data — API ingestion pipelines, form processing, event consumers — where input quality is unpredictable.
The monitoring challenge: Vex validation failures don't crash your app. They return {:error, errors} tuples, which your code handles gracefully. But when validation failure rates spike — because an upstream API changed its schema, a frontend form was updated, or a data migration introduced unexpected formats — you want to know before users do.
This tutorial wires up monitoring for a Vex-powered application:
- A health endpoint that reports validation pipeline status
- HTTP uptime monitoring with Vigilmon
- Heartbeat monitoring tied to validation error rates
- Alerts on validation failure spikes
Why Monitor Vex?
| Signal | What it catches | |---|---| | Validation error rate | Upstream schema changes, bad data ingestion | | Validation pipeline availability | Application crashes in processing workers | | Data quality regression | New form fields missing validation rules | | Processing throughput | Validation bottlenecks under load | | Rule coverage | Missing validators on new fields |
Vex validations are a contract between your application and its data sources. Monitoring that contract gives you early warning when the contract breaks.
Step 1: Track Validation Metrics in Your Application
Add a simple ETS-based counter for validation outcomes:
# lib/my_app/validation_metrics.ex
defmodule MyApp.ValidationMetrics do
use GenServer
def start_link(_), do: GenServer.start_link(__MODULE__, nil, name: __MODULE__)
def init(_) do
:ets.new(:validation_counts, [:named_table, :public, :set])
:ets.insert(:validation_counts, {:ok, 0})
:ets.insert(:validation_counts, {:error, 0})
{:ok, nil}
end
def record(:ok) do
:ets.update_counter(:validation_counts, :ok, 1)
end
def record(:error) do
:ets.update_counter(:validation_counts, :error, 1)
end
def stats do
ok = :ets.lookup_element(:validation_counts, :ok, 2)
error = :ets.lookup_element(:validation_counts, :error, 2)
total = ok + error
error_rate = if total > 0, do: Float.round(error / total * 100, 2), else: 0.0
%{ok: ok, error: error, total: total, error_rate_pct: error_rate}
end
def reset do
:ets.insert(:validation_counts, {:ok, 0})
:ets.insert(:validation_counts, {:error, 0})
end
end
Wrap your Vex validation calls to record outcomes:
# lib/my_app/user_validator.ex
defmodule MyApp.UserValidator do
use Vex.Struct
defstruct [:name, :email, :age]
validates :name, presence: true, length: [min: 2, max: 100]
validates :email, presence: true, format: ~r/@/
validates :age, presence: true, inclusion: 18..120
def validate(attrs) do
struct = struct(__MODULE__, attrs)
result = Vex.validate(struct)
case result do
{:ok, _} -> MyApp.ValidationMetrics.record(:ok)
{:error, _} -> MyApp.ValidationMetrics.record(:error)
end
result
end
end
Add ValidationMetrics to your supervision tree:
# lib/my_app/application.ex
children = [
MyApp.ValidationMetrics,
# ... other children
]
Step 2: Expose a Health Endpoint
Add an HTTP health endpoint that reports validation stats:
# lib/my_app_web/controllers/health_controller.ex
defmodule MyAppWeb.HealthController do
use MyAppWeb, :controller
@error_rate_threshold 20.0
def index(conn, _params) do
stats = MyApp.ValidationMetrics.stats()
status = if stats.error_rate_pct < @error_rate_threshold, do: "ok", else: "degraded"
http_status = if status == "ok", do: 200, else: 503
conn
|> put_status(http_status)
|> json(%{
status: status,
validation_stats: stats,
timestamp: DateTime.utc_now() |> DateTime.to_iso8601()
})
end
end
Add the route:
# lib/my_app_web/router.ex
scope "/", MyAppWeb do
pipe_through :api
get "/health", HealthController, :index
end
Test it:
curl http://localhost:4000/health
# => {"status":"ok","validation_stats":{"ok":142,"error":3,"total":145,"error_rate_pct":2.07},...}
The endpoint returns 503 when the validation error rate exceeds 20%, giving Vigilmon a clear signal.
Step 3: Add Vigilmon HTTP Monitoring
- Log in to vigilmon.online and click Add Monitor.
- Set Type to
HTTP / HTTPS. - Enter your health endpoint:
https://yourapp.com/health. - Set Check interval to
1 minute. - Set Expected HTTP status to
200. - Under Advanced, add Response body contains:
"status":"ok". - Click Save.
Vigilmon now polls your application every minute. When validation failures spike past 20%, the health endpoint returns 503 and Vigilmon alerts you within two check cycles (2 minutes).
Step 4: Heartbeat Monitoring for Processing Workers
If Vex runs inside a GenServer or Task-based processing pipeline, add a heartbeat to confirm the pipeline is alive:
# lib/my_app/processing_heartbeat.ex
defmodule MyApp.ProcessingHeartbeat do
use GenServer
@interval :timer.minutes(1)
@vigilmon_heartbeat_url System.get_env("VIGILMON_PROCESSING_HEARTBEAT_URL") ||
"https://vigilmon.online/api/heartbeat/YOUR_HEARTBEAT_ID"
def start_link(_), do: GenServer.start_link(__MODULE__, nil, name: __MODULE__)
def init(_) do
schedule()
{:ok, nil}
end
def handle_info(:ping, state) do
stats = MyApp.ValidationMetrics.stats()
if stats.error_rate_pct < 20.0 do
:httpc.request(:get, {String.to_charlist(@vigilmon_heartbeat_url), []}, [], [])
end
MyApp.ValidationMetrics.reset()
schedule()
{:noreply, state}
end
defp schedule, do: Process.send_after(self(), :ping, @interval)
end
Add ProcessingHeartbeat to your supervision tree.
To create the heartbeat in Vigilmon:
- Click Add Monitor → Cron / Heartbeat.
- Set expected interval to
1 minute. - Copy the heartbeat URL into
VIGILMON_PROCESSING_HEARTBEAT_URL.
Step 5: Test Your Validation Rules
Use Vex's built-in test helpers to confirm your validators behave as expected — and catch regressions before they reach production:
# test/my_app/user_validator_test.exs
defmodule MyApp.UserValidatorTest do
use ExUnit.Case
test "valid user passes" do
assert {:ok, _} = MyApp.UserValidator.validate(%{
name: "Alice",
email: "alice@example.com",
age: 30
})
end
test "missing email fails" do
assert {:error, errors} = MyApp.UserValidator.validate(%{
name: "Alice",
age: 30
})
assert Enum.any?(errors, fn {_, :email, _, _} -> true; _ -> false end)
end
test "underage user fails" do
assert {:error, _} = MyApp.UserValidator.validate(%{
name: "Alice",
email: "alice@example.com",
age: 16
})
end
end
Run tests in CI to ensure validation rules never silently regress.
Step 6: Set Up Alerts
Configure alert channels for both monitors:
- Go to Alert Channels → Add Channel.
- Choose Slack, Email, or PagerDuty.
- Set alert threshold to 2 consecutive failures for the HTTP monitor.
- For the heartbeat monitor, set Missing heartbeat to alert immediately (1 missed ping).
- Assign the channel to both monitors.
For data processing pipelines, consider also enabling Escalation — if the validation error rate alert is not acknowledged within 10 minutes, escalate to a second channel.
Key Metrics to Watch
| Metric | Vigilmon feature | What to alert on |
|---|---|---|
| Application availability | HTTP monitor on /health | Any non-200 or timeout |
| Validation error rate | HTTP monitor body check | "status":"degraded" (>20% errors) |
| Processing pipeline liveness | Heartbeat monitor | Any missed heartbeat |
| Response time | Response time chart | P95 > 500ms over 5 minutes |
| SSL certificate | Cert expiry monitor | Expires in < 14 days |
Conclusion
Vex makes it easy to validate data in Elixir without coupling to Ecto — but validation failures are business logic errors that happen silently unless you track them. By exposing validation metrics through a health endpoint, tying a heartbeat to your processing pipeline's error rate, and alerting on spikes, you get early warning when upstream data quality degrades.
Get started free at vigilmon.online.