tutorial

How to Monitor TypedStruct with Vigilmon

TypedStruct adds compile-time type specs and enforced fields to Elixir structs. Learn how to monitor the services that use TypedStruct-defined data structures with Vigilmon.

TypedStruct is an Elixir macro library that brings compile-time rigour to struct definitions — automatic typespecs, enforced required fields, and a clean DSL that eliminates boilerplate. It's a library, not a running service, but the Elixir applications that use TypedStruct — APIs, data pipelines, Phoenix services — absolutely need monitoring. Vigilmon helps you monitor those applications: catching struct validation failures at runtime, confirming data pipeline health, and alerting when the services that produce and consume TypedStruct-typed data go down.

What You'll Set Up

  • HTTP health endpoint validating TypedStruct-enforced data integrity
  • Cron heartbeat for data pipeline workers using TypedStruct domain models
  • Runtime struct validation error alerting via health checks
  • API endpoint monitoring for services built around TypedStruct schemas

Prerequisites

  • Elixir 1.14+ with typed_struct in your mix.exs
  • An application using TypedStruct for domain modelling (Phoenix, GenServer workers, etc.)
  • A free Vigilmon account

Step 1: Define a Health Struct with TypedStruct

Use TypedStruct to define a canonical health response shape — this ensures your health endpoint always returns consistent, type-safe data:

defmodule MyApp.Health do
  use TypedStruct

  typedstruct do
    field :status, String.t(), enforce: true
    field :version, String.t(), enforce: true
    field :database, boolean(), default: false
    field :cache, boolean(), default: false
    field :checked_at, DateTime.t(), enforce: true
  end
end

Build and serialise the health struct in your controller:

defmodule MyAppWeb.HealthController do
  use MyAppWeb, :controller

  def index(conn, _params) do
    health = %MyApp.Health{
      status: "ok",
      version: Application.spec(:my_app, :vsn) |> to_string(),
      database: database_alive?(),
      cache: cache_alive?(),
      checked_at: DateTime.utc_now()
    }

    status_code = if health.database and health.cache, do: 200, else: 503
    conn |> put_status(status_code) |> json(health)
  end

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

  defp cache_alive? do
    case Redix.command(:redix, ["PING"]) do
      {:ok, "PONG"} -> true
      _ -> false
    end
  end
end

TypedStruct's enforce: true means the struct construction will raise a ArgumentError at compile time if you forget a required field — your health endpoint can never return an incomplete response.


Step 2: Add the Monitor in Vigilmon

  1. Log in to vigilmon.online and click Add Monitor.
  2. Set Type to HTTP / HTTPS.
  3. Enter https://myapp.example.com/health.
  4. Set Check interval to 1 minute.
  5. Set Expected HTTP status to 200.
  6. Under Response body, enable Contains keyword and enter "ok".
  7. Click Save.

The keyword check ensures Vigilmon fails the probe if your TypedStruct health response contains "error" or "degraded" in the status field, even if the HTTP layer returns 200.


Step 3: Validate Incoming Data and Surface Failures

TypedStruct is most valuable at domain boundaries — when external data arrives (API calls, message queues, CSV imports). When validation fails, it's a signal that an upstream producer changed their schema. Surface these failures to Vigilmon via a health degradation:

defmodule MyApp.DataIngestion do
  require Logger

  @typedstruct_errors_key :typedstruct_validation_errors

  def ingest(raw_map) do
    try do
      event = struct!(MyApp.Event, raw_map)
      process(event)
    rescue
      e in [ArgumentError, KeyError] ->
        Logger.error("TypedStruct validation failed: #{inspect(e)}")
        increment_error_count()
        {:error, :validation_failed}
    end
  end

  defp increment_error_count do
    current = :persistent_term.get(@typedstruct_errors_key, 0)
    :persistent_term.put(@typedstruct_errors_key, current + 1)
  end

  def error_count, do: :persistent_term.get(@typedstruct_errors_key, 0)
end

Expose the error count in your health endpoint:

def index(conn, _params) do
  error_count = MyApp.DataIngestion.error_count()
  status = if error_count > 100, do: 503, else: 200

  health = %MyApp.Health{
    status: if(error_count > 100, do: "degraded", else: "ok"),
    version: Application.spec(:my_app, :vsn) |> to_string(),
    database: database_alive?(),
    cache: cache_alive?(),
    checked_at: DateTime.utc_now()
  }

  conn |> put_status(status) |> json(Map.put(health, :validation_errors, error_count))
end

Vigilmon will detect the 503 and fire your alert when validation error counts spike.


Step 4: Heartbeat for Data Pipeline Workers

If you run Elixir workers that process TypedStruct-typed domain events (GenStage producers, Broadway pipelines, Task.Supervisor jobs), add a Vigilmon heartbeat:

  1. In Vigilmon, click Add MonitorCron Heartbeat.
  2. Set the interval to match your pipeline's expected throughput (e.g. 10 minutes if the pipeline processes events every few minutes).
  3. Copy the heartbeat URL.

Send the ping after each successful processing batch:

defmodule MyApp.EventPipeline do
  use GenStage
  require Logger

  @heartbeat_url System.get_env("VIGILMON_HEARTBEAT_URL")

  def handle_events(events, _from, state) do
    processed =
      Enum.map(events, fn raw ->
        struct!(MyApp.Event, raw)
      end)

    Enum.each(processed, &persist/1)

    ping_vigilmon()
    {:noreply, [], state}
  end

  defp ping_vigilmon do
    :httpc.request(:get, {String.to_charlist(@heartbeat_url), []}, [{:timeout, 5000}], [])
  rescue
    e -> Logger.warning("Vigilmon ping failed: #{inspect(e)}")
  end
end

The pipeline only pings on successful batch processing. A crash between events means no ping, and Vigilmon alerts after the expected interval.


Step 5: Monitor Your API Surface

Applications built with TypedStruct often expose typed JSON APIs. Add Vigilmon monitors for each critical endpoint:

defmodule MyApp.UserController do
  use MyAppWeb, :controller

  def show(conn, %{"id" => id}) do
    case MyApp.Users.get(id) do
      %MyApp.User{} = user ->
        json(conn, user)
      nil ->
        conn |> put_status(404) |> json(%{error: "not_found"})
    end
  end
end

For each critical API route, add a Vigilmon monitor:

  1. Add a dedicated health or smoke-test endpoint that exercises your TypedStruct data layer.
  2. Create an HTTP monitor in Vigilmon for that endpoint.
  3. Set the keyword assertion to match expected TypedStruct field names (e.g. "email" for a user endpoint).

This confirms not just that the HTTP server is up, but that the TypedStruct domain layer is returning well-formed data.


Step 6: Configure Alert Channels

  1. Go to Alert Channels in Vigilmon and connect Slack or PagerDuty.
  2. Set Consecutive failures before alert to 2 for HTTP monitors to absorb deploy restarts.
  3. Set it to 1 for heartbeat monitors on data pipelines — a single missed beat usually indicates a real problem.
  4. Use maintenance windows during deploys:
curl -X POST https://vigilmon.online/api/maintenance \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{"monitor_id": "abc123", "duration_minutes": 5}'

Summary

| Monitor | Target | What It Catches | |---|---|---| | HTTP health check | /health with TypedStruct response | App down, dependency failure | | Response keyword | "ok" in body | Degraded status from validation errors | | Cron heartbeat | Heartbeat URL | Pipeline worker stopped processing | | API endpoint check | Critical route keyword | Domain layer returning malformed data |

TypedStruct enforces correctness at compile time — Vigilmon enforces reliability at runtime. Together they give your Elixir application both the type safety of explicit struct contracts and the operational confidence of continuous uptime monitoring.

Monitor your app with Vigilmon

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

Start free →