How to Monitor Jason with Vigilmon
Jason is the de facto standard JSON library for Elixir and Phoenix. It's fast, safe, and handles encoding and decoding of Elixir terms to and from JSON with excellent performance. Phoenix uses Jason by default for JSON responses, and it's the standard choice in the broader Elixir ecosystem.
Jason doesn't have a service to monitor — it's a library. But the places where Jason is used are worth monitoring closely: JSON API endpoints, webhook receivers that decode incoming payloads, background jobs that serialize data to queues, and external API integrations where a schema change can cause silent parse failures.
This tutorial shows you how to use Vigilmon to monitor the systems that rely on Jason for correctness and uptime.
Why Monitor Jason-Dependent Systems?
Jason itself is reliable and well-tested. The risk is at the boundaries:
- External API schema changes — a third-party JSON response adds or removes fields; your Jason decode logic silently drops data or crashes
- Invalid JSON from upstream — a webhook sender sends malformed JSON; Jason returns
{:error, %Jason.DecodeError{}}which, if unhandled, crashes the receiving process - JSON encoding failures — trying to encode a struct without a
Jason.Encoderimplementation raises at runtime; if it's in a request path, users see 500s - API endpoint returning non-JSON — your Phoenix JSON endpoint returns HTML error pages under certain failure conditions; callers crash on decode
- Schema version drift — your API changes its JSON shape without a version bump; clients fail silently
Key Metrics to Track
| Metric | Why It Matters |
|--------|---------------|
| JSON API endpoint uptime | Phoenix JSON endpoints can return non-JSON on config errors |
| Decode error rate | Rising Jason.DecodeError frequency signals upstream schema drift |
| Encode error rate | Jason.EncodeError in request paths causes 500s |
| API response schema validity | Structural validation beyond HTTP status codes |
| Webhook receiver health | Malformed inbound JSON must be caught, not crash the receiver |
Step 1: Add a JSON Health Endpoint
Your Phoenix app's health endpoint should itself return valid JSON and include a check on JSON encoding correctness:
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(),
json_encode: check_json_encode()
}
status = if Enum.all?(checks, fn {_, v} -> v == :ok end), do: 200, else: 503
payload = %{
status: if(status == 200, do: "ok", else: "degraded"),
checks: Map.new(checks, fn {k, v} -> {k, Atom.to_string(v)} end)
}
conn
|> put_resp_content_type("application/json")
|> send_resp(status, Jason.encode!(payload))
|> 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_json_encode do
# Round-trip encode/decode a representative payload
sample = %{id: 1, name: "test", timestamp: DateTime.utc_now()}
case Jason.encode(sample) do
{:ok, json} ->
case Jason.decode(json) do
{:ok, _} -> :ok
_ -> :error
end
{:error, _} -> :error
end
end
end
Step 2: Instrument Jason Decode Errors
Track Jason.DecodeError occurrences with telemetry so you can spot schema drift:
defmodule MyApp.JSON do
require Logger
def decode(string, opts \\ []) do
case Jason.decode(string, opts) do
{:ok, result} ->
{:ok, result}
{:error, %Jason.DecodeError{} = error} ->
:telemetry.execute(
[:my_app, :json, :decode_error],
%{count: 1},
%{
position: error.position,
token: error.token,
preview: String.slice(string, 0, 100)
}
)
Logger.warning("JSON decode error at position #{error.position}: #{Jason.DecodeError.message(error)}")
{:error, error}
end
end
def decode!(string, opts \\ []) do
case decode(string, opts) do
{:ok, result} -> result
{:error, error} -> raise error
end
end
end
Attach a telemetry handler:
defmodule MyApp.JSONTelemetry do
require Logger
def attach do
:telemetry.attach(
"json-decode-error-logger",
[:my_app, :json, :decode_error],
&handle_event/4,
nil
)
end
def handle_event([:my_app, :json, :decode_error], %{count: _}, meta, _) do
Logger.error("""
JSON decode failure — possible upstream schema change
Position: #{meta.position}
Token: #{inspect(meta.token)}
Input preview: #{meta.preview}
""")
end
end
Register in application.ex:
def start(_type, _args) do
MyApp.JSONTelemetry.attach()
# ... rest of start
end
Step 3: Harden Webhook Receivers
Webhook endpoints are the most common place Jason decode errors occur. Harden them to catch and report failures without crashing:
defmodule MyAppWeb.WebhookController do
use MyAppWeb, :controller
require Logger
def receive(conn, _params) do
with {:ok, body, conn} <- read_body(conn),
{:ok, payload} <- MyApp.JSON.decode(body),
:ok <- validate_payload(payload) do
MyApp.WebhookWorker.enqueue(payload)
json(conn, %{received: true})
else
{:error, %Jason.DecodeError{} = error} ->
Logger.error("Webhook received invalid JSON: #{Jason.DecodeError.message(error)}")
conn
|> put_status(400)
|> json(%{error: "invalid_json"})
{:error, :invalid_payload} ->
conn
|> put_status(422)
|> json(%{error: "schema_validation_failed"})
{:error, reason} ->
Logger.error("Webhook processing error: #{inspect(reason)}")
conn
|> put_status(500)
|> json(%{error: "internal_error"})
end
end
defp validate_payload(%{"event" => _, "data" => _}), do: :ok
defp validate_payload(_), do: {:error, :invalid_payload}
end
Step 4: Set Up HTTP Monitoring in Vigilmon
Monitor your JSON API endpoints to catch non-JSON responses, downtime, and schema regressions:
- Sign in at vigilmon.online
- Click New Monitor → HTTP
- Enter
https://yourdomain.com/health - Set check interval: 1 minute
- Add keyword check:
"status":"ok"(confirms the response body is valid JSON with the expected field)
Add a second monitor for your primary API endpoint:
- Click New Monitor → HTTP
- URL:
https://yourdomain.com/api/status(or any stable JSON endpoint) - Keyword check:
"version":or another stable field from your API contract - Interval: 5 minutes
The keyword check catches the most dangerous failure mode: your server returns a 200 with an HTML error page instead of JSON. Jason would raise on that response; Vigilmon catches it before your callers do.
Step 5: Monitor External JSON APIs
If your app calls external JSON APIs and relies on their response structure, monitor them directly:
defmodule MyApp.ExternalAPIChecker do
require Logger
def check_github_api do
case Req.get("https://api.github.com/zen", receive_timeout: 5_000) do
{:ok, %{status: 200, body: body}} when is_binary(body) ->
:ok
_ ->
:error
end
end
end
And in Vigilmon, add HTTP monitors for each external JSON API your app depends on:
| Monitor | URL | Keyword Check |
|---------|-----|---------------|
| GitHub API | https://api.github.com/zen | any response |
| Your internal API | https://api.yourdomain.com/v1/status | "status":"ok" |
| Third-party service | their status page URL | "All Systems Operational" |
Step 6: Heartbeat for JSON-Serialization-Heavy Workers
Background jobs that serialize large payloads to queues or message brokers deserve heartbeat monitoring:
defmodule MyApp.DataExportWorker do
use GenServer
require Logger
@interval :timer.hours(1)
def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
@impl true
def init(state) do
schedule_export()
{:ok, state}
end
@impl true
def handle_info(:export, state) do
records = MyApp.Repo.all(MyApp.Record)
case Jason.encode(%{records: records, exported_at: DateTime.utc_now()}) do
{:ok, json} ->
MyApp.Storage.write("export-#{Date.utc_today()}.json", json)
ping_heartbeat()
{:error, error} ->
Logger.error("Export serialization failed: #{inspect(error)}")
# No ping → Vigilmon alerts after grace window
end
schedule_export()
{:noreply, state}
end
defp schedule_export, do: Process.send_after(self(), :export, @interval)
defp ping_heartbeat do
url = Application.get_env(:my_app, :vigilmon)[:export_heartbeat_url]
if url, do: Req.get(url, receive_timeout: 5_000)
end
end
Create a Heartbeat Monitor in Vigilmon with a 1-hour interval for the export worker.
Step 7: Set Up Alerts
Go to Notifications → New Channel in Vigilmon and configure Slack or email:
Recommended alert rules:
- Health endpoint down or keyword missing → page on-call (your JSON API may be returning garbage)
- API endpoint keyword check fails → alert dev team (schema drift or server error)
- Export worker heartbeat missed → alert data team
Example Slack alert when the JSON API fails its keyword check:
⚠️ DEGRADED: Production API JSON Check
URL: https://api.yourdomain.com/v1/status
Keyword "status":"ok" not found in response
HTTP status: 200 (server returned non-JSON error page)
Detected from: EU-West, US-East
This is exactly the alert that saves you from 30 minutes of confused users before someone checks the logs.
What You've Built
| What | How |
|------|-----|
| JSON health check endpoint | Round-trip encode/decode in /health plug |
| Decode error tracking | Telemetry wrapper around Jason.decode/2 |
| Hardened webhook receiver | Error handling for Jason.DecodeError |
| JSON API uptime monitoring | Vigilmon HTTP monitor with keyword check |
| External API schema monitoring | Vigilmon HTTP monitors with response keyword validation |
| Serialization worker liveness | Heartbeat ping after successful Jason.encode/1 |
| Slack alerts | Vigilmon notification channel |
Jason is fast and silent. Vigilmon makes sure what it's encoding and decoding is always what you expect.
Next Steps
- Add response schema validation with a library like
DropsorNormand track validation failure rates - Use Vigilmon's multi-region checks to ensure your JSON API is reachable globally, not just from one probe location
- Add
Content-Type: application/jsonheader verification to your Vigilmon HTTP monitors - Monitor the size of your API responses over time — unexpected growth often indicates runaway serialization
Get started free at vigilmon.online.