tutorial

How to Monitor Jason with Vigilmon

Monitor Elixir services that encode and decode JSON with Jason — track decode failures, malformed payloads, and API health to catch schema drift before it breaks your app.

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.Encoder implementation 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:

  1. Sign in at vigilmon.online
  2. Click New Monitor → HTTP
  3. Enter https://yourdomain.com/health
  4. Set check interval: 1 minute
  5. 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:

  1. Click New Monitor → HTTP
  2. URL: https://yourdomain.com/api/status (or any stable JSON endpoint)
  3. Keyword check: "version": or another stable field from your API contract
  4. 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 Drops or Norm and 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/json header 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.

Monitor your app with Vigilmon

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

Start free →